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