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 2010 Sun Microsystems, Inc. 015 * Portions Copyright 2011-2016 ForgeRock AS. 016 */ 017package org.forgerock.opendj.ldap; 018 019import java.util.ArrayList; 020import java.util.Collection; 021import java.util.Collections; 022import java.util.Comparator; 023import java.util.HashSet; 024import java.util.Iterator; 025import java.util.LinkedHashSet; 026import java.util.List; 027import java.util.Set; 028 029import org.forgerock.i18n.LocalizableMessage; 030import org.forgerock.i18n.LocalizedIllegalArgumentException; 031import org.forgerock.opendj.ldap.controls.PermissiveModifyRequestControl; 032import org.forgerock.opendj.ldap.requests.ModifyRequest; 033import org.forgerock.opendj.ldap.requests.Requests; 034import org.forgerock.opendj.ldap.schema.ObjectClass; 035import org.forgerock.opendj.ldap.schema.ObjectClassType; 036import org.forgerock.opendj.ldap.schema.Schema; 037import org.forgerock.opendj.ldap.schema.SchemaValidationPolicy; 038import org.forgerock.opendj.ldap.schema.UnknownSchemaElementException; 039import org.forgerock.opendj.ldif.LDIF; 040import org.forgerock.util.Reject; 041import org.forgerock.util.Function; 042import org.forgerock.util.promise.NeverThrowsException; 043 044import com.forgerock.opendj.util.Iterables; 045 046import static org.forgerock.opendj.ldap.AttributeDescription.*; 047import static org.forgerock.opendj.ldap.LdapException.*; 048 049import static com.forgerock.opendj.ldap.CoreMessages.*; 050 051/** 052 * This class contains methods for creating and manipulating entries. 053 * 054 * @see Entry 055 */ 056public final class Entries { 057 /** 058 * Options for controlling the behavior of the 059 * {@link Entries#diffEntries(Entry, Entry, DiffOptions) diffEntries} 060 * method. {@code DiffOptions} specify which attributes are compared, how 061 * they are compared, and the type of modifications generated. 062 * 063 * @see Entries#diffEntries(Entry, Entry, DiffOptions) 064 */ 065 public static final class DiffOptions { 066 /** Selects which attributes will be compared. By default all user attributes will be compared. */ 067 private AttributeFilter attributeFilter = USER_ATTRIBUTES_ONLY_FILTER; 068 069 /** 070 * When true, attribute values are compared byte for byte, otherwise 071 * they are compared using their matching rules. 072 */ 073 private boolean useExactMatching; 074 075 /** 076 * When greater than 0, modifications with REPLACE type will be 077 * generated for the new attributes containing at least 078 * "useReplaceMaxValues" attribute values. Otherwise, modifications with 079 * DELETE + ADD types will be generated. 080 */ 081 private int useReplaceMaxValues; 082 083 private DiffOptions() { 084 // Nothing to do. 085 } 086 087 /** 088 * Specifies an attribute filter which will be used to determine which 089 * attributes will be compared. By default only user attributes will be 090 * compared. 091 * 092 * @param attributeFilter 093 * The filter which will be used to determine which 094 * attributes will be compared. 095 * @return A reference to this set of options. 096 */ 097 public DiffOptions attributes(final AttributeFilter attributeFilter) { 098 Reject.ifNull(attributeFilter); 099 this.attributeFilter = attributeFilter; 100 return this; 101 } 102 103 /** 104 * Specifies the list of attributes to be compared. By default only user 105 * attributes will be compared. 106 * 107 * @param attributeDescriptions 108 * The names of the attributes to be compared. 109 * @return A reference to this set of options. 110 */ 111 public DiffOptions attributes(final String... attributeDescriptions) { 112 return attributes(new AttributeFilter(attributeDescriptions)); 113 } 114 115 /** 116 * Requests that attribute values should be compared byte for byte, 117 * rather than using their matching rules. This is useful when a client 118 * wishes to perform trivial changes to an attribute value which would 119 * otherwise be ignored by the matching rule, such as removing extra 120 * white space from an attribute, or capitalizing a user's name. 121 * 122 * @return A reference to this set of options. 123 */ 124 public DiffOptions useExactMatching() { 125 this.useExactMatching = true; 126 return this; 127 } 128 129 /** 130 * Requests that all generated changes should use the 131 * {@link ModificationType#REPLACE REPLACE} modification type, rather 132 * than a combination of {@link ModificationType#DELETE DELETE} and 133 * {@link ModificationType#ADD ADD}. 134 * <p> 135 * Note that the generated changes will not be reversible, nor will they 136 * be efficient for attributes containing many values (such as groups). 137 * Enabling this option may result in more efficient updates for single 138 * valued attributes and reduce the amount of replication meta-data that 139 * needs to be maintained.. 140 * 141 * @return A reference to this set of options. 142 */ 143 public DiffOptions alwaysReplaceAttributes() { 144 return replaceMaxValues(Integer.MAX_VALUE); 145 } 146 147 /** 148 * Requests that the generated changes should use the 149 * {@link ModificationType#REPLACE REPLACE} modification type when the 150 * new attribute contains at most one attribute value. All other changes 151 * will use a combination of {@link ModificationType#DELETE DELETE} then 152 * {@link ModificationType#ADD ADD}. 153 * <p> 154 * Specifying this option will usually provide the best overall 155 * performance for single and multi-valued attribute updates, but the 156 * generated changes will probably not be reversible. 157 * 158 * @return A reference to this set of options. 159 */ 160 public DiffOptions replaceSingleValuedAttributes() { 161 return replaceMaxValues(1); 162 } 163 164 /** 165 * Requests that the generated changes should use the 166 * {@link ModificationType#REPLACE REPLACE} modification type when the 167 * new attribute contains {@code maxValues} attribute values or less. 168 * All other changes will use a combination of 169 * {@link ModificationType#DELETE DELETE} then 170 * {@link ModificationType#ADD ADD}. 171 * 172 * @param maxValues 173 * The maximum number of attribute values a modified 174 * attribute can contain before reversible changes will be 175 * generated. 176 * @return A reference to this set of options. 177 */ 178 private DiffOptions replaceMaxValues(final int maxValues) { 179 // private until we can think of a good use case and better name. 180 Reject.ifFalse(maxValues >= 0, "maxValues must be >= 0"); 181 this.useReplaceMaxValues = maxValues; 182 return this; 183 } 184 185 private Entry filter(final Entry entry) { 186 return attributeFilter.filteredViewOf(entry); 187 } 188 } 189 190 private static final class UnmodifiableEntry implements Entry { 191 private final Entry entry; 192 193 private UnmodifiableEntry(final Entry entry) { 194 this.entry = entry; 195 } 196 197 @Override 198 public boolean addAttribute(final Attribute attribute) { 199 throw new UnsupportedOperationException(); 200 } 201 202 @Override 203 public boolean addAttribute(final Attribute attribute, 204 final Collection<? super ByteString> duplicateValues) { 205 throw new UnsupportedOperationException(); 206 } 207 208 @Override 209 public Entry addAttribute(final String attributeDescription, final Object... values) { 210 throw new UnsupportedOperationException(); 211 } 212 213 @Override 214 public Entry clearAttributes() { 215 throw new UnsupportedOperationException(); 216 } 217 218 @Override 219 public boolean containsAttribute(final Attribute attribute, 220 final Collection<? super ByteString> missingValues) { 221 return entry.containsAttribute(attribute, missingValues); 222 } 223 224 @Override 225 public boolean containsAttribute(final String attributeDescription, final Object... values) { 226 return entry.containsAttribute(attributeDescription, values); 227 } 228 229 @Override 230 public boolean equals(final Object object) { 231 return object == this || entry.equals(object); 232 } 233 234 @Override 235 public Iterable<Attribute> getAllAttributes() { 236 return Iterables.unmodifiableIterable(Iterables.transformedIterable(entry 237 .getAllAttributes(), UNMODIFIABLE_ATTRIBUTE_FUNCTION)); 238 } 239 240 @Override 241 public Iterable<Attribute> getAllAttributes(final AttributeDescription attributeDescription) { 242 return Iterables.unmodifiableIterable(Iterables.transformedIterable(entry 243 .getAllAttributes(attributeDescription), UNMODIFIABLE_ATTRIBUTE_FUNCTION)); 244 } 245 246 @Override 247 public Iterable<Attribute> getAllAttributes(final String attributeDescription) { 248 return Iterables.unmodifiableIterable(Iterables.transformedIterable(entry 249 .getAllAttributes(attributeDescription), UNMODIFIABLE_ATTRIBUTE_FUNCTION)); 250 } 251 252 @Override 253 public Attribute getAttribute(final AttributeDescription attributeDescription) { 254 final Attribute attribute = entry.getAttribute(attributeDescription); 255 if (attribute != null) { 256 return Attributes.unmodifiableAttribute(attribute); 257 } else { 258 return null; 259 } 260 } 261 262 @Override 263 public Attribute getAttribute(final String attributeDescription) { 264 final Attribute attribute = entry.getAttribute(attributeDescription); 265 if (attribute != null) { 266 return Attributes.unmodifiableAttribute(attribute); 267 } else { 268 return null; 269 } 270 } 271 272 @Override 273 public int getAttributeCount() { 274 return entry.getAttributeCount(); 275 } 276 277 @Override 278 public DN getName() { 279 return entry.getName(); 280 } 281 282 @Override 283 public int hashCode() { 284 return entry.hashCode(); 285 } 286 287 @Override 288 public AttributeParser parseAttribute(final AttributeDescription attributeDescription) { 289 return entry.parseAttribute(attributeDescription); 290 } 291 292 @Override 293 public AttributeParser parseAttribute(final String attributeDescription) { 294 return entry.parseAttribute(attributeDescription); 295 } 296 297 @Override 298 public boolean removeAttribute(final Attribute attribute, 299 final Collection<? super ByteString> missingValues) { 300 throw new UnsupportedOperationException(); 301 } 302 303 @Override 304 public boolean removeAttribute(final AttributeDescription attributeDescription) { 305 throw new UnsupportedOperationException(); 306 } 307 308 @Override 309 public Entry removeAttribute(final String attributeDescription, final Object... values) { 310 throw new UnsupportedOperationException(); 311 } 312 313 @Override 314 public boolean replaceAttribute(final Attribute attribute) { 315 throw new UnsupportedOperationException(); 316 } 317 318 @Override 319 public Entry replaceAttribute(final String attributeDescription, final Object... values) { 320 throw new UnsupportedOperationException(); 321 } 322 323 @Override 324 public Entry setName(final DN dn) { 325 throw new UnsupportedOperationException(); 326 } 327 328 @Override 329 public Entry setName(final String dn) { 330 throw new UnsupportedOperationException(); 331 } 332 333 @Override 334 public String toString() { 335 return entry.toString(); 336 } 337 } 338 339 private static final Comparator<Entry> COMPARATOR = new Comparator<Entry>() { 340 @Override 341 public int compare(final Entry o1, final Entry o2) { 342 return o1.getName().compareTo(o2.getName()); 343 } 344 }; 345 346 private static final AttributeFilter USER_ATTRIBUTES_ONLY_FILTER = new AttributeFilter(); 347 private static final DiffOptions DEFAULT_DIFF_OPTIONS = new DiffOptions(); 348 349 private static final Function<Attribute, Attribute, NeverThrowsException> UNMODIFIABLE_ATTRIBUTE_FUNCTION = 350 new Function<Attribute, Attribute, NeverThrowsException>() { 351 @Override 352 public Attribute apply(final Attribute value) { 353 return Attributes.unmodifiableAttribute(value); 354 } 355 356 }; 357 358 /** 359 * Returns a {@code Comparator} which can be used to compare entries by name 360 * using the natural order for DN comparisons (parent before children). 361 * <p> 362 * In order to sort entries in reverse order (children first) use the 363 * following code: 364 * 365 * <pre> 366 * Collections.reverseOrder(Entries.compareByName()); 367 * </pre> 368 * 369 * For more complex sort orders involving one or more attributes refer to 370 * the {@link SortKey} class. 371 * 372 * @return The {@code Comparator}. 373 */ 374 public static Comparator<Entry> compareByName() { 375 return COMPARATOR; 376 } 377 378 /** 379 * Returns {@code true} if the provided entry is valid according to the 380 * specified schema and schema validation policy. 381 * <p> 382 * If attribute value validation is enabled then following checks will be 383 * performed: 384 * <ul> 385 * <li>checking that there is at least one value 386 * <li>checking that single-valued attributes contain only a single value 387 * </ul> 388 * In particular, attribute values will not be checked for conformance to 389 * their syntax since this is expected to have already been performed while 390 * adding the values to the entry. 391 * 392 * @param entry 393 * The entry to be validated. 394 * @param schema 395 * The schema against which the entry will be validated. 396 * @param policy 397 * The schema validation policy. 398 * @param errorMessages 399 * A collection into which any schema validation warnings or 400 * error messages can be placed, or {@code null} if they should 401 * not be saved. 402 * @return {@code true} if the provided entry is valid according to the 403 * specified schema and schema validation policy. 404 * @see Schema#validateEntry(Entry, SchemaValidationPolicy, Collection) 405 */ 406 public static boolean conformsToSchema(final Entry entry, final Schema schema, 407 final SchemaValidationPolicy policy, final Collection<LocalizableMessage> errorMessages) { 408 return schema.validateEntry(entry, policy, errorMessages); 409 } 410 411 /** 412 * Returns {@code true} if the provided entry is valid according to the 413 * default schema and schema validation policy. 414 * <p> 415 * If attribute value validation is enabled then following checks will be 416 * performed: 417 * <ul> 418 * <li>checking that there is at least one value 419 * <li>checking that single-valued attributes contain only a single value 420 * </ul> 421 * In particular, attribute values will not be checked for conformance to 422 * their syntax since this is expected to have already been performed while 423 * adding the values to the entry. 424 * 425 * @param entry 426 * The entry to be validated. 427 * @param policy 428 * The schema validation policy. 429 * @param errorMessages 430 * A collection into which any schema validation warnings or 431 * error messages can be placed, or {@code null} if they should 432 * not be saved. 433 * @return {@code true} if the provided entry is valid according to the 434 * default schema and schema validation policy. 435 * @see Schema#validateEntry(Entry, SchemaValidationPolicy, Collection) 436 */ 437 public static boolean conformsToSchema(final Entry entry, final SchemaValidationPolicy policy, 438 final Collection<LocalizableMessage> errorMessages) { 439 return conformsToSchema(entry, Schema.getDefaultSchema(), policy, errorMessages); 440 } 441 442 /** 443 * Check if the provided entry contains the provided object class. 444 * <p> 445 * This method uses the default schema for decoding the object class 446 * attribute values. 447 * <p> 448 * The provided object class must be recognized by the schema, otherwise the 449 * method returns false. 450 * 451 * @param entry 452 * The entry which is checked against the object class. 453 * @param objectClass 454 * The object class to check. 455 * @return {@code true} if and only if entry contains the object class and 456 * the object class is recognized by the default schema, 457 * {@code false} otherwise 458 */ 459 public static boolean containsObjectClass(final Entry entry, final ObjectClass objectClass) { 460 return containsObjectClass(entry, Schema.getDefaultSchema(), objectClass); 461 } 462 463 /** 464 * Check if the provided entry contains the provided object class. 465 * <p> 466 * The provided object class must be recognized by the provided schema, 467 * otherwise the method returns false. 468 * 469 * @param entry 470 * The entry which is checked against the object class. 471 * @param schema 472 * The schema which should be used for decoding the object class 473 * attribute values. 474 * @param objectClass 475 * The object class to check. 476 * @return {@code true} if and only if entry contains the object class and 477 * the object class is recognized by the provided schema, 478 * {@code false} otherwise 479 */ 480 public static boolean containsObjectClass(final Entry entry, final Schema schema, final ObjectClass objectClass) { 481 return getObjectClasses(entry, schema).contains(objectClass); 482 } 483 484 /** 485 * Creates a new modify request containing a list of modifications which can 486 * be used to transform {@code fromEntry} into entry {@code toEntry}. 487 * <p> 488 * The changes will be generated using a default set of {@link DiffOptions 489 * options}. More specifically, only user attributes will be compared, 490 * attributes will be compared using their matching rules, and all generated 491 * changes will be reversible: it will contain only modifications of type 492 * {@link ModificationType#DELETE DELETE} then {@link ModificationType#ADD 493 * ADD}. 494 * <p> 495 * Finally, the modify request will use the distinguished name taken from 496 * {@code fromEntry}. This method will not check to see if both 497 * {@code fromEntry} and {@code toEntry} have the same distinguished name. 498 * <p> 499 * This method is equivalent to: 500 * 501 * <pre> 502 * ModifyRequest request = Requests.newModifyRequest(fromEntry, toEntry); 503 * </pre> 504 * 505 * Or: 506 * 507 * <pre> 508 * ModifyRequest request = diffEntries(fromEntry, toEntry, Entries.diffOptions()); 509 * </pre> 510 * 511 * @param fromEntry 512 * The source entry. 513 * @param toEntry 514 * The destination entry. 515 * @return A modify request containing a list of modifications which can be 516 * used to transform {@code fromEntry} into entry {@code toEntry}. 517 * The returned request will always be non-{@code null} but may not 518 * contain any modifications. 519 * @throws NullPointerException 520 * If {@code fromEntry} or {@code toEntry} were {@code null}. 521 * @see Requests#newModifyRequest(Entry, Entry) 522 */ 523 public static ModifyRequest diffEntries(final Entry fromEntry, final Entry toEntry) { 524 return diffEntries(fromEntry, toEntry, DEFAULT_DIFF_OPTIONS); 525 } 526 527 /** 528 * Creates a new modify request containing a list of modifications which can 529 * be used to transform {@code fromEntry} into entry {@code toEntry}. 530 * <p> 531 * The changes will be generated using the provided set of 532 * {@link DiffOptions}. 533 * <p> 534 * Finally, the modify request will use the distinguished name taken from 535 * {@code fromEntry}. This method will not check to see if both 536 * {@code fromEntry} and {@code toEntry} have the same distinguished name. 537 * 538 * @param fromEntry 539 * The source entry. 540 * @param toEntry 541 * The destination entry. 542 * @param options 543 * The set of options which will control which attributes are 544 * compared, how they are compared, and the type of modifications 545 * generated. 546 * @return A modify request containing a list of modifications which can be 547 * used to transform {@code fromEntry} into entry {@code toEntry}. 548 * The returned request will always be non-{@code null} but may not 549 * contain any modifications. 550 * @throws NullPointerException 551 * If {@code fromEntry}, {@code toEntry}, or {@code options} 552 * were {@code null}. 553 */ 554 public static ModifyRequest diffEntries(final Entry fromEntry, final Entry toEntry, 555 final DiffOptions options) { 556 Reject.ifNull(fromEntry, toEntry, options); 557 558 final ModifyRequest request = Requests.newModifyRequest(fromEntry.getName()); 559 final Entry tfrom = toFilteredTreeMapEntry(fromEntry, options); 560 final Entry tto = toFilteredTreeMapEntry(toEntry, options); 561 final Iterator<Attribute> ifrom = tfrom.getAllAttributes().iterator(); 562 final Iterator<Attribute> ito = tto.getAllAttributes().iterator(); 563 564 Attribute afrom = ifrom.hasNext() ? ifrom.next() : null; 565 Attribute ato = ito.hasNext() ? ito.next() : null; 566 567 while (afrom != null && ato != null) { 568 final AttributeDescription adfrom = afrom.getAttributeDescription(); 569 final AttributeDescription adto = ato.getAttributeDescription(); 570 571 final int cmp = adfrom.compareTo(adto); 572 if (cmp == 0) { 573 /* Attribute is in both entries so compute the differences between the old and new. */ 574 if (options.useReplaceMaxValues >= ato.size()) { 575 // This attribute is a candidate for replacing. 576 if (diffAttributeNeedsReplacing(afrom, ato, options)) { 577 request.addModification(new Modification(ModificationType.REPLACE, ato)); 578 } 579 } else if (afrom.size() == 1 && ato.size() == 1) { 580 // Fast-path for single valued attributes. 581 if (diffFirstValuesAreDifferent(options, afrom, ato)) { 582 diffDeleteValues(request, afrom); 583 diffAddValues(request, ato); 584 } 585 } else if (options.useExactMatching) { 586 /* 587 * Compare multi-valued attributes using exact matching. Use 588 * a hash sets for membership checking rather than the 589 * attributes in order to avoid matching rule based 590 * comparisons. 591 */ 592 final Set<ByteString> oldValues = new LinkedHashSet<>(afrom); 593 final Set<ByteString> newValues = new LinkedHashSet<>(ato); 594 595 final Set<ByteString> deletedValues = new LinkedHashSet<>(oldValues); 596 deletedValues.removeAll(newValues); 597 diffDeleteValues(request, deletedValues.size() == afrom.size() ? afrom 598 : new LinkedAttribute(adfrom, deletedValues)); 599 600 final Set<ByteString> addedValues = newValues; 601 addedValues.removeAll(oldValues); 602 diffAddValues(request, addedValues.size() == ato.size() ? ato 603 : new LinkedAttribute(adto, addedValues)); 604 } else { 605 // Compare multi-valued attributes using matching rules. 606 final Attribute deletedValues = new LinkedAttribute(afrom); 607 deletedValues.removeAll(ato); 608 diffDeleteValues(request, deletedValues); 609 610 final Attribute addedValues = new LinkedAttribute(ato); 611 addedValues.removeAll(afrom); 612 diffAddValues(request, addedValues); 613 } 614 615 afrom = ifrom.hasNext() ? ifrom.next() : null; 616 ato = ito.hasNext() ? ito.next() : null; 617 } else if (cmp < 0) { 618 // afrom in source, but not destination. 619 diffDeleteAttribute(request, afrom, options); 620 afrom = ifrom.hasNext() ? ifrom.next() : null; 621 } else { 622 // ato in destination, but not in source. 623 diffAddAttribute(request, ato, options); 624 ato = ito.hasNext() ? ito.next() : null; 625 } 626 } 627 628 // Additional attributes in source entry: these must be deleted. 629 if (afrom != null) { 630 diffDeleteAttribute(request, afrom, options); 631 } 632 while (ifrom.hasNext()) { 633 diffDeleteAttribute(request, ifrom.next(), options); 634 } 635 636 // Additional attributes in destination entry: these must be added. 637 if (ato != null) { 638 diffAddAttribute(request, ato, options); 639 } 640 while (ito.hasNext()) { 641 diffAddAttribute(request, ito.next(), options); 642 } 643 644 return request; 645 } 646 647 /** 648 * Returns a new set of options which may be used to control how entries are 649 * compared and changes generated using 650 * {@link #diffEntries(Entry, Entry, DiffOptions)}. By default only user 651 * attributes will be compared, matching rules will be used for comparisons, 652 * and all generated changes will be reversible. 653 * 654 * @return A new set of options which may be used to control how entries are 655 * compared and changes generated. 656 */ 657 public static DiffOptions diffOptions() { 658 return new DiffOptions(); 659 } 660 661 /** 662 * Returns an unmodifiable set containing the object classes associated with 663 * the provided entry. This method will ignore unrecognized object classes. 664 * <p> 665 * This method uses the default schema for decoding the object class 666 * attribute values. 667 * 668 * @param entry 669 * The entry whose object classes are required. 670 * @return An unmodifiable set containing the object classes associated with 671 * the provided entry. 672 */ 673 public static Set<ObjectClass> getObjectClasses(final Entry entry) { 674 return getObjectClasses(entry, Schema.getDefaultSchema()); 675 } 676 677 /** 678 * Returns an unmodifiable set containing the object classes associated with 679 * the provided entry. This method will ignore unrecognized object classes. 680 * 681 * @param entry 682 * The entry whose object classes are required. 683 * @param schema 684 * The schema which should be used for decoding the object class 685 * attribute values. 686 * @return An unmodifiable set containing the object classes associated with 687 * the provided entry. 688 */ 689 public static Set<ObjectClass> getObjectClasses(final Entry entry, final Schema schema) { 690 final Attribute objectClassAttribute = 691 entry.getAttribute(AttributeDescription.objectClass()); 692 if (objectClassAttribute == null) { 693 return Collections.emptySet(); 694 } else { 695 final Set<ObjectClass> objectClasses = new HashSet<>(objectClassAttribute.size()); 696 for (final ByteString v : objectClassAttribute) { 697 final String objectClassName = v.toString(); 698 final ObjectClass objectClass; 699 try { 700 objectClass = schema.asStrictSchema().getObjectClass(objectClassName); 701 objectClasses.add(objectClass); 702 } catch (final UnknownSchemaElementException e) { 703 // Ignore. 704 continue; 705 } 706 } 707 return Collections.unmodifiableSet(objectClasses); 708 } 709 } 710 711 /** 712 * Returns the structural object class associated with the provided entry, 713 * or {@code null} if none was found. If the entry contains multiple 714 * structural object classes then the first will be returned. This method 715 * will ignore unrecognized object classes. 716 * <p> 717 * This method uses the default schema for decoding the object class 718 * attribute values. 719 * 720 * @param entry 721 * The entry whose structural object class is required. 722 * @return The structural object class associated with the provided entry, 723 * or {@code null} if none was found. 724 */ 725 public static ObjectClass getStructuralObjectClass(final Entry entry) { 726 return getStructuralObjectClass(entry, Schema.getDefaultSchema()); 727 } 728 729 /** 730 * Builds an entry from the provided lines of LDIF. 731 * <p> 732 * Sample usage: 733 * <pre> 734 * Entry john = makeEntry( 735 * "dn: cn=John Smith,dc=example,dc=com", 736 * "objectclass: inetorgperson", 737 * "cn: John Smith", 738 * "sn: Smith", 739 * "givenname: John"); 740 * </pre> 741 * 742 * @param ldifLines 743 * LDIF lines that contains entry definition. 744 * @return an entry 745 * @throws LocalizedIllegalArgumentException 746 * If {@code ldifLines} did not contain an LDIF entry, or 747 * contained multiple entries, or contained malformed LDIF, or 748 * if the entry could not be decoded using the default schema. 749 * @throws NullPointerException 750 * If {@code ldifLines} was {@code null}. 751 */ 752 public static Entry makeEntry(String... ldifLines) { 753 return LDIF.makeEntry(ldifLines); 754 } 755 756 /** 757 * Builds a list of entries from the provided lines of LDIF. 758 * <p> 759 * Sample usage: 760 * <pre> 761 * List<Entry> smiths = TestCaseUtils.makeEntries( 762 * "dn: cn=John Smith,dc=example,dc=com", 763 * "objectclass: inetorgperson", 764 * "cn: John Smith", 765 * "sn: Smith", 766 * "givenname: John", 767 * "", 768 * "dn: cn=Jane Smith,dc=example,dc=com", 769 * "objectclass: inetorgperson", 770 * "cn: Jane Smith", 771 * "sn: Smith", 772 * "givenname: Jane"); 773 * </pre> 774 * @param ldifLines 775 * LDIF lines that contains entries definition. 776 * Entries are separated by an empty string: {@code ""}. 777 * @return a non empty list of entries 778 * @throws LocalizedIllegalArgumentException 779 * If {@code ldifLines} did not contain LDIF entries, 780 * or contained malformed LDIF, or if the entries 781 * could not be decoded using the default schema. 782 * @throws NullPointerException 783 * If {@code ldifLines} was {@code null}. 784 */ 785 public static List<Entry> makeEntries(String... ldifLines) { 786 return LDIF.makeEntries(ldifLines); 787 } 788 789 /** 790 * Returns the structural object class associated with the provided entry, 791 * or {@code null} if none was found. If the entry contains multiple 792 * structural object classes then the first will be returned. This method 793 * will ignore unrecognized object classes. 794 * 795 * @param entry 796 * The entry whose structural object class is required. 797 * @param schema 798 * The schema which should be used for decoding the object class 799 * attribute values. 800 * @return The structural object class associated with the provided entry, 801 * or {@code null} if none was found. 802 */ 803 public static ObjectClass getStructuralObjectClass(final Entry entry, final Schema schema) { 804 ObjectClass structuralObjectClass = null; 805 final Attribute objectClassAttribute = entry.getAttribute(objectClass()); 806 807 if (objectClassAttribute == null) { 808 return null; 809 } 810 811 for (final ByteString v : objectClassAttribute) { 812 final String objectClassName = v.toString(); 813 final ObjectClass objectClass; 814 try { 815 objectClass = schema.asStrictSchema().getObjectClass(objectClassName); 816 } catch (final UnknownSchemaElementException e) { 817 // Ignore. 818 continue; 819 } 820 821 if (objectClass.getObjectClassType() == ObjectClassType.STRUCTURAL 822 && (structuralObjectClass == null || objectClass.isDescendantOf(structuralObjectClass))) { 823 structuralObjectClass = objectClass; 824 } 825 } 826 827 return structuralObjectClass; 828 } 829 830 /** 831 * Applies the provided modification to an entry. This method implements 832 * "permissive" modify semantics, ignoring attempts to add duplicate values 833 * or attempts to remove values which do not exist. 834 * 835 * @param entry 836 * The entry to be modified. 837 * @param change 838 * The modification to be applied to the entry. 839 * @return A reference to the updated entry. 840 * @throws LdapException 841 * If an error occurred while performing the change such as an 842 * attempt to increment a value which is not a number. The entry 843 * will not have been modified. 844 */ 845 public static Entry modifyEntry(final Entry entry, final Modification change) throws LdapException { 846 return modifyEntry(entry, change, null); 847 } 848 849 /** 850 * Applies the provided modification to an entry. This method implements 851 * "permissive" modify semantics, recording attempts to add duplicate values 852 * or attempts to remove values which do not exist in the provided 853 * collection if provided. 854 * 855 * @param entry 856 * The entry to be modified. 857 * @param change 858 * The modification to be applied to the entry. 859 * @param conflictingValues 860 * A collection into which duplicate or missing values will be 861 * added, or {@code null} if conflicting values should not be 862 * saved. 863 * @return A reference to the updated entry. 864 * @throws LdapException 865 * If an error occurred while performing the change such as an 866 * attempt to increment a value which is not a number. The entry 867 * will not have been modified. 868 */ 869 public static Entry modifyEntry(final Entry entry, final Modification change, 870 final Collection<? super ByteString> conflictingValues) throws LdapException { 871 return modifyEntry0(entry, change, conflictingValues, true); 872 } 873 874 /** 875 * Applies the provided modification request to an entry. This method will 876 * utilize "permissive" modify semantics if the request contains the 877 * {@link PermissiveModifyRequestControl}. 878 * 879 * @param entry 880 * The entry to be modified. 881 * @param changes 882 * The modification request to be applied to the entry. 883 * @return A reference to the updated entry. 884 * @throws LdapException 885 * If an error occurred while performing the changes such as an 886 * attempt to add duplicate values, remove values which do not 887 * exist, or increment a value which is not a number. The entry 888 * may have been modified. 889 */ 890 public static Entry modifyEntry(final Entry entry, final ModifyRequest changes) throws LdapException { 891 final boolean isPermissive = changes.containsControl(PermissiveModifyRequestControl.OID); 892 return modifyEntry0(entry, changes.getModifications(), isPermissive); 893 } 894 895 /** 896 * Applies the provided modifications to an entry using "permissive" modify 897 * semantics. 898 * 899 * @param entry 900 * The entry to be modified. 901 * @param changes 902 * The modification request to be applied to the entry. 903 * @return A reference to the updated entry. 904 * @throws LdapException 905 * If an error occurred while performing the changes such as an 906 * attempt to increment a value which is not a number. The entry 907 * may have been modified. 908 */ 909 public static Entry modifyEntryPermissive(final Entry entry, 910 final Collection<Modification> changes) throws LdapException { 911 return modifyEntry0(entry, changes, true); 912 } 913 914 /** 915 * Applies the provided modifications to an entry using "strict" modify 916 * semantics. Attempts to add duplicate values or attempts to remove values 917 * which do not exist will cause the update to fail. 918 * 919 * @param entry 920 * The entry to be modified. 921 * @param changes 922 * The modification request to be applied to the entry. 923 * @return A reference to the updated entry. 924 * @throws LdapException 925 * If an error occurred while performing the changes such as an 926 * attempt to add duplicate values, remove values which do not 927 * exist, or increment a value which is not a number. The entry 928 * may have been modified. 929 */ 930 public static Entry modifyEntryStrict(final Entry entry, final Collection<Modification> changes) 931 throws LdapException { 932 return modifyEntry0(entry, changes, false); 933 } 934 935 /** 936 * Returns a read-only view of {@code entry} and its attributes. Query 937 * operations on the returned entry and its attributes "read-through" to the 938 * underlying entry or attribute, and attempts to modify the returned entry 939 * and its attributes either directly or indirectly via an iterator result 940 * in an {@code UnsupportedOperationException}. 941 * 942 * @param entry 943 * The entry for which a read-only view is to be returned. 944 * @return A read-only view of {@code entry}. 945 * @throws NullPointerException 946 * If {@code entry} was {@code null}. 947 */ 948 public static Entry unmodifiableEntry(final Entry entry) { 949 if (entry instanceof UnmodifiableEntry) { 950 return entry; 951 } else { 952 return new UnmodifiableEntry(entry); 953 } 954 } 955 956 private static void diffAddAttribute(final ModifyRequest request, final Attribute ato, 957 final DiffOptions diffOptions) { 958 if (diffOptions.useReplaceMaxValues > 0) { 959 request.addModification(new Modification(ModificationType.REPLACE, ato)); 960 } else { 961 request.addModification(new Modification(ModificationType.ADD, ato)); 962 } 963 } 964 965 private static void diffAddValues(final ModifyRequest request, final Attribute addedValues) { 966 if (addedValues != null && !addedValues.isEmpty()) { 967 request.addModification(new Modification(ModificationType.ADD, addedValues)); 968 } 969 } 970 971 private static boolean diffAttributeNeedsReplacing(final Attribute afrom, final Attribute ato, 972 final DiffOptions options) { 973 if (afrom.size() != ato.size()) { 974 return true; 975 } else if (afrom.size() == 1) { 976 return diffFirstValuesAreDifferent(options, afrom, ato); 977 } else if (options.useExactMatching) { 978 /* 979 * Use a hash set for membership checking rather than the attribute 980 * in order to avoid matching rule based comparisons. 981 */ 982 final Set<ByteString> oldValues = new LinkedHashSet<>(afrom); 983 return !oldValues.containsAll(ato); 984 } else { 985 return !afrom.equals(ato); 986 } 987 } 988 989 private static void diffDeleteAttribute(final ModifyRequest request, final Attribute afrom, 990 final DiffOptions diffOptions) { 991 if (diffOptions.useReplaceMaxValues > 0) { 992 request.addModification(new Modification(ModificationType.REPLACE, Attributes 993 .emptyAttribute(afrom.getAttributeDescription()))); 994 } else { 995 request.addModification(new Modification(ModificationType.DELETE, afrom)); 996 } 997 } 998 999 private static void diffDeleteValues(final ModifyRequest request, final Attribute deletedValues) { 1000 if (deletedValues != null && !deletedValues.isEmpty()) { 1001 request.addModification(new Modification(ModificationType.DELETE, deletedValues)); 1002 } 1003 } 1004 1005 private static boolean diffFirstValuesAreDifferent(final DiffOptions diffOptions, 1006 final Attribute afrom, final Attribute ato) { 1007 if (diffOptions.useExactMatching) { 1008 return !afrom.firstValue().equals(ato.firstValue()); 1009 } else { 1010 return !afrom.contains(ato.firstValue()); 1011 } 1012 } 1013 1014 private static void incrementAttribute(final Entry entry, final Attribute change) 1015 throws LdapException { 1016 // First parse the change. 1017 final AttributeDescription deltaAd = change.getAttributeDescription(); 1018 if (change.size() != 1) { 1019 throw newLdapException(ResultCode.CONSTRAINT_VIOLATION, 1020 ERR_ENTRY_INCREMENT_INVALID_VALUE_COUNT.get(deltaAd.toString()).toString()); 1021 } 1022 final long delta; 1023 try { 1024 delta = change.parse().asLong(); 1025 } catch (final Exception e) { 1026 throw newLdapException(ResultCode.CONSTRAINT_VIOLATION, 1027 ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT.get(deltaAd.toString()).toString()); 1028 } 1029 1030 // Now apply the increment to the attribute. 1031 final Attribute oldAttribute = entry.getAttribute(deltaAd); 1032 if (oldAttribute == null) { 1033 throw newLdapException(ResultCode.NO_SUCH_ATTRIBUTE, 1034 ERR_ENTRY_INCREMENT_NO_SUCH_ATTRIBUTE.get(deltaAd.toString()).toString()); 1035 } 1036 1037 // Re-use existing attribute description in case it differs in case, etc. 1038 final Attribute newAttribute = new LinkedAttribute(oldAttribute.getAttributeDescription()); 1039 try { 1040 for (final Long value : oldAttribute.parse().asSetOfLong()) { 1041 newAttribute.add(value + delta); 1042 } 1043 } catch (final Exception e) { 1044 throw newLdapException(ResultCode.CONSTRAINT_VIOLATION, 1045 ERR_ENTRY_INCREMENT_CANNOT_PARSE_AS_INT.get(deltaAd.toString()).toString()); 1046 } 1047 entry.replaceAttribute(newAttribute); 1048 } 1049 1050 private static Entry modifyEntry0(final Entry entry, final Collection<Modification> changes, 1051 final boolean isPermissive) throws LdapException { 1052 final Collection<ByteString> conflictingValues = 1053 isPermissive ? null : new ArrayList<ByteString>(0); 1054 for (final Modification change : changes) { 1055 modifyEntry0(entry, change, conflictingValues, isPermissive); 1056 } 1057 return entry; 1058 } 1059 1060 private static Entry modifyEntry0(final Entry entry, final Modification change, 1061 final Collection<? super ByteString> conflictingValues, final boolean isPermissive) 1062 throws LdapException { 1063 final ModificationType modType = change.getModificationType(); 1064 if (modType.equals(ModificationType.ADD)) { 1065 entry.addAttribute(change.getAttribute(), conflictingValues); 1066 if (!isPermissive && !conflictingValues.isEmpty()) { 1067 // Duplicate values. 1068 throw newLdapException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS, 1069 ERR_ENTRY_DUPLICATE_VALUES.get( 1070 change.getAttribute().getAttributeDescriptionAsString()).toString()); 1071 } 1072 } else if (modType.equals(ModificationType.DELETE)) { 1073 final boolean hasChanged = 1074 entry.removeAttribute(change.getAttribute(), conflictingValues); 1075 if (!isPermissive && (!hasChanged || !conflictingValues.isEmpty())) { 1076 // Missing attribute or values. 1077 throw newLdapException(ResultCode.NO_SUCH_ATTRIBUTE, ERR_ENTRY_NO_SUCH_VALUE.get( 1078 change.getAttribute().getAttributeDescriptionAsString()).toString()); 1079 } 1080 } else if (modType.equals(ModificationType.REPLACE)) { 1081 entry.replaceAttribute(change.getAttribute()); 1082 } else if (modType.equals(ModificationType.INCREMENT)) { 1083 incrementAttribute(entry, change.getAttribute()); 1084 } else { 1085 throw newLdapException(ResultCode.UNWILLING_TO_PERFORM, 1086 ERR_ENTRY_UNKNOWN_MODIFICATION_TYPE.get(String.valueOf(modType)).toString()); 1087 } 1088 return entry; 1089 } 1090 1091 private static Entry toFilteredTreeMapEntry(final Entry entry, final DiffOptions options) { 1092 if (entry instanceof TreeMapEntry) { 1093 return options.filter(entry); 1094 } else { 1095 return new TreeMapEntry(options.filter(entry)); 1096 } 1097 } 1098 1099 /** Prevent instantiation. */ 1100 private Entries() { 1101 // Nothing to do. 1102 } 1103}