001 /* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at 010 * trunk/opends/resource/legal-notices/OpenDS.LICENSE 011 * or https://OpenDS.dev.java.net/OpenDS.LICENSE. 012 * See the License for the specific language governing permissions 013 * and limitations under the License. 014 * 015 * When distributing Covered Code, include this CDDL HEADER in each 016 * file and include the License file at 017 * trunk/opends/resource/legal-notices/OpenDS.LICENSE. If applicable, 018 * add the following below this CDDL HEADER, with the fields enclosed 019 * by brackets "[]" replaced with your own identifying information: 020 * Portions Copyright [yyyy] [name of copyright owner] 021 * 022 * CDDL HEADER END 023 * 024 * 025 * Copyright 2006-2008 Sun Microsystems, Inc. 026 */ 027 package org.opends.server.types; 028 029 030 031 import java.util.Iterator; 032 import java.util.LinkedHashMap; 033 import java.util.LinkedHashSet; 034 import java.util.LinkedList; 035 import java.util.List; 036 import java.util.Map; 037 import java.util.Set; 038 039 import org.opends.server.schema.DITContentRuleSyntax; 040 041 import static org.opends.server.loggers.debug.DebugLogger.*; 042 import org.opends.server.loggers.debug.DebugTracer; 043 import static org.opends.server.util.ServerConstants.*; 044 import static org.opends.server.util.Validator.*; 045 046 047 048 /** 049 * This class defines a DIT content rule, which defines the set of 050 * allowed, required, and prohibited attributes for entries with a 051 * given structural objectclass, and also indicates which auxiliary 052 * classes that may be included in the entry. 053 */ 054 @org.opends.server.types.PublicAPI( 055 stability=org.opends.server.types.StabilityLevel.UNCOMMITTED, 056 mayInstantiate=false, 057 mayExtend=false, 058 mayInvoke=true) 059 public final class DITContentRule 060 implements SchemaFileElement 061 { 062 /** 063 * The tracer object for the debug logger. 064 */ 065 private static final DebugTracer TRACER = getTracer(); 066 067 // Indicates whether this content rule is declared "obsolete". 068 private final boolean isObsolete; 069 070 // The set of additional name-value pairs associated with this 071 // content rule definition. 072 private final Map<String,List<String>> extraProperties; 073 074 // The set of names for this DIT content rule, in a mapping between 075 // the all-lowercase form and the user-defined form. 076 private final Map<String,String> names; 077 078 // The structural objectclass for this DIT content rule. 079 private final ObjectClass structuralClass; 080 081 // The set of auxiliary objectclasses that entries with this content 082 // rule may contain, in a mapping between the objectclass and the 083 // user-defined name for that class. 084 private final Set<ObjectClass> auxiliaryClasses; 085 086 // The set of optional attribute types for this DIT content rule. 087 private final Set<AttributeType> optionalAttributes; 088 089 // The set of prohibited attribute types for this DIT content rule. 090 private final Set<AttributeType> prohibitedAttributes; 091 092 // The set of required attribute types for this DIT content rule. 093 private final Set<AttributeType> requiredAttributes; 094 095 // The definition string used to create this DIT content rule. 096 private final String definition; 097 098 // The description for this DIT content rule. 099 private final String description; 100 101 102 103 /** 104 * Creates a new DIT content rule definition with the provided 105 * information. 106 * 107 * @param definition The definition string used to 108 * create this DIT content rule. It 109 * must not be {@code null}. 110 * @param structuralClass The structural objectclass for this 111 * DIT content rule. It must not be 112 * {@code null}. 113 * @param names The set of names that may be used 114 * to reference this DIT content rule. 115 * @param description The description for this DIT 116 * content rule. 117 * @param auxiliaryClasses The set of auxiliary classes for 118 * this DIT content rule 119 * @param requiredAttributes The set of required attribute types 120 * for this DIT content rule. 121 * @param optionalAttributes The set of optional attribute types 122 * for this DIT content rule. 123 * @param prohibitedAttributes The set of prohibited attribute 124 * types for this DIT content rule. 125 * @param isObsolete Indicates whether this DIT content 126 * rule is declared "obsolete". 127 * @param extraProperties A set of extra properties for this 128 * DIT content rule. 129 */ 130 public DITContentRule(String definition, 131 ObjectClass structuralClass, 132 Map<String,String> names, String description, 133 Set<ObjectClass> auxiliaryClasses, 134 Set<AttributeType> requiredAttributes, 135 Set<AttributeType> optionalAttributes, 136 Set<AttributeType> prohibitedAttributes, 137 boolean isObsolete, 138 Map<String,List<String>> extraProperties) 139 { 140 ensureNotNull(definition, structuralClass); 141 142 this.structuralClass = structuralClass; 143 this.description = description; 144 this.isObsolete = isObsolete; 145 146 int schemaFilePos = definition.indexOf(SCHEMA_PROPERTY_FILENAME); 147 if (schemaFilePos > 0) 148 { 149 String defStr; 150 try 151 { 152 int firstQuotePos = definition.indexOf('\'', schemaFilePos); 153 int secondQuotePos = definition.indexOf('\'', 154 firstQuotePos+1); 155 156 defStr = definition.substring(0, schemaFilePos).trim() + " " + 157 definition.substring(secondQuotePos+1).trim(); 158 } 159 catch (Exception e) 160 { 161 if (debugEnabled()) 162 { 163 TRACER.debugCaught(DebugLogLevel.ERROR, e); 164 } 165 166 defStr = definition; 167 } 168 169 this.definition = defStr; 170 } 171 else 172 { 173 this.definition = definition; 174 } 175 176 if ((names == null) || names.isEmpty()) 177 { 178 this.names = new LinkedHashMap<String,String>(0); 179 } 180 else 181 { 182 this.names = new LinkedHashMap<String,String>(names); 183 } 184 185 if ((auxiliaryClasses == null) || auxiliaryClasses.isEmpty()) 186 { 187 this.auxiliaryClasses = new LinkedHashSet<ObjectClass>(0); 188 } 189 else 190 { 191 this.auxiliaryClasses = 192 new LinkedHashSet<ObjectClass>(auxiliaryClasses); 193 } 194 195 if ((requiredAttributes == null) || requiredAttributes.isEmpty()) 196 { 197 this.requiredAttributes = new LinkedHashSet<AttributeType>(0); 198 } 199 else 200 { 201 this.requiredAttributes = 202 new LinkedHashSet<AttributeType>(requiredAttributes); 203 } 204 205 if ((optionalAttributes == null) || optionalAttributes.isEmpty()) 206 { 207 this.optionalAttributes = new LinkedHashSet<AttributeType>(0); 208 } 209 else 210 { 211 this.optionalAttributes = 212 new LinkedHashSet<AttributeType>(optionalAttributes); 213 } 214 215 if ((prohibitedAttributes == null) || 216 prohibitedAttributes.isEmpty()) 217 { 218 this.prohibitedAttributes = new LinkedHashSet<AttributeType>(0); 219 } 220 else 221 { 222 this.prohibitedAttributes = 223 new LinkedHashSet<AttributeType>(prohibitedAttributes); 224 } 225 226 if ((extraProperties == null) || extraProperties.isEmpty()) 227 { 228 this.extraProperties = 229 new LinkedHashMap<String,List<String>>(0); 230 } 231 else 232 { 233 this.extraProperties = 234 new LinkedHashMap<String,List<String>>(extraProperties); 235 } 236 } 237 238 239 240 /** 241 * Retrieves the definition string used to create this DIT content 242 * rule. 243 * 244 * @return The definition string used to create this DIT content 245 * rule. 246 */ 247 public String getDefinition() 248 { 249 return definition; 250 } 251 252 253 254 /** 255 * Creates a new instance of this DIT content rule based on the 256 * definition string. It will also preserve other state information 257 * associated with this DIT content rule that is not included in the 258 * definition string (e.g., the name of the schema file with which 259 * it is associated). 260 * 261 * @return The new instance of this DIT content rule based on the 262 * definition string. 263 * 264 * @throws DirectoryException If a problem occurs while attempting 265 * to create a new DIT content rule 266 * instance from the definition string. 267 */ 268 public DITContentRule recreateFromDefinition() 269 throws DirectoryException 270 { 271 ByteString value = ByteStringFactory.create(definition); 272 Schema schema = DirectoryConfig.getSchema(); 273 274 DITContentRule dcr = 275 DITContentRuleSyntax.decodeDITContentRule(value, schema, 276 false); 277 dcr.setSchemaFile(getSchemaFile()); 278 279 return dcr; 280 } 281 282 283 284 /** 285 * Retrieves the structural objectclass for this DIT content rule. 286 * 287 * @return The structural objectclass for this DIT content rule. 288 */ 289 public ObjectClass getStructuralClass() 290 { 291 return structuralClass; 292 } 293 294 295 296 /** 297 * Retrieves the set of names that may be used to reference this DIT 298 * content rule. The returned object will be a mapping between each 299 * name in all lowercase characters and that name in a user-defined 300 * form (which may include mixed capitalization). 301 * 302 * @return The set of names that may be used to reference this DIT 303 * content rule. 304 */ 305 public Map<String,String> getNames() 306 { 307 return names; 308 } 309 310 311 312 /** 313 * Retrieves the primary name to use to reference this DIT content 314 * rule. 315 * 316 * @return The primary name to use to reference this DIT content 317 * rule, or {@code null} if there is none. 318 */ 319 public String getName() 320 { 321 if (names.isEmpty()) 322 { 323 return null; 324 } 325 else 326 { 327 return names.values().iterator().next(); 328 } 329 } 330 331 332 333 /** 334 * Indicates whether the provided lowercase name may be used to 335 * reference this DIT content rule. 336 * 337 * @param lowerName The name for which to make the determination, 338 * in all lowercase characters. 339 * 340 * @return {@code true} if the provided lowercase name may be used 341 * to reference this DIT content rule, or {@code false} if 342 * not. 343 */ 344 public boolean hasName(String lowerName) 345 { 346 return names.containsKey(lowerName); 347 } 348 349 350 351 /** 352 * Retrieves the name of the schema file that contains the 353 * definition for this DIT content rule. 354 * 355 * @return The name of the schema file that contains the definition 356 * for this DIT content rule, or {@code null} if it is not 357 * known or if it is not stored in any schema file. 358 */ 359 public String getSchemaFile() 360 { 361 List<String> values = 362 extraProperties.get(SCHEMA_PROPERTY_FILENAME); 363 if ((values == null) || values.isEmpty()) 364 { 365 return null; 366 } 367 368 return values.get(0); 369 } 370 371 372 373 /** 374 * Specifies the name of the schema file that contains the 375 * definition for this DIT content rule. 376 * 377 * @param schemaFile The name of the schema file that contains the 378 * definition for this DIT content rule. 379 */ 380 public void setSchemaFile(String schemaFile) 381 { 382 setExtraProperty(SCHEMA_PROPERTY_FILENAME, schemaFile); 383 } 384 385 386 387 /** 388 * Retrieves the description for this DIT content rule. 389 * 390 * @return The description for this DIT content rule, or 391 * {@code null} if there is none. 392 */ 393 public String getDescription() 394 { 395 return description; 396 } 397 398 399 400 /** 401 * Retrieves the set of auxiliary objectclasses that may be used for 402 * entries associated with this DIT content rule. 403 * 404 * @return The set of auxiliary objectclasses that may be used for 405 * entries associated with this DIT content rule. 406 */ 407 public Set<ObjectClass> getAuxiliaryClasses() 408 { 409 return auxiliaryClasses; 410 } 411 412 413 414 /** 415 * Indicates whether the provided auxiliary objectclass is allowed 416 * for use by this DIT content rule. 417 * 418 * @param auxiliaryClass The auxiliary objectclass for which to 419 * make the determination. 420 * 421 * @return {@code true} if the provided auxiliary objectclass is 422 * allowed for use by this DIT content rule, or 423 * {@code false} if not. 424 */ 425 public boolean isAllowedAuxiliaryClass(ObjectClass auxiliaryClass) 426 { 427 return auxiliaryClasses.contains(auxiliaryClass); 428 } 429 430 431 432 /** 433 * Retrieves the set of required attributes for this DIT content 434 * rule. 435 * 436 * @return The set of required attributes for this DIT content 437 * rule. 438 */ 439 public Set<AttributeType> getRequiredAttributes() 440 { 441 return requiredAttributes; 442 } 443 444 445 446 /** 447 * Indicates whether the provided attribute type is included in the 448 * required attribute list for this DIT content rule. 449 * 450 * @param attributeType The attribute type for which to make the 451 * determination. 452 * 453 * @return {@code true} if the provided attribute type is required 454 * by this DIT content rule, or {@code false} if not. 455 */ 456 public boolean isRequired(AttributeType attributeType) 457 { 458 return requiredAttributes.contains(attributeType); 459 } 460 461 462 463 /** 464 * Retrieves the set of optional attributes for this DIT content 465 * rule. 466 * 467 * @return The set of optional attributes for this DIT content 468 * rule. 469 */ 470 public Set<AttributeType> getOptionalAttributes() 471 { 472 return optionalAttributes; 473 } 474 475 476 477 /** 478 * Indicates whether the provided attribute type is included in the 479 * optional attribute list for this DIT content rule. 480 * 481 * @param attributeType The attribute type for which to make the 482 * determination. 483 * 484 * @return {@code true} if the provided attribute type is optional 485 * for this DIT content rule, or {@code false} if not. 486 */ 487 public boolean isOptional(AttributeType attributeType) 488 { 489 return optionalAttributes.contains(attributeType); 490 } 491 492 493 494 /** 495 * Indicates whether the provided attribute type is in the list of 496 * required or optional attributes for this DIT content rule. 497 * 498 * @param attributeType The attribute type for which to make the 499 * determination. 500 * 501 * @return {@code true} if the provided attribute type is required 502 * or allowed for this DIT content rule, or {@code false} 503 * if it is not. 504 */ 505 public boolean isRequiredOrOptional(AttributeType attributeType) 506 { 507 return (requiredAttributes.contains(attributeType) || 508 optionalAttributes.contains(attributeType)); 509 } 510 511 512 513 /** 514 * Indicates whether the provided attribute type is in the list of 515 * required or optional attributes for this DIT content rule. 516 * 517 * @param attributeType The attribute type for which to make the 518 * determination. 519 * @param acceptEmpty Indicates whether an empty list of 520 * required or optional attributes should be 521 * taken to indicate that all attributes 522 * allowed for an objectclass will be 523 * acceptable. 524 * 525 * @return {@code true} if the provided attribute type is required 526 * or allowed for this DIT content rule, or {@code false} 527 * if it is not. 528 */ 529 public boolean isRequiredOrOptional(AttributeType attributeType, 530 boolean acceptEmpty) 531 { 532 if (acceptEmpty && 533 (requiredAttributes.isEmpty() || 534 optionalAttributes.isEmpty())) 535 { 536 return true; 537 } 538 539 return (requiredAttributes.contains(attributeType) || 540 optionalAttributes.contains(attributeType)); 541 } 542 543 544 545 /** 546 * Retrieves the set of prohibited attributes for this DIT content 547 * rule. 548 * 549 * @return The set of prohibited attributes for this DIT content 550 * rule. 551 */ 552 public Set<AttributeType> getProhibitedAttributes() 553 { 554 return prohibitedAttributes; 555 } 556 557 558 559 /** 560 * Indicates whether the provided attribute type is included in the 561 * prohibited attribute list for this DIT content rule. 562 * 563 * @param attributeType The attribute type for which to make the 564 * determination. 565 * 566 * @return {@code true} if the provided attribute type is 567 * prohibited for this DIT content rule, or {@code false} 568 * if not. 569 */ 570 public boolean isProhibited(AttributeType attributeType) 571 { 572 return prohibitedAttributes.contains(attributeType); 573 } 574 575 576 577 /** 578 * Indicates whether this DIT content rule is declared "obsolete". 579 * 580 * @return {@code true} if this DIT content rule is declared 581 * "obsolete", or {@code false} if it is not. 582 */ 583 public boolean isObsolete() 584 { 585 return isObsolete; 586 } 587 588 589 590 /** 591 * Retrieves a mapping between the names of any extra non-standard 592 * properties that may be associated with this DIT content rule and 593 * the value for that property. 594 * 595 * @return A mapping between the names of any extra non-standard 596 * properties that may be associated with this DIT content 597 * rule and the value for that property. 598 */ 599 public Map<String,List<String>> getExtraProperties() 600 { 601 return extraProperties; 602 } 603 604 605 606 /** 607 * Retrieves the value of the specified "extra" property for this 608 * DIT content rule. 609 * 610 * @param propertyName The name of the "extra" property for which 611 * to retrieve the value. 612 * 613 * @return The value of the specified "extra" property for this DIT 614 * content rule, or {@code null} if no such property is 615 * defined. 616 */ 617 public List<String> getExtraProperty(String propertyName) 618 { 619 return extraProperties.get(propertyName); 620 } 621 622 623 624 /** 625 * Specifies the provided "extra" property for this DIT content 626 * rule. 627 * 628 * @param name The name for the "extra" property. It must not be 629 * {@code null}. 630 * @param value The value for the "extra" property, or 631 * {@code null} if the property is to be removed. 632 */ 633 public void setExtraProperty(String name, String value) 634 { 635 ensureNotNull(name); 636 637 if (value == null) 638 { 639 extraProperties.remove(name); 640 } 641 else 642 { 643 LinkedList<String> values = new LinkedList<String>(); 644 values.add(value); 645 646 extraProperties.put(name, values); 647 } 648 } 649 650 651 652 /** 653 * Specifies the provided "extra" property for this DIT content 654 * rule. 655 * 656 * @param name The name for the "extra" property. It must not 657 * be {@code null}. 658 * @param values The set of value for the "extra" property, or 659 * {@code null} if the property is to be removed. 660 */ 661 public void setExtraProperty(String name, List<String> values) 662 { 663 ensureNotNull(name); 664 665 if ((values == null) || values.isEmpty()) 666 { 667 extraProperties.remove(name); 668 } 669 else 670 { 671 LinkedList<String> valuesCopy = new LinkedList<String>(values); 672 extraProperties.put(name, valuesCopy); 673 } 674 } 675 676 677 678 /** 679 * Indicates whether the provided object is equal to this DIT 680 * content rule. The object will be considered equal if it is a DIT 681 * content rule for the same structural objectclass and the same 682 * sets of names. For performance reasons, the set of auxiliary 683 * classes, and the sets of required, optional, and prohibited 684 * attribute types will not be checked, so that should be done 685 * manually if a more thorough equality comparison is required. 686 * 687 * @param o The object for which to make the determination. 688 * 689 * @return {@code true} if the provided object is equal to 690 * this DIT content rule, or {@code false} if not. 691 */ 692 public boolean equals(Object o) 693 { 694 if (this == o) 695 { 696 return true; 697 } 698 699 if ((o == null) || (! (o instanceof DITContentRule))) 700 { 701 return false; 702 } 703 704 DITContentRule dcr = (DITContentRule) o; 705 if (! structuralClass.equals(dcr.structuralClass)) 706 { 707 return false; 708 } 709 710 if (names.size() != dcr.names.size()) 711 { 712 return false; 713 } 714 715 Iterator<String> iterator = names.keySet().iterator(); 716 while (iterator.hasNext()) 717 { 718 if (! dcr.names.containsKey(iterator.next())) 719 { 720 return false; 721 } 722 } 723 724 return true; 725 } 726 727 728 729 /** 730 * Retrieves the hash code for this DIT content rule. It will be 731 * equal to the hash code for the associated structural objectclass. 732 * 733 * @return The hash code for this DIT content rule. 734 */ 735 public int hashCode() 736 { 737 return structuralClass.hashCode(); 738 } 739 740 741 742 /** 743 * Retrieves the string representation of this DIT content rule in 744 * the form specified in RFC 2252. 745 * 746 * @return The string representation of this DIT content rule in 747 * the form specified in RFC 2252. 748 */ 749 public String toString() 750 { 751 StringBuilder buffer = new StringBuilder(); 752 toString(buffer, true); 753 return buffer.toString(); 754 } 755 756 757 758 /** 759 * Appends a string representation of this attribute type in the 760 * form specified in RFC 2252 to the provided buffer. 761 * 762 * @param buffer The buffer to which the information 763 * should be appended. 764 * @param includeFileElement Indicates whether to include an 765 * "extra" property that specifies the 766 * path to the schema file from which 767 * this DIT content rule was loaded. 768 */ 769 public void toString(StringBuilder buffer, 770 boolean includeFileElement) 771 { 772 buffer.append("( "); 773 buffer.append(structuralClass.getOID()); 774 775 if (! names.isEmpty()) 776 { 777 Iterator<String> iterator = names.values().iterator(); 778 779 String firstName = iterator.next(); 780 if (iterator.hasNext()) 781 { 782 buffer.append(" NAME ( '"); 783 buffer.append(firstName); 784 785 while (iterator.hasNext()) 786 { 787 buffer.append("' '"); 788 buffer.append(iterator.next()); 789 } 790 791 buffer.append("' )"); 792 } 793 else 794 { 795 buffer.append(" NAME '"); 796 buffer.append(firstName); 797 buffer.append("'"); 798 } 799 } 800 801 if ((description != null) && (description.length() > 0)) 802 { 803 buffer.append(" DESC '"); 804 buffer.append(description); 805 buffer.append("'"); 806 } 807 808 if (isObsolete) 809 { 810 buffer.append(" OBSOLETE"); 811 } 812 813 if (! auxiliaryClasses.isEmpty()) 814 { 815 Iterator<ObjectClass> iterator = auxiliaryClasses.iterator(); 816 817 String firstClass = iterator.next().getNameOrOID(); 818 if (iterator.hasNext()) 819 { 820 buffer.append(" AUX ("); 821 buffer.append(firstClass); 822 823 while (iterator.hasNext()) 824 { 825 buffer.append(" $ "); 826 buffer.append(iterator.next()); 827 } 828 829 buffer.append(" )"); 830 } 831 else 832 { 833 buffer.append(" AUX "); 834 buffer.append(firstClass); 835 } 836 } 837 838 if (! requiredAttributes.isEmpty()) 839 { 840 Iterator<AttributeType> iterator = 841 requiredAttributes.iterator(); 842 843 String firstName = iterator.next().getNameOrOID(); 844 if (iterator.hasNext()) 845 { 846 buffer.append(" MUST ( "); 847 buffer.append(firstName); 848 849 while (iterator.hasNext()) 850 { 851 buffer.append(" $ "); 852 buffer.append(iterator.next().getNameOrOID()); 853 } 854 855 buffer.append(" )"); 856 } 857 else 858 { 859 buffer.append(" MUST "); 860 buffer.append(firstName); 861 } 862 } 863 864 if (! optionalAttributes.isEmpty()) 865 { 866 Iterator<AttributeType> iterator = 867 optionalAttributes.iterator(); 868 869 String firstName = iterator.next().getNameOrOID(); 870 if (iterator.hasNext()) 871 { 872 buffer.append(" MAY ( "); 873 buffer.append(firstName); 874 875 while (iterator.hasNext()) 876 { 877 buffer.append(" $ "); 878 buffer.append(iterator.next().getNameOrOID()); 879 } 880 881 buffer.append(" )"); 882 } 883 else 884 { 885 buffer.append(" MAY "); 886 buffer.append(firstName); 887 } 888 } 889 890 if (! prohibitedAttributes.isEmpty()) 891 { 892 Iterator<AttributeType> iterator = 893 prohibitedAttributes.iterator(); 894 895 String firstName = iterator.next().getNameOrOID(); 896 if (iterator.hasNext()) 897 { 898 buffer.append(" NOT ( "); 899 buffer.append(firstName); 900 901 while (iterator.hasNext()) 902 { 903 buffer.append(" $ "); 904 buffer.append(iterator.next().getNameOrOID()); 905 } 906 907 buffer.append(" )"); 908 } 909 else 910 { 911 buffer.append(" NOT "); 912 buffer.append(firstName); 913 } 914 } 915 916 if (! extraProperties.isEmpty()) 917 { 918 for (String property : extraProperties.keySet()) 919 { 920 if ((! includeFileElement) && 921 property.equals(SCHEMA_PROPERTY_FILENAME)) 922 { 923 continue; 924 } 925 926 List<String> valueList = extraProperties.get(property); 927 928 buffer.append(" "); 929 buffer.append(property); 930 931 if (valueList.size() == 1) 932 { 933 buffer.append(" '"); 934 buffer.append(valueList.get(0)); 935 buffer.append("'"); 936 } 937 else 938 { 939 buffer.append(" ( "); 940 941 for (String value : valueList) 942 { 943 buffer.append("'"); 944 buffer.append(value); 945 buffer.append("' "); 946 } 947 948 buffer.append(")"); 949 } 950 } 951 } 952 953 buffer.append(" )"); 954 } 955 } 956