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