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 }