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     * Returns an attribute description having the same attribute type as this attribute description
531     * except that all options has been removed.
532     * <p>
533     * This method is idempotent: if this attribute description does not contain
534     * option then this attribute description will be returned.
535     *
536     * @return The new attribute description excluding all {@code option}.
537     * @throws NullPointerException
538     *             If {@code attributeDescription} or {@code option} was
539     *             {@code null}.
540     */
541    public AttributeDescription withoutAnyOptions() {
542        if (!optionsPimpl.hasOptions()) {
543            return this;
544        }
545        final String newAttributeDescription = attributeDescription.substring(0, attributeDescription.indexOf(';'));
546        return new AttributeDescription(newAttributeDescription, nameOrOid, attributeType, ZERO_OPTION_IMPL);
547    }
548
549    /**
550     * Creates an attribute description having the provided attribute type and no options.
551     *
552     * @param attributeType
553     *            The attribute type.
554     * @return The attribute description.
555     * @throws NullPointerException
556     *             If {@code attributeType} was {@code null}.
557     */
558    public static AttributeDescription create(final AttributeType attributeType) {
559        Reject.ifNull(attributeType);
560
561        // Use object identity in case attribute type does not come from core schema.
562        if (attributeType == OBJECT_CLASS.getAttributeType()) {
563            return OBJECT_CLASS;
564        }
565        String attributeName = attributeType.getNameOrOID();
566        return new AttributeDescription(attributeName, attributeName, attributeType, ZERO_OPTION_IMPL);
567    }
568
569    /**
570     * Creates an attribute description having the provided attribute name, type and no options.
571     *
572     * @param attributeName
573     *            The attribute name.
574     * @param attributeType
575     *            The attribute type.
576     * @return The attribute description.
577     * @throws NullPointerException
578     *             If {@code attributeType} was {@code null}.
579     * @deprecated This method may be removed at any time
580     * @since OPENDJ-2803 Migrate Attribute
581     */
582    @Deprecated
583    public static AttributeDescription create(final String attributeName, final AttributeType attributeType) {
584        Reject.ifNull(attributeName, attributeType);
585
586        if (attributeType == OBJECT_CLASS.getAttributeType()
587                && attributeName.equals(attributeType.getNameOrOID())) {
588            return OBJECT_CLASS;
589        }
590        return new AttributeDescription(attributeName, attributeName, attributeType, ZERO_OPTION_IMPL);
591    }
592
593    /**
594     * Creates an attribute description having the provided attribute type and single option.
595     *
596     * @param attributeType
597     *            The attribute type.
598     * @param option
599     *            The attribute option.
600     * @return The attribute description.
601     * @throws NullPointerException
602     *             If {@code attributeType} or {@code option} was {@code null}.
603     */
604    public static AttributeDescription create(final AttributeType attributeType, final String option) {
605        return create(attributeType.getNameOrOID(), attributeType, option);
606    }
607
608    /**
609     * Creates an attribute description having the provided attribute name, type and single option.
610     *
611     * @param attributeName
612     *            The attribute name.
613     * @param attributeType
614     *            The attribute type.
615     * @param option
616     *            The attribute option.
617     * @return The attribute description.
618     * @throws NullPointerException
619     *             If {@code attributeType} or {@code option} was {@code null}.
620     * @deprecated This method may be removed at any time
621     * @since OPENDJ-2803 Migrate Attribute
622     */
623    @Deprecated
624    public static AttributeDescription create(
625            final String attributeName, final AttributeType attributeType, final String option) {
626        Reject.ifNull(attributeName, attributeType, option);
627
628        final String attributeDescription = appendOption(attributeName, option);
629        final String normalizedOption = toLowerCase(option);
630
631        return new AttributeDescription(attributeDescription, attributeName, attributeType,
632            new SingleOptionImpl(option, normalizedOption));
633    }
634
635    private static String appendOption(final String oid, final String option) {
636        final StringBuilder builder = new StringBuilder(oid.length() + option.length() + 1);
637        builder.append(oid);
638        builder.append(';');
639        builder.append(option);
640        return builder.toString();
641    }
642
643    /**
644     * Creates an attribute description having the provided attribute name, type and options.
645     *
646     * @param attributeName
647     *            The attribute name.
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     * @deprecated This method may be removed at any time
656     * @since OPENDJ-2803 Migrate Attribute
657     */
658    @Deprecated
659    public static AttributeDescription create(
660            final String attributeName, final AttributeType attributeType, final String... options) {
661        Reject.ifNull(options);
662        return create(attributeName, attributeType, Arrays.asList(options));
663    }
664
665    /**
666     * Creates an attribute description having the provided attribute type and options.
667     *
668     * @param attributeType
669     *            The attribute type.
670     * @param options
671     *            The attribute options.
672     * @return The attribute description.
673     * @throws NullPointerException
674     *             If {@code attributeType} or {@code options} was {@code null}.
675     */
676    public static AttributeDescription create(final AttributeType attributeType, final String... options) {
677        Reject.ifNull(options);
678        return create(attributeType.getNameOrOID(), attributeType, Arrays.asList(options));
679    }
680
681    /**
682     * Creates an attribute description having the provided attribute type and options.
683     *
684     * @param attributeType
685     *            The attribute type.
686     * @param options
687     *            The attribute options.
688     * @return The attribute description.
689     * @throws NullPointerException
690     *             If {@code attributeType} or {@code options} was {@code null}.
691     */
692    public static AttributeDescription create(final AttributeType attributeType, final Collection<String> options) {
693        return create(attributeType.getNameOrOID(), attributeType, options);
694    }
695
696    /**
697     * Creates an attribute description having the provided attribute name, type and options.
698     *
699     * @param attributeName
700     *            The attribute name.
701     * @param attributeType
702     *            The attribute type.
703     * @param options
704     *            The attribute options.
705     * @return The attribute description.
706     * @throws NullPointerException
707     *             If {@code attributeType} or {@code options} was {@code null}.
708     * @deprecated This method may be removed at any time
709     * @since OPENDJ-2803 Migrate Attribute
710     */
711    @Deprecated
712    public static AttributeDescription create(
713            final String attributeName, final AttributeType attributeType, final Collection<String> options) {
714        Reject.ifNull(attributeName, attributeType);
715
716        final Collection<String> opts = options != null ? options : Collections.<String> emptySet();
717        switch (opts.size()) {
718        case 0:
719            return create(attributeName, attributeType);
720        case 1:
721            return create(attributeName, attributeType, opts.iterator().next());
722        default:
723            final String[] optionsList = new String[opts.size()];
724            final String[] normalizedOptions = new String[opts.size()];
725
726            final Iterator<String> it = opts.iterator();
727            final StringBuilder builder =
728                    new StringBuilder(attributeName.length() + it.next().length() + it.next().length() + 2);
729            builder.append(attributeName);
730
731            int i = 0;
732            for (final String option : opts) {
733                builder.append(';');
734                builder.append(option);
735                optionsList[i] = option;
736                normalizedOptions[i++] = toLowerCase(option);
737            }
738            Arrays.sort(normalizedOptions);
739
740            final String attributeDescription = builder.toString();
741            return new AttributeDescription(attributeDescription, attributeName, attributeType,
742                    new MultiOptionImpl(optionsList, normalizedOptions));
743        }
744    }
745
746    /**
747     * Returns an attribute description representing the object class attribute
748     * type with no options.
749     *
750     * @return The object class attribute description.
751     */
752    public static AttributeDescription objectClass() {
753        return OBJECT_CLASS;
754    }
755
756    /**
757     * Parses the provided LDAP string representation of an attribute
758     * description using the default schema.
759     *
760     * @param attributeDescription
761     *            The LDAP string representation of an attribute description.
762     * @return The parsed attribute description.
763     * @throws UnknownSchemaElementException
764     *             If {@code attributeDescription} contains an attribute type
765     *             which is not contained in the default schema and the schema
766     *             is strict.
767     * @throws LocalizedIllegalArgumentException
768     *             If {@code attributeDescription} is not a valid LDAP string
769     *             representation of an attribute description.
770     * @throws NullPointerException
771     *             If {@code attributeDescription} was {@code null}.
772     */
773    public static AttributeDescription valueOf(final String attributeDescription) {
774        return valueOf(attributeDescription, Schema.getDefaultSchema());
775    }
776
777    /**
778     * Parses the provided LDAP string representation of an attribute
779     * description using the provided schema.
780     *
781     * @param attributeDescription
782     *            The LDAP string representation of an attribute description.
783     * @param schema
784     *            The schema to use when parsing the attribute description.
785     * @return The parsed attribute description.
786     * @throws UnknownSchemaElementException
787     *             If {@code attributeDescription} contains an attribute type
788     *             which is not contained in the provided schema and the schema
789     *             is strict.
790     * @throws LocalizedIllegalArgumentException
791     *             If {@code attributeDescription} is not a valid LDAP string
792     *             representation of an attribute description.
793     * @throws NullPointerException
794     *             If {@code attributeDescription} or {@code schema} was
795     *             {@code null}.
796     */
797    public static AttributeDescription valueOf(final String attributeDescription,
798            final Schema schema) {
799        Reject.ifNull(attributeDescription, schema);
800
801        // First look up the attribute description in the cache.
802        final Map<String, Pair<Schema, AttributeDescription>> threadLocalCache = CACHE.get();
803        Pair<Schema, AttributeDescription> ad = threadLocalCache.get(attributeDescription);
804        // WARNING: When we'll support multiple schema, this schema equality check will be a problem
805        // for heavily used core attributes like "cn" which will be inherited in any sub-schema.
806        // See OPENDJ-3191
807        if (ad == null || ad.getFirst() != schema) {
808            // Cache miss: decode and cache.
809            ad = Pair.of(schema, valueOf0(attributeDescription, schema));
810            threadLocalCache.put(attributeDescription, ad);
811        }
812        return ad.getSecond();
813    }
814
815    private static int skipTrailingWhiteSpace(final String attributeDescription, int i,
816            final int length) {
817        char c;
818        while (i < length) {
819            c = attributeDescription.charAt(i);
820            if (c != ' ') {
821                final LocalizableMessage message =
822                        ERR_ATTRIBUTE_DESCRIPTION_INTERNAL_WHITESPACE.get(attributeDescription);
823                throw new LocalizedIllegalArgumentException(message);
824            }
825            i++;
826        }
827        return i;
828    }
829
830    /** Uncached valueOf implementation. */
831    private static AttributeDescription valueOf0(final String attributeDescription, final Schema schema) {
832        final boolean allowMalformedNamesAndOptions = schema.getOption(ALLOW_MALFORMED_NAMES_AND_OPTIONS);
833        int i = 0;
834        final int length = attributeDescription.length();
835        char c = 0;
836
837        // Skip leading white space.
838        while (i < length) {
839            c = attributeDescription.charAt(i);
840            if (c != ' ') {
841                break;
842            }
843            i++;
844        }
845
846        // If we're already at the end then the attribute description only
847        // contained whitespace.
848        if (i == length) {
849            final LocalizableMessage message =
850                    ERR_ATTRIBUTE_DESCRIPTION_EMPTY.get(attributeDescription);
851            throw new LocalizedIllegalArgumentException(message);
852        }
853
854        // Validate the first non-whitespace character.
855        ASCIICharProp cp = ASCIICharProp.valueOf(c);
856        if (cp == null) {
857            throw illegalCharacter(attributeDescription, i, c);
858        }
859
860        // Mark the attribute type start position.
861        final int attributeTypeStart = i;
862        if (cp.isLetter()) {
863            // Non-numeric OID: letter + zero or more keychars.
864            i++;
865            while (i < length) {
866                c = attributeDescription.charAt(i);
867                if (c == ';' || c == ' ') {
868                    break;
869                }
870
871                cp = ASCIICharProp.valueOf(c);
872                if (cp == null || !cp.isKeyChar(allowMalformedNamesAndOptions)) {
873                    throw illegalCharacter(attributeDescription, i, c);
874                }
875                i++;
876            }
877
878            // (charAt(i) == ';' || c == ' ' || i == length)
879        } else if (cp.isDigit()) {
880            // Numeric OID: decimal digit + zero or more dots or decimals.
881            i++;
882            while (i < length) {
883                c = attributeDescription.charAt(i);
884                if (c == ';' || c == ' ') {
885                    break;
886                }
887
888                cp = ASCIICharProp.valueOf(c);
889                if (cp == null || (c != '.' && !cp.isDigit())) {
890                    throw illegalCharacter(attributeDescription, i, c);
891                }
892                i++;
893            }
894
895            // (charAt(i) == ';' || charAt(i) == ' ' || i == length)
896        } else {
897            throw illegalCharacter(attributeDescription, i, c);
898        }
899
900        // Skip trailing white space.
901        final int attributeTypeEnd = i;
902        if (c == ' ') {
903            i = skipTrailingWhiteSpace(attributeDescription, i + 1, length);
904        }
905
906        // Determine the portion of the string containing the attribute type name.
907        String oid;
908        if (attributeTypeStart == 0 && attributeTypeEnd == length) {
909            oid = attributeDescription;
910        } else {
911            oid = attributeDescription.substring(attributeTypeStart, attributeTypeEnd);
912        }
913
914        if (oid.length() == 0) {
915            final LocalizableMessage message =
916                    ERR_ATTRIBUTE_DESCRIPTION_NO_TYPE.get(attributeDescription);
917            throw new LocalizedIllegalArgumentException(message);
918        }
919
920        // Get the attribute type from the schema.
921        final AttributeType attributeType = schema.getAttributeType(oid);
922
923        // If we're already at the end of the attribute description then it
924        // does not contain any options.
925        if (i == length) {
926            // Use object identity in case attribute type does not come from core schema.
927            if (attributeType == OBJECT_CLASS.getAttributeType()
928                    && attributeDescription.equals(OBJECT_CLASS.toString())) {
929                return OBJECT_CLASS;
930            }
931            return new AttributeDescription(attributeDescription, oid, attributeType, ZERO_OPTION_IMPL);
932        }
933
934        // At this point 'i' must point at a semi-colon.
935        i++;
936        StringBuilder builder = null;
937        int optionStart = i;
938        while (i < length) {
939            c = attributeDescription.charAt(i);
940            if (c == ' ' || c == ';') {
941                break;
942            }
943
944            cp = ASCIICharProp.valueOf(c);
945            if (cp == null || !cp.isKeyChar(allowMalformedNamesAndOptions)) {
946                throw illegalCharacter(attributeDescription, i, c);
947            }
948
949            if (builder == null) {
950                if (cp.isUpperCase()) {
951                    // Need to normalize the option.
952                    builder = new StringBuilder(length - optionStart);
953                    builder.append(attributeDescription, optionStart, i);
954                    builder.append(cp.toLowerCase());
955                }
956            } else {
957                builder.append(cp.toLowerCase());
958            }
959            i++;
960        }
961
962        String option = attributeDescription.substring(optionStart, i);
963        String normalizedOption;
964        if (builder != null) {
965            normalizedOption = builder.toString();
966        } else {
967            normalizedOption = option;
968        }
969
970        if (option.length() == 0) {
971            final LocalizableMessage message =
972                    ERR_ATTRIBUTE_DESCRIPTION_EMPTY_OPTION.get(attributeDescription);
973            throw new LocalizedIllegalArgumentException(message);
974        }
975
976        // Skip trailing white space.
977        if (c == ' ') {
978            i = skipTrailingWhiteSpace(attributeDescription, i + 1, length);
979        }
980
981        // If we're already at the end of the attribute description then it
982        // only contains a single option.
983        if (i == length) {
984            return new AttributeDescription(attributeDescription, oid, attributeType,
985                    new SingleOptionImpl(option, normalizedOption));
986        }
987
988        // Multiple options need sorting and duplicates removed - we could
989        // optimize a bit further here for 2 option attribute descriptions.
990        final List<String> options = new LinkedList<>();
991        options.add(option);
992
993        final SortedSet<String> normalizedOptions = new TreeSet<>();
994        normalizedOptions.add(normalizedOption);
995
996        while (i < length) {
997            // At this point 'i' must point at a semi-colon.
998            i++;
999            builder = null;
1000            optionStart = i;
1001            while (i < length) {
1002                c = attributeDescription.charAt(i);
1003                if (c == ' ' || c == ';') {
1004                    break;
1005                }
1006
1007                cp = ASCIICharProp.valueOf(c);
1008                if (cp == null || !cp.isKeyChar(allowMalformedNamesAndOptions)) {
1009                    throw illegalCharacter(attributeDescription, i, c);
1010                }
1011
1012                if (builder == null) {
1013                    if (cp.isUpperCase()) {
1014                        // Need to normalize the option.
1015                        builder = new StringBuilder(length - optionStart);
1016                        builder.append(attributeDescription, optionStart, i);
1017                        builder.append(cp.toLowerCase());
1018                    }
1019                } else {
1020                    builder.append(cp.toLowerCase());
1021                }
1022                i++;
1023            }
1024
1025            option = attributeDescription.substring(optionStart, i);
1026            if (builder != null) {
1027                normalizedOption = builder.toString();
1028            } else {
1029                normalizedOption = option;
1030            }
1031
1032            if (option.length() == 0) {
1033                final LocalizableMessage message =
1034                        ERR_ATTRIBUTE_DESCRIPTION_EMPTY_OPTION.get(attributeDescription);
1035                throw new LocalizedIllegalArgumentException(message);
1036            }
1037
1038            // Skip trailing white space.
1039            if (c == ' ') {
1040                i = skipTrailingWhiteSpace(attributeDescription, i + 1, length);
1041            }
1042
1043            if (normalizedOptions.add(normalizedOption)) {
1044                options.add(option);
1045            }
1046        }
1047
1048        final Impl pimpl = normalizedOptions.size() > 1
1049            ? new MultiOptionImpl(toArray(options), toArray(normalizedOptions))
1050            : new SingleOptionImpl(options.get(0), normalizedOptions.first());
1051        return new AttributeDescription(attributeDescription, oid, attributeType, pimpl);
1052    }
1053
1054    private static String[] toArray(final Collection<String> col) {
1055        return col.toArray(new String[col.size()]);
1056    }
1057
1058    private static LocalizedIllegalArgumentException illegalCharacter(
1059            final String attributeDescription, int i, char c) {
1060        return new LocalizedIllegalArgumentException(
1061                ERR_ATTRIBUTE_DESCRIPTION_ILLEGAL_CHARACTER.get(attributeDescription, c, i));
1062    }
1063
1064    private final String attributeDescription;
1065    private final String nameOrOid;
1066    private final AttributeType attributeType;
1067    private final Impl optionsPimpl;
1068
1069    /** Private constructor. */
1070    private AttributeDescription(final String attributeDescription, final String attributeName,
1071            final AttributeType attributeType, final Impl pimpl) {
1072        this.attributeDescription = attributeDescription;
1073        this.nameOrOid = attributeName;
1074        this.attributeType = attributeType;
1075        this.optionsPimpl = pimpl;
1076    }
1077
1078    /**
1079     * Compares this attribute description to the provided attribute
1080     * description. The attribute types are compared first and then, if equal,
1081     * the options are normalized, sorted, and compared.
1082     *
1083     * @param other
1084     *            The attribute description to be compared.
1085     * @return A negative integer, zero, or a positive integer as this attribute
1086     *         description is less than, equal to, or greater than the specified
1087     *         attribute description.
1088     * @throws NullPointerException
1089     *             If {@code name} was {@code null}.
1090     */
1091    @Override
1092    public int compareTo(final AttributeDescription other) {
1093        final int result = attributeType.compareTo(other.attributeType);
1094        if (result != 0) {
1095            return result;
1096        } else {
1097            // Attribute type is the same, so compare options.
1098            return optionsPimpl.compareTo(other.optionsPimpl);
1099        }
1100    }
1101
1102    /**
1103     * Indicates whether this attribute description contains the provided option.
1104     *
1105     * @param option
1106     *            The option for which to make the determination.
1107     * @return {@code true} if this attribute description has the provided
1108     *         option, or {@code false} if not.
1109     * @throws NullPointerException
1110     *             If {@code option} was {@code null}.
1111     */
1112    public boolean hasOption(final String option) {
1113        final String normalizedOption = toLowerCase(option);
1114        return optionsPimpl.hasOption(normalizedOption);
1115    }
1116
1117    /**
1118     * Indicates whether the provided object is an attribute description which
1119     * is equal to this attribute description. It will be considered equal if
1120     * the attribute types are {@link AttributeType#equals equal} and normalized
1121     * sorted list of options are identical.
1122     *
1123     * @param o
1124     *            The object for which to make the determination.
1125     * @return {@code true} if the provided object is an attribute description
1126     *         that is equal to this attribute description, or {@code false} if
1127     *         not.
1128     */
1129    @Override
1130    public boolean equals(final Object o) {
1131        if (this == o) {
1132            return true;
1133        } else if (o instanceof AttributeDescription) {
1134            final AttributeDescription other = (AttributeDescription) o;
1135            return attributeType.equals(other.attributeType) && optionsPimpl.equals(other.optionsPimpl);
1136        } else {
1137            return false;
1138        }
1139    }
1140
1141    /**
1142     * Returns the attribute type associated with this attribute description.
1143     *
1144     * @return The attribute type associated with this attribute description.
1145     */
1146    public AttributeType getAttributeType() {
1147        return attributeType;
1148    }
1149
1150    /**
1151     * Returns the attribute name or the oid provided by the user associated with this attribute
1152     * description.
1153     * <p>
1154     * In other words, it returns the user-provided name or oid of this attribute description,
1155     * leaving out the option(s).
1156     *
1157     * @return The attribute name or the oid provided by the user associated with this attribute
1158     *         description.
1159     * @deprecated This method may be removed at any time
1160     * @since OPENDJ-2803 Migrate Attribute
1161     */
1162    @Deprecated
1163    public String getNameOrOID() {
1164        return nameOrOid;
1165    }
1166
1167    /**
1168     * Returns an {@code Iterable} containing the options contained in this
1169     * attribute description. Attempts to remove options using an iterator's
1170     * {@code remove()} method are not permitted and will result in an
1171     * {@code UnsupportedOperationException} being thrown.
1172     *
1173     * @return An {@code Iterable} containing the options.
1174     */
1175    public Iterable<String> getOptions() {
1176        return optionsPimpl;
1177    }
1178
1179    /**
1180     * Returns the hash code for this attribute description. It will be
1181     * calculated as the sum of the hash codes of the attribute type and
1182     * normalized sorted list of options.
1183     *
1184     * @return The hash code for this attribute description.
1185     */
1186    @Override
1187    public int hashCode() {
1188        // FIXME: should we cache this?
1189        return attributeType.hashCode() * 31 + optionsPimpl.hashCode();
1190    }
1191
1192    /**
1193     * Indicates whether this attribute description has any options.
1194     *
1195     * @return {@code true} if this attribute description has any options, or
1196     *         {@code false} if not.
1197     */
1198    public boolean hasOptions() {
1199        return optionsPimpl.hasOptions();
1200    }
1201
1202    /**
1203     * Indicates whether this attribute description is the
1204     * {@code objectClass} attribute description with no options.
1205     *
1206     * @return {@code true} if this attribute description is the
1207     *         {@code objectClass} attribute description with no options, or
1208     *         {@code false} if not.
1209     */
1210    public boolean isObjectClass() {
1211        return attributeType.isObjectClass() && !hasOptions();
1212    }
1213
1214    /**
1215     * Indicates whether this attribute description is a temporary place-holder
1216     * allocated dynamically by a non-strict schema when no corresponding
1217     * registered attribute type was found.
1218     * <p>
1219     * Place holder attribute descriptions have an attribute type whose OID is
1220     * the normalized attribute name with the string {@code -oid} appended. In
1221     * addition, they will use the directory string syntax and case ignore
1222     * matching rule.
1223     *
1224     * @return {@code true} if this is a temporary place-holder attribute
1225     *         description allocated dynamically by a non-strict schema when no
1226     *         corresponding registered attribute type was found.
1227     * @see Schema#getAttributeType(String)
1228     * @see AttributeType#isPlaceHolder()
1229     */
1230    public boolean isPlaceHolder() {
1231        return attributeType.isPlaceHolder();
1232    }
1233
1234    /**
1235     * Indicates whether this attribute description is a sub-type of the
1236     * provided attribute description as defined in RFC 4512 section 2.5.
1237     * Specifically, this method will return {@code true} if and only if the
1238     * following conditions are both {@code true}:
1239     * <ul>
1240     * <li>This attribute description has an attribute type which
1241     * {@link AttributeType#matches matches}, or is a sub-type of, the attribute
1242     * type in the provided attribute description.
1243     * <li>This attribute description contains all of the options contained in
1244     * the provided attribute description.
1245     * </ul>
1246     * Note that this method will return {@code true} if this attribute
1247     * description is equal to the provided attribute description.
1248     *
1249     * @param other
1250     *            The attribute description for which to make the determination.
1251     * @return {@code true} if this attribute description is a sub-type of the
1252     *         provided attribute description, or {@code false} if not.
1253     * @throws NullPointerException
1254     *             If {@code name} was {@code null}.
1255     */
1256    public boolean isSubTypeOf(final AttributeDescription other) {
1257        return attributeType.isSubTypeOf(other.attributeType)
1258            && optionsPimpl.isSubTypeOf(other.optionsPimpl);
1259    }
1260
1261    /**
1262     * Indicates whether this attribute description is a super-type of
1263     * the provided attribute description as defined in RFC 4512 section 2.5.
1264     * Specifically, this method will return {@code true} if and only if the
1265     * following conditions are both {@code true}:
1266     * <ul>
1267     * <li>This attribute description has an attribute type which
1268     * {@link AttributeType#matches matches}, or is a super-type of, the
1269     * attribute type in the provided attribute description.
1270     * <li>This attribute description contains a sub-set of the options
1271     * contained in the provided attribute description.
1272     * </ul>
1273     * Note that this method will return {@code true} if this attribute
1274     * description is equal to the provided attribute description.
1275     *
1276     * @param other
1277     *            The attribute description for which to make the determination.
1278     * @return {@code true} if this attribute description is a super-type of the
1279     *         provided attribute description, or {@code false} if not.
1280     * @throws NullPointerException
1281     *             If {@code name} was {@code null}.
1282     */
1283    public boolean isSuperTypeOf(final AttributeDescription other) {
1284        return attributeType.isSuperTypeOf(other.attributeType)
1285            && optionsPimpl.isSuperTypeOf(other.optionsPimpl);
1286    }
1287
1288    /**
1289     * Indicates whether the provided attribute description matches this
1290     * attribute description. It will be considered a match if the attribute
1291     * types {@link AttributeType#matches match} and the normalized sorted list
1292     * of options are identical.
1293     *
1294     * @param other
1295     *            The attribute description for which to make the determination.
1296     * @return {@code true} if the provided attribute description matches this
1297     *         attribute description, or {@code false} if not.
1298     */
1299    public boolean matches(final AttributeDescription other) {
1300        if (this == other) {
1301            return true;
1302        } else {
1303            return attributeType.matches(other.attributeType) && optionsPimpl.equals(other.optionsPimpl);
1304        }
1305    }
1306
1307    /**
1308     * Returns the string representation of this attribute description as
1309     * defined in RFC4512 section 2.5.
1310     *
1311     * @return The string representation of this attribute description.
1312     */
1313    @Override
1314    public String toString() {
1315        return attributeDescription;
1316    }
1317}