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