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
016package com.ibm.rational.stp.client.samples;
017
018import java.util.ArrayList;
019import java.util.Arrays;
020import java.util.List;
021import java.util.ListIterator;
022
023import javax.wvcm.PropertyNameList;
024import javax.wvcm.WvcmException;
025import javax.wvcm.PropertyRequestItem.PropertyRequest;
026
027import com.ibm.rational.wvcm.stp.StpLocation;
028import com.ibm.rational.wvcm.stp.StpLocation.Namespace;
029import com.ibm.rational.wvcm.stp.StpProvider.Domain;
030import com.ibm.rational.wvcm.stp.cq.CqProvider;
031import com.ibm.rational.wvcm.stp.cq.CqQuery;
032import com.ibm.rational.wvcm.stp.cq.CqRecordType;
033import com.ibm.rational.wvcm.stp.cq.CqResultSet;
034import com.ibm.rational.wvcm.stp.cq.CqRowData;
035import com.ibm.rational.wvcm.stp.cq.CqQuery.DisplayField;
036import 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 */
085public 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}