001/* 002 * The contents of this file are subject to the terms of the Common Development and 003 * Distribution License (the License). You may not use this file except in compliance with the 004 * License. 005 * 006 * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the 007 * specific language governing permission and limitations under the License. 008 * 009 * When distributing Covered Software, include this CDDL Header Notice in each file and include 010 * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL 011 * Header, with the fields enclosed by brackets [] replaced by your own identifying 012 * information: "Portions Copyright [year] [name of copyright owner]". 013 * 014 * Copyright 2009-2010 Sun Microsystems, Inc. 015 * Portions copyright 2011-2016 ForgeRock AS. 016 */ 017package org.forgerock.opendj.ldap; 018 019import java.util.Arrays; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.Iterator; 023import java.util.LinkedHashMap; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.Map; 027import java.util.SortedSet; 028import java.util.TreeSet; 029 030import org.forgerock.i18n.LocalizableMessage; 031import org.forgerock.i18n.LocalizedIllegalArgumentException; 032import org.forgerock.opendj.ldap.schema.AttributeType; 033import org.forgerock.opendj.ldap.schema.Schema; 034import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException; 035import org.forgerock.util.Pair; 036import org.forgerock.util.Reject; 037 038import com.forgerock.opendj.util.ASCIICharProp; 039import com.forgerock.opendj.util.Iterators; 040 041import static org.forgerock.opendj.ldap.schema.SchemaOptions.*; 042 043import static com.forgerock.opendj.ldap.CoreMessages.*; 044import static com.forgerock.opendj.util.StaticUtils.*; 045 046/** 047 * An attribute description as defined in RFC 4512 section 2.5. Attribute 048 * descriptions are used to identify an attribute in an entry and are composed 049 * of an attribute type and a set of zero or more attribute options. 050 * 051 * @see <a href="http://tools.ietf.org/html/rfc4512#section-2.5">RFC 4512 - 052 * Lightweight Directory Access Protocol (LDAP): Directory Information 053 * Models </a> 054 */ 055public final class AttributeDescription implements Comparable<AttributeDescription> { 056 private static abstract class Impl implements Iterable<String> { 057 protected Impl() { 058 // Nothing to do. 059 } 060 061 public abstract int compareTo(Impl other); 062 063 public abstract boolean hasOption(String normalizedOption); 064 065 public abstract boolean equals(Impl other); 066 067 public abstract String firstNormalizedOption(); 068 069 @Override 070 public abstract int hashCode(); 071 072 public abstract boolean hasOptions(); 073 074 public abstract boolean isSubTypeOf(Impl other); 075 076 public abstract boolean isSuperTypeOf(Impl other); 077 078 public abstract int size(); 079 } 080 081 private static final class MultiOptionImpl extends Impl { 082 083 private final String[] normalizedOptions; 084 private final String[] options; 085 086 private MultiOptionImpl(final String[] options, final String[] normalizedOptions) { 087 if (normalizedOptions.length < 2) { 088 throw new AssertionError(); 089 } 090 091 this.options = options; 092 this.normalizedOptions = normalizedOptions; 093 } 094 095 @Override 096 public int compareTo(final Impl other) { 097 final int thisSize = normalizedOptions.length; 098 final int otherSize = other.size(); 099 100 if (thisSize < otherSize) { 101 return -1; 102 } else if (thisSize > otherSize) { 103 return 1; 104 } else { 105 // Same number of options. 106 final MultiOptionImpl otherImpl = (MultiOptionImpl) other; 107 for (int i = 0; i < thisSize; i++) { 108 final String o1 = normalizedOptions[i]; 109 final String o2 = otherImpl.normalizedOptions[i]; 110 final int result = o1.compareTo(o2); 111 if (result != 0) { 112 return result; 113 } 114 } 115 116 // All options the same. 117 return 0; 118 } 119 } 120 121 @Override 122 public boolean hasOption(final String normalizedOption) { 123 final int sz = normalizedOptions.length; 124 for (int i = 0; i < sz; i++) { 125 if (normalizedOptions[i].equals(normalizedOption)) { 126 return true; 127 } 128 } 129 return false; 130 } 131 132 @Override 133 public boolean equals(final Impl other) { 134 if (other instanceof MultiOptionImpl) { 135 final MultiOptionImpl tmp = (MultiOptionImpl) other; 136 return Arrays.equals(normalizedOptions, tmp.normalizedOptions); 137 } else { 138 return false; 139 } 140 } 141 142 @Override 143 public String firstNormalizedOption() { 144 return normalizedOptions[0]; 145 } 146 147 @Override 148 public int hashCode() { 149 return Arrays.hashCode(normalizedOptions); 150 } 151 152 @Override 153 public boolean hasOptions() { 154 return true; 155 } 156 157 @Override 158 public boolean isSubTypeOf(final Impl other) { 159 // Must contain a super-set of other's options. 160 if (other == ZERO_OPTION_IMPL) { 161 return true; 162 } else if (other.size() == 1) { 163 return hasOption(other.firstNormalizedOption()); 164 } else if (other.size() > size()) { 165 return false; 166 } else { 167 // Check this contains other's options. 168 // This could be optimized more if required, but it's probably not worth it. 169 final MultiOptionImpl tmp = (MultiOptionImpl) other; 170 for (final String normalizedOption : tmp.normalizedOptions) { 171 if (!hasOption(normalizedOption)) { 172 return false; 173 } 174 } 175 return true; 176 } 177 } 178 179 @Override 180 public boolean isSuperTypeOf(final Impl other) { 181 // Must contain a sub-set of other's options. 182 for (final String normalizedOption : normalizedOptions) { 183 if (!other.hasOption(normalizedOption)) { 184 return false; 185 } 186 } 187 return true; 188 } 189 190 @Override 191 public Iterator<String> iterator() { 192 return Iterators.arrayIterator(options); 193 } 194 195 @Override 196 public int size() { 197 return normalizedOptions.length; 198 } 199 200 } 201 202 private static final class SingleOptionImpl extends Impl { 203 204 private final String normalizedOption; 205 private final String option; 206 207 private SingleOptionImpl(final String option, final String normalizedOption) { 208 this.option = option; 209 this.normalizedOption = normalizedOption; 210 } 211 212 @Override 213 public int compareTo(final Impl other) { 214 if (other == ZERO_OPTION_IMPL) { 215 // If other has zero options then this sorts after. 216 return 1; 217 } else if (other.size() == 1) { 218 // Same number of options, so compare. 219 return normalizedOption.compareTo(other.firstNormalizedOption()); 220 } else { 221 // Other has more options, so comes after. 222 return -1; 223 } 224 } 225 226 @Override 227 public boolean hasOption(final String normalizedOption) { 228 return this.normalizedOption.equals(normalizedOption); 229 } 230 231 @Override 232 public boolean equals(final Impl other) { 233 return other.size() == 1 && other.hasOption(normalizedOption); 234 } 235 236 @Override 237 public String firstNormalizedOption() { 238 return normalizedOption; 239 } 240 241 @Override 242 public int hashCode() { 243 return normalizedOption.hashCode(); 244 } 245 246 @Override 247 public boolean hasOptions() { 248 return true; 249 } 250 251 @Override 252 public boolean isSubTypeOf(final Impl other) { 253 // Other must have no options or the same option. 254 return other == ZERO_OPTION_IMPL || equals(other); 255 } 256 257 @Override 258 public boolean isSuperTypeOf(final Impl other) { 259 // Other must have this option. 260 return other.hasOption(normalizedOption); 261 } 262 263 @Override 264 public Iterator<String> iterator() { 265 return Iterators.singletonIterator(option); 266 } 267 268 @Override 269 public int size() { 270 return 1; 271 } 272 273 } 274 275 private static final class ZeroOptionImpl extends Impl { 276 private ZeroOptionImpl() { 277 // Nothing to do. 278 } 279 280 @Override 281 public int compareTo(final Impl other) { 282 // If other has options then this sorts before. 283 return this == other ? 0 : -1; 284 } 285 286 @Override 287 public boolean hasOption(final String normalizedOption) { 288 return false; 289 } 290 291 @Override 292 public boolean equals(final Impl other) { 293 return this == other; 294 } 295 296 @Override 297 public String firstNormalizedOption() { 298 // No first option. 299 return null; 300 } 301 302 @Override 303 public int hashCode() { 304 // Use attribute type hash code. 305 return 0; 306 } 307 308 @Override 309 public boolean hasOptions() { 310 return false; 311 } 312 313 @Override 314 public boolean isSubTypeOf(final Impl other) { 315 // Can only be a sub-type if other has no options. 316 return this == other; 317 } 318 319 @Override 320 public boolean isSuperTypeOf(final Impl other) { 321 // Will always be a super-type. 322 return true; 323 } 324 325 @Override 326 public Iterator<String> iterator() { 327 return Iterators.emptyIterator(); 328 } 329 330 @Override 331 public int size() { 332 return 0; 333 } 334 335 } 336 337 private static final ThreadLocal<Map<String, Pair<Schema, AttributeDescription>>> CACHE = 338 new ThreadLocal<Map<String, Pair<Schema, AttributeDescription>>>() { 339 @SuppressWarnings("serial") 340 @Override 341 protected Map<String, Pair<Schema, AttributeDescription>> initialValue() { 342 return new LinkedHashMap<String, Pair<Schema, AttributeDescription>>( 343 ATTRIBUTE_DESCRIPTION_CACHE_SIZE, 0.75f, true) { 344 @Override 345 protected boolean removeEldestEntry( 346 final Map.Entry<String, Pair<Schema, AttributeDescription>> eldest) { 347 return size() > ATTRIBUTE_DESCRIPTION_CACHE_SIZE; 348 } 349 }; 350 } 351 }; 352 353 /** Object class attribute description. */ 354 private static final ZeroOptionImpl ZERO_OPTION_IMPL = new ZeroOptionImpl(); 355 356 private static final AttributeDescription OBJECT_CLASS; 357 static { 358 final AttributeType attributeType = Schema.getCoreSchema().getAttributeType("2.5.4.0"); 359 final String attributeName = attributeType.getNameOrOID(); 360 OBJECT_CLASS = new AttributeDescription(attributeName, attributeName, attributeType, ZERO_OPTION_IMPL); 361 } 362 363 /** 364 * This is the size of the per-thread per-schema attribute description 365 * cache. We should be conservative here in case there are many 366 * threads. 367 */ 368 private static final int ATTRIBUTE_DESCRIPTION_CACHE_SIZE = 512; 369 370 /** 371 * Returns an attribute description having the same attribute type and 372 * options as this attribute description as well as the provided option. 373 * 374 * @param option 375 * The attribute option. 376 * @return The new attribute description containing {@code option}. 377 * @throws NullPointerException 378 * If {@code attributeDescription} or {@code option} was 379 * {@code null}. 380 */ 381 public AttributeDescription withOption(final String option) { 382 Reject.ifNull(option); 383 384 final String normalizedOption = toLowerCase(option); 385 if (optionsPimpl.hasOption(normalizedOption)) { 386 return this; 387 } 388 389 final String newAttributeDescription = appendOption(attributeDescription, option); 390 391 final Impl impl = optionsPimpl; 392 if (impl instanceof ZeroOptionImpl) { 393 return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType, 394 new SingleOptionImpl(option, normalizedOption)); 395 } else if (impl instanceof SingleOptionImpl) { 396 final SingleOptionImpl simpl = (SingleOptionImpl) impl; 397 398 final String[] newOptions = new String[2]; 399 newOptions[0] = simpl.option; 400 newOptions[1] = option; 401 402 final String[] newNormalizedOptions = new String[2]; 403 if (normalizedOption.compareTo(simpl.normalizedOption) < 0) { 404 newNormalizedOptions[0] = normalizedOption; 405 newNormalizedOptions[1] = simpl.normalizedOption; 406 } else { 407 newNormalizedOptions[0] = simpl.normalizedOption; 408 newNormalizedOptions[1] = normalizedOption; 409 } 410 411 return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType, 412 new MultiOptionImpl(newOptions, newNormalizedOptions)); 413 } else { 414 final MultiOptionImpl mimpl = (MultiOptionImpl) impl; 415 416 final int sz1 = mimpl.options.length; 417 final String[] newOptions = Arrays.copyOf(mimpl.options, sz1 + 1); 418 newOptions[sz1] = option; 419 420 final int sz2 = mimpl.normalizedOptions.length; 421 final String[] newNormalizedOptions = new String[sz2 + 1]; 422 boolean inserted = false; 423 for (int i = 0; i < sz2; i++) { 424 if (!inserted) { 425 final String s = mimpl.normalizedOptions[i]; 426 if (normalizedOption.compareTo(s) < 0) { 427 newNormalizedOptions[i] = normalizedOption; 428 newNormalizedOptions[i + 1] = s; 429 inserted = true; 430 } else { 431 newNormalizedOptions[i] = s; 432 } 433 } else { 434 newNormalizedOptions[i + 1] = mimpl.normalizedOptions[i]; 435 } 436 } 437 438 if (!inserted) { 439 newNormalizedOptions[sz2] = normalizedOption; 440 } 441 442 return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType, 443 new MultiOptionImpl(newOptions, newNormalizedOptions)); 444 } 445 } 446 447 /** 448 * Returns an attribute description having the same attribute type and 449 * options as this attribute description except for the provided option. 450 * <p> 451 * This method is idempotent: if this attribute description does not contain 452 * the provided option then this attribute description will be returned. 453 * 454 * @param option 455 * The attribute option. 456 * @return The new attribute description excluding {@code option}. 457 * @throws NullPointerException 458 * If {@code attributeDescription} or {@code option} was 459 * {@code null}. 460 */ 461 public AttributeDescription withoutOption(final String option) { 462 Reject.ifNull(option); 463 464 final String normalizedOption = toLowerCase(option); 465 if (!optionsPimpl.hasOption(normalizedOption)) { 466 return this; 467 } 468 469 final String oldAttributeDescription = attributeDescription; 470 final StringBuilder builder = 471 new StringBuilder(oldAttributeDescription.length() - option.length() - 1); 472 473 final String normalizedOldAttributeDescription = toLowerCase(oldAttributeDescription); 474 final int index = normalizedOldAttributeDescription.indexOf(normalizedOption); 475 builder.append(oldAttributeDescription, 0, index - 1 /* to semi-colon */); 476 builder.append(oldAttributeDescription, index + option.length(), oldAttributeDescription 477 .length()); 478 final String newAttributeDescription = builder.toString(); 479 480 final Impl impl = optionsPimpl; 481 if (impl instanceof ZeroOptionImpl) { 482 throw new IllegalStateException("ZeroOptionImpl unexpected"); 483 } else if (impl instanceof SingleOptionImpl) { 484 return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType, 485 ZERO_OPTION_IMPL); 486 } else { 487 final MultiOptionImpl mimpl = (MultiOptionImpl) impl; 488 if (mimpl.options.length == 2) { 489 final String remainingOption; 490 final String remainingNormalizedOption; 491 492 if (toLowerCase(mimpl.options[0]).equals(normalizedOption)) { 493 remainingOption = mimpl.options[1]; 494 } else { 495 remainingOption = mimpl.options[0]; 496 } 497 498 if (mimpl.normalizedOptions[0].equals(normalizedOption)) { 499 remainingNormalizedOption = mimpl.normalizedOptions[1]; 500 } else { 501 remainingNormalizedOption = mimpl.normalizedOptions[0]; 502 } 503 504 return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType, 505 new SingleOptionImpl(remainingOption, remainingNormalizedOption)); 506 } else { 507 final String[] newOptions = new String[mimpl.options.length - 1]; 508 final String[] newNormalizedOptions = 509 new String[mimpl.normalizedOptions.length - 1]; 510 511 for (int i = 0, j = 0; i < mimpl.options.length; i++) { 512 if (!toLowerCase(mimpl.options[i]).equals(normalizedOption)) { 513 newOptions[j++] = mimpl.options[i]; 514 } 515 } 516 517 for (int i = 0, j = 0; i < mimpl.normalizedOptions.length; i++) { 518 if (!mimpl.normalizedOptions[i].equals(normalizedOption)) { 519 newNormalizedOptions[j++] = mimpl.normalizedOptions[i]; 520 } 521 } 522 523 return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType, 524 new MultiOptionImpl(newOptions, newNormalizedOptions)); 525 } 526 } 527 } 528 529 /** 530 * Creates an attribute description having the provided attribute type and no options. 531 * 532 * @param attributeType 533 * The attribute type. 534 * @return The attribute description. 535 * @throws NullPointerException 536 * If {@code attributeType} was {@code null}. 537 */ 538 public static AttributeDescription create(final AttributeType attributeType) { 539 Reject.ifNull(attributeType); 540 541 // Use object identity in case attribute type does not come from core schema. 542 if (attributeType == OBJECT_CLASS.getAttributeType()) { 543 return OBJECT_CLASS; 544 } 545 String attributeName = attributeType.getNameOrOID(); 546 return new AttributeDescription(attributeName, attributeName, attributeType, ZERO_OPTION_IMPL); 547 } 548 549 /** 550 * Creates an attribute description having the provided attribute name, type and no options. 551 * 552 * @param attributeName 553 * The attribute name. 554 * @param attributeType 555 * The attribute type. 556 * @return The attribute description. 557 * @throws NullPointerException 558 * If {@code attributeType} was {@code null}. 559 * @deprecated This method may be removed at any time 560 * @since OPENDJ-2803 Migrate Attribute 561 */ 562 @Deprecated 563 public static AttributeDescription create(final String attributeName, final AttributeType attributeType) { 564 Reject.ifNull(attributeName, attributeType); 565 566 if (attributeType == OBJECT_CLASS.getAttributeType() 567 && attributeName.equals(attributeType.getNameOrOID())) { 568 return OBJECT_CLASS; 569 } 570 return new AttributeDescription(attributeName, attributeName, attributeType, ZERO_OPTION_IMPL); 571 } 572 573 /** 574 * Creates an attribute description having the provided attribute type and single option. 575 * 576 * @param attributeType 577 * The attribute type. 578 * @param option 579 * The attribute option. 580 * @return The attribute description. 581 * @throws NullPointerException 582 * If {@code attributeType} or {@code option} was {@code null}. 583 */ 584 public static AttributeDescription create(final AttributeType attributeType, final String option) { 585 return create(attributeType.getNameOrOID(), attributeType, option); 586 } 587 588 /** 589 * Creates an attribute description having the provided attribute name, type and single option. 590 * 591 * @param attributeName 592 * The attribute name. 593 * @param attributeType 594 * The attribute type. 595 * @param option 596 * The attribute option. 597 * @return The attribute description. 598 * @throws NullPointerException 599 * If {@code attributeType} or {@code option} was {@code null}. 600 * @deprecated This method may be removed at any time 601 * @since OPENDJ-2803 Migrate Attribute 602 */ 603 @Deprecated 604 public static AttributeDescription create( 605 final String attributeName, final AttributeType attributeType, final String option) { 606 Reject.ifNull(attributeName, attributeType, option); 607 608 final String attributeDescription = appendOption(attributeName, option); 609 final String normalizedOption = toLowerCase(option); 610 611 return new AttributeDescription(attributeDescription, attributeName, attributeType, 612 new SingleOptionImpl(option, normalizedOption)); 613 } 614 615 private static String appendOption(final String oid, final String option) { 616 final StringBuilder builder = new StringBuilder(oid.length() + option.length() + 1); 617 builder.append(oid); 618 builder.append(';'); 619 builder.append(option); 620 return builder.toString(); 621 } 622 623 /** 624 * Creates an attribute description having the provided attribute name, type and options. 625 * 626 * @param attributeName 627 * The attribute name. 628 * @param attributeType 629 * The attribute type. 630 * @param options 631 * The attribute options. 632 * @return The attribute description. 633 * @throws NullPointerException 634 * If {@code attributeType} or {@code options} was {@code null}. 635 * @deprecated This method may be removed at any time 636 * @since OPENDJ-2803 Migrate Attribute 637 */ 638 @Deprecated 639 public static AttributeDescription create( 640 final String attributeName, final AttributeType attributeType, final String... options) { 641 Reject.ifNull(options); 642 return create(attributeName, attributeType, Arrays.asList(options)); 643 } 644 645 /** 646 * Creates an attribute description having the provided attribute type and options. 647 * 648 * @param attributeType 649 * The attribute type. 650 * @param options 651 * The attribute options. 652 * @return The attribute description. 653 * @throws NullPointerException 654 * If {@code attributeType} or {@code options} was {@code null}. 655 */ 656 public static AttributeDescription create(final AttributeType attributeType, final String... options) { 657 Reject.ifNull(options); 658 return create(attributeType.getNameOrOID(), attributeType, Arrays.asList(options)); 659 } 660 661 /** 662 * Creates an attribute description having the provided attribute type and options. 663 * 664 * @param attributeType 665 * The attribute type. 666 * @param options 667 * The attribute options. 668 * @return The attribute description. 669 * @throws NullPointerException 670 * If {@code attributeType} or {@code options} was {@code null}. 671 */ 672 public static AttributeDescription create(final AttributeType attributeType, final Collection<String> options) { 673 return create(attributeType.getNameOrOID(), attributeType, options); 674 } 675 676 /** 677 * Creates an attribute description having the provided attribute name, type and options. 678 * 679 * @param attributeName 680 * The attribute name. 681 * @param attributeType 682 * The attribute type. 683 * @param options 684 * The attribute options. 685 * @return The attribute description. 686 * @throws NullPointerException 687 * If {@code attributeType} or {@code options} was {@code null}. 688 * @deprecated This method may be removed at any time 689 * @since OPENDJ-2803 Migrate Attribute 690 */ 691 @Deprecated 692 public static AttributeDescription create( 693 final String attributeName, final AttributeType attributeType, final Collection<String> options) { 694 Reject.ifNull(attributeName, attributeType); 695 696 final Collection<String> opts = options != null ? options : Collections.<String> emptySet(); 697 switch (opts.size()) { 698 case 0: 699 return create(attributeName, attributeType); 700 case 1: 701 return create(attributeName, attributeType, opts.iterator().next()); 702 default: 703 final String[] optionsList = new String[opts.size()]; 704 final String[] normalizedOptions = new String[opts.size()]; 705 706 final Iterator<String> it = opts.iterator(); 707 final StringBuilder builder = 708 new StringBuilder(attributeName.length() + it.next().length() + it.next().length() + 2); 709 builder.append(attributeName); 710 711 int i = 0; 712 for (final String option : opts) { 713 builder.append(';'); 714 builder.append(option); 715 optionsList[i] = option; 716 normalizedOptions[i++] = toLowerCase(option); 717 } 718 Arrays.sort(normalizedOptions); 719 720 final String attributeDescription = builder.toString(); 721 return new AttributeDescription(attributeDescription, attributeName, attributeType, 722 new MultiOptionImpl(optionsList, normalizedOptions)); 723 } 724 } 725 726 /** 727 * Returns an attribute description representing the object class attribute 728 * type with no options. 729 * 730 * @return The object class attribute description. 731 */ 732 public static AttributeDescription objectClass() { 733 return OBJECT_CLASS; 734 } 735 736 /** 737 * Parses the provided LDAP string representation of an attribute 738 * description using the default schema. 739 * 740 * @param attributeDescription 741 * The LDAP string representation of an attribute description. 742 * @return The parsed attribute description. 743 * @throws UnknownSchemaElementException 744 * If {@code attributeDescription} contains an attribute type 745 * which is not contained in the default schema and the schema 746 * is strict. 747 * @throws LocalizedIllegalArgumentException 748 * If {@code attributeDescription} is not a valid LDAP string 749 * representation of an attribute description. 750 * @throws NullPointerException 751 * If {@code attributeDescription} was {@code null}. 752 */ 753 public static AttributeDescription valueOf(final String attributeDescription) { 754 return valueOf(attributeDescription, Schema.getDefaultSchema()); 755 } 756 757 /** 758 * Parses the provided LDAP string representation of an attribute 759 * description using the provided schema. 760 * 761 * @param attributeDescription 762 * The LDAP string representation of an attribute description. 763 * @param schema 764 * The schema to use when parsing the attribute description. 765 * @return The parsed attribute description. 766 * @throws UnknownSchemaElementException 767 * If {@code attributeDescription} contains an attribute type 768 * which is not contained in the provided schema and the schema 769 * is strict. 770 * @throws LocalizedIllegalArgumentException 771 * If {@code attributeDescription} is not a valid LDAP string 772 * representation of an attribute description. 773 * @throws NullPointerException 774 * If {@code attributeDescription} or {@code schema} was 775 * {@code null}. 776 */ 777 public static AttributeDescription valueOf(final String attributeDescription, 778 final Schema schema) { 779 Reject.ifNull(attributeDescription, schema); 780 781 // First look up the attribute description in the cache. 782 final Map<String, Pair<Schema, AttributeDescription>> threadLocalCache = CACHE.get(); 783 Pair<Schema, AttributeDescription> ad = threadLocalCache.get(attributeDescription); 784 // WARNING: When we'll support multiple schema, this schema equality check will be a problem 785 // for heavily used core attributes like "cn" which will be inherited in any sub-schema. 786 // See OPENDJ-3191 787 if (ad == null || ad.getFirst() != schema) { 788 // Cache miss: decode and cache. 789 ad = Pair.of(schema, valueOf0(attributeDescription, schema)); 790 threadLocalCache.put(attributeDescription, ad); 791 } 792 return ad.getSecond(); 793 } 794 795 private static int skipTrailingWhiteSpace(final String attributeDescription, int i, 796 final int length) { 797 char c; 798 while (i < length) { 799 c = attributeDescription.charAt(i); 800 if (c != ' ') { 801 final LocalizableMessage message = 802 ERR_ATTRIBUTE_DESCRIPTION_INTERNAL_WHITESPACE.get(attributeDescription); 803 throw new LocalizedIllegalArgumentException(message); 804 } 805 i++; 806 } 807 return i; 808 } 809 810 /** Uncached valueOf implementation. */ 811 private static AttributeDescription valueOf0(final String attributeDescription, final Schema schema) { 812 final boolean allowMalformedNamesAndOptions = schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS); 813 int i = 0; 814 final int length = attributeDescription.length(); 815 char c = 0; 816 817 // Skip leading white space. 818 while (i < length) { 819 c = attributeDescription.charAt(i); 820 if (c != ' ') { 821 break; 822 } 823 i++; 824 } 825 826 // If we're already at the end then the attribute description only 827 // contained whitespace. 828 if (i == length) { 829 final LocalizableMessage message = 830 ERR_ATTRIBUTE_DESCRIPTION_EMPTY.get(attributeDescription); 831 throw new LocalizedIllegalArgumentException(message); 832 } 833 834 // Validate the first non-whitespace character. 835 ASCIICharProp cp = ASCIICharProp.valueOf(c); 836 if (cp == null) { 837 throw illegalCharacter(attributeDescription, i, c); 838 } 839 840 // Mark the attribute type start position. 841 final int attributeTypeStart = i; 842 if (cp.isLetter()) { 843 // Non-numeric OID: letter + zero or more keychars. 844 i++; 845 while (i < length) { 846 c = attributeDescription.charAt(i); 847 if (c == ';' || c == ' ') { 848 break; 849 } 850 851 cp = ASCIICharProp.valueOf(c); 852 if (cp == null || !cp.isKeyChar(allowMalformedNamesAndOptions)) { 853 throw illegalCharacter(attributeDescription, i, c); 854 } 855 i++; 856 } 857 858 // (charAt(i) == ';' || c == ' ' || i == length) 859 } else if (cp.isDigit()) { 860 // Numeric OID: decimal digit + zero or more dots or decimals. 861 i++; 862 while (i < length) { 863 c = attributeDescription.charAt(i); 864 if (c == ';' || c == ' ') { 865 break; 866 } 867 868 cp = ASCIICharProp.valueOf(c); 869 if (cp == null || (c != '.' && !cp.isDigit())) { 870 throw illegalCharacter(attributeDescription, i, c); 871 } 872 i++; 873 } 874 875 // (charAt(i) == ';' || charAt(i) == ' ' || i == length) 876 } else { 877 throw illegalCharacter(attributeDescription, i, c); 878 } 879 880 // Skip trailing white space. 881 final int attributeTypeEnd = i; 882 if (c == ' ') { 883 i = skipTrailingWhiteSpace(attributeDescription, i + 1, length); 884 } 885 886 // Determine the portion of the string containing the attribute type name. 887 String oid; 888 if (attributeTypeStart == 0 && attributeTypeEnd == length) { 889 oid = attributeDescription; 890 } else { 891 oid = attributeDescription.substring(attributeTypeStart, attributeTypeEnd); 892 } 893 894 if (oid.length() == 0) { 895 final LocalizableMessage message = 896 ERR_ATTRIBUTE_DESCRIPTION_NO_TYPE.get(attributeDescription); 897 throw new LocalizedIllegalArgumentException(message); 898 } 899 900 // Get the attribute type from the schema. 901 final AttributeType attributeType = schema.getAttributeType(oid); 902 903 // If we're already at the end of the attribute description then it 904 // does not contain any options. 905 if (i == length) { 906 // Use object identity in case attribute type does not come from core schema. 907 if (attributeType == OBJECT_CLASS.getAttributeType() 908 && attributeDescription.equals(OBJECT_CLASS.toString())) { 909 return OBJECT_CLASS; 910 } 911 return new AttributeDescription(attributeDescription, oid, attributeType, ZERO_OPTION_IMPL); 912 } 913 914 // At this point 'i' must point at a semi-colon. 915 i++; 916 StringBuilder builder = null; 917 int optionStart = i; 918 while (i < length) { 919 c = attributeDescription.charAt(i); 920 if (c == ' ' || c == ';') { 921 break; 922 } 923 924 cp = ASCIICharProp.valueOf(c); 925 if (cp == null || !cp.isKeyChar(allowMalformedNamesAndOptions)) { 926 throw illegalCharacter(attributeDescription, i, c); 927 } 928 929 if (builder == null) { 930 if (cp.isUpperCase()) { 931 // Need to normalize the option. 932 builder = new StringBuilder(length - optionStart); 933 builder.append(attributeDescription, optionStart, i); 934 builder.append(cp.toLowerCase()); 935 } 936 } else { 937 builder.append(cp.toLowerCase()); 938 } 939 i++; 940 } 941 942 String option = attributeDescription.substring(optionStart, i); 943 String normalizedOption; 944 if (builder != null) { 945 normalizedOption = builder.toString(); 946 } else { 947 normalizedOption = option; 948 } 949 950 if (option.length() == 0) { 951 final LocalizableMessage message = 952 ERR_ATTRIBUTE_DESCRIPTION_EMPTY_OPTION.get(attributeDescription); 953 throw new LocalizedIllegalArgumentException(message); 954 } 955 956 // Skip trailing white space. 957 if (c == ' ') { 958 i = skipTrailingWhiteSpace(attributeDescription, i + 1, length); 959 } 960 961 // If we're already at the end of the attribute description then it 962 // only contains a single option. 963 if (i == length) { 964 return new AttributeDescription(attributeDescription, oid, attributeType, 965 new SingleOptionImpl(option, normalizedOption)); 966 } 967 968 // Multiple options need sorting and duplicates removed - we could 969 // optimize a bit further here for 2 option attribute descriptions. 970 final List<String> options = new LinkedList<>(); 971 options.add(option); 972 973 final SortedSet<String> normalizedOptions = new TreeSet<>(); 974 normalizedOptions.add(normalizedOption); 975 976 while (i < length) { 977 // At this point 'i' must point at a semi-colon. 978 i++; 979 builder = null; 980 optionStart = i; 981 while (i < length) { 982 c = attributeDescription.charAt(i); 983 if (c == ' ' || c == ';') { 984 break; 985 } 986 987 cp = ASCIICharProp.valueOf(c); 988 if (cp == null || !cp.isKeyChar(allowMalformedNamesAndOptions)) { 989 throw illegalCharacter(attributeDescription, i, c); 990 } 991 992 if (builder == null) { 993 if (cp.isUpperCase()) { 994 // Need to normalize the option. 995 builder = new StringBuilder(length - optionStart); 996 builder.append(attributeDescription, optionStart, i); 997 builder.append(cp.toLowerCase()); 998 } 999 } else { 1000 builder.append(cp.toLowerCase()); 1001 } 1002 i++; 1003 } 1004 1005 option = attributeDescription.substring(optionStart, i); 1006 if (builder != null) { 1007 normalizedOption = builder.toString(); 1008 } else { 1009 normalizedOption = option; 1010 } 1011 1012 if (option.length() == 0) { 1013 final LocalizableMessage message = 1014 ERR_ATTRIBUTE_DESCRIPTION_EMPTY_OPTION.get(attributeDescription); 1015 throw new LocalizedIllegalArgumentException(message); 1016 } 1017 1018 // Skip trailing white space. 1019 if (c == ' ') { 1020 i = skipTrailingWhiteSpace(attributeDescription, i + 1, length); 1021 } 1022 1023 if (normalizedOptions.add(normalizedOption)) { 1024 options.add(option); 1025 } 1026 } 1027 1028 final Impl pimpl = normalizedOptions.size() > 1 1029 ? new MultiOptionImpl(toArray(options), toArray(normalizedOptions)) 1030 : new SingleOptionImpl(options.get(0), normalizedOptions.first()); 1031 return new AttributeDescription(attributeDescription, oid, attributeType, pimpl); 1032 } 1033 1034 private static String[] toArray(final Collection<String> col) { 1035 return col.toArray(new String[col.size()]); 1036 } 1037 1038 private static LocalizedIllegalArgumentException illegalCharacter( 1039 final String attributeDescription, int i, char c) { 1040 return new LocalizedIllegalArgumentException( 1041 ERR_ATTRIBUTE_DESCRIPTION_ILLEGAL_CHARACTER.get(attributeDescription, c, i)); 1042 } 1043 1044 private final String attributeDescription; 1045 private final String nameOrOid; 1046 private final AttributeType attributeType; 1047 private final Impl optionsPimpl; 1048 1049 /** Private constructor. */ 1050 private AttributeDescription(final String attributeDescription, final String attributeName, 1051 final AttributeType attributeType, final Impl pimpl) { 1052 this.attributeDescription = attributeDescription; 1053 this.nameOrOid = attributeName; 1054 this.attributeType = attributeType; 1055 this.optionsPimpl = pimpl; 1056 } 1057 1058 /** 1059 * Compares this attribute description to the provided attribute 1060 * description. The attribute types are compared first and then, if equal, 1061 * the options are normalized, sorted, and compared. 1062 * 1063 * @param other 1064 * The attribute description to be compared. 1065 * @return A negative integer, zero, or a positive integer as this attribute 1066 * description is less than, equal to, or greater than the specified 1067 * attribute description. 1068 * @throws NullPointerException 1069 * If {@code name} was {@code null}. 1070 */ 1071 @Override 1072 public int compareTo(final AttributeDescription other) { 1073 final int result = attributeType.compareTo(other.attributeType); 1074 if (result != 0) { 1075 return result; 1076 } else { 1077 // Attribute type is the same, so compare options. 1078 return optionsPimpl.compareTo(other.optionsPimpl); 1079 } 1080 } 1081 1082 /** 1083 * Indicates whether this attribute description contains the provided option. 1084 * 1085 * @param option 1086 * The option for which to make the determination. 1087 * @return {@code true} if this attribute description has the provided 1088 * option, or {@code false} if not. 1089 * @throws NullPointerException 1090 * If {@code option} was {@code null}. 1091 */ 1092 public boolean hasOption(final String option) { 1093 final String normalizedOption = toLowerCase(option); 1094 return optionsPimpl.hasOption(normalizedOption); 1095 } 1096 1097 /** 1098 * Indicates whether the provided object is an attribute description which 1099 * is equal to this attribute description. It will be considered equal if 1100 * the attribute types are {@link AttributeType#equals equal} and normalized 1101 * sorted list of options are identical. 1102 * 1103 * @param o 1104 * The object for which to make the determination. 1105 * @return {@code true} if the provided object is an attribute description 1106 * that is equal to this attribute description, or {@code false} if 1107 * not. 1108 */ 1109 @Override 1110 public boolean equals(final Object o) { 1111 if (this == o) { 1112 return true; 1113 } else if (o instanceof AttributeDescription) { 1114 final AttributeDescription other = (AttributeDescription) o; 1115 return attributeType.equals(other.attributeType) && optionsPimpl.equals(other.optionsPimpl); 1116 } else { 1117 return false; 1118 } 1119 } 1120 1121 /** 1122 * Returns the attribute type associated with this attribute description. 1123 * 1124 * @return The attribute type associated with this attribute description. 1125 */ 1126 public AttributeType getAttributeType() { 1127 return attributeType; 1128 } 1129 1130 /** 1131 * Returns the attribute name or the oid provided by the user associated with this attribute 1132 * description. 1133 * <p> 1134 * In other words, it returns the user-provided name or oid of this attribute description, 1135 * leaving out the option(s). 1136 * 1137 * @return The attribute name or the oid provided by the user associated with this attribute 1138 * description. 1139 * @deprecated This method may be removed at any time 1140 * @since OPENDJ-2803 Migrate Attribute 1141 */ 1142 @Deprecated 1143 public String getNameOrOID() { 1144 return nameOrOid; 1145 } 1146 1147 /** 1148 * Returns an {@code Iterable} containing the options contained in this 1149 * attribute description. Attempts to remove options using an iterator's 1150 * {@code remove()} method are not permitted and will result in an 1151 * {@code UnsupportedOperationException} being thrown. 1152 * 1153 * @return An {@code Iterable} containing the options. 1154 */ 1155 public Iterable<String> getOptions() { 1156 return optionsPimpl; 1157 } 1158 1159 /** 1160 * Returns the hash code for this attribute description. It will be 1161 * calculated as the sum of the hash codes of the attribute type and 1162 * normalized sorted list of options. 1163 * 1164 * @return The hash code for this attribute description. 1165 */ 1166 @Override 1167 public int hashCode() { 1168 // FIXME: should we cache this? 1169 return attributeType.hashCode() * 31 + optionsPimpl.hashCode(); 1170 } 1171 1172 /** 1173 * Indicates whether this attribute description has any options. 1174 * 1175 * @return {@code true} if this attribute description has any options, or 1176 * {@code false} if not. 1177 */ 1178 public boolean hasOptions() { 1179 return optionsPimpl.hasOptions(); 1180 } 1181 1182 /** 1183 * Indicates whether this attribute description is the 1184 * {@code objectClass} attribute description with no options. 1185 * 1186 * @return {@code true} if this attribute description is the 1187 * {@code objectClass} attribute description with no options, or 1188 * {@code false} if not. 1189 */ 1190 public boolean isObjectClass() { 1191 return attributeType.isObjectClass() && !hasOptions(); 1192 } 1193 1194 /** 1195 * Indicates whether this attribute description is a temporary place-holder 1196 * allocated dynamically by a non-strict schema when no corresponding 1197 * registered attribute type was found. 1198 * <p> 1199 * Place holder attribute descriptions have an attribute type whose OID is 1200 * the normalized attribute name with the string {@code -oid} appended. In 1201 * addition, they will use the directory string syntax and case ignore 1202 * matching rule. 1203 * 1204 * @return {@code true} if this is a temporary place-holder attribute 1205 * description allocated dynamically by a non-strict schema when no 1206 * corresponding registered attribute type was found. 1207 * @see Schema#getAttributeType(String) 1208 * @see AttributeType#isPlaceHolder() 1209 */ 1210 public boolean isPlaceHolder() { 1211 return attributeType.isPlaceHolder(); 1212 } 1213 1214 /** 1215 * Indicates whether this attribute description is a sub-type of the 1216 * provided attribute description as defined in RFC 4512 section 2.5. 1217 * Specifically, this method will return {@code true} if and only if the 1218 * following conditions are both {@code true}: 1219 * <ul> 1220 * <li>This attribute description has an attribute type which 1221 * {@link AttributeType#matches matches}, or is a sub-type of, the attribute 1222 * type in the provided attribute description. 1223 * <li>This attribute description contains all of the options contained in 1224 * the provided attribute description. 1225 * </ul> 1226 * Note that this method will return {@code true} if this attribute 1227 * description is equal to the provided attribute description. 1228 * 1229 * @param other 1230 * The attribute description for which to make the determination. 1231 * @return {@code true} if this attribute description is a sub-type of the 1232 * provided attribute description, or {@code false} if not. 1233 * @throws NullPointerException 1234 * If {@code name} was {@code null}. 1235 */ 1236 public boolean isSubTypeOf(final AttributeDescription other) { 1237 return attributeType.isSubTypeOf(other.attributeType) 1238 && optionsPimpl.isSubTypeOf(other.optionsPimpl); 1239 } 1240 1241 /** 1242 * Indicates whether this attribute description is a super-type of 1243 * the provided attribute description as defined in RFC 4512 section 2.5. 1244 * Specifically, this method will return {@code true} if and only if the 1245 * following conditions are both {@code true}: 1246 * <ul> 1247 * <li>This attribute description has an attribute type which 1248 * {@link AttributeType#matches matches}, or is a super-type of, the 1249 * attribute type in the provided attribute description. 1250 * <li>This attribute description contains a sub-set of the options 1251 * contained in the provided attribute description. 1252 * </ul> 1253 * Note that this method will return {@code true} if this attribute 1254 * description is equal to the provided attribute description. 1255 * 1256 * @param other 1257 * The attribute description for which to make the determination. 1258 * @return {@code true} if this attribute description is a super-type of the 1259 * provided attribute description, or {@code false} if not. 1260 * @throws NullPointerException 1261 * If {@code name} was {@code null}. 1262 */ 1263 public boolean isSuperTypeOf(final AttributeDescription other) { 1264 return attributeType.isSuperTypeOf(other.attributeType) 1265 && optionsPimpl.isSuperTypeOf(other.optionsPimpl); 1266 } 1267 1268 /** 1269 * Indicates whether the provided attribute description matches this 1270 * attribute description. It will be considered a match if the attribute 1271 * types {@link AttributeType#matches match} and the normalized sorted list 1272 * of options are identical. 1273 * 1274 * @param other 1275 * The attribute description for which to make the determination. 1276 * @return {@code true} if the provided attribute description matches this 1277 * attribute description, or {@code false} if not. 1278 */ 1279 public boolean matches(final AttributeDescription other) { 1280 if (this == other) { 1281 return true; 1282 } else { 1283 return attributeType.matches(other.attributeType) && optionsPimpl.equals(other.optionsPimpl); 1284 } 1285 } 1286 1287 /** 1288 * Returns the string representation of this attribute description as 1289 * defined in RFC4512 section 2.5. 1290 * 1291 * @return The string representation of this attribute description. 1292 */ 1293 @Override 1294 public String toString() { 1295 return attributeDescription; 1296 } 1297}