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     */
015    package com.ibm.rational.stp.client.samples;
016    
017    import java.util.ArrayList;
018    import java.util.EnumSet;
019    import java.util.HashMap;
020    import java.util.Iterator;
021    import java.util.List;
022    
023    import javax.wvcm.ResourceList;
024    import javax.wvcm.WvcmException;
025    import javax.wvcm.PropertyRequestItem.PropertyRequest;
026    
027    import com.ibm.rational.wvcm.stp.StpException;
028    import com.ibm.rational.wvcm.stp.StpLocation;
029    import com.ibm.rational.wvcm.stp.cq.CqFieldDefinition;
030    import com.ibm.rational.wvcm.stp.cq.CqFieldValue;
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.CqQuery.DisplayField;
034    import com.ibm.rational.wvcm.stp.cq.CqQuery.DisplayField.SortType;
035    import com.ibm.rational.wvcm.stp.cq.CqQuery.Filter.Operation;
036    import com.ibm.rational.wvcm.stp.cq.CqQuery.FilterLeaf.TargetType;
037    
038    /**
039     * A CmdBaseQuery
040     */
041    public 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    }