001/*
002 * file QueryUtilities.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.QueryUtilities
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 */
015package com.ibm.rational.stp.client.samples;
016
017import java.util.ArrayList;
018import java.util.EnumSet;
019import java.util.HashMap;
020import java.util.Iterator;
021import java.util.List;
022
023import javax.wvcm.ResourceList;
024import javax.wvcm.WvcmException;
025import javax.wvcm.PropertyRequestItem.PropertyRequest;
026
027import com.ibm.rational.wvcm.stp.StpException;
028import com.ibm.rational.wvcm.stp.StpLocation;
029import com.ibm.rational.wvcm.stp.cq.CqFieldDefinition;
030import com.ibm.rational.wvcm.stp.cq.CqFieldValue;
031import com.ibm.rational.wvcm.stp.cq.CqQuery;
032import com.ibm.rational.wvcm.stp.cq.CqRecordType;
033import com.ibm.rational.wvcm.stp.cq.CqQuery.DisplayField;
034import com.ibm.rational.wvcm.stp.cq.CqQuery.DisplayField.SortType;
035import com.ibm.rational.wvcm.stp.cq.CqQuery.Filter.Operation;
036import com.ibm.rational.wvcm.stp.cq.CqQuery.FilterLeaf.TargetType;
037
038/**
039 * A CmdBaseQuery
040 */
041public abstract class QueryUtilities {
042    static private enum Kind {
043        /** FilterSym.kind for an operation with no operands */
044        NILARY, 
045        /** FilterSym.kind for an operation with one operand */
046        UNARY, 
047        /** FilterSym.kind for an operation with two operands */
048        BINARY, 
049        /** FilterSym.kind for an operation with one or more operands */
050        VARIADIC, 
051        /** FilterSym.kind for target specification token */
052        TARGET, 
053        /** FilterSym.kind for a target separator token */
054        SEPARATOR, 
055        /** FilterSym.kind for the end of a FilterLeaf expression */
056        END
057    };
058
059    /** Kind mask for target specification token */
060    private static final EnumSet<Kind> TARGET_MASK = EnumSet.of(Kind.TARGET);
061
062    /** Kind mask for a target separator token */
063    private static final EnumSet<Kind> SEPARATOR_MASK = EnumSet.of(Kind.SEPARATOR);
064
065    /** Kind mask for the end of a FilterLeaf expression */
066    private static final EnumSet<Kind> END_MASK = EnumSet.of(Kind.END);
067
068    
069    /**
070     * The representation for a symbol in a filtering expression
071     */
072    private static class FilterSym<T> {
073        /** The Operation or TargetType for this filter symbol */
074        T code;
075
076        /** The token image used to represent this filter symbol */
077        String symbol;
078
079        /** The kind of this symbol */
080        Kind kind;
081
082        /**
083         * Creates a new FilterSym object for an Operation.
084         * 
085         * @param o The Operation enumerator
086         * @param i The Operation symbol
087         * @param t The kind code
088         */
089        private FilterSym(T o, String i, Kind t)
090        {
091            code = o;
092            symbol = i;
093            kind = t;
094        }
095
096        /**
097         * Creates a new FilterSym object for a TargetType enumerator.
098         * 
099         * @param o The TargetType code
100         * @param i The target name
101         */
102        private FilterSym(T o, String i)
103        {
104            code = o;
105            symbol = i;
106            kind = Kind.TARGET;
107        }
108
109        /**
110         * Creates a new FilterSym object for a target token.
111         * 
112         * @param token The token string.
113         */
114        private FilterSym(String token)
115        {
116            symbol = token;
117            kind = Kind.TARGET;
118            code = StpException.<T>unchecked_cast(TargetType.CONSTANT);
119//
120//            if (token.startsWith("cq.")
121//                || token.startsWith("record:")) {
122//                code = StpException.<T>unchecked_cast(TargetType.REFERENCE);
123//            }
124        }
125
126        /**
127         * Creates a new FilterSym object for a parameterized special token
128         * such as TargetType.PROMPTED
129         * 
130         * @param sym The (parameterless) FilterSym for the special token
131         * @param param The parameter for the special token.
132         */
133        private FilterSym(FilterSym<T> sym, String param)
134        {
135            code = sym.code;
136            symbol = param;
137            kind = sym.kind;
138        }
139    }
140
141    /** Pre-defined Filter token definitions */
142    private static FilterSym[] symbol = {
143        new FilterSym<Operation>(Operation.CONJUNCTION, "[and]", Kind.SEPARATOR),
144        new FilterSym<Operation>(Operation.DISJUNCTION, "[or]", Kind.SEPARATOR),
145        new FilterSym<Operation>(Operation.IS_EQUAL, "[eq]", Kind.UNARY),
146        new FilterSym<Operation>(Operation.IS_NOT_EQUAL, "[ne]", Kind.UNARY),
147        new FilterSym<Operation>(Operation.IS_LESS_THAN, "[lt]", Kind.UNARY),
148        new FilterSym<Operation>(Operation.IS_LESS_THAN_OR_EQUAL, "[le]", Kind.UNARY),
149        new FilterSym<Operation>(Operation.IS_GREATER_THAN, "[gt]", Kind.UNARY),
150        new FilterSym<Operation>(Operation.IS_GREATER_THAN_OR_EQUAL, "[ge]", Kind.UNARY),
151        new FilterSym<Operation>(Operation.HAS_SUBSTRING, "[like]", Kind.UNARY),
152        new FilterSym<Operation>(Operation.HAS_SUBSTRING, "[has]", Kind.UNARY),
153        new FilterSym<Operation>(Operation.HAS_SUBSTRING, "[contains]", Kind.UNARY),
154        new FilterSym<Operation>(Operation.HAS_NO_SUBSTRING, "[unlike]", Kind.UNARY),
155        new FilterSym<Operation>(Operation.HAS_NO_SUBSTRING, "[lacks]", Kind.UNARY),
156        new FilterSym<Operation>(Operation.IS_BETWEEN, "[between]", Kind.BINARY),
157        new FilterSym<Operation>(Operation.IS_NOT_BETWEEN, "[outside]", Kind.BINARY),
158        new FilterSym<Operation>(Operation.IS_NULL, "[null]", Kind.NILARY),
159        new FilterSym<Operation>(Operation.IS_NOT_NULL, "[set]", Kind.NILARY),
160        new FilterSym<Operation>(Operation.IS_IN_SET, "[is]", Kind.VARIADIC),
161        new FilterSym<Operation>(Operation.IS_NOT_IN_SET, "[isnt]", Kind.VARIADIC),
162        new FilterSym<TargetType>(TargetType.USER, "[user]"),
163        new FilterSym<TargetType>(TargetType.USER, "[me]"),
164        new FilterSym<TargetType>(TargetType.YESTERDAY, "[yesterday]"),
165        new FilterSym<TargetType>(TargetType.TODAY, "[today]"),
166        new FilterSym<TargetType>(TargetType.TOMORROW, "[tomorrow]"),
167        new FilterSym<TargetType>(TargetType.DATE_ONLY, "[date]"),
168        new FilterSym<TargetType>(TargetType.DATE_TIME, "[time]"),
169        new FilterSym<TargetType>(TargetType.PROMPTED, "[prompt:]"),
170//        new FilterSym<TargetType>(TargetType.FIELD, "[field:]"),
171        null };
172
173    /**
174     * Tokenizes a stream containing a filtering expression from the command
175     * line
176     */
177    private static class Tokenizer {
178        /**
179         * Creates a new FilterTokenizer object based on a given stream.
180         * 
181         * @param stream The String to be tokenized
182         */
183        Tokenizer(String stream)
184        {
185            m_stream = stream;
186            m_text = stream;
187        }
188
189        /**
190         * Fetches the next token in the stream, verifies that its kind
191         * satisfies the mask parameter; and, if specified, verifies that its
192         * code matches the match parameter, and finally, returns its FilterSym
193         * structure;
194         * 
195         * @param mask A bit mask specifying the type(s) of token that are
196         *            acceptable return values.
197         * @param match A specific FilterSym.code value that the token is
198         *            required to match; may be null if no specific code match
199         *            is required.
200         * @return A FilterSym object representing the next token in the stream.
201         */
202        <T> FilterSym<T> next(EnumSet<Kind> mask, Object match)
203        {
204            String token = null;
205
206            if (m_stream == null || m_stream.length() == 0) {
207                // End of stream; result is zero
208                if (mask.contains(Kind.END)) {
209                    return null;
210                }
211
212                throw new IllegalArgumentException("Unexpected end of filter in '"
213                    + m_text + "'");
214            } else {
215                // All blanks in the stream are significant
216                // A token is either text enclosed in []
217                // or the longest span of text containing no '['
218                int lb = m_stream.indexOf("[");
219
220                if (lb < 0) {
221                    // The last token in the stream; a Target
222                    token = m_stream;
223                    m_stream = null;
224
225                    if (mask.contains(Kind.TARGET)) {
226                        return new FilterSym<T>(token);
227                    }
228
229                    throw new IllegalArgumentException("Unexpected symbol '"
230                        + token + "' in filter '" + m_text + "'");
231                } else if (lb == 0) {
232                    // Have a special token [...]
233                    int rb = m_stream.indexOf("]");
234
235                    if (rb < 0) {
236                        throw new IllegalArgumentException("Special token '"
237                            + m_stream + "' has no closing ']' in '"
238                            + m_text + "'");
239                    }
240
241                    token = m_stream.substring(0, rb + 1); // includes "[" and
242                    // "]"
243                    m_stream = m_stream.substring(rb + 1);
244
245                    int colon = token.indexOf(":");
246                    String arg = null;
247
248                    if (colon > 0) {
249                        // Elide argument for lookup purposes
250                        arg = token.substring(colon + 1, token.length()-1);
251                        token = token.substring(0, colon) + ":]";
252                    }
253
254                    // Look up special token in symbol table
255
256                    for (FilterSym<T> result: symbol) {
257                        if (result.symbol.equals(token)) {
258                            if (mask.contains(result.kind)
259                                && (match == null || match.equals(result.code))) {
260                                if (arg != null) {
261                                    return new FilterSym<T>(result, arg);
262                                } else {
263                                    return result;
264                                }
265                            }
266
267                            throw new IllegalArgumentException("Unexpected token '"
268                                + token + "' in '" + m_text + "'");
269                        }
270                    }
271
272                    // Symbol not found in symbol table
273                    throw new IllegalArgumentException("Undefined token '"
274                        + token + "' in '" + m_text + "'");
275                } else {
276                    // Not a special symbol; a TARGET token
277                    token = m_stream.substring(0, lb);
278                    m_stream = m_stream.substring(lb);
279
280                    if (mask.contains(Kind.TARGET)) {
281                        return new FilterSym<T>(token);
282                    }
283
284                    throw new IllegalArgumentException("Unexpected symbol '"
285                        + token + "' in '" + m_text + "'");
286                }
287            }
288        }
289
290        /**
291         * Fetches the next token in the stream, verifies that its kind
292         * satisfies the mask parameter and finally, returns its FilterSym
293         * structure;
294         * 
295         * @param mask A bit mask specifying the type(s) of token that are
296         *            acceptable return values.
297         * 
298         * @return A FilterSym object representing the next token in the stream.
299         */
300        FilterSym next(EnumSet<Kind> mask)
301        {
302            return next(mask, null);
303        }
304
305        FilterSym<TargetType> nextTarget()
306        {
307            return next(TARGET_MASK, null);
308        }
309        
310        FilterSym<Operation> nextOperation()
311        {
312            return next(EnumSet.of(Kind.NILARY, Kind.UNARY, Kind.BINARY, Kind.VARIADIC), 
313                        null);
314        }
315
316        /**
317         * Fetches the next token in the stream, without verification, and
318         * returns its FilterSym structure;
319         * 
320         * @return A FilterSym object representing the next token in the stream.
321         */
322        FilterSym next()
323        {
324            return next(EnumSet.allOf(Kind.class));
325        }
326
327        /** The remainder of the character stream from which tokens are fetched */
328        private String m_stream;
329
330        /** The original input stream */
331        private String m_text;
332    };
333
334    /**
335     * Locates that CqFieldDefinition that corresponds to a given field name.
336     * Errors or warnings are generated if the given name doesn't match a
337     * defined field exactly.
338     *
339     * @param  name  The field name to be matched. Must not be null.
340     * @param  fields  The list of FieldDefinitions against which the name is to
341     *                 be compared.
342     *
343     * @return  The CqFieldDefinition that matches the given name; <b>null</b>
344     *          returned if no match is found.
345     *
346     * @throws  WvcmException  Thrown if the supplied FieldDefinitions do not
347     *                         define the DISPLAY_NAME property.
348     */
349    private static CqFieldDefinition findField(String name, ResourceList fields)
350        throws WvcmException
351    {
352        CqFieldDefinition fieldDef = null;
353
354        for (int i = 0; i < fields.size(); ++i) {
355            fieldDef = (CqFieldDefinition) fields.get(i);
356            String fieldName = fieldDef.getDisplayName();
357
358            if (name.compareToIgnoreCase(fieldName) == 0) {
359                if (!name.equals(fieldName))
360                    System.out.println("Field '" + name
361                        + "' should be spelled " + fieldName);
362
363                return fieldDef;
364            }
365        }
366
367        throw new IllegalArgumentException("'" + name
368            + "' does not name a field of record type "
369            + fieldDef.getRecordType().getDisplayName());
370    }
371
372    /**
373     * Constructs a CqFieldDefinition[] from a field path specification
374     * consisting of dotted segments: field1.field2. ... .fieldN
375     * 
376     * @param query A CqQuery proxy for the query in which the field path will
377     *            be used. Must define
378     *            PRIMARY_RECORD_TYPE.nest(FIELD_DEFINITIONS.nest(FIELD_DEFINITION_PROPERTIES))
379     * @param path A dot-separated list of field names describing the path
380     * from a field of the primary resource type, through references, to the
381     * targeted field.
382     * @return A CqFieldDefinition[] containing a valid CqFieldDefinition proxy 
383     * for each segment of the path.
384     * @throws WvcmException If the information needed to analyze the path is
385     * unavailable or unobtainable.
386     */
387    private static CqFieldDefinition[] buildFieldPath(CqQuery query, String path)
388        throws WvcmException
389    {
390        String[] segments = path.split("\\.");
391        CqFieldDefinition[] result = new CqFieldDefinition[segments.length];
392        CqRecordType recType = (CqRecordType)query.getPrimaryRecordType();
393
394        for (int i = 0; i < segments.length; ++i) {
395            String fieldname = segments[i];
396
397            if (recType == null)
398                throw new IllegalArgumentException
399                    ("No record type information available for field path segment "
400                         + fieldname);
401
402            CqFieldDefinition def = findField(fieldname, 
403                                              recType.getFieldDefinitions());
404
405            recType = getReferencedRecordType(result[i] = def);
406        }
407
408      return result;
409    }
410
411    /**
412     * Returns a fully-populated proxy for the record type of the records
413     * referenced by a given field definition.
414     * 
415     * @param def The CqFieldDefinition whose referenced record type is desired.
416     *            must not be null.
417     * @return A fully-populated CqRecordType proxy for the record type of the
418     *         records referenced by the values of the given field definition;
419     *         null if the field value doesn't reference a record (or record
420     *         list). The results are cached to avoid unnecessary interactions
421     *         with the serve.
422     * @throws WvcmException
423     */
424    private static CqRecordType getReferencedRecordType(CqFieldDefinition def)
425    throws WvcmException
426    {   
427        if (def.getFieldType() != CqFieldValue.ValueType.RESOURCE
428                && def.getFieldType() != CqFieldValue.ValueType.RESOURCE_LIST)
429            return null;
430            
431        CqRecordType recType = def.getReferencedRecordType();
432        CqRecordType result = (CqRecordType) 
433            g_recordTypeMap.get(recType.getUserFriendlyLocation());
434        
435        if (result == null) {
436            result = (CqRecordType) recType
437                .doReadProperties(RECORD_TYPE_WITH_FIELDS);
438            g_recordTypeMap.put(result.getUserFriendlyLocation(), result);
439        }
440        
441        return result;
442    }
443
444    /**
445     * A Map from user-friendly location to fully-populated record type proxy.
446     * Used to avoid unnecessary trips to the server to get this static
447     * information
448     */
449    private static HashMap<StpLocation, CqRecordType> g_recordTypeMap = 
450        new HashMap<StpLocation, CqRecordType>();
451    
452    /**
453     * Verifies that the operation in the input is consistent with the previous
454     * operations used.
455     * 
456     * @param oldOp The previous Operation used; may be null if this is first
457     *            use of an Operation.
458     * @param newOp The current Operation in the input.
459     * @return The newOp.
460     * @throws RuntimeException if the newOp is not compatible with the oldOp.
461     */
462    private static Operation checkOp(Operation oldOp, Operation newOp)
463    {
464        if (oldOp != null && !oldOp.equals(newOp))
465            throw new RuntimeException("Unexpected operation '" + newOp
466                + "' in filtering expression");
467        
468        return newOp;
469    }
470
471    /**
472     * Parses a single filter leaf specification and construct the corresponding
473     * FilterLeaf structure for it. The specification is in the form
474     * <i>field-path</i>[op]<i>target-list</i> Example specifications are...
475     * 
476     * <pre>
477     *     owner[eq][user]
478     *     date[between]10/26/94[and]10/24/95
479     *     submit_date[outside]8-feb-02[and]10-mar-02
480     *     owner.name[unlike]Fred
481     *     State[is]Submitted[or]Closed
482     *     Severity[isnt]High[or]Low
483     *     Description[null]
484     *     Priority[set]
485     * </pre>
486     * 
487     * @param query The Query proxy for the query that will be using the filter
488     * @param image String containing the filter leaf expression.
489     * 
490     * @return A Query.FilterLeaf structure for the filter leaf expression
491     * 
492     * @throws WvcmException If the necessary information is not available or
493     *             attainable from the server.
494     */
495    protected static CqQuery.FilterLeaf parseFilterLeaf(CqQuery query, 
496                                                String image)
497        throws WvcmException
498    {
499        Tokenizer token = new Tokenizer(image);
500        FilterSym<TargetType> field = token.nextTarget();
501        FilterSym<Operation> op = token.nextOperation();
502        ArrayList<FilterSym<TargetType>> targets = new ArrayList<FilterSym<TargetType>>();
503    
504        switch (op.kind) {
505        case NILARY: // field op
506            token.next(END_MASK);
507            break;
508    
509        case UNARY: // field op target
510            targets.add(token.nextTarget());
511            token.next(END_MASK);
512            break;
513    
514        case BINARY: // field op target [and] target
515            targets.add(token.nextTarget());
516            token.next(SEPARATOR_MASK, Operation.CONJUNCTION);
517            targets.add(token.nextTarget());
518            token.next(END_MASK);
519            break;
520    
521        case VARIADIC: // field op target [or] target [or] target ...
522            targets.add(token.nextTarget());
523    
524            for (;;) {
525                if (token.next(EnumSet.of(Kind.SEPARATOR, Kind.END),
526                               Operation.DISJUNCTION) == null) {
527                    break;
528                }
529    
530                targets.add(token.nextTarget());
531            }
532    
533            break;
534        }
535    
536        CqFieldDefinition[] path = buildFieldPath(query, field.symbol);
537        CqQuery.FilterLeaf result = query.cqProvider().buildFilterLeaf(path, op.code);
538    
539        for (FilterSym<TargetType> target: targets)
540            result.addTarget(target.code, target.symbol);
541    
542        return result;
543    }
544    
545    /**
546     * Parses the specification for the parameter value that matches a dynamic
547     * filter.
548     * 
549     * @param query The query for which the parameter is being specified
550     * @param n The index of the dynamic filter in the query's dynamic filter
551     *            list.
552     * @param param The parameter value specification, which is a filter leaf
553     *            specification without the field path and optionally without
554     *            the operator, these being defaulted from the dynamic filter.
555     *            Must not be <b>null</b>, but may be "" to indicate no value
556     *            is to be supplied for the dynamic filter
557     * @return A FilterLeaf object representing the parameter operation and 
558     * values to be used for the n-th dynamic filter of the query as specified
559     * by the parameter string.
560     * @throws WvcmException
561     */
562    protected static CqQuery.FilterLeaf parseDynamicFilter(CqQuery query, 
563                                                   int n, 
564                                                   String param)
565    throws WvcmException
566    {
567        if (param.equals(""))
568            return null;
569        
570        CqQuery.FilterLeaf filter = query.getDynamicFilters()[n];
571        
572        if (!param.startsWith("[")) {
573            Operation op = filter.getOperation();
574            
575            for (int i=0; i < symbol.length; ++i)
576                if (symbol[i].code == op) {
577                    param = symbol[i].symbol + param;
578                    break;
579                }
580        }
581        
582        return parseFilterLeaf(query, filter.getSourceName() + param);
583    }
584
585    /**
586     * Parses a filtering expression, builds a Filter structure for it, and
587     * defines that as the value of the query's FILTERING property. The
588     * filtering expression must conform to the following grammar for a filter
589     * 
590     * <pre>
591     * filter ::= filterLeaf
592     * filter ::= [(] filter [)]
593     * filter ::= filter ([or] filter)+
594     * filter ::= filter ([and] filter)+
595     * </pre>
596     * 
597     * Examples
598     * <ul>
599     * <li>name[eq]Fred
600     * <li>name[eq]Fred [and] owner[ne]George
601     * <li>name[eq]Fred [or] owner[ne]George
602     * <li>name[eq]Fred [and] status[isnt]fired[or]dead [and] owner[ne]George
603     * <li>name[eq]Fred [or] submit_date[between]1/1/2001[and]3/3/2003 [or]
604     * owner[ne]George
605     * <li>name[eq]Fred [or] [(] submit_date[between]1/1/2001[and]3/3/2003
606     * [and] owner[ne]George [)]
607     * <li>name[eq]Fred [or] [(] submit_date[between]1/1/2001[and]3/3/2003
608     * [and] reason[null] [)] [or] owner[ne]George]
609     * <li>[(] submit_date[between]1/1/2001[and]3/3/2003 [and] reason[null] [)]
610     * [or] name[eq]Fred [or] owner[ne]George]
611     * </ul>
612     * 
613     * @param query The CqQuery proxy in which the FILTERING property is to be
614     *            defined. This proxy must define the PRIMARY_RESOURCE property
615     *            using a CqRecordType proxy that defines the FIELD_DEFINITIONS 
616     *            property.
617     * @param filterItems An Iterator over String objects producing the terms of
618     *            the filtering expression in order left to right, where a term
619     *            is one of the elements, <code>filterLeaf</code>,
620     *            <code>[(]</code>, <code>[)]</code>, <code>[and]</code>,
621     *            or <code>[or]</code> in the above grammar
622     * @return A Filter object representing the specified filtering expression
623     * @throws WvcmException
624     */
625    protected static CqQuery.Filter parseFilter(CqQuery query, Iterator filterItems)
626        throws WvcmException
627    {
628        List<CqQuery.Filter> operands = new ArrayList<CqQuery.Filter>();
629        Operation op = null;
630
631        String term = (String) filterItems.next();
632
633        if (term.equals("[(]")) {
634            operands.add(parseFilter(query, filterItems));
635        } else
636            operands.add(parseFilterLeaf(query, term));
637        
638        while (filterItems.hasNext()) {
639            term = (String)filterItems.next();
640            
641            if (term.equals("[and]")) {
642                op = checkOp(op, Operation.CONJUNCTION);
643            } else if (term.equals("[or]")) {
644                op = checkOp(op, Operation.DISJUNCTION);
645            } else if (term.equals("[)]")){
646                break;
647            } else
648                throw new IllegalArgumentException("Unexpected token '" + term + "'");
649
650            operands.add(parseFilter(query, filterItems));
651        }
652        
653        if (op == null)
654            return (CqQuery.Filter) operands.get(0);
655        
656        return query.cqProvider().buildFilterNode(op, (CqQuery.Filter[]) operands
657            .toArray(new CqQuery.Filter[operands.size()]));
658    }
659
660    /**
661     * Parses a specification for a query display field and constructs a
662     * DisplayField object to represent it. The specification takes the general
663     * form:<br>
664     * <br>
665     * <b>[</b><i>sort-key-position</i><b>]</b><i>field-path</i><b>{</b><i>label</i><b>}</b><br>
666     * <br>
667     * Both <b>[</b><i>sort-key-position</i><b>]</b> and the {</b><i>label</i><b>}</b>
668     * are optional. The first position in the sort key is index 1. The index
669     * may be preceded by <b>D</b> to indicate a descending sort. Enclose the
670     * entire specification in square brackets (<b>[]</b>)to make the field not visible.
671     * 
672     * @param query A CqQuery proxy for the query for which the DisplayField is
673     *            to be constructed. Must define the properties required by
674     *            buildFieldPath.
675     * @param fieldSpec A String containing the specification for one display
676     *            field of a query.
677     * @return A DisplayField object implementing the String specification.
678     * @throws WvcmException If the necessary information is not available or
679     *             obtainable from the server.
680     */
681    protected static DisplayField parseDisplayField(CqQuery query,
682                                                    String fieldSpec)
683        throws WvcmException
684    {
685        boolean isVisible = true;
686        long sortOrder = 0;
687        SortType sortType = SortType.NO_SORT;
688        String label = null;
689
690        // [field] => invisible field
691        if (fieldSpec.startsWith("[")
692            && fieldSpec.endsWith("]")) {
693            isVisible = false;
694            fieldSpec = fieldSpec.substring(1, fieldSpec.length() - 1);
695        }
696
697        // [n]field ==> position n in sort key; [Dn]field ==> descending sort
698        if (fieldSpec.startsWith("[")) {
699            int rb = fieldSpec.indexOf("]");
700            String sortOrderSpec = fieldSpec.substring(1, rb);
701
702            fieldSpec = fieldSpec.substring(rb + 1);
703
704            if (sortOrderSpec.startsWith("D")) {
705                sortType = CqQuery.DisplayField.SortType.DESCENDING;
706                sortOrderSpec = sortOrderSpec.substring(1);
707            } else {
708                sortType = CqQuery.DisplayField.SortType.ASCENDING;
709            }
710
711            try {
712                sortOrder = Integer.parseInt(sortOrderSpec);
713            } catch (Exception ex) {
714                throw new IllegalArgumentException("Cannot interpret '"
715                    + sortOrderSpec + "' as a sort specification in '"
716                    + fieldSpec + "'");
717            }
718        }
719
720        // field{label} ==> alternate specification for field label
721        if (fieldSpec.endsWith("}")) {
722            int lb = fieldSpec.lastIndexOf("{");
723
724            label = fieldSpec.substring(lb + 1, fieldSpec.length() - 1);
725            fieldSpec = fieldSpec.substring(0, lb);
726        } else {
727            label = fieldSpec;
728        }
729
730        CqQuery.DisplayField field = query.cqProvider().buildDisplayField();
731
732        field.setIsVisible(isVisible);
733        field.setSortOrder(sortOrder);
734        field.setSortType(sortType);
735        field.setLabel(label);
736        field.setPath(buildFieldPath(query, fieldSpec));
737
738        // TODO Aggregates
739        // TODO Functions
740        // TODO Group By
741
742        return field;
743    }
744
745   /** 
746    * Properties needed from a record type of a database 
747    * (without its field definitions) 
748    */
749   private static final PropertyRequest RECORD_TYPE_WITHOUT_FIELDS = 
750       new PropertyRequest(
751            CqRecordType.DISPLAY_NAME,
752            CqRecordType.USER_FRIENDLY_LOCATION);
753
754   /** CqFieldDefinition properties we will be needing */
755   private static final PropertyRequest FIELD_DEFINITION_PROPERTIES = 
756       new PropertyRequest(
757            CqFieldDefinition.DISPLAY_NAME,
758            CqFieldDefinition.RECORD_TYPE.nest(RECORD_TYPE_WITHOUT_FIELDS),
759            CqFieldDefinition.VALUE_TYPE,
760            CqFieldDefinition.FIELD_TYPE,
761            CqFieldDefinition.REFERENCED_RECORD_TYPE
762                .nest(RECORD_TYPE_WITHOUT_FIELDS),
763            CqFieldDefinition.USER_FRIENDLY_LOCATION);
764
765   /** Properties needed from a record type of a database */
766   protected static final PropertyRequest RECORD_TYPE_WITH_FIELDS =
767       new PropertyRequest(
768            CqRecordType.DISPLAY_NAME,
769            CqRecordType.USER_FRIENDLY_LOCATION,
770            CqRecordType.FIELD_DEFINITIONS.nest(FIELD_DEFINITION_PROPERTIES)
771        );
772}