001    /*
002     * file QueryCommand.java
003     * 
004     * Licensed Materials - Property of IBM
005     * Restricted Materials of IBM - you are allowed to copy, modify and 
006     * redistribute this file as part of any program that interfaces with 
007     * IBM Rational CM API.
008     *
009     * com.ibm.rational.stp.client.samples.QueryCommand
010     *
011     * (C) Copyright IBM Corporation 2004, 2008.  All Rights Reserved.
012     * Note to U.S. Government Users Restricted Rights:  Use, duplication or 
013     * disclosure restricted by GSA ADP  Schedule Contract with IBM Corp.
014     */
015    
016    package com.ibm.rational.stp.client.samples;
017    
018    import java.util.ArrayList;
019    import java.util.Arrays;
020    import java.util.List;
021    import java.util.ListIterator;
022    
023    import javax.wvcm.PropertyNameList;
024    import javax.wvcm.WvcmException;
025    import javax.wvcm.PropertyRequestItem.PropertyRequest;
026    
027    import com.ibm.rational.wvcm.stp.StpLocation;
028    import com.ibm.rational.wvcm.stp.StpLocation.Namespace;
029    import com.ibm.rational.wvcm.stp.StpProvider.Domain;
030    import com.ibm.rational.wvcm.stp.cq.CqProvider;
031    import com.ibm.rational.wvcm.stp.cq.CqQuery;
032    import com.ibm.rational.wvcm.stp.cq.CqRecordType;
033    import com.ibm.rational.wvcm.stp.cq.CqResultSet;
034    import com.ibm.rational.wvcm.stp.cq.CqRowData;
035    import com.ibm.rational.wvcm.stp.cq.CqQuery.DisplayField;
036    import com.ibm.rational.wvcm.stp.cq.CqQuery.Filter.Operation;
037    
038    /**
039     * A sample CM API application for defining and executing ClearQuest queries
040     * using a command-line interface. Command line arguments are
041     * <dl>
042     * <dt> <b>-name</b> <i>existing-query-location</i>
043     * <dd>Specifies an existing query to execute and possibly modify and save.</dd>
044     * <dt> <b>-save</b> [<i>new-query-location</i>]
045     * <dd>Specifies that the query is to be saved after definition or modification
046     * and before attempting execution. If no location is specified here it must be
047     * specified by a <b>-name</b> clause</dd>
048     * <dt> <b>-select</b> <i>display-field-specification</i>+
049     * <dd>Specifies the (new) display fields for the query. Requires either
050     * <b>-from</b> or <b>-name</b>
051     * <dd>
052     * <dt> <b>-from</b> <i>primary-record-type-location</i>
053     * <dd>Specifies the primary record type for the query. Requires either
054     * <b>-select</b> or <b>-name</b>. The <b>-save</b> location, if specified
055     * with <b>-name</b>, must be different from the <b>-name</b> location for
056     * this to be valid, since the primary type of an existing query cannot be
057     * changed.</dd>
058     * <dt> <b>-where</b> <i>filtering-expression-term</i>*
059     * <dd>Specifies the (new) filtering expression for the query. A <b>-where</b>
060     * clause with no filtering-expression-terms forces the query to be run without
061     * a filtering expression.</dd>
062     * <dt> <b>-min</b> <i>first-row-to-show</i>
063     * <dd>The first row of the result set to be displayed; defaults to 1, the
064     * first row. Must be positive.</dd>
065     * <dt> <b>-max</b> <i>last-row-to-show</i>
066     * <dd>the last row of the result set to be displayed; defaults to the last
067     * row. If zero, the number of rows found by the query will be reported.
068     * <dt> <b>-with</b> <i>filter-leaf-expression</i>+
069     * <dd>Specifies a value (and, optionally an operation) to fill in the dynamic
070     * filters of the query.</dd>
071     * <dt>-count
072     * <dd>Request a count of the number of rows matched by the query, even if not
073     * all rows are returned (as specified by the <b>-max</b> parameter)</dd>
074     * </dl>
075     * 
076     * </pre>
077     * 
078     * The implementation is divided into four methods: collectArgs, buildQuery,
079     * executeQuery, and printResults. As a sample CM API application, the
080     * buildQuery and executeQuery methods are the most instructive. To make the
081     * example realistic, a syntax for specifying display fields and filters on the
082     * command line was contrived. The parsing of this representation is implemented
083     * in the QueryUtilities class.
084     */
085    public class QueryCommand extends QueryUtilities
086    {
087        /** The CqProvider instance used by the class and its instances */
088        private static CqProvider g_provider = null;
089        
090        /** The target ClearQuest user database, derived from command-line input */
091        private static String g_repo = "";
092    
093        /** -name operand: The name of an existing query */
094        StpLocation m_oldName = null;
095        /** -save operand: the name under which the query is to be saved */
096        StpLocation m_saveName = null;
097        /** -from operand: the CqRecordType defining the type of records to query */
098        StpLocation m_primaryType = null;
099        /** -select operand: Used to build the DisplayField[] for the query */
100        List<String> m_select = null;
101        /** -where operand: Used to build the filtering expression for the query */
102        List<String> m_where = null;
103        /** -with operand: Used to build the FilterLeaf[] for query parameters */
104        List<String> m_with = null;
105        /** -min operand: The first row of the result set to return */
106        long m_min = 1;
107        /** -max operand: The last row of the result set to return */
108        long m_max = Long.MAX_VALUE;
109        /** Whether or not to count all rows: set by presence of -count */
110        CqQuery.ListOptions m_countRows = null;
111        
112        /**
113         * Collects the command-line arguments into object fields. Variables for
114         * unspecified arguments are left at their initial value. As the arguments
115         * are collected, the resource names are converted to a complete CM API
116         * StpLocation and from that the targeted ClearQuest database is determined.
117         * 
118         * @param args A String array containing the command-line arguments
119         * @throws WvcmException if StpLocation objects cannot be constructed from
120         *             the resource names on the command line.
121         */
122        void collectArgs(String[] args)
123        throws WvcmException
124        {
125            ListIterator<String> argList = Arrays.asList(args).listIterator(0);
126            
127            while (argList.hasNext()) {
128                String arg = argList.next();
129                
130                if (arg.equals("-url"))
131                    g_provider.setServerUrl(argList.next());
132                else if (arg.equals("-name"))
133                    m_oldName = toSelector(Namespace.QUERY, argList.next());
134                else if (arg.equals("-save")) {
135                    int next = argList.nextIndex();
136                    
137                    if (next < args.length && !args[next].startsWith("-")) {
138                        m_saveName = toSelector(Namespace.QUERY, args[next]);
139                        argList.next();
140                    } else
141                        m_saveName = m_oldName;
142                } else if (arg.equals("-from"))
143                    m_primaryType = toSelector(Namespace.RECORD, argList.next());
144                else if (arg.equals("-min"))
145                    m_min = Long.parseLong(argList.next());
146                else if (arg.equals("-max"))
147                    m_max = Long.parseLong(argList.next());
148                else if (arg.equals("-select"))
149                    m_select = getArgList(argList);
150                else if (arg.equals("-where"))
151                    m_where = getArgList(argList);
152                else if (arg.equals("-with"))
153                    m_with = getArgList(argList);
154                else if (arg.equals("-count"))
155                    m_countRows = CqQuery.COUNT_ROWS;
156                else if (arg.equals("-user"))
157                    Utilities.g_user = argList.next();
158                else if (arg.equals("-password"))
159                    Utilities.g_pass = argList.next();
160            }
161        }
162    
163        /**
164         * Constructs a CqQuery proxy in which the query specification data
165         * presented on the command line is stored. If -save was specified, the
166         * proxy will use that location since the query will be executed from that
167         * location. If -save was not specified but -name was, then that location is
168         * used since either the results will come directly from the named query or
169         * the query will be done anonymously; Otherwise, a dummy location is used.
170         */
171        CqQuery buildQuery()
172        throws WvcmException
173        {
174            CqQuery query = null;
175    
176            StpLocation queryLoc = m_saveName != null? m_saveName: g_provider
177                .userFriendlySelector(Domain.CLEAR_QUEST,
178                                      Namespace.QUERY,
179                                      "anonymous",
180                                      g_repo);
181    
182            query = g_provider.cqQuery(queryLoc);
183            
184            // If an existing query was specified, read it's specification from
185            // the database and use that data to initialize the query proxy.
186            if (m_oldName != null) {
187                CqQuery oldQuery = (CqQuery) g_provider.cqQuery(m_oldName)
188                    .doReadProperties(QUERY_PROPERTIES);
189                
190                if (m_saveName == null || m_saveName.equals(m_oldName)) {
191                    query = oldQuery;
192                } else {
193                    query.setPrimaryRecordType(oldQuery.getPrimaryRecordType());
194                    query.setDisplayFields(oldQuery.getDisplayFields());
195                    query.setFiltering(oldQuery.getFiltering());
196                }
197            } else
198                query.setFiltering(query.cqProvider().buildFilterNode(Operation.CONJUNCTION, 
199                                                             new CqQuery.Filter[0]));
200            
201            // Now populate the query with modifications specified on the command
202            // line.
203            
204            // If a primary record type was specified, read the required information
205            // about it from the database so that field and filter specifications
206            // can be converted properly.
207            if (m_primaryType != null) {
208                CqRecordType queryRecordType = 
209                    (CqRecordType) g_provider.cqRecordType(m_primaryType)
210                        .doReadProperties(RECORD_TYPE_WITH_FIELDS);
211    
212                query.setPrimaryRecordType(queryRecordType);
213            }
214    
215            // If a -select clause was specified, parse it into a DisplayField
216            // array and add it to the query proxy.
217            if (m_select != null) {
218                DisplayField[] fields = new DisplayField[m_select.size()];
219        
220                for (int i=0; i < fields.length; ++i)
221                    fields[i] = parseDisplayField(query, m_select.get(i));
222        
223                query.setDisplayFields(fields);
224            }
225            
226            // If a -where clause was specified, parse it into a Filter object
227            // and add it to the query proxy.
228            if (m_where != null) {
229                query.setFiltering(parseFilter(query, m_where.iterator()));
230            }
231            
232            return query;
233        }
234    
235        /**
236         * Executes the constructed query.
237         * <p>
238         * At this point, if the user followed the rules, we should be able to
239         * execute the query. If -save was specified, we need to define the query in
240         * the database and then use doExecute to get the results. If an existing
241         * query is being executed unmodified, then we use doExecute as well;
242         * Otherwise we use CqRecordType.doQuery() passing the data from the command
243         * line to ClearQuest.
244         */
245        CqResultSet executeQuery(CqQuery input)
246        throws WvcmException
247        {
248            CqQuery query = input;
249            
250            // Create the query if so directed by the input.
251            if (m_saveName != null && !m_saveName.equals(m_oldName)) {
252                System.out.println("Creating " + query + " as " + describe(query));
253                query = query.doCreateQuery(QUERY_PROPERTIES);
254                
255                // Copy new name to caller's proxy
256                input.setProperty(CqQuery.USER_FRIENDLY_LOCATION, 
257                                  query.getUserFriendlyLocation());
258            }
259    
260            CqResultSet results;
261    
262            // Use doExecute if the query is completely defined in the database (or
263            // will be once dirty properties in the proxy have been written). There
264            // will be dirty properties only if the input has directed that 
265            // modifications to an existing query are to be written back to the same
266            // location.
267            if (query.updatedPropertyNameList().getPropertyNames().length == 0
268                                                           || m_saveName != null) {
269                CqQuery.FilterLeaf[] params = null;
270                
271                // If a -with clause was specified, parse the given
272                // parameter values into a FilterLeaf array for use in doExecute;
273                // otherwise leave the dynamic parameters null.
274                
275                if (m_with != null) {
276                    params = new CqQuery.FilterLeaf[query.getDynamicFilters().length];
277                    String sep = "With parameter(s) ";
278                    
279                    for (int i=0; i < params.length; ++i) {
280                        String param = i < m_with.size()? m_with.get(i): "";
281    
282                        params[i] = parseDynamicFilter(query, i, param);
283                        
284                        System.out.print(sep + params[i]);  sep = ", ";
285                    }
286                    
287                    System.out.println("...");
288                }
289    
290                // Note: If the display field and/or filter has been modified, it 
291                // will be written to the definition before the query executes as a
292                // side-effect of either of these do-methods.
293                results = query.doExecute(m_min, m_max-m_min+1, m_countRows, params);
294            } else
295                results = query.getPrimaryRecordType()
296                            .doQuery(query.getDisplayFields(), 
297                                     query.getFiltering(), 
298                                     m_min, m_max-m_min+1, 
299                                     m_countRows);
300    
301            return results;
302        }
303        
304        /**
305         * Display the rows returned by the query on the console
306         * @param query The query that was executed.
307         * @param results The CqResultSet containing the rows of the result
308         * set.
309         * @throws WvcmException If interactions with the server fail or have failed
310         * to obtained the necessary information.
311         */
312        void printResults(CqQuery query, CqResultSet results)
313        throws WvcmException
314        {
315            System.out.print("Query '" + describe(query) + "' found ");
316    
317            // Display the results
318            if (m_countRows != null)
319                System.out.print(results.getRowCount() + " rows ");
320            
321            System.out.println("...");
322            
323            if (results.hasNext()) {
324                // Print the column headings
325                DisplayField[] fields = query.getDisplayFields();
326                System.out.print("Row");
327                
328                for (int i=0; i < fields.length; ++i)
329                    if (fields[i].getIsVisible())
330                        System.out.print("\t" + fields[i].getLabel());
331                
332                System.out.println();
333        
334                // Print the rows
335                while (results.hasNext()) {
336                    CqRowData data = (CqRowData)results.next();
337                    Object[] row = data.getValues();
338                    
339                    System.out.print(data.getRowNumber());
340                    
341                    for (int i=0; i < row.length; ++i)
342                        System.out.print("\t" + row[i]);
343                    
344                    System.out.println();
345                }
346            }
347        }
348        
349        /**
350         * Assembles a String containing information about the query executed.
351         * @param query A CqQuery proxy for the query to be described. Must define
352         * the DISPLAY_FIELDS, PRIMARY_RECORD_TYPE, and FILTERING properties
353         * if the query has not been saved; USER_FRIENDLY_LOCATION, otherwise.
354         * @return A String containing the name of the query if saved or a stylized
355         * SQL select statement describing the query if not.
356         */
357        private String describe(CqQuery query)
358        throws WvcmException
359        {
360            PropertyNameList updated = query.updatedPropertyNameList();
361            
362            if (updated.getPropertyNames().length == 0)
363                return query.getUserFriendlyLocation().toString();
364            
365            StringBuffer sb = new StringBuffer();
366            DisplayField[] fields = query.getDisplayFields(); 
367    
368            sb.append("select");
369            
370            for (int i=0; i < fields.length; ++i)
371                sb.append (" " + fields[i]);
372            
373            sb.append(" from ");
374            sb.append(((CqRecordType)query.getPrimaryRecordType()).getDisplayName());
375            
376            String filter = query.getFiltering().toString();
377            
378            if (!filter.equals(""))
379                sb.append(" where " + filter);
380            
381            return sb.toString();
382        }
383        
384        void run(String[] args)
385        throws WvcmException
386        {
387            collectArgs(args);
388            CqQuery query = buildQuery();
389            CqResultSet results = executeQuery(query);
390            printResults(query, results);
391        }
392    
393        /**
394         * @param args
395         * @throws Exception 
396         */
397        public static void main(String[] args) throws Exception
398        {
399            if (g_provider == null)
400                g_provider = Utilities.getStaticProvider();
401            
402            g_repo = "";
403            
404            new QueryCommand().run(args);
405            System.exit(0);
406        }
407        
408        /**
409         * Collects the arguments in the command line upto, but not including, the
410         * first argument that begins with "-"
411         * 
412         * @param args The command line argument iterator positioned on the argument
413         *            that precedes the arguments to be collected. Must not be
414         *            <b>null</b>.
415         * @return A List of the collected arguments. Will not be <b>null</b>, but
416         *         may be empty.
417         */
418        private static List<String> getArgList(ListIterator<String> args)
419        {
420            List<String> list = new ArrayList<String>();
421            
422            while (args.hasNext()) {
423                String arg = args.next();
424                
425                if (arg.startsWith("-")) {
426                    args.previous();
427                    break;
428                }
429                
430                list.add(arg);
431            }
432            
433            return list;
434        }
435        
436        /**
437         * Converts command-line input specifying a resource into an StpLocation,
438         * adding domain and namespace if omitted by the user
439         * 
440         * @param namespace The expected namespace for the location. Must not be
441         *            <b>null</b>.
442         * @param str The location as input by the user. Expect simple name and
443         *            possibly repository information. Must not be <b>null</b>.
444         * @return An StpLocation specifying the same resource as the given string
445         *         argument based on context.
446         * @throws WvcmException
447         */
448        private static StpLocation toSelector(Namespace namespace, String str)
449            throws WvcmException
450        {
451            StpLocation loc = g_provider.stpLocation(str);
452    
453            if (loc.getDomain() == Domain.NONE)
454                loc = loc.recomposeWithDomain(Domain.CLEAR_QUEST);
455    
456            if (loc.getNamespace() == Namespace.NONE)
457                loc = loc.recomposeWithNamespace(namespace);
458    
459            if (g_repo.equals(""))
460                g_repo = loc.getRepo();
461            else if (loc.getRepo().equals(""))
462                loc = loc.recomposeWithRepo(g_repo);
463    
464            return loc;
465        }
466        
467        private static final PropertyRequest QUERY_PROPERTIES = 
468            new PropertyRequest(CqQuery.PRIMARY_RECORD_TYPE
469                                                     .nest(RECORD_TYPE_WITH_FIELDS),
470                                CqQuery.DISPLAY_FIELDS,
471                                CqQuery.DYNAMIC_FILTERS,
472                                CqQuery.FILTERING,
473                                CqQuery.USER_FRIENDLY_LOCATION
474            );
475    }