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 * © 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 }