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&lt;Entry&gt; 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}