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 2014 Manuel Gaupp
016 * Portions Copyright 2011-2016 ForgeRock AS.
017 */
018package org.forgerock.opendj.ldap.schema;
019
020import static java.util.Collections.*;
021
022import static org.forgerock.opendj.ldap.LdapException.*;
023import static org.forgerock.opendj.ldap.schema.ObjectClass.*;
024import static org.forgerock.opendj.ldap.schema.ObjectClassType.*;
025import static org.forgerock.opendj.ldap.schema.Schema.*;
026import static org.forgerock.opendj.ldap.schema.SchemaConstants.*;
027import static org.forgerock.opendj.ldap.schema.SchemaOptions.*;
028import static org.forgerock.opendj.ldap.schema.SchemaUtils.*;
029
030import static com.forgerock.opendj.ldap.CoreMessages.*;
031import static com.forgerock.opendj.util.StaticUtils.*;
032
033import java.util.ArrayList;
034import java.util.Collection;
035import java.util.Collections;
036import java.util.HashMap;
037import java.util.LinkedHashMap;
038import java.util.LinkedList;
039import java.util.List;
040import java.util.Map;
041import java.util.Set;
042import java.util.concurrent.atomic.AtomicInteger;
043import java.util.regex.Pattern;
044
045import org.forgerock.i18n.LocalizableMessage;
046import org.forgerock.i18n.LocalizedIllegalArgumentException;
047import org.forgerock.opendj.ldap.Attribute;
048import org.forgerock.opendj.ldap.ByteString;
049import org.forgerock.opendj.ldap.Connection;
050import org.forgerock.opendj.ldap.DN;
051import org.forgerock.opendj.ldap.DecodeException;
052import org.forgerock.opendj.ldap.Entry;
053import org.forgerock.opendj.ldap.EntryNotFoundException;
054import org.forgerock.opendj.ldap.Filter;
055import org.forgerock.opendj.ldap.LdapException;
056import org.forgerock.opendj.ldap.LdapPromise;
057import org.forgerock.opendj.ldap.ResultCode;
058import org.forgerock.opendj.ldap.SearchScope;
059import org.forgerock.opendj.ldap.requests.Requests;
060import org.forgerock.opendj.ldap.requests.SearchRequest;
061import org.forgerock.opendj.ldap.responses.SearchResultEntry;
062import org.forgerock.opendj.ldap.schema.DITContentRule.Builder;
063import org.forgerock.util.AsyncFunction;
064import org.forgerock.util.Function;
065import org.forgerock.util.Option;
066import org.forgerock.util.Options;
067import org.forgerock.util.Reject;
068import org.forgerock.util.promise.Promise;
069
070import com.forgerock.opendj.util.StaticUtils;
071import com.forgerock.opendj.util.SubstringReader;
072
073/** Schema builders should be used for incremental construction of new schemas. */
074public final class SchemaBuilder {
075
076    /** Constant used for name to oid mapping when one name actually maps to multiple numerical OID. */
077    public static final String AMBIGUOUS_OID = "<ambiguous-oid>";
078
079    private static final String ATTR_SUBSCHEMA_SUBENTRY = "subschemaSubentry";
080
081    private static final String[] SUBSCHEMA_ATTRS = { ATTR_LDAP_SYNTAXES,
082        ATTR_ATTRIBUTE_TYPES, ATTR_DIT_CONTENT_RULES, ATTR_DIT_STRUCTURE_RULES,
083        ATTR_MATCHING_RULE_USE, ATTR_MATCHING_RULES, ATTR_NAME_FORMS, ATTR_OBJECT_CLASSES };
084
085    private static final Filter SUBSCHEMA_FILTER = Filter.valueOf("(objectClass=subschema)");
086
087    private static final String[] SUBSCHEMA_SUBENTRY_ATTRS = { ATTR_SUBSCHEMA_SUBENTRY };
088
089    /**
090     * Constructs a search request for retrieving the subschemaSubentry
091     * attribute from the named entry.
092     */
093    private static SearchRequest getReadSchemaForEntrySearchRequest(final DN dn) {
094        return Requests.newSearchRequest(dn, SearchScope.BASE_OBJECT, Filter.objectClassPresent(),
095                SUBSCHEMA_SUBENTRY_ATTRS);
096    }
097
098    /** Constructs a search request for retrieving the named subschema sub-entry. */
099    private static SearchRequest getReadSchemaSearchRequest(final DN dn) {
100        return Requests.newSearchRequest(dn, SearchScope.BASE_OBJECT, SUBSCHEMA_FILTER,
101                SUBSCHEMA_ATTRS);
102    }
103
104    private static DN getSubschemaSubentryDN(final DN name, final Entry entry) throws LdapException {
105        final Attribute subentryAttr = entry.getAttribute(ATTR_SUBSCHEMA_SUBENTRY);
106
107        if (subentryAttr == null || subentryAttr.isEmpty()) {
108            // Did not get the subschema sub-entry attribute.
109            throw newLdapException(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED,
110                    ERR_NO_SUBSCHEMA_SUBENTRY_ATTR.get(name.toString()).toString());
111        }
112
113        final String dnString = subentryAttr.iterator().next().toString();
114        DN subschemaDN;
115        try {
116            subschemaDN = DN.valueOf(dnString);
117        } catch (final LocalizedIllegalArgumentException e) {
118            throw newLdapException(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED,
119                    ERR_INVALID_SUBSCHEMA_SUBENTRY_ATTR.get(name.toString(), dnString,
120                            e.getMessageObject()).toString());
121        }
122        return subschemaDN;
123    }
124
125    /** Allows to perform modifications on element's builders before adding the result to this schema builder. */
126    public interface SchemaBuilderHook {
127        /**
128         * Allow to modify the builder before its inclusion in schema.
129         *
130         * @param builder
131         *            Schema's element builder.
132         */
133        public void beforeAddSyntax(Syntax.Builder builder);
134
135        /**
136         * Allow to modify the builder before its inclusion in schema.
137         *
138         * @param builder
139         *            Schema's element builder.
140         */
141        public void beforeAddAttribute(AttributeType.Builder builder);
142
143        /**
144         * Allow to modify the builder before its inclusion in schema.
145         *
146         * @param builder
147         *            Schema's element builder.
148         */
149        public void beforeAddObjectClass(ObjectClass.Builder builder);
150
151        /**
152         * Allow to modify the builder before its inclusion in schema.
153         *
154         * @param builder
155         *            Schema's element builder.
156         */
157        public void beforeAddMatchingRuleUse(MatchingRuleUse.Builder builder);
158
159        /**
160         * Allow to modify the builder before its inclusion in schema.
161         *
162         * @param builder
163         *            Schema's element builder.
164         */
165        public void beforeAddMatchingRule(MatchingRule.Builder builder);
166
167        /**
168         * Allow to modify the builder before its inclusion in schema.
169         *
170         * @param builder
171         *            Schema's element builder.
172         */
173        public void beforeAddDitContentRule(DITContentRule.Builder builder);
174
175        /**
176         * Allow to modify the builder before its inclusion in schema.
177         *
178         * @param builder
179         *            Schema's element builder.
180         */
181        public void beforeAddDitStructureRule(DITStructureRule.Builder builder);
182
183        /**
184         * Allow to modify the builder before its inclusion in schema.
185         *
186         * @param builder
187         *            Schema's element builder.
188         */
189        public void beforeAddNameForm(NameForm.Builder builder);
190    }
191
192    private Map<Integer, DITStructureRule> id2StructureRules;
193    private Map<String, List<AttributeType>> name2AttributeTypes;
194    private Map<String, List<DITContentRule>> name2ContentRules;
195    private Map<String, List<MatchingRule>> name2MatchingRules;
196    private Map<String, List<MatchingRuleUse>> name2MatchingRuleUses;
197    private Map<String, List<NameForm>> name2NameForms;
198    private Map<String, List<ObjectClass>> name2ObjectClasses;
199    private Map<String, List<DITStructureRule>> name2StructureRules;
200    private Map<String, List<DITStructureRule>> nameForm2StructureRules;
201    private Map<String, AttributeType> numericOID2AttributeTypes;
202    private Map<String, DITContentRule> numericOID2ContentRules;
203    private Map<String, MatchingRule> numericOID2MatchingRules;
204    private Map<String, MatchingRuleUse> numericOID2MatchingRuleUses;
205    private Map<String, NameForm> numericOID2NameForms;
206    private Map<String, ObjectClass> numericOID2ObjectClasses;
207    private Map<String, Syntax> numericOID2Syntaxes;
208    private Map<String, List<NameForm>> objectClass2NameForms;
209    private String schemaName;
210    private List<LocalizableMessage> warnings;
211    private Options options;
212
213    /** A schema which should be copied into this builder on any mutation. */
214    private Schema copyOnWriteSchema;
215
216    /** A unique ID which can be used to uniquely identify schemas constructed without a name. */
217    private static final AtomicInteger NEXT_SCHEMA_ID = new AtomicInteger();
218
219    /** Creates a new schema builder with no schema elements and default compatibility options. */
220    public SchemaBuilder() {
221        preLazyInitBuilder(null, null);
222    }
223
224    /**
225     * Creates a new schema builder containing all of the schema elements
226     * contained in the provided subschema subentry. Any problems encountered
227     * while parsing the entry can be retrieved using the returned schema's
228     * {@link Schema#getWarnings()} method.
229     *
230     * @param entry
231     *            The subschema subentry to be parsed.
232     * @throws NullPointerException
233     *             If {@code entry} was {@code null}.
234     */
235    public SchemaBuilder(final Entry entry) {
236        preLazyInitBuilder(entry.getName().toString(), null);
237        addSchema(entry, true, null);
238    }
239
240    /**
241     * Creates a new schema builder containing all of the schema elements from
242     * the provided schema and its compatibility options.
243     *
244     * @param schema
245     *            The initial contents of the schema builder.
246     * @throws NullPointerException
247     *             If {@code schema} was {@code null}.
248     */
249    public SchemaBuilder(final Schema schema) {
250        preLazyInitBuilder(schema.getSchemaName(), schema);
251    }
252
253    /**
254     * Creates a new schema builder with no schema elements and default
255     * compatibility options.
256     *
257     * @param schemaName
258     *            The user-friendly name of this schema which may be used for
259     *            debugging purposes.
260     */
261    public SchemaBuilder(final String schemaName) {
262        preLazyInitBuilder(schemaName, null);
263    }
264
265    private Boolean allowsMalformedNamesAndOptions() {
266        return options.get(ALLOW_MALFORMED_NAMES_AND_OPTIONS);
267    }
268
269    /**
270     * Adds the provided attribute type definition to this schema builder.
271     *
272     * @param definition
273     *            The attribute type definition.
274     * @param overwrite
275     *            {@code true} if any existing attribute type with the same OID
276     *            should be overwritten.
277     * @return A reference to this schema builder.
278     * @throws ConflictingSchemaElementException
279     *             If {@code overwrite} was {@code false} and a conflicting
280     *             schema element was found.
281     * @throws LocalizedIllegalArgumentException
282     *             If the provided attribute type definition could not be
283     *             parsed.
284     * @throws NullPointerException
285     *             If {@code definition} was {@code null}.
286     */
287    public SchemaBuilder addAttributeType(final String definition, final boolean overwrite) {
288        return addAttributeType(definition, overwrite, null);
289    }
290
291    SchemaBuilder addAttributeType(final String definition, final boolean overwrite, SchemaBuilderHook hook) {
292        Reject.ifNull(definition);
293
294        lazyInitBuilder();
295
296        try {
297            final SubstringReader reader = new SubstringReader(definition);
298
299            // We'll do this a character at a time. First, skip over any
300            // leading whitespace.
301            reader.skipWhitespaces();
302
303            if (reader.remaining() <= 0) {
304                // This means that the definition was empty or contained only
305                // whitespace. That is illegal.
306                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_ATTRTYPE_EMPTY_VALUE1.get(definition));
307            }
308
309            // The next character must be an open parenthesis. If it is not,
310            // then that is an error.
311            final char c = reader.read();
312            if (c != '(') {
313                final LocalizableMessage message = ERR_ATTR_SYNTAX_ATTRTYPE_EXPECTED_OPEN_PARENTHESIS.get(
314                    definition, reader.pos() - 1, String.valueOf(c));
315                throw new LocalizedIllegalArgumentException(message);
316            }
317
318            // Skip over any spaces immediately following the opening
319            // parenthesis.
320            reader.skipWhitespaces();
321
322            // The next set of characters must be the OID.
323            final String oid = readOID(reader, allowsMalformedNamesAndOptions());
324            AttributeType.Builder atBuilder = new AttributeType.Builder(oid, this);
325            atBuilder.definition(definition);
326            String superiorType = null;
327            String syntax = null;
328            // At this point, we should have a pretty specific syntax that
329            // describes what may come next, but some of the components are
330            // optional and it would be pretty easy to put something in the
331            // wrong order, so we will be very flexible about what we can
332            // accept. Just look at the next token, figure out what it is and
333            // how to treat what comes after it, then repeat until we get to
334            // the end of the definition. But before we start, set default
335            // values for everything else we might need to know.
336            while (true) {
337                final String tokenName = readTokenName(reader);
338
339                if (tokenName == null) {
340                    // No more tokens.
341                    break;
342                } else if ("name".equalsIgnoreCase(tokenName)) {
343                    atBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
344                } else if ("desc".equalsIgnoreCase(tokenName)) {
345                    // This specifies the description for the attribute type. It
346                    // is an arbitrary string of characters enclosed in single
347                    // quotes.
348                    atBuilder.description(readQuotedString(reader));
349                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
350                    // This indicates whether the attribute type should be
351                    // considered obsolete.
352                    atBuilder.obsolete(true);
353                } else if ("sup".equalsIgnoreCase(tokenName)) {
354                    // This specifies the name or OID of the superior attribute
355                    // type from which this attribute type should inherit its
356                    // properties.
357                    superiorType = readOID(reader, allowsMalformedNamesAndOptions());
358                } else if ("equality".equalsIgnoreCase(tokenName)) {
359                    // This specifies the name or OID of the equality matching
360                    // rule to use for this attribute type.
361                    atBuilder.equalityMatchingRule(readOID(reader, allowsMalformedNamesAndOptions()));
362                } else if ("ordering".equalsIgnoreCase(tokenName)) {
363                    // This specifies the name or OID of the ordering matching
364                    // rule to use for this attribute type.
365                    atBuilder.orderingMatchingRule(readOID(reader, allowsMalformedNamesAndOptions()));
366                } else if ("substr".equalsIgnoreCase(tokenName)) {
367                    // This specifies the name or OID of the substring matching
368                    // rule to use for this attribute type.
369                    atBuilder.substringMatchingRule(readOID(reader, allowsMalformedNamesAndOptions()));
370                } else if ("syntax".equalsIgnoreCase(tokenName)) {
371                    // This specifies the numeric OID of the syntax for this
372                    // matching rule. It may optionally be immediately followed
373                    // by an open curly brace, an integer definition, and a close
374                    // curly brace to suggest the minimum number of characters
375                    // that should be allowed in values of that type. This
376                    // implementation will ignore any such length because it
377                    // does not impose any practical limit on the length of attribute
378                    // values.
379                    syntax = readOIDLen(reader, allowsMalformedNamesAndOptions());
380                } else if ("single-value".equalsIgnoreCase(tokenName)) {
381                    // This indicates that attributes of this type are allowed
382                    // to have at most one value.
383                    atBuilder.singleValue(true);
384                } else if ("collective".equalsIgnoreCase(tokenName)) {
385                    // This indicates that attributes of this type are collective
386                    // (i.e., have their values generated dynamically in some way).
387                    atBuilder.collective(true);
388                } else if ("no-user-modification".equalsIgnoreCase(tokenName)) {
389                    // This indicates that the values of attributes of this type
390                    // are not to be modified by end users.
391                    atBuilder.noUserModification(true);
392                } else if ("usage".equalsIgnoreCase(tokenName)) {
393                    // This specifies the usage string for this attribute type.
394                    // It should be followed by one of the strings
395                    // "userApplications", "directoryOperation",
396                    // "distributedOperation", or "dSAOperation".
397                    int length = 0;
398
399                    reader.skipWhitespaces();
400                    reader.mark();
401
402                    while (" )".indexOf(reader.read()) == -1) {
403                        length++;
404                    }
405                    reader.reset();
406                    final String usageStr = reader.read(length);
407                    if ("userapplications".equalsIgnoreCase(usageStr)) {
408                        atBuilder.usage(AttributeUsage.USER_APPLICATIONS);
409                    } else if ("directoryoperation".equalsIgnoreCase(usageStr)) {
410                        atBuilder.usage(AttributeUsage.DIRECTORY_OPERATION);
411                    } else if ("distributedoperation".equalsIgnoreCase(usageStr)) {
412                        atBuilder.usage(AttributeUsage.DISTRIBUTED_OPERATION);
413                    } else if ("dsaoperation".equalsIgnoreCase(usageStr)) {
414                        atBuilder.usage(AttributeUsage.DSA_OPERATION);
415                    } else {
416                        throw new LocalizedIllegalArgumentException(
417                            WARN_ATTR_SYNTAX_ATTRTYPE_INVALID_ATTRIBUTE_USAGE1.get(definition, usageStr));
418                    }
419                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
420                    // This must be a non-standard property and it must be
421                    // followed by either a single definition in single quotes
422                    // or an open parenthesis followed by one or more values in
423                    // single quotes separated by spaces followed by a close
424                    // parenthesis.
425                    atBuilder.extraProperties(tokenName, readExtensions(reader));
426                } else {
427                    throw new LocalizedIllegalArgumentException(
428                        ERR_ATTR_SYNTAX_ATTRTYPE_ILLEGAL_TOKEN1.get(definition, tokenName));
429                }
430            }
431
432            final List<String> approxRules = atBuilder.getExtraProperties().get(SCHEMA_PROPERTY_APPROX_RULE);
433            if (approxRules != null && !approxRules.isEmpty()) {
434                atBuilder.approximateMatchingRule(approxRules.get(0));
435            }
436
437            if (superiorType == null && syntax == null && !options.get(ALLOW_ATTRIBUTE_TYPES_WITH_NO_SUP_OR_SYNTAX)) {
438                throw new LocalizedIllegalArgumentException(
439                    WARN_ATTR_SYNTAX_ATTRTYPE_MISSING_SYNTAX_AND_SUPERIOR.get(definition));
440            }
441
442            atBuilder.superiorType(superiorType)
443                     .syntax(syntax);
444
445            if (hook != null) {
446                hook.beforeAddAttribute(atBuilder);
447            }
448            return atBuilder.addToSchema(overwrite);
449        } catch (final DecodeException e) {
450            final LocalizableMessage msg = ERR_ATTR_SYNTAX_ATTRTYPE_INVALID1.get(definition, e.getMessageObject());
451            throw new LocalizedIllegalArgumentException(msg, e.getCause());
452        }
453    }
454
455    /**
456     * Adds the provided DIT content rule definition to this schema builder.
457     *
458     * @param definition
459     *            The DIT content rule definition.
460     * @param overwrite
461     *            {@code true} if any existing DIT content rule with the same
462     *            OID should be overwritten.
463     * @return A reference to this schema builder.
464     * @throws ConflictingSchemaElementException
465     *             If {@code overwrite} was {@code false} and a conflicting
466     *             schema element was found.
467     * @throws LocalizedIllegalArgumentException
468     *             If the provided DIT content rule definition could not be
469     *             parsed.
470     * @throws NullPointerException
471     *             If {@code definition} was {@code null}.
472     */
473    public SchemaBuilder addDITContentRule(final String definition, final boolean overwrite) {
474        return addDITContentRule(definition, overwrite, null);
475    }
476
477    SchemaBuilder addDITContentRule(final String definition, final boolean overwrite, SchemaBuilderHook hook) {
478        Reject.ifNull(definition);
479
480        lazyInitBuilder();
481
482        try {
483            final SubstringReader reader = new SubstringReader(definition);
484
485            // We'll do this a character at a time. First, skip over any
486            // leading whitespace.
487            reader.skipWhitespaces();
488
489            if (reader.remaining() <= 0) {
490                // This means that the value was empty or contained only
491                // whitespace. That is illegal.
492                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DCR_EMPTY_VALUE1.get(definition));
493            }
494
495            // The next character must be an open parenthesis. If it is not,
496            // then that is an error.
497            final char c = reader.read();
498            if (c != '(') {
499                final LocalizableMessage message = ERR_ATTR_SYNTAX_DCR_EXPECTED_OPEN_PARENTHESIS.get(
500                    definition, reader.pos() - 1, String.valueOf(c));
501                throw new LocalizedIllegalArgumentException(message);
502            }
503
504            // Skip over any spaces immediately following the opening
505            // parenthesis.
506            reader.skipWhitespaces();
507
508            // The next set of characters must be the OID.
509            final DITContentRule.Builder contentRuleBuilder =
510                    buildDITContentRule(readOID(reader, allowsMalformedNamesAndOptions()));
511            contentRuleBuilder.definition(definition);
512
513            // At this point, we should have a pretty specific syntax that
514            // describes what may come next, but some of the components are
515            // optional and it would be pretty easy to put something in the
516            // wrong order, so we will be very flexible about what we can
517            // accept. Just look at the next token, figure out what it is and
518            // how to treat what comes after it, then repeat until we get to
519            // the end of the value. But before we start, set default values
520            // for everything else we might need to know.
521            while (true) {
522                final String tokenName = readTokenName(reader);
523
524                if (tokenName == null) {
525                    // No more tokens.
526                    break;
527                } else if ("name".equalsIgnoreCase(tokenName)) {
528                    contentRuleBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
529                } else if ("desc".equalsIgnoreCase(tokenName)) {
530                    // This specifies the description for the attribute type. It
531                    // is an arbitrary string of characters enclosed in single
532                    // quotes.
533                    contentRuleBuilder.description(readQuotedString(reader));
534                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
535                    // This indicates whether the attribute type should be
536                    // considered obsolete.
537                    contentRuleBuilder.obsolete(true);
538                } else if ("aux".equalsIgnoreCase(tokenName)) {
539                    contentRuleBuilder.auxiliaryObjectClasses(readOIDs(reader, allowsMalformedNamesAndOptions()));
540                } else if ("must".equalsIgnoreCase(tokenName)) {
541                    contentRuleBuilder.requiredAttributes(readOIDs(reader, allowsMalformedNamesAndOptions()));
542                } else if ("may".equalsIgnoreCase(tokenName)) {
543                    contentRuleBuilder.optionalAttributes(readOIDs(reader, allowsMalformedNamesAndOptions()));
544                } else if ("not".equalsIgnoreCase(tokenName)) {
545                    contentRuleBuilder.prohibitedAttributes(readOIDs(reader, allowsMalformedNamesAndOptions()));
546                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
547                    // This must be a non-standard property and it must be
548                    // followed by either a single definition in single quotes
549                    // or an open parenthesis followed by one or more values in
550                    // single quotes separated by spaces followed by a close
551                    // parenthesis.
552                    contentRuleBuilder.extraProperties(tokenName, readExtensions(reader));
553                } else {
554                    throw new LocalizedIllegalArgumentException(
555                        ERR_ATTR_SYNTAX_DCR_ILLEGAL_TOKEN1.get(definition, tokenName));
556                }
557            }
558
559            if (hook != null) {
560                hook.beforeAddDitContentRule(contentRuleBuilder);
561            }
562            return contentRuleBuilder.addToSchema(overwrite);
563        } catch (final DecodeException e) {
564            final LocalizableMessage msg = ERR_ATTR_SYNTAX_DCR_INVALID1.get(definition, e.getMessageObject());
565            throw new LocalizedIllegalArgumentException(msg, e.getCause());
566        }
567    }
568
569    /**
570     * Adds the provided DIT structure rule definition to this schema builder.
571     *
572     * @param definition
573     *            The DIT structure rule definition.
574     * @param overwrite
575     *            {@code true} if any existing DIT structure rule with the same
576     *            OID should be overwritten.
577     * @return A reference to this schema builder.
578     * @throws ConflictingSchemaElementException
579     *             If {@code overwrite} was {@code false} and a conflicting
580     *             schema element was found.
581     * @throws LocalizedIllegalArgumentException
582     *             If the provided DIT structure rule definition could not be
583     *             parsed.
584     * @throws NullPointerException
585     *             If {@code definition} was {@code null}.
586     */
587    public SchemaBuilder addDITStructureRule(final String definition, final boolean overwrite) {
588        return addDITStructureRule(definition, overwrite, null);
589    }
590
591    SchemaBuilder addDITStructureRule(final String definition, final boolean overwrite, SchemaBuilderHook hook) {
592        Reject.ifNull(definition);
593
594        lazyInitBuilder();
595
596        try {
597            final SubstringReader reader = new SubstringReader(definition);
598
599            // We'll do this a character at a time. First, skip over any
600            // leading whitespace.
601            reader.skipWhitespaces();
602
603            if (reader.remaining() <= 0) {
604                // This means that the value was empty or contained only
605                // whitespace. That is illegal.
606                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DSR_EMPTY_VALUE1.get(definition));
607            }
608
609            // The next character must be an open parenthesis. If it is not,
610            // then that is an error.
611            final char c = reader.read();
612            if (c != '(') {
613                final LocalizableMessage message = ERR_ATTR_SYNTAX_DSR_EXPECTED_OPEN_PARENTHESIS.get(
614                    definition, reader.pos() - 1, String.valueOf(c));
615                throw new LocalizedIllegalArgumentException(message);
616            }
617
618            // Skip over any spaces immediately following the opening
619            // parenthesis.
620            reader.skipWhitespaces();
621
622            // The next set of characters must be the OID.
623            final DITStructureRule.Builder ruleBuilder = new DITStructureRule.Builder(readRuleID(reader), this);
624            String nameForm = null;
625
626            // At this point, we should have a pretty specific syntax that
627            // describes what may come next, but some of the components are
628            // optional and it would be pretty easy to put something in the
629            // wrong order, so we will be very flexible about what we can
630            // accept. Just look at the next token, figure out what it is and
631            // how to treat what comes after it, then repeat until we get to
632            // the end of the value. But before we start, set default values
633            // for everything else we might need to know.
634            while (true) {
635                final String tokenName = readTokenName(reader);
636
637                if (tokenName == null) {
638                    // No more tokens.
639                    break;
640                } else if ("name".equalsIgnoreCase(tokenName)) {
641                    ruleBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
642                } else if ("desc".equalsIgnoreCase(tokenName)) {
643                    // This specifies the description for the attribute type. It
644                    // is an arbitrary string of characters enclosed in single
645                    // quotes.
646                    ruleBuilder.description(readQuotedString(reader));
647                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
648                    // This indicates whether the attribute type should be
649                    // considered obsolete.
650                    ruleBuilder.obsolete(true);
651                } else if ("form".equalsIgnoreCase(tokenName)) {
652                    nameForm = readOID(reader, allowsMalformedNamesAndOptions());
653                } else if ("sup".equalsIgnoreCase(tokenName)) {
654                    ruleBuilder.superiorRules(readRuleIDs(reader));
655                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
656                    // This must be a non-standard property and it must be
657                    // followed by either a single definition in single quotes
658                    // or an open parenthesis followed by one or more values in
659                    // single quotes separated by spaces followed by a close
660                    // parenthesis.
661                    ruleBuilder.extraProperties(tokenName, readExtensions(reader));
662                } else {
663                    throw new LocalizedIllegalArgumentException(
664                            ERR_ATTR_SYNTAX_DSR_ILLEGAL_TOKEN1.get(definition, tokenName));
665                }
666            }
667
668            if (nameForm == null) {
669                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_DSR_NO_NAME_FORM.get(definition));
670            }
671            ruleBuilder.nameForm(nameForm);
672
673            if (hook != null) {
674                hook.beforeAddDitStructureRule(ruleBuilder);
675            }
676            return ruleBuilder.addToSchema(overwrite);
677        } catch (final DecodeException e) {
678            final LocalizableMessage msg = ERR_ATTR_SYNTAX_DSR_INVALID1.get(definition, e.getMessageObject());
679            throw new LocalizedIllegalArgumentException(msg, e.getCause());
680        }
681    }
682
683    /**
684     * Adds the provided enumeration syntax definition to this schema builder.
685     *
686     * @param oid
687     *            The OID of the enumeration syntax definition.
688     * @param description
689     *            The description of the enumeration syntax definition.
690     * @param overwrite
691     *            {@code true} if any existing syntax with the same OID should
692     *            be overwritten.
693     * @param enumerations
694     *            The range of values which attribute values must match in order
695     *            to be valid.
696     * @return A reference to this schema builder.
697     * @throws ConflictingSchemaElementException
698     *             If {@code overwrite} was {@code false} and a conflicting
699     *             schema element was found.
700     */
701    public SchemaBuilder addEnumerationSyntax(final String oid, final String description,
702            final boolean overwrite, final String... enumerations) {
703        Reject.ifNull((Object) enumerations);
704        return buildSyntax(oid)
705                .description(description)
706                .extraProperties("X-ENUM", enumerations)
707                .addToSchema(overwrite);
708    }
709
710    /**
711     * Adds the provided matching rule definition to this schema builder.
712     *
713     * @param definition
714     *            The matching rule definition.
715     * @param overwrite
716     *            {@code true} if any existing matching rule with the same OID
717     *            should be overwritten.
718     * @return A reference to this schema builder.
719     * @throws ConflictingSchemaElementException
720     *             If {@code overwrite} was {@code false} and a conflicting
721     *             schema element was found.
722     * @throws LocalizedIllegalArgumentException
723     *             If the provided matching rule definition could not be parsed.
724     * @throws NullPointerException
725     *             If {@code definition} was {@code null}.
726     */
727    public SchemaBuilder addMatchingRule(final String definition, final boolean overwrite) {
728        return addMatchingRule(definition, overwrite, null);
729    }
730
731    SchemaBuilder addMatchingRule(final String definition, final boolean overwrite, SchemaBuilderHook hook) {
732        Reject.ifNull(definition);
733
734        lazyInitBuilder();
735
736        try {
737            final SubstringReader reader = new SubstringReader(definition);
738
739            // We'll do this a character at a time. First, skip over any
740            // leading whitespace.
741            reader.skipWhitespaces();
742
743            if (reader.remaining() <= 0) {
744                // This means that the value was empty or contained only
745                // whitespace. That is illegal.
746                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_MR_EMPTY_VALUE1.get(definition));
747            }
748
749            // The next character must be an open parenthesis. If it is not,
750            // then that is an error.
751            final char c = reader.read();
752            if (c != '(') {
753                final LocalizableMessage message = ERR_ATTR_SYNTAX_MR_EXPECTED_OPEN_PARENTHESIS.get(
754                    definition, reader.pos() - 1, String.valueOf(c));
755                throw new LocalizedIllegalArgumentException(message);
756            }
757
758            // Skip over any spaces immediately following the opening
759            // parenthesis.
760            reader.skipWhitespaces();
761
762            // The next set of characters must be the OID.
763            final MatchingRule.Builder matchingRuleBuilder = new MatchingRule.Builder(
764                readOID(reader, allowsMalformedNamesAndOptions()), this);
765            matchingRuleBuilder.definition(definition);
766
767            String syntax = null;
768            // At this point, we should have a pretty specific syntax that
769            // describes what may come next, but some of the components are
770            // optional and it would be pretty easy to put something in the
771            // wrong order, so we will be very flexible about what we can
772            // accept. Just look at the next token, figure out what it is and
773            // how to treat what comes after it, then repeat until we get to
774            // the end of the value. But before we start, set default values
775            // for everything else we might need to know.
776            while (true) {
777                final String tokenName = readTokenName(reader);
778
779                if (tokenName == null) {
780                    // No more tokens.
781                    break;
782                } else if ("name".equalsIgnoreCase(tokenName)) {
783                    matchingRuleBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
784                } else if ("desc".equalsIgnoreCase(tokenName)) {
785                    // This specifies the description for the matching rule. It
786                    // is an arbitrary string of characters enclosed in single
787                    // quotes.
788                    matchingRuleBuilder.description(readQuotedString(reader));
789                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
790                    // This indicates whether the matching rule should be
791                    // considered obsolete. We do not need to do any more
792                    // parsing for this token.
793                    matchingRuleBuilder.obsolete(true);
794                } else if ("syntax".equalsIgnoreCase(tokenName)) {
795                    syntax = readOID(reader, allowsMalformedNamesAndOptions());
796                    matchingRuleBuilder.syntaxOID(syntax);
797                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
798                    // This must be a non-standard property and it must be
799                    // followed by either a single definition in single quotes
800                    // or an open parenthesis followed by one or more values in
801                    // single quotes separated by spaces followed by a close
802                    // parenthesis.
803                    final List<String> extensions = readExtensions(reader);
804                    matchingRuleBuilder.extraProperties(tokenName, extensions.toArray(new String[extensions.size()]));
805                } else {
806                    throw new LocalizedIllegalArgumentException(
807                        ERR_ATTR_SYNTAX_MR_ILLEGAL_TOKEN1.get(definition, tokenName));
808                }
809            }
810
811            // Make sure that a syntax was specified.
812            if (syntax == null) {
813                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_MR_NO_SYNTAX.get(definition));
814            }
815            if (hook != null) {
816                hook.beforeAddMatchingRule(matchingRuleBuilder);
817            }
818            matchingRuleBuilder.addToSchema(overwrite);
819        } catch (final DecodeException e) {
820            final LocalizableMessage msg =
821                    ERR_ATTR_SYNTAX_MR_INVALID1.get(definition, e.getMessageObject());
822            throw new LocalizedIllegalArgumentException(msg, e.getCause());
823        }
824        return this;
825    }
826
827    /**
828     * Adds the provided matching rule use definition to this schema builder.
829     *
830     * @param definition
831     *            The matching rule use definition.
832     * @param overwrite
833     *            {@code true} if any existing matching rule use with the same
834     *            OID should be overwritten.
835     * @return A reference to this schema builder.
836     * @throws ConflictingSchemaElementException
837     *             If {@code overwrite} was {@code false} and a conflicting
838     *             schema element was found.
839     * @throws LocalizedIllegalArgumentException
840     *             If the provided matching rule use definition could not be
841     *             parsed.
842     * @throws NullPointerException
843     *             If {@code definition} was {@code null}.
844     */
845    public SchemaBuilder addMatchingRuleUse(final String definition, final boolean overwrite) {
846        return addMatchingRuleUse(definition, overwrite, null);
847    }
848
849    SchemaBuilder addMatchingRuleUse(final String definition, final boolean overwrite, SchemaBuilderHook hook) {
850        Reject.ifNull(definition);
851
852        lazyInitBuilder();
853
854        try {
855            final SubstringReader reader = new SubstringReader(definition);
856
857            // We'll do this a character at a time. First, skip over any
858            // leading whitespace.
859            reader.skipWhitespaces();
860
861            if (reader.remaining() <= 0) {
862                // This means that the value was empty or contained only
863                // whitespace. That is illegal.
864                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_MRUSE_EMPTY_VALUE1.get(definition));
865            }
866
867            // The next character must be an open parenthesis. If it is not,
868            // then that is an error.
869            final char c = reader.read();
870            if (c != '(') {
871                final LocalizableMessage message = ERR_ATTR_SYNTAX_MRUSE_EXPECTED_OPEN_PARENTHESIS.get(
872                    definition, reader.pos() - 1, String.valueOf(c));
873                throw new LocalizedIllegalArgumentException(message);
874            }
875
876            // Skip over any spaces immediately following the opening
877            // parenthesis.
878            reader.skipWhitespaces();
879
880            // The next set of characters must be the OID.
881            final MatchingRuleUse.Builder useBuilder =
882                    buildMatchingRuleUse(readOID(reader, allowsMalformedNamesAndOptions()));
883            Set<String> attributes = null;
884
885            // At this point, we should have a pretty specific syntax that
886            // describes what may come next, but some of the components are
887            // optional and it would be pretty easy to put something in the
888            // wrong order, so we will be very flexible about what we can
889            // accept. Just look at the next token, figure out what it is and
890            // how to treat what comes after it, then repeat until we get to
891            // the end of the value. But before we start, set default values
892            // for everything else we might need to know.
893            while (true) {
894                final String tokenName = readTokenName(reader);
895
896                if (tokenName == null) {
897                    // No more tokens.
898                    break;
899                } else if ("name".equalsIgnoreCase(tokenName)) {
900                    useBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
901                } else if ("desc".equalsIgnoreCase(tokenName)) {
902                    // This specifies the description for the attribute type. It
903                    // is an arbitrary string of characters enclosed in single
904                    // quotes.
905                    useBuilder.description(readQuotedString(reader));
906                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
907                    // This indicates whether the attribute type should be
908                    // considered obsolete.
909                    useBuilder.obsolete(true);
910                } else if ("applies".equalsIgnoreCase(tokenName)) {
911                    attributes = readOIDs(reader, allowsMalformedNamesAndOptions());
912                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
913                    // This must be a non-standard property and it must be
914                    // followed by either a single definition in single quotes
915                    // or an open parenthesis followed by one or more values in
916                    // single quotes separated by spaces followed by a close
917                    // parenthesis.
918                    useBuilder.extraProperties(tokenName, readExtensions(reader));
919                } else {
920                    throw new LocalizedIllegalArgumentException(
921                        ERR_ATTR_SYNTAX_MRUSE_ILLEGAL_TOKEN1.get(definition, tokenName));
922                }
923            }
924
925            // Make sure that the set of attributes was defined.
926            if (attributes == null || attributes.isEmpty()) {
927                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_MRUSE_NO_ATTR.get(definition));
928            }
929            useBuilder.attributes(attributes);
930
931            if (hook != null) {
932                hook.beforeAddMatchingRuleUse(useBuilder);
933            }
934            return useBuilder.addToSchema(overwrite);
935        } catch (final DecodeException e) {
936            final LocalizableMessage msg = ERR_ATTR_SYNTAX_MRUSE_INVALID1.get(definition, e.getMessageObject());
937            throw new LocalizedIllegalArgumentException(msg, e.getCause());
938        }
939    }
940
941    /**
942     * Returns a builder which can be used for incrementally constructing a new
943     * attribute type before adding it to the schema. Example usage:
944     *
945     * <pre>
946     * SchemaBuilder builder = ...;
947     * builder.buildAttributeType("attributetype-oid").name("attribute type name").addToSchema();
948     * </pre>
949     *
950     * @param oid
951     *            The OID of the attribute type definition.
952     * @return A builder to continue building the AttributeType.
953     */
954    public AttributeType.Builder buildAttributeType(final String oid) {
955        lazyInitBuilder();
956        return new AttributeType.Builder(oid, this);
957    }
958
959    /**
960     * Returns a builder which can be used for incrementally constructing a new
961     * DIT structure rule before adding it to the schema. Example usage:
962     *
963     * <pre>
964     * SchemaBuilder builder = ...;
965     * final int myRuleID = ...;
966     * builder.buildDITStructureRule(myRuleID).name("DIT structure rule name").addToSchema();
967     * </pre>
968     *
969     * @param ruleID
970     *            The ID of the DIT structure rule.
971     * @return A builder to continue building the DITStructureRule.
972     */
973    public DITStructureRule.Builder buildDITStructureRule(final int ruleID) {
974        lazyInitBuilder();
975        return new DITStructureRule.Builder(ruleID, this);
976    }
977
978    /**
979     * Returns a builder which can be used for incrementally constructing a new matching rule before adding it to the
980     * schema. Example usage:
981     *
982     * <pre>
983     * SchemaBuilder builder = ...;
984     * builder.buildMatchingRule("matchingrule-oid").name("matching rule name").addToSchema();
985     * </pre>
986     *
987     * @param oid
988     *            The OID of the matching rule definition.
989     * @return A builder to continue building the MatchingRule.
990     */
991    public MatchingRule.Builder buildMatchingRule(final String oid) {
992        lazyInitBuilder();
993        return new MatchingRule.Builder(oid, this);
994    }
995
996    /**
997     * Returns a builder which can be used for incrementally constructing a new
998     * matching rule use before adding it to the schema. Example usage:
999     *
1000     * <pre>
1001     * SchemaBuilder builder = ...;
1002     * builder.buildMatchingRuleUse("matchingrule-oid")
1003     *        .name("matching rule use name")
1004     *        .addToSchema();
1005     * </pre>
1006     *
1007     * @param oid
1008     *            The OID of the matching rule definition.
1009     * @return A builder to continue building the MatchingRuleUse.
1010     */
1011    public MatchingRuleUse.Builder buildMatchingRuleUse(final String oid) {
1012        lazyInitBuilder();
1013        return new MatchingRuleUse.Builder(oid, this);
1014    }
1015
1016    /**
1017     * Adds the provided name form definition to this schema builder.
1018     *
1019     * @param definition
1020     *            The name form definition.
1021     * @param overwrite
1022     *            {@code true} if any existing name form with the same OID
1023     *            should be overwritten.
1024     * @return A reference to this schema builder.
1025     * @throws ConflictingSchemaElementException
1026     *             If {@code overwrite} was {@code false} and a conflicting
1027     *             schema element was found.
1028     * @throws LocalizedIllegalArgumentException
1029     *             If the provided name form definition could not be parsed.
1030     * @throws NullPointerException
1031     *             If {@code definition} was {@code null}.
1032     */
1033    public SchemaBuilder addNameForm(final String definition, final boolean overwrite) {
1034        return addNameForm(definition, overwrite, null);
1035    }
1036
1037    SchemaBuilder addNameForm(final String definition, final boolean overwrite, SchemaBuilderHook hook) {
1038        Reject.ifNull(definition);
1039
1040        lazyInitBuilder();
1041
1042        try {
1043            final SubstringReader reader = new SubstringReader(definition);
1044
1045            // We'll do this a character at a time. First, skip over any
1046            // leading whitespace.
1047            reader.skipWhitespaces();
1048
1049            if (reader.remaining() <= 0) {
1050                // This means that the value was empty or contained only
1051                // whitespace. That is illegal.
1052                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_NAME_FORM_EMPTY_VALUE1.get(definition));
1053            }
1054
1055            // The next character must be an open parenthesis. If it is not,
1056            // then that is an error.
1057            final char c = reader.read();
1058            if (c != '(') {
1059                final LocalizableMessage message = ERR_ATTR_SYNTAX_NAME_FORM_EXPECTED_OPEN_PARENTHESIS.get(
1060                    definition, reader.pos() - 1, c);
1061                throw new LocalizedIllegalArgumentException(message);
1062            }
1063
1064            // Skip over any spaces immediately following the opening
1065            // parenthesis.
1066            reader.skipWhitespaces();
1067
1068            // The next set of characters must be the OID.
1069            final NameForm.Builder nameFormBuilder = new NameForm.Builder(
1070                readOID(reader, allowsMalformedNamesAndOptions()), this);
1071            nameFormBuilder.definition(definition);
1072
1073            // Required properties :
1074            String structuralOID = null;
1075            Collection<String> requiredAttributes = Collections.emptyList();
1076
1077            // At this point, we should have a pretty specific syntax that
1078            // describes what may come next, but some of the components are
1079            // optional and it would be pretty easy to put something in the
1080            // wrong order, so we will be very flexible about what we can
1081            // accept. Just look at the next token, figure out what it is and
1082            // how to treat what comes after it, then repeat until we get to
1083            // the end of the value. But before we start, set default values
1084            // for everything else we might need to know.
1085            while (true) {
1086                final String tokenName = readTokenName(reader);
1087
1088                if (tokenName == null) {
1089                    // No more tokens.
1090                    break;
1091                } else if ("name".equalsIgnoreCase(tokenName)) {
1092                    nameFormBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
1093                } else if ("desc".equalsIgnoreCase(tokenName)) {
1094                    // This specifies the description for the attribute type. It
1095                    // is an arbitrary string of characters enclosed in single
1096                    // quotes.
1097                    nameFormBuilder.description(readQuotedString(reader));
1098                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
1099                    // This indicates whether the attribute type should be
1100                    // considered obsolete. We do not need to do any more
1101                    // parsing for this token.
1102                    nameFormBuilder.obsolete(true);
1103                } else if ("oc".equalsIgnoreCase(tokenName)) {
1104                    structuralOID = readOID(reader, allowsMalformedNamesAndOptions());
1105                    nameFormBuilder.structuralObjectClassOID(structuralOID);
1106                } else if ("must".equalsIgnoreCase(tokenName)) {
1107                    requiredAttributes = readOIDs(reader, allowsMalformedNamesAndOptions());
1108                    nameFormBuilder.requiredAttributes(requiredAttributes);
1109                } else if ("may".equalsIgnoreCase(tokenName)) {
1110                    nameFormBuilder.optionalAttributes(readOIDs(reader, allowsMalformedNamesAndOptions()));
1111                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
1112                    // This must be a non-standard property and it must be
1113                    // followed by either a single definition in single quotes
1114                    // or an open parenthesis followed by one or more values in
1115                    // single quotes separated by spaces followed by a close
1116                    // parenthesis.
1117                    final List<String> extensions = readExtensions(reader);
1118                    nameFormBuilder.extraProperties(tokenName, extensions.toArray(new String[extensions.size()]));
1119                } else {
1120                    throw new LocalizedIllegalArgumentException(
1121                        ERR_ATTR_SYNTAX_NAME_FORM_ILLEGAL_TOKEN1.get(definition, tokenName));
1122                }
1123            }
1124
1125            // Make sure that a structural class was specified. If not, then
1126            // it cannot be valid and the name form cannot be build.
1127            if (structuralOID == null) {
1128                throw new LocalizedIllegalArgumentException(
1129                    ERR_ATTR_SYNTAX_NAME_FORM_NO_STRUCTURAL_CLASS1.get(definition));
1130            }
1131
1132            if (requiredAttributes.isEmpty()) {
1133                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_NAME_FORM_NO_REQUIRED_ATTR.get(definition));
1134            }
1135
1136            if (hook != null) {
1137                hook.beforeAddNameForm(nameFormBuilder);
1138            }
1139            nameFormBuilder.addToSchema(overwrite);
1140        } catch (final DecodeException e) {
1141            final LocalizableMessage msg =
1142                    ERR_ATTR_SYNTAX_NAME_FORM_INVALID1.get(definition, e.getMessageObject());
1143            throw new LocalizedIllegalArgumentException(msg, e.getCause());
1144        }
1145        return this;
1146    }
1147
1148    /**
1149     * Returns a builder which can be used for incrementally constructing a new
1150     * DIT content rule before adding it to the schema. Example usage:
1151     *
1152     * <pre>
1153     * SchemaBuilder builder = ...;
1154     * builder.buildDITContentRule("structuralobjectclass-oid").name("DIT content rule name").addToSchema();
1155     * </pre>
1156     *
1157     * @param structuralClassOID
1158     *            The OID of the structural objectclass for the DIT content rule to build.
1159     * @return A builder to continue building the DITContentRule.
1160     */
1161    public Builder buildDITContentRule(String structuralClassOID) {
1162        lazyInitBuilder();
1163        return new DITContentRule.Builder(structuralClassOID, this);
1164    }
1165
1166    /**
1167     * Returns a builder which can be used for incrementally constructing a new
1168     * name form before adding it to the schema. Example usage:
1169     *
1170     * <pre>
1171     * SchemaBuilder builder = ...;
1172     * builder.buildNameForm("1.2.3.4").name("myNameform").addToSchema();
1173     * </pre>
1174     *
1175     * @param oid
1176     *            The OID of the name form definition.
1177     * @return A builder to continue building the NameForm.
1178     */
1179    public NameForm.Builder buildNameForm(final String oid) {
1180        lazyInitBuilder();
1181        return new NameForm.Builder(oid, this);
1182    }
1183
1184    /**
1185     * Returns a builder which can be used for incrementally constructing a new
1186     * object class before adding it to the schema. Example usage:
1187     *
1188     * <pre>
1189     * SchemaBuilder builder = ...;
1190     * builder.buildObjectClass("objectclass-oid").name("object class name").addToSchema();
1191     * </pre>
1192     *
1193     * @param oid
1194     *            The OID of the object class definition.
1195     * @return A builder to continue building the ObjectClass.
1196     */
1197    public ObjectClass.Builder buildObjectClass(final String oid) {
1198        lazyInitBuilder();
1199        return new ObjectClass.Builder(oid, this);
1200    }
1201
1202    /**
1203     * Returns a builder which can be used for incrementally constructing a new
1204     * syntax before adding it to the schema. Example usage:
1205     *
1206     * <pre>
1207     * SchemaBuilder builder = ...;
1208     * builder.buildSyntax("1.2.3.4").addToSchema();
1209     * </pre>
1210     *
1211     * @param oid
1212     *            The OID of the syntax definition.
1213     * @return A builder to continue building the syntax.
1214     */
1215    public Syntax.Builder buildSyntax(final String oid) {
1216        lazyInitBuilder();
1217        return new Syntax.Builder(oid, this);
1218    }
1219
1220    /**
1221     * Returns an attribute type builder whose fields are initialized to the
1222     * values of the provided attribute type. This method should be used when
1223     * duplicating attribute types from external schemas or when modifying
1224     * existing attribute types.
1225     *
1226     * @param attributeType
1227     *            The attribute type source.
1228     * @return A builder to continue building the AttributeType.
1229     */
1230    public AttributeType.Builder buildAttributeType(final AttributeType attributeType) {
1231        lazyInitBuilder();
1232        return new AttributeType.Builder(attributeType, this);
1233    }
1234
1235    /**
1236     * Returns a DIT content rule builder whose fields are initialized to the
1237     * values of the provided DIT content rule. This method should be used when
1238     * duplicating DIT content rules from external schemas or when modifying
1239     * existing DIT content rules.
1240     *
1241     * @param contentRule
1242     *            The DIT content rule source.
1243     * @return A builder to continue building the DITContentRule.
1244     */
1245    public DITContentRule.Builder buildDITContentRule(DITContentRule contentRule) {
1246        lazyInitBuilder();
1247        return new DITContentRule.Builder(contentRule, this);
1248    }
1249
1250    /**
1251     * Returns an DIT structure rule builder whose fields are initialized to the
1252     * values of the provided rule. This method should be used when duplicating
1253     * structure rules from external schemas or when modifying existing
1254     * structure rules.
1255     *
1256     * @param structureRule
1257     *            The DIT structure rule source.
1258     * @return A builder to continue building the DITStructureRule.
1259     */
1260    public DITStructureRule.Builder buildDITStructureRule(final DITStructureRule structureRule) {
1261        lazyInitBuilder();
1262        return new DITStructureRule.Builder(structureRule, this);
1263    }
1264
1265    /**
1266     * Returns a matching rule builder whose fields are initialized to the
1267     * values of the provided matching rule. This method should be used when
1268     * duplicating matching rules from external schemas or when modifying
1269     * existing matching rules.
1270     *
1271     * @param matchingRule
1272     *            The matching rule source.
1273     * @return A builder to continue building the MatchingRule.
1274     */
1275    public MatchingRule.Builder buildMatchingRule(final MatchingRule matchingRule) {
1276        lazyInitBuilder();
1277        return new MatchingRule.Builder(matchingRule, this);
1278    }
1279
1280    /**
1281     * Returns a matching rule use builder whose fields are initialized to the
1282     * values of the provided matching rule use object. This method should be used when
1283     * duplicating matching rule uses from external schemas or when modifying
1284     * existing matching rule uses.
1285     *
1286     * @param matchingRuleUse
1287     *            The matching rule use source.
1288     * @return A builder to continue building the MatchingRuleUse.
1289     */
1290    public MatchingRuleUse.Builder buildMatchingRuleUse(final MatchingRuleUse matchingRuleUse) {
1291        lazyInitBuilder();
1292        return new MatchingRuleUse.Builder(matchingRuleUse, this);
1293    }
1294
1295    /**
1296     * Returns a name form builder whose fields are initialized to the
1297     * values of the provided name form. This method should be used when
1298     * duplicating name forms from external schemas or when modifying
1299     * existing names forms.
1300     *
1301     * @param nameForm
1302     *            The name form source.
1303     * @return A builder to continue building the NameForm.
1304     */
1305    public NameForm.Builder buildNameForm(final NameForm nameForm) {
1306        lazyInitBuilder();
1307        return new NameForm.Builder(nameForm, this);
1308    }
1309
1310    /**
1311     * Returns an object class builder whose fields are initialized to the
1312     * values of the provided object class. This method should be used when
1313     * duplicating object classes from external schemas or when modifying
1314     * existing object classes.
1315     *
1316     * @param objectClass
1317     *            The object class source.
1318     * @return A builder to continue building the ObjectClass.
1319     */
1320    public ObjectClass.Builder buildObjectClass(final ObjectClass objectClass) {
1321        lazyInitBuilder();
1322        return new ObjectClass.Builder(objectClass, this);
1323    }
1324
1325    /**
1326     * Returns a syntax builder whose fields are initialized to the
1327     * values of the provided syntax. This method should be used when
1328     * duplicating syntaxes from external schemas or when modifying
1329     * existing syntaxes.
1330     *
1331     * @param syntax
1332     *            The syntax source.
1333     * @return A builder to continue building the Syntax.
1334     */
1335    public Syntax.Builder buildSyntax(final Syntax syntax) {
1336        lazyInitBuilder();
1337        return new Syntax.Builder(syntax, this);
1338    }
1339
1340    /**
1341     * Adds the provided object class definition to this schema builder.
1342     *
1343     * @param definition
1344     *            The object class definition.
1345     * @param overwrite
1346     *            {@code true} if any existing object class with the same OID
1347     *            should be overwritten.
1348     * @return A reference to this schema builder.
1349     * @throws ConflictingSchemaElementException
1350     *             If {@code overwrite} was {@code false} and a conflicting
1351     *             schema element was found.
1352     * @throws LocalizedIllegalArgumentException
1353     *             If the provided object class definition could not be parsed.
1354     * @throws NullPointerException
1355     *             If {@code definition} was {@code null}.
1356     */
1357    public SchemaBuilder addObjectClass(final String definition, final boolean overwrite) {
1358        return addObjectClass(definition, overwrite, null);
1359    }
1360
1361    SchemaBuilder addObjectClass(final String definition, final boolean overwrite, SchemaBuilderHook hook) {
1362        Reject.ifNull(definition);
1363
1364        lazyInitBuilder();
1365
1366        try {
1367            final SubstringReader reader = new SubstringReader(definition);
1368
1369            // We'll do this a character at a time. First, skip over any
1370            // leading whitespace.
1371            reader.skipWhitespaces();
1372
1373            if (reader.remaining() <= 0) {
1374                // This means that the value was empty or contained only
1375                // whitespace. That is illegal.
1376                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_OBJECTCLASS_EMPTY_VALUE1.get(definition));
1377            }
1378
1379            // The next character must be an open parenthesis. If it is not,
1380            // then that is an error.
1381            final char c = reader.read();
1382            if (c != '(') {
1383                final LocalizableMessage message =  ERR_ATTR_SYNTAX_OBJECTCLASS_EXPECTED_OPEN_PARENTHESIS1.get(
1384                            definition, reader.pos() - 1, String.valueOf(c));
1385                throw new LocalizedIllegalArgumentException(message);
1386            }
1387
1388            // Skip over any spaces immediately following the opening
1389            // parenthesis.
1390            reader.skipWhitespaces();
1391
1392            // The next set of characters is the OID.
1393            final String oid = readOID(reader, allowsMalformedNamesAndOptions());
1394            Set<String> superiorClasses = emptySet();
1395            ObjectClassType ocType = null;
1396            ObjectClass.Builder ocBuilder = new ObjectClass.Builder(oid, this).definition(definition);
1397
1398            // At this point, we should have a pretty specific syntax that
1399            // describes what may come next, but some of the components are
1400            // optional and it would be pretty easy to put something in the
1401            // wrong order, so we will be very flexible about what we can
1402            // accept. Just look at the next token, figure out what it is and
1403            // how to treat what comes after it, then repeat until we get to
1404            // the end of the value. But before we start, set default values
1405            // for everything else we might need to know.
1406            while (true) {
1407                final String tokenName = readTokenName(reader);
1408
1409                if (tokenName == null) {
1410                    // No more tokens.
1411                    break;
1412                } else if ("name".equalsIgnoreCase(tokenName)) {
1413                    ocBuilder.names(readNameDescriptors(reader, allowsMalformedNamesAndOptions()));
1414                } else if ("desc".equalsIgnoreCase(tokenName)) {
1415                    // This specifies the description for the attribute type. It
1416                    // is an arbitrary string of characters enclosed in single
1417                    // quotes.
1418                    ocBuilder.description(readQuotedString(reader));
1419                } else if ("obsolete".equalsIgnoreCase(tokenName)) {
1420                    // This indicates whether the attribute type should be
1421                    // considered obsolete.
1422                    ocBuilder.obsolete(true);
1423                } else if ("sup".equalsIgnoreCase(tokenName)) {
1424                    superiorClasses = readOIDs(reader, allowsMalformedNamesAndOptions());
1425                } else if ("abstract".equalsIgnoreCase(tokenName)) {
1426                    // This indicates that entries must not include this
1427                    // objectclass unless they also include a non-abstract
1428                    // objectclass that inherits from this class.
1429                    ocType = ABSTRACT;
1430                } else if ("structural".equalsIgnoreCase(tokenName)) {
1431                    ocType = STRUCTURAL;
1432                } else if ("auxiliary".equalsIgnoreCase(tokenName)) {
1433                    ocType = AUXILIARY;
1434                } else if ("must".equalsIgnoreCase(tokenName)) {
1435                    ocBuilder.requiredAttributes(readOIDs(reader, allowsMalformedNamesAndOptions()));
1436                } else if ("may".equalsIgnoreCase(tokenName)) {
1437                    ocBuilder.optionalAttributes(readOIDs(reader, allowsMalformedNamesAndOptions()));
1438                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
1439                    // This must be a non-standard property and it must be
1440                    // followed by either a single definition in single quotes
1441                    // or an open parenthesis followed by one or more values in
1442                    // single quotes separated by spaces followed by a close
1443                    // parenthesis.
1444                    final List<String> extensions = readExtensions(reader);
1445                    ocBuilder.extraProperties(tokenName, extensions.toArray(new String[extensions.size()]));
1446                } else {
1447                    throw new LocalizedIllegalArgumentException(
1448                        ERR_ATTR_SYNTAX_OBJECTCLASS_ILLEGAL_TOKEN1.get(definition, tokenName));
1449                }
1450            }
1451            if (hook != null) {
1452                hook.beforeAddObjectClass(ocBuilder);
1453            }
1454
1455            if (EXTENSIBLE_OBJECT_OBJECTCLASS_OID.equals(oid)) {
1456                addObjectClass(newExtensibleObjectObjectClass(
1457                    ocBuilder.getDescription(), ocBuilder.getExtraProperties(), this), overwrite);
1458                return this;
1459            }
1460
1461            ocType = ocType != null ? ocType : STRUCTURAL;
1462            ocBuilder.superiorObjectClasses(superiorClasses)
1463                     .type(ocType);
1464            return ocBuilder.addToSchema(overwrite);
1465        } catch (final DecodeException e) {
1466            throw new LocalizedIllegalArgumentException(
1467                ERR_ATTR_SYNTAX_OBJECTCLASS_INVALID1.get(definition, e.getMessageObject()), e.getCause());
1468        }
1469    }
1470
1471    /**
1472     * Adds the provided pattern syntax definition to this schema builder.
1473     *
1474     * @param oid
1475     *            The OID of the pattern syntax definition.
1476     * @param description
1477     *            The description of the pattern syntax definition.
1478     * @param pattern
1479     *            The regular expression pattern which attribute values must
1480     *            match in order to be valid.
1481     * @param overwrite
1482     *            {@code true} if any existing syntax with the same OID should
1483     *            be overwritten.
1484     * @return A reference to this schema builder.
1485     * @throws ConflictingSchemaElementException
1486     *             If {@code overwrite} was {@code false} and a conflicting
1487     *             schema element was found.
1488     */
1489    public SchemaBuilder addPatternSyntax(final String oid, final String description,
1490            final Pattern pattern, final boolean overwrite) {
1491        Reject.ifNull(pattern);
1492        return buildSyntax(oid)
1493                .description(description)
1494                .extraProperties("X-PATTERN", pattern.toString())
1495                .addToSchema(overwrite);
1496    }
1497
1498    /**
1499     * Reads the schema elements contained in the named subschema sub-entry and
1500     * adds them to this schema builder.
1501     * <p>
1502     * If the requested schema is not returned by the Directory Server then the
1503     * request will fail with an {@link EntryNotFoundException}.
1504     *
1505     * @param connection
1506     *            A connection to the Directory Server whose schema is to be
1507     *            read.
1508     * @param name
1509     *            The distinguished name of the subschema sub-entry.
1510     * @param overwrite
1511     *            {@code true} if existing schema elements with the same
1512     *            conflicting OIDs should be overwritten.
1513     * @return A reference to this schema builder.
1514     * @throws LdapException
1515     *             If the result code indicates that the request failed for some
1516     *             reason.
1517     * @throws UnsupportedOperationException
1518     *             If the connection does not support search operations.
1519     * @throws IllegalStateException
1520     *             If the connection has already been closed, i.e. if
1521     *             {@code isClosed() == true}.
1522     * @throws NullPointerException
1523     *             If the {@code connection} or {@code name} was {@code null}.
1524     */
1525    public SchemaBuilder addSchema(final Connection connection, final DN name,
1526            final boolean overwrite) throws LdapException {
1527        // The call to addSchema will perform copyOnWrite.
1528        final SearchRequest request = getReadSchemaSearchRequest(name);
1529        final Entry entry = connection.searchSingleEntry(request);
1530        return addSchema(entry, overwrite, null);
1531    }
1532
1533    /**
1534     * Adds all of the schema elements contained in the provided subschema
1535     * subentry to this schema builder. Any problems encountered while parsing
1536     * the entry can be retrieved using the returned schema's
1537     * {@link Schema#getWarnings()} method.
1538     *
1539     * @param entry
1540     *            The subschema subentry to be parsed.
1541     * @param overwrite
1542     *            {@code true} if existing schema elements with the same
1543     *            conflicting OIDs should be overwritten.
1544     * @return A reference to this schema builder.
1545     * @throws NullPointerException
1546     *             If {@code entry} was {@code null}.
1547     */
1548    public SchemaBuilder addSchema(final Entry entry, final boolean overwrite) {
1549        return addSchema(entry, overwrite, null);
1550    }
1551
1552    /**
1553     * Adds all of the schema elements contained in the provided subschema
1554     * subentry to this schema builder. Any problems encountered while parsing
1555     * the entry can be retrieved using the returned schema's
1556     * {@link Schema#getWarnings()} method.
1557     *
1558     * @param entry
1559     *            The subschema subentry to be parsed.
1560     * @param overwrite
1561     *            {@code true} if existing schema elements with the same
1562     *            conflicting OIDs should be overwritten.
1563     * @param hook
1564     *            Allows to perform modifications on element's builders before adding the result to this schema builder.
1565     * @return A reference to this schema builder.
1566     * @throws NullPointerException
1567     *             If {@code entry} was {@code null}.
1568     */
1569    public SchemaBuilder addSchema(final Entry entry, final boolean overwrite, SchemaBuilderHook hook) {
1570        Reject.ifNull(entry);
1571
1572        lazyInitBuilder();
1573
1574        Attribute attr = entry.getAttribute(Schema.ATTR_LDAP_SYNTAXES);
1575        if (attr != null) {
1576            for (final ByteString def : attr) {
1577                try {
1578                    addSyntax(def.toString(), overwrite, hook);
1579                } catch (final LocalizedIllegalArgumentException e) {
1580                    warnings.add(e.getMessageObject());
1581                }
1582            }
1583        }
1584
1585        attr = entry.getAttribute(Schema.ATTR_ATTRIBUTE_TYPES);
1586        if (attr != null) {
1587            for (final ByteString def : attr) {
1588                try {
1589                    addAttributeType(def.toString(), overwrite, hook);
1590                } catch (final LocalizedIllegalArgumentException e) {
1591                    warnings.add(e.getMessageObject());
1592                }
1593            }
1594        }
1595
1596        attr = entry.getAttribute(Schema.ATTR_OBJECT_CLASSES);
1597        if (attr != null) {
1598            for (final ByteString def : attr) {
1599                try {
1600                    addObjectClass(def.toString(), overwrite, hook);
1601                } catch (final LocalizedIllegalArgumentException e) {
1602                    warnings.add(e.getMessageObject());
1603                }
1604            }
1605        }
1606
1607        attr = entry.getAttribute(Schema.ATTR_MATCHING_RULE_USE);
1608        if (attr != null) {
1609            for (final ByteString def : attr) {
1610                try {
1611                    addMatchingRuleUse(def.toString(), overwrite, hook);
1612                } catch (final LocalizedIllegalArgumentException e) {
1613                    warnings.add(e.getMessageObject());
1614                }
1615            }
1616        }
1617
1618        attr = entry.getAttribute(Schema.ATTR_MATCHING_RULES);
1619        if (attr != null) {
1620            for (final ByteString def : attr) {
1621                try {
1622                    addMatchingRule(def.toString(), overwrite, hook);
1623                } catch (final LocalizedIllegalArgumentException e) {
1624                    warnings.add(e.getMessageObject());
1625                }
1626            }
1627        }
1628
1629        attr = entry.getAttribute(Schema.ATTR_DIT_CONTENT_RULES);
1630        if (attr != null) {
1631            for (final ByteString def : attr) {
1632                try {
1633                    addDITContentRule(def.toString(), overwrite, hook);
1634                } catch (final LocalizedIllegalArgumentException e) {
1635                    warnings.add(e.getMessageObject());
1636                }
1637            }
1638        }
1639
1640        attr = entry.getAttribute(Schema.ATTR_DIT_STRUCTURE_RULES);
1641        if (attr != null) {
1642            for (final ByteString def : attr) {
1643                try {
1644                    addDITStructureRule(def.toString(), overwrite, hook);
1645                } catch (final LocalizedIllegalArgumentException e) {
1646                    warnings.add(e.getMessageObject());
1647                }
1648            }
1649        }
1650
1651        attr = entry.getAttribute(Schema.ATTR_NAME_FORMS);
1652        if (attr != null) {
1653            for (final ByteString def : attr) {
1654                try {
1655                    addNameForm(def.toString(), overwrite, hook);
1656                } catch (final LocalizedIllegalArgumentException e) {
1657                    warnings.add(e.getMessageObject());
1658                }
1659            }
1660        }
1661
1662        return this;
1663    }
1664
1665    /**
1666     * Adds all of the schema elements in the provided schema to this schema
1667     * builder.
1668     *
1669     * @param schema
1670     *            The schema to be copied into this schema builder.
1671     * @param overwrite
1672     *            {@code true} if existing schema elements with the same
1673     *            conflicting OIDs should be overwritten.
1674     * @return A reference to this schema builder.
1675     * @throws ConflictingSchemaElementException
1676     *             If {@code overwrite} was {@code false} and conflicting schema
1677     *             elements were found.
1678     * @throws NullPointerException
1679     *             If {@code schema} was {@code null}.
1680     */
1681    public SchemaBuilder addSchema(final Schema schema, final boolean overwrite) {
1682        Reject.ifNull(schema);
1683
1684        lazyInitBuilder();
1685
1686        addSchema0(schema, overwrite);
1687        return this;
1688    }
1689
1690    /**
1691     * Asynchronously reads the schema elements contained in the named subschema
1692     * sub-entry and adds them to this schema builder.
1693     * <p>
1694     * If the requested schema is not returned by the Directory Server then the
1695     * request will fail with an {@link EntryNotFoundException}.
1696     *
1697     * @param connection
1698     *            A connection to the Directory Server whose schema is to be
1699     *            read.
1700     * @param name
1701     *            The distinguished name of the subschema sub-entry.
1702     *            the operation result when it is received, may be {@code null}.
1703     * @param overwrite
1704     *            {@code true} if existing schema elements with the same
1705     *            conflicting OIDs should be overwritten.
1706     * @return A promise representing the updated schema builder.
1707     * @throws UnsupportedOperationException
1708     *             If the connection does not support search operations.
1709     * @throws IllegalStateException
1710     *             If the connection has already been closed, i.e. if
1711     *             {@code connection.isClosed() == true}.
1712     * @throws NullPointerException
1713     *             If the {@code connection} or {@code name} was {@code null}.
1714     */
1715    public LdapPromise<SchemaBuilder> addSchemaAsync(final Connection connection, final DN name,
1716        final boolean overwrite) {
1717        // The call to addSchema will perform copyOnWrite.
1718        return connection.searchSingleEntryAsync(getReadSchemaSearchRequest(name)).then(
1719                new Function<SearchResultEntry, SchemaBuilder, LdapException>() {
1720                    @Override
1721                    public SchemaBuilder apply(SearchResultEntry result) throws LdapException {
1722                        addSchema(result, overwrite, null);
1723                        return SchemaBuilder.this;
1724                    }
1725                });
1726    }
1727
1728    /**
1729     * Reads the schema elements contained in the subschema sub-entry which
1730     * applies to the named entry and adds them to this schema builder.
1731     * <p>
1732     * If the requested entry or its associated schema are not returned by the
1733     * Directory Server then the request will fail with an
1734     * {@link EntryNotFoundException}.
1735     * <p>
1736     * This implementation first reads the {@code subschemaSubentry} attribute
1737     * of the entry in order to identify the schema and then invokes
1738     * {@link #addSchemaForEntry(Connection, DN, boolean)} to read the schema.
1739     *
1740     * @param connection
1741     *            A connection to the Directory Server whose schema is to be
1742     *            read.
1743     * @param name
1744     *            The distinguished name of the entry whose schema is to be
1745     *            located.
1746     * @param overwrite
1747     *            {@code true} if existing schema elements with the same
1748     *            conflicting OIDs should be overwritten.
1749     * @return A reference to this schema builder.
1750     * @throws LdapException
1751     *             If the result code indicates that the request failed for some
1752     *             reason.
1753     * @throws UnsupportedOperationException
1754     *             If the connection does not support search operations.
1755     * @throws IllegalStateException
1756     *             If the connection has already been closed, i.e. if
1757     *             {@code connection.isClosed() == true}.
1758     * @throws NullPointerException
1759     *             If the {@code connection} or {@code name} was {@code null}.
1760     */
1761    public SchemaBuilder addSchemaForEntry(final Connection connection, final DN name,
1762            final boolean overwrite) throws LdapException {
1763        // The call to addSchema will perform copyOnWrite.
1764        final SearchRequest request = getReadSchemaForEntrySearchRequest(name);
1765        final Entry entry = connection.searchSingleEntry(request);
1766        final DN subschemaDN = getSubschemaSubentryDN(name, entry);
1767        return addSchema(connection, subschemaDN, overwrite);
1768    }
1769
1770    /**
1771     * Asynchronously reads the schema elements contained in the subschema
1772     * sub-entry which applies to the named entry and adds them to this schema
1773     * builder.
1774     * <p>
1775     * If the requested entry or its associated schema are not returned by the
1776     * Directory Server then the request will fail with an
1777     * {@link EntryNotFoundException}.
1778     * <p>
1779     * This implementation first reads the {@code subschemaSubentry} attribute
1780     * of the entry in order to identify the schema and then invokes
1781     * {@link #addSchemaAsync(Connection, DN, boolean)} to read the schema.
1782     *
1783     * @param connection
1784     *            A connection to the Directory Server whose schema is to be
1785     *            read.
1786     * @param name
1787     *            The distinguished name of the entry whose schema is to be
1788     *            located.
1789     * @param overwrite
1790     *            {@code true} if existing schema elements with the same
1791     *            conflicting OIDs should be overwritten.
1792     * @return A promise representing the updated schema builder.
1793     * @throws UnsupportedOperationException
1794     *             If the connection does not support search operations.
1795     * @throws IllegalStateException
1796     *             If the connection has already been closed, i.e. if
1797     *             {@code connection.isClosed() == true}.
1798     * @throws NullPointerException
1799     *             If the {@code connection} or {@code name} was {@code null}.
1800     */
1801    public LdapPromise<SchemaBuilder> addSchemaForEntryAsync(final Connection connection, final DN name,
1802            final boolean overwrite) {
1803        return connection.searchSingleEntryAsync(getReadSchemaForEntrySearchRequest(name)).thenAsync(
1804                new AsyncFunction<SearchResultEntry, SchemaBuilder, LdapException>() {
1805                    @Override
1806                    public Promise<SchemaBuilder, LdapException> apply(SearchResultEntry result) throws LdapException {
1807                        final DN subschemaDN = getSubschemaSubentryDN(name, result);
1808                        return addSchemaAsync(connection, subschemaDN, overwrite);
1809                    }
1810                });
1811    }
1812
1813    /**
1814     * Adds the provided substitution syntax definition to this schema builder.
1815     *
1816     * @param oid
1817     *            The OID of the substitution syntax definition.
1818     * @param description
1819     *            The description of the substitution syntax definition.
1820     * @param substituteSyntax
1821     *            The OID of the syntax whose implementation should be
1822     *            substituted.
1823     * @param overwrite
1824     *            {@code true} if any existing syntax with the same OID should
1825     *            be overwritten.
1826     * @return A reference to this schema builder.
1827     * @throws ConflictingSchemaElementException
1828     *             If {@code overwrite} was {@code false} and a conflicting
1829     *             schema element was found.
1830     */
1831    public SchemaBuilder addSubstitutionSyntax(final String oid, final String description,
1832            final String substituteSyntax, final boolean overwrite) {
1833        Reject.ifNull(substituteSyntax);
1834        return buildSyntax(oid)
1835                .description(description)
1836                .extraProperties("X-SUBST", substituteSyntax)
1837                .addToSchema(overwrite);
1838    }
1839
1840    /**
1841     * Adds the provided syntax definition to this schema builder.
1842     *
1843     * @param definition
1844     *            The syntax definition.
1845     * @param overwrite
1846     *            {@code true} if any existing syntax with the same OID should
1847     *            be overwritten.
1848     * @return A reference to this schema builder.
1849     * @throws ConflictingSchemaElementException
1850     *             If {@code overwrite} was {@code false} and a conflicting
1851     *             schema element was found.
1852     * @throws LocalizedIllegalArgumentException
1853     *             If the provided syntax definition could not be parsed.
1854     * @throws NullPointerException
1855     *             If {@code definition} was {@code null}.
1856     */
1857    public SchemaBuilder addSyntax(final String definition, final boolean overwrite) {
1858        return addSyntax(definition, overwrite, null);
1859    }
1860
1861    SchemaBuilder addSyntax(final String definition, final boolean overwrite, SchemaBuilderHook hook) {
1862        Reject.ifNull(definition);
1863
1864        lazyInitBuilder();
1865
1866        try {
1867            final SubstringReader reader = new SubstringReader(definition);
1868
1869            // We'll do this a character at a time. First, skip over any
1870            // leading whitespace.
1871            reader.skipWhitespaces();
1872
1873            if (reader.remaining() <= 0) {
1874                // This means that the value was empty or contained only
1875                // whitespace. That is illegal.
1876                throw new LocalizedIllegalArgumentException(ERR_ATTR_SYNTAX_ATTRSYNTAX_EMPTY_VALUE1.get(definition));
1877            }
1878
1879            // The next character must be an open parenthesis. If it is not,
1880            // then that is an error.
1881            final char c = reader.read();
1882            if (c != '(') {
1883                final LocalizableMessage message = ERR_ATTR_SYNTAX_ATTRSYNTAX_EXPECTED_OPEN_PARENTHESIS.get(
1884                    definition, reader.pos() - 1, String.valueOf(c));
1885                throw new LocalizedIllegalArgumentException(message);
1886            }
1887
1888            // Skip over any spaces immediately following the opening
1889            // parenthesis.
1890            reader.skipWhitespaces();
1891
1892            // The next set of characters must be the OID.
1893            final String oid = readOID(reader, allowsMalformedNamesAndOptions());
1894            final Syntax.Builder syntaxBuilder = new Syntax.Builder(oid, this).definition(definition);
1895
1896            // At this point, we should have a pretty specific syntax that
1897            // describes what may come next, but some of the components are
1898            // optional and it would be pretty easy to put something in the
1899            // wrong order, so we will be very flexible about what we can
1900            // accept. Just look at the next token, figure out what it is and
1901            // how to treat what comes after it, then repeat until we get to
1902            // the end of the value. But before we start, set default values
1903            // for everything else we might need to know.
1904            while (true) {
1905                final String tokenName = readTokenName(reader);
1906
1907                if (tokenName == null) {
1908                    // No more tokens.
1909                    break;
1910                } else if ("desc".equalsIgnoreCase(tokenName)) {
1911                    // This specifies the description for the syntax. It is an
1912                    // arbitrary string of characters enclosed in single quotes.
1913                    syntaxBuilder.description(readQuotedString(reader));
1914                } else if (tokenName.matches("^X-[A-Za-z_-]+$")) {
1915                    // This must be a non-standard property and it must be
1916                    // followed by either a single definition in single quotes
1917                    // or an open parenthesis followed by one or more values in
1918                    // single quotes separated by spaces followed by a close
1919                    // parenthesis.
1920                    final List<String> extensions = readExtensions(reader);
1921                    syntaxBuilder.extraProperties(tokenName, extensions.toArray(new String[extensions.size()]));
1922                } else {
1923                    throw new LocalizedIllegalArgumentException(
1924                        ERR_ATTR_SYNTAX_ATTRSYNTAX_ILLEGAL_TOKEN1.get(definition, tokenName));
1925                }
1926            }
1927
1928            if (hook != null) {
1929                hook.beforeAddSyntax(syntaxBuilder);
1930            }
1931            syntaxBuilder.addToSchema(overwrite);
1932        } catch (final DecodeException e) {
1933            final LocalizableMessage msg =
1934                    ERR_ATTR_SYNTAX_ATTRSYNTAX_INVALID1.get(definition, e.getMessageObject());
1935            throw new LocalizedIllegalArgumentException(msg, e.getCause());
1936        }
1937        return this;
1938    }
1939
1940    Options getOptions() {
1941        lazyInitBuilder();
1942
1943        return options;
1944    }
1945
1946    /**
1947     * Removes the named attribute type from this schema builder.
1948     *
1949     * @param nameOrOid
1950     *            The name or OID of the attribute type to be removed.
1951     * @return {@code true} if the attribute type was found.
1952     */
1953    public boolean removeAttributeType(final String nameOrOid) {
1954        lazyInitBuilder();
1955
1956        final AttributeType element = numericOID2AttributeTypes.get(nameOrOid);
1957        if (element != null) {
1958            removeAttributeType(element);
1959            return true;
1960        }
1961        final List<AttributeType> elements = name2AttributeTypes.get(toLowerCase(nameOrOid));
1962        if (elements != null) {
1963            for (final AttributeType e : elements) {
1964                removeAttributeType(e);
1965            }
1966            return true;
1967        }
1968        return false;
1969    }
1970
1971    /**
1972     * Removes the named DIT content rule from this schema builder.
1973     *
1974     * @param nameOrOid
1975     *            The name or OID of the DIT content rule to be removed.
1976     * @return {@code true} if the DIT content rule was found.
1977     */
1978    public boolean removeDITContentRule(final String nameOrOid) {
1979        lazyInitBuilder();
1980
1981        final DITContentRule element = numericOID2ContentRules.get(nameOrOid);
1982        if (element != null) {
1983            removeDITContentRule(element);
1984            return true;
1985        }
1986        final List<DITContentRule> elements = name2ContentRules.get(toLowerCase(nameOrOid));
1987        if (elements != null) {
1988            for (final DITContentRule e : elements) {
1989                removeDITContentRule(e);
1990            }
1991            return true;
1992        }
1993        return false;
1994    }
1995
1996    /**
1997     * Removes the specified DIT structure rule from this schema builder.
1998     *
1999     * @param ruleID
2000     *            The ID of the DIT structure rule to be removed.
2001     * @return {@code true} if the DIT structure rule was found.
2002     */
2003    public boolean removeDITStructureRule(final int ruleID) {
2004        lazyInitBuilder();
2005
2006        final DITStructureRule element = id2StructureRules.get(ruleID);
2007        if (element != null) {
2008            removeDITStructureRule(element);
2009            return true;
2010        }
2011        return false;
2012    }
2013
2014    /**
2015     * Removes the named matching rule from this schema builder.
2016     *
2017     * @param nameOrOid
2018     *            The name or OID of the matching rule to be removed.
2019     * @return {@code true} if the matching rule was found.
2020     */
2021    public boolean removeMatchingRule(final String nameOrOid) {
2022        lazyInitBuilder();
2023
2024        final MatchingRule element = numericOID2MatchingRules.get(nameOrOid);
2025        if (element != null) {
2026            removeMatchingRule(element);
2027            return true;
2028        }
2029        final List<MatchingRule> elements = name2MatchingRules.get(toLowerCase(nameOrOid));
2030        if (elements != null) {
2031            for (final MatchingRule e : elements) {
2032                removeMatchingRule(e);
2033            }
2034            return true;
2035        }
2036        return false;
2037    }
2038
2039    /**
2040     * Removes the named matching rule use from this schema builder.
2041     *
2042     * @param nameOrOid
2043     *            The name or OID of the matching rule use to be removed.
2044     * @return {@code true} if the matching rule use was found.
2045     */
2046    public boolean removeMatchingRuleUse(final String nameOrOid) {
2047        lazyInitBuilder();
2048
2049        final MatchingRuleUse element = numericOID2MatchingRuleUses.get(nameOrOid);
2050        if (element != null) {
2051            removeMatchingRuleUse(element);
2052            return true;
2053        }
2054        final List<MatchingRuleUse> elements = name2MatchingRuleUses.get(toLowerCase(nameOrOid));
2055        if (elements != null) {
2056            for (final MatchingRuleUse e : elements) {
2057                removeMatchingRuleUse(e);
2058            }
2059            return true;
2060        }
2061        return false;
2062    }
2063
2064    /**
2065     * Removes the named name form from this schema builder.
2066     *
2067     * @param nameOrOid
2068     *            The name or OID of the name form to be removed.
2069     * @return {@code true} if the name form was found.
2070     */
2071    public boolean removeNameForm(final String nameOrOid) {
2072        lazyInitBuilder();
2073
2074        final NameForm element = numericOID2NameForms.get(nameOrOid);
2075        if (element != null) {
2076            removeNameForm(element);
2077            return true;
2078        }
2079        final List<NameForm> elements = name2NameForms.get(toLowerCase(nameOrOid));
2080        if (elements != null) {
2081            for (final NameForm e : elements) {
2082                removeNameForm(e);
2083            }
2084            return true;
2085        }
2086        return false;
2087    }
2088
2089    /**
2090     * Removes the named object class from this schema builder.
2091     *
2092     * @param nameOrOid
2093     *            The name or OID of the object class to be removed.
2094     * @return {@code true} if the object class was found.
2095     */
2096    public boolean removeObjectClass(final String nameOrOid) {
2097        lazyInitBuilder();
2098
2099        final ObjectClass element = numericOID2ObjectClasses.get(nameOrOid);
2100        if (element != null) {
2101            removeObjectClass(element);
2102            return true;
2103        }
2104        final List<ObjectClass> elements = name2ObjectClasses.get(toLowerCase(nameOrOid));
2105        if (elements != null) {
2106            for (final ObjectClass e : elements) {
2107                removeObjectClass(e);
2108            }
2109            return true;
2110        }
2111        return false;
2112    }
2113
2114    /**
2115     * Removes the named syntax from this schema builder.
2116     *
2117     * @param numericOID
2118     *            The name of the syntax to be removed.
2119     * @return {@code true} if the syntax was found.
2120     */
2121    public boolean removeSyntax(final String numericOID) {
2122        lazyInitBuilder();
2123
2124        final Syntax element = numericOID2Syntaxes.get(numericOID);
2125        if (element != null) {
2126            removeSyntax(element);
2127            return true;
2128        }
2129        return false;
2130    }
2131
2132    /**
2133     * Sets a schema option overriding any previous values for the option.
2134     *
2135     * @param <T>
2136     *            The option type.
2137     * @param option
2138     *            Option with which the specified value is to be associated.
2139     * @param value
2140     *            Value to be associated with the specified option.
2141     * @return A reference to this schema builder.
2142     * @throws UnsupportedOperationException
2143     *             If the schema builder options are read only.
2144     */
2145    public <T> SchemaBuilder setOption(final Option<T> option, T value) {
2146        getOptions().set(option, value);
2147        return this;
2148    }
2149
2150    /**
2151     * Returns a strict {@code Schema} containing all of the schema elements
2152     * contained in this schema builder as well as the same set of schema
2153     * compatibility options.
2154     * <p>
2155     * This method does not alter the contents of this schema builder.
2156     *
2157     * @return A {@code Schema} containing all of the schema elements contained
2158     *         in this schema builder as well as the same set of schema
2159     *         compatibility options
2160     */
2161    public Schema toSchema() {
2162        // If this schema builder was initialized from another schema and no
2163        // modifications have been made since then we can simply return the
2164        // original schema.
2165        if (copyOnWriteSchema != null) {
2166            return copyOnWriteSchema;
2167        }
2168
2169        // We still need to ensure that this builder has been initialized
2170        // (otherwise some fields may still be null).
2171        lazyInitBuilder();
2172
2173        final String localSchemaName;
2174        if (schemaName != null) {
2175            localSchemaName = schemaName + "-" + NEXT_SCHEMA_ID.getAndIncrement();
2176        } else {
2177            localSchemaName = "Schema#" + NEXT_SCHEMA_ID.getAndIncrement();
2178        }
2179
2180        Syntax defaultSyntax = numericOID2Syntaxes.get(options.get(DEFAULT_SYNTAX_OID));
2181        if (defaultSyntax == null) {
2182            defaultSyntax = Schema.getCoreSchema().getDefaultSyntax();
2183        }
2184
2185        MatchingRule defaultMatchingRule =  numericOID2MatchingRules.get(options.get(DEFAULT_MATCHING_RULE_OID));
2186        if (defaultMatchingRule == null) {
2187            defaultMatchingRule = Schema.getCoreSchema().getDefaultMatchingRule();
2188        }
2189
2190        final Schema schema =
2191                new Schema.StrictImpl(localSchemaName, options,
2192                        defaultSyntax, defaultMatchingRule, numericOID2Syntaxes,
2193                        numericOID2MatchingRules, numericOID2MatchingRuleUses,
2194                        numericOID2AttributeTypes, numericOID2ObjectClasses, numericOID2NameForms,
2195                        numericOID2ContentRules, id2StructureRules, name2MatchingRules,
2196                        name2MatchingRuleUses, name2AttributeTypes, name2ObjectClasses,
2197                        name2NameForms, name2ContentRules, name2StructureRules,
2198                        objectClass2NameForms, nameForm2StructureRules, warnings).asStrictSchema();
2199        validate(schema);
2200
2201        // Re-init this builder so that it can continue to be used afterwards.
2202        preLazyInitBuilder(schemaName, schema);
2203
2204        return schema;
2205    }
2206
2207    SchemaBuilder addAttributeType(final AttributeType attribute, final boolean overwrite) {
2208        AttributeType conflictingAttribute;
2209        if (numericOID2AttributeTypes.containsKey(attribute.getOID())) {
2210            conflictingAttribute = numericOID2AttributeTypes.get(attribute.getOID());
2211            if (!overwrite) {
2212                final LocalizableMessage message =
2213                        ERR_SCHEMA_CONFLICTING_ATTRIBUTE_OID.get(attribute.getNameOrOID(),
2214                                attribute.getOID(), conflictingAttribute.getNameOrOID());
2215                throw new ConflictingSchemaElementException(message);
2216            }
2217            removeAttributeType(conflictingAttribute);
2218        }
2219
2220        numericOID2AttributeTypes.put(attribute.getOID(), attribute);
2221        for (final String name : attribute.getNames()) {
2222            final String lowerName = StaticUtils.toLowerCase(name);
2223            List<AttributeType> attrs = name2AttributeTypes.get(lowerName);
2224            if (attrs == null) {
2225                name2AttributeTypes.put(lowerName, Collections.singletonList(attribute));
2226            } else if (attrs.size() == 1) {
2227                attrs = new ArrayList<>(attrs);
2228                attrs.add(attribute);
2229                name2AttributeTypes.put(lowerName, attrs);
2230            } else {
2231                attrs.add(attribute);
2232            }
2233        }
2234
2235        return this;
2236    }
2237
2238    SchemaBuilder addDITContentRule(final DITContentRule rule, final boolean overwrite) {
2239        DITContentRule conflictingRule;
2240        if (numericOID2ContentRules.containsKey(rule.getStructuralClassOID())) {
2241            conflictingRule = numericOID2ContentRules.get(rule.getStructuralClassOID());
2242            if (!overwrite) {
2243                final LocalizableMessage message =
2244                        ERR_SCHEMA_CONFLICTING_DIT_CONTENT_RULE1.get(rule.getNameOrOID(), rule
2245                                .getStructuralClassOID(), conflictingRule.getNameOrOID());
2246                throw new ConflictingSchemaElementException(message);
2247            }
2248            removeDITContentRule(conflictingRule);
2249        }
2250
2251        numericOID2ContentRules.put(rule.getStructuralClassOID(), rule);
2252        for (final String name : rule.getNames()) {
2253            final String lowerName = StaticUtils.toLowerCase(name);
2254            List<DITContentRule> rules = name2ContentRules.get(lowerName);
2255            if (rules == null) {
2256                name2ContentRules.put(lowerName, Collections.singletonList(rule));
2257            } else if (rules.size() == 1) {
2258                rules = new ArrayList<>(rules);
2259                rules.add(rule);
2260                name2ContentRules.put(lowerName, rules);
2261            } else {
2262                rules.add(rule);
2263            }
2264        }
2265
2266        return this;
2267    }
2268
2269    SchemaBuilder addDITStructureRule(final DITStructureRule rule, final boolean overwrite) {
2270        DITStructureRule conflictingRule;
2271        if (id2StructureRules.containsKey(rule.getRuleID())) {
2272            conflictingRule = id2StructureRules.get(rule.getRuleID());
2273            if (!overwrite) {
2274                final LocalizableMessage message =
2275                        ERR_SCHEMA_CONFLICTING_DIT_STRUCTURE_RULE_ID.get(rule.getNameOrRuleID(),
2276                                rule.getRuleID(), conflictingRule.getNameOrRuleID());
2277                throw new ConflictingSchemaElementException(message);
2278            }
2279            removeDITStructureRule(conflictingRule);
2280        }
2281
2282        id2StructureRules.put(rule.getRuleID(), rule);
2283        for (final String name : rule.getNames()) {
2284            final String lowerName = StaticUtils.toLowerCase(name);
2285            List<DITStructureRule> rules = name2StructureRules.get(lowerName);
2286            if (rules == null) {
2287                name2StructureRules.put(lowerName, Collections.singletonList(rule));
2288            } else if (rules.size() == 1) {
2289                rules = new ArrayList<>(rules);
2290                rules.add(rule);
2291                name2StructureRules.put(lowerName, rules);
2292            } else {
2293                rules.add(rule);
2294            }
2295        }
2296
2297        return this;
2298    }
2299
2300    SchemaBuilder addMatchingRuleUse(final MatchingRuleUse use, final boolean overwrite) {
2301        MatchingRuleUse conflictingUse;
2302        if (numericOID2MatchingRuleUses.containsKey(use.getMatchingRuleOID())) {
2303            conflictingUse = numericOID2MatchingRuleUses.get(use.getMatchingRuleOID());
2304            if (!overwrite) {
2305                final LocalizableMessage message =
2306                        ERR_SCHEMA_CONFLICTING_MATCHING_RULE_USE.get(use.getNameOrOID(), use
2307                                .getMatchingRuleOID(), conflictingUse.getNameOrOID());
2308                throw new ConflictingSchemaElementException(message);
2309            }
2310            removeMatchingRuleUse(conflictingUse);
2311        }
2312
2313        numericOID2MatchingRuleUses.put(use.getMatchingRuleOID(), use);
2314        for (final String name : use.getNames()) {
2315            final String lowerName = StaticUtils.toLowerCase(name);
2316            List<MatchingRuleUse> uses = name2MatchingRuleUses.get(lowerName);
2317            if (uses == null) {
2318                name2MatchingRuleUses.put(lowerName, Collections.singletonList(use));
2319            } else if (uses.size() == 1) {
2320                uses = new ArrayList<>(uses);
2321                uses.add(use);
2322                name2MatchingRuleUses.put(lowerName, uses);
2323            } else {
2324                uses.add(use);
2325            }
2326        }
2327
2328        return this;
2329    }
2330
2331    SchemaBuilder addMatchingRule(final MatchingRule rule, final boolean overwrite) {
2332        Reject.ifTrue(rule.isValidated(),
2333                "Matching rule has already been validated, it can't be added");
2334        MatchingRule conflictingRule;
2335        if (numericOID2MatchingRules.containsKey(rule.getOID())) {
2336            conflictingRule = numericOID2MatchingRules.get(rule.getOID());
2337            if (!overwrite) {
2338                final LocalizableMessage message =
2339                        ERR_SCHEMA_CONFLICTING_MR_OID.get(rule.getNameOrOID(), rule.getOID(),
2340                                conflictingRule.getNameOrOID());
2341                throw new ConflictingSchemaElementException(message);
2342            }
2343            removeMatchingRule(conflictingRule);
2344        }
2345
2346        numericOID2MatchingRules.put(rule.getOID(), rule);
2347        for (final String name : rule.getNames()) {
2348            final String lowerName = StaticUtils.toLowerCase(name);
2349            List<MatchingRule> rules = name2MatchingRules.get(lowerName);
2350            if (rules == null) {
2351                name2MatchingRules.put(lowerName, Collections.singletonList(rule));
2352            } else if (rules.size() == 1) {
2353                rules = new ArrayList<>(rules);
2354                rules.add(rule);
2355                name2MatchingRules.put(lowerName, rules);
2356            } else {
2357                rules.add(rule);
2358            }
2359        }
2360        return this;
2361    }
2362
2363    SchemaBuilder addNameForm(final NameForm form, final boolean overwrite) {
2364        NameForm conflictingForm;
2365        if (numericOID2NameForms.containsKey(form.getOID())) {
2366            conflictingForm = numericOID2NameForms.get(form.getOID());
2367            if (!overwrite) {
2368                final LocalizableMessage message =
2369                        ERR_SCHEMA_CONFLICTING_NAME_FORM_OID.get(form.getNameOrOID(),
2370                                form.getOID(), conflictingForm.getNameOrOID());
2371                throw new ConflictingSchemaElementException(message);
2372            }
2373            removeNameForm(conflictingForm);
2374        }
2375
2376        numericOID2NameForms.put(form.getOID(), form);
2377        for (final String name : form.getNames()) {
2378            final String lowerName = StaticUtils.toLowerCase(name);
2379            List<NameForm> forms = name2NameForms.get(lowerName);
2380            if (forms == null) {
2381                name2NameForms.put(lowerName, Collections.singletonList(form));
2382            } else if (forms.size() == 1) {
2383                forms = new ArrayList<>(forms);
2384                forms.add(form);
2385                name2NameForms.put(lowerName, forms);
2386            } else {
2387                forms.add(form);
2388            }
2389        }
2390        return this;
2391    }
2392
2393    SchemaBuilder addObjectClass(final ObjectClass oc, final boolean overwrite) {
2394        ObjectClass conflictingOC;
2395        if (numericOID2ObjectClasses.containsKey(oc.getOID())) {
2396            conflictingOC = numericOID2ObjectClasses.get(oc.getOID());
2397            if (!overwrite) {
2398                final LocalizableMessage message =
2399                        ERR_SCHEMA_CONFLICTING_OBJECTCLASS_OID1.get(oc.getNameOrOID(), oc.getOID(),
2400                                conflictingOC.getNameOrOID());
2401                throw new ConflictingSchemaElementException(message);
2402            }
2403            removeObjectClass(conflictingOC);
2404        }
2405
2406        numericOID2ObjectClasses.put(oc.getOID(), oc);
2407        for (final String name : oc.getNames()) {
2408            final String lowerName = StaticUtils.toLowerCase(name);
2409            List<ObjectClass> classes = name2ObjectClasses.get(lowerName);
2410            if (classes == null) {
2411                name2ObjectClasses.put(lowerName, Collections.singletonList(oc));
2412            } else if (classes.size() == 1) {
2413                classes = new ArrayList<>(classes);
2414                classes.add(oc);
2415                name2ObjectClasses.put(lowerName, classes);
2416            } else {
2417                classes.add(oc);
2418            }
2419        }
2420
2421        return this;
2422    }
2423
2424    private void addSchema0(final Schema schema, final boolean overwrite) {
2425        // All of the schema elements must be duplicated because validation will
2426        // cause them to update all their internal references which, although
2427        // unlikely, may be different in the new schema.
2428
2429        for (final Syntax syntax : schema.getSyntaxes()) {
2430            buildSyntax(syntax).addToSchema(overwrite);
2431        }
2432
2433        for (final MatchingRule matchingRule : schema.getMatchingRules()) {
2434            buildMatchingRule(matchingRule).addToSchema(overwrite);
2435        }
2436
2437        for (final MatchingRuleUse matchingRuleUse : schema.getMatchingRuleUses()) {
2438            buildMatchingRuleUse(matchingRuleUse).addToSchema(overwrite);
2439        }
2440
2441        for (final AttributeType attributeType : schema.getAttributeTypes()) {
2442            buildAttributeType(attributeType).addToSchema(overwrite);
2443        }
2444
2445        for (final ObjectClass objectClass : schema.getObjectClasses()) {
2446            buildObjectClass(objectClass).addToSchema(overwrite);
2447        }
2448
2449        for (final NameForm nameForm : schema.getNameForms()) {
2450            buildNameForm(nameForm).addToSchema(overwrite);
2451        }
2452
2453        for (final DITContentRule contentRule : schema.getDITContentRules()) {
2454            buildDITContentRule(contentRule).addToSchema(overwrite);
2455        }
2456
2457        for (final DITStructureRule structureRule : schema.getDITStuctureRules()) {
2458            buildDITStructureRule(structureRule).addToSchema(overwrite);
2459        }
2460    }
2461
2462    SchemaBuilder addSyntax(final Syntax syntax, final boolean overwrite) {
2463        Reject.ifTrue(syntax.isValidated(), "Syntax has already been validated, it can't be added");
2464        Syntax conflictingSyntax;
2465        if (numericOID2Syntaxes.containsKey(syntax.getOID())) {
2466            conflictingSyntax = numericOID2Syntaxes.get(syntax.getOID());
2467            if (!overwrite) {
2468                final LocalizableMessage message = ERR_SCHEMA_CONFLICTING_SYNTAX_OID.get(syntax.toString(),
2469                        syntax.getOID(), conflictingSyntax.getOID());
2470                throw new ConflictingSchemaElementException(message);
2471            }
2472            removeSyntax(conflictingSyntax);
2473        }
2474
2475        numericOID2Syntaxes.put(syntax.getOID(), syntax);
2476        return this;
2477    }
2478
2479    private void lazyInitBuilder() {
2480        // Lazy initialization.
2481        if (numericOID2Syntaxes == null) {
2482            options = Options.defaultOptions();
2483
2484            numericOID2Syntaxes = new LinkedHashMap<>();
2485            numericOID2MatchingRules = new LinkedHashMap<>();
2486            numericOID2MatchingRuleUses = new LinkedHashMap<>();
2487            numericOID2AttributeTypes = new LinkedHashMap<>();
2488            numericOID2ObjectClasses = new LinkedHashMap<>();
2489            numericOID2NameForms = new LinkedHashMap<>();
2490            numericOID2ContentRules = new LinkedHashMap<>();
2491            id2StructureRules = new LinkedHashMap<>();
2492
2493            name2MatchingRules = new LinkedHashMap<>();
2494            name2MatchingRuleUses = new LinkedHashMap<>();
2495            name2AttributeTypes = new LinkedHashMap<>();
2496            name2ObjectClasses = new LinkedHashMap<>();
2497            name2NameForms = new LinkedHashMap<>();
2498            name2ContentRules = new LinkedHashMap<>();
2499            name2StructureRules = new LinkedHashMap<>();
2500
2501            objectClass2NameForms = new HashMap<>();
2502            nameForm2StructureRules = new HashMap<>();
2503            warnings = new LinkedList<>();
2504
2505            if (copyOnWriteSchema != null) {
2506                // Copy the schema.
2507                addSchema0(copyOnWriteSchema, true);
2508                options = Options.copyOf(copyOnWriteSchema.getOptions());
2509                copyOnWriteSchema = null;
2510            }
2511        }
2512    }
2513
2514    private void preLazyInitBuilder(final String schemaName, final Schema copyOnWriteSchema) {
2515        this.schemaName = schemaName;
2516        this.copyOnWriteSchema = copyOnWriteSchema;
2517
2518        this.options = null;
2519
2520        this.numericOID2Syntaxes = null;
2521        this.numericOID2MatchingRules = null;
2522        this.numericOID2MatchingRuleUses = null;
2523        this.numericOID2AttributeTypes = null;
2524        this.numericOID2ObjectClasses = null;
2525        this.numericOID2NameForms = null;
2526        this.numericOID2ContentRules = null;
2527        this.id2StructureRules = null;
2528
2529        this.name2MatchingRules = null;
2530        this.name2MatchingRuleUses = null;
2531        this.name2AttributeTypes = null;
2532        this.name2ObjectClasses = null;
2533        this.name2NameForms = null;
2534        this.name2ContentRules = null;
2535        this.name2StructureRules = null;
2536
2537        this.objectClass2NameForms = null;
2538        this.nameForm2StructureRules = null;
2539        this.warnings = null;
2540    }
2541
2542    private void removeAttributeType(final AttributeType attributeType) {
2543        numericOID2AttributeTypes.remove(attributeType.getOID());
2544        for (final String name : attributeType.getNames()) {
2545            final String lowerName = StaticUtils.toLowerCase(name);
2546            final List<AttributeType> attributes = name2AttributeTypes.get(lowerName);
2547            if (attributes != null && attributes.contains(attributeType)) {
2548                if (attributes.size() <= 1) {
2549                    name2AttributeTypes.remove(lowerName);
2550                } else {
2551                    attributes.remove(attributeType);
2552                }
2553            }
2554        }
2555    }
2556
2557    private void removeDITContentRule(final DITContentRule rule) {
2558        numericOID2ContentRules.remove(rule.getStructuralClassOID());
2559        for (final String name : rule.getNames()) {
2560            final String lowerName = StaticUtils.toLowerCase(name);
2561            final List<DITContentRule> rules = name2ContentRules.get(lowerName);
2562            if (rules != null && rules.contains(rule)) {
2563                if (rules.size() <= 1) {
2564                    name2ContentRules.remove(lowerName);
2565                } else {
2566                    rules.remove(rule);
2567                }
2568            }
2569        }
2570    }
2571
2572    private void removeDITStructureRule(final DITStructureRule rule) {
2573        id2StructureRules.remove(rule.getRuleID());
2574        for (final String name : rule.getNames()) {
2575            final String lowerName = StaticUtils.toLowerCase(name);
2576            final List<DITStructureRule> rules = name2StructureRules.get(lowerName);
2577            if (rules != null && rules.contains(rule)) {
2578                if (rules.size() <= 1) {
2579                    name2StructureRules.remove(lowerName);
2580                } else {
2581                    rules.remove(rule);
2582                }
2583            }
2584        }
2585    }
2586
2587    private void removeMatchingRule(final MatchingRule rule) {
2588        numericOID2MatchingRules.remove(rule.getOID());
2589        for (final String name : rule.getNames()) {
2590            final String lowerName = StaticUtils.toLowerCase(name);
2591            final List<MatchingRule> rules = name2MatchingRules.get(lowerName);
2592            if (rules != null && rules.contains(rule)) {
2593                if (rules.size() <= 1) {
2594                    name2MatchingRules.remove(lowerName);
2595                } else {
2596                    rules.remove(rule);
2597                }
2598            }
2599        }
2600    }
2601
2602    private void removeMatchingRuleUse(final MatchingRuleUse use) {
2603        numericOID2MatchingRuleUses.remove(use.getMatchingRuleOID());
2604        for (final String name : use.getNames()) {
2605            final String lowerName = StaticUtils.toLowerCase(name);
2606            final List<MatchingRuleUse> uses = name2MatchingRuleUses.get(lowerName);
2607            if (uses != null && uses.contains(use)) {
2608                if (uses.size() <= 1) {
2609                    name2MatchingRuleUses.remove(lowerName);
2610                } else {
2611                    uses.remove(use);
2612                }
2613            }
2614        }
2615    }
2616
2617    private void removeNameForm(final NameForm form) {
2618        numericOID2NameForms.remove(form.getOID());
2619        name2NameForms.remove(form.getOID());
2620        for (final String name : form.getNames()) {
2621            final String lowerName = StaticUtils.toLowerCase(name);
2622            final List<NameForm> forms = name2NameForms.get(lowerName);
2623            if (forms != null && forms.contains(form)) {
2624                if (forms.size() <= 1) {
2625                    name2NameForms.remove(lowerName);
2626                } else {
2627                    forms.remove(form);
2628                }
2629            }
2630        }
2631    }
2632
2633    private void removeObjectClass(final ObjectClass oc) {
2634        numericOID2ObjectClasses.remove(oc.getOID());
2635        name2ObjectClasses.remove(oc.getOID());
2636        for (final String name : oc.getNames()) {
2637            final String lowerName = StaticUtils.toLowerCase(name);
2638            final List<ObjectClass> classes = name2ObjectClasses.get(lowerName);
2639            if (classes != null && classes.contains(oc)) {
2640                if (classes.size() <= 1) {
2641                    name2ObjectClasses.remove(lowerName);
2642                } else {
2643                    classes.remove(oc);
2644                }
2645            }
2646        }
2647    }
2648
2649    private void removeSyntax(final Syntax syntax) {
2650        for (Map.Entry<String, List<String>> property : syntax.getExtraProperties().entrySet()) {
2651            if ("x-enum".equalsIgnoreCase(property.getKey())) {
2652                removeMatchingRule(OMR_OID_GENERIC_ENUM + "." + syntax.getOID());
2653                break;
2654            }
2655        }
2656        numericOID2Syntaxes.remove(syntax.getOID());
2657    }
2658
2659    private void validate(final Schema schema) {
2660        // Verify all references in all elements
2661        for (final Syntax syntax : numericOID2Syntaxes.values().toArray(
2662                new Syntax[numericOID2Syntaxes.values().size()])) {
2663            try {
2664                syntax.validate(schema, warnings);
2665            } catch (final SchemaException e) {
2666                removeSyntax(syntax);
2667                warnings.add(ERR_SYNTAX_VALIDATION_FAIL
2668                        .get(syntax.toString(), e.getMessageObject()));
2669            }
2670        }
2671
2672        for (final MatchingRule rule : numericOID2MatchingRules.values().toArray(
2673                new MatchingRule[numericOID2MatchingRules.values().size()])) {
2674            try {
2675                rule.validate(schema, warnings);
2676            } catch (final SchemaException e) {
2677                removeMatchingRule(rule);
2678                warnings.add(ERR_MR_VALIDATION_FAIL.get(rule.toString(), e.getMessageObject()));
2679            }
2680        }
2681
2682        // Attribute types need special processing because they have
2683        // hierarchical dependencies.
2684        final List<AttributeType> invalidAttributeTypes = new LinkedList<>();
2685        for (final AttributeType attributeType : numericOID2AttributeTypes.values()) {
2686            attributeType.validate(schema, invalidAttributeTypes, warnings);
2687        }
2688
2689        for (final AttributeType attributeType : invalidAttributeTypes) {
2690            removeAttributeType(attributeType);
2691        }
2692
2693        // Object classes need special processing because they have hierarchical
2694        // dependencies.
2695        final List<ObjectClass> invalidObjectClasses = new LinkedList<>();
2696        for (final ObjectClass objectClass : numericOID2ObjectClasses.values()) {
2697            objectClass.validate(schema, invalidObjectClasses, warnings);
2698        }
2699
2700        for (final ObjectClass objectClass : invalidObjectClasses) {
2701            removeObjectClass(objectClass);
2702        }
2703
2704        for (final MatchingRuleUse use : numericOID2MatchingRuleUses.values().toArray(
2705                new MatchingRuleUse[numericOID2MatchingRuleUses.values().size()])) {
2706            try {
2707                use.validate(schema);
2708            } catch (final SchemaException e) {
2709                removeMatchingRuleUse(use);
2710                warnings.add(ERR_MRU_VALIDATION_FAIL.get(use.toString(), e.getMessageObject()));
2711            }
2712        }
2713
2714        for (final NameForm form : numericOID2NameForms.values().toArray(
2715                new NameForm[numericOID2NameForms.values().size()])) {
2716            try {
2717                form.validate(schema);
2718
2719                // build the objectClass2NameForms map
2720                final String ocOID = form.getStructuralClass().getOID();
2721                List<NameForm> forms = objectClass2NameForms.get(ocOID);
2722                if (forms == null) {
2723                    objectClass2NameForms.put(ocOID, Collections.singletonList(form));
2724                } else if (forms.size() == 1) {
2725                    forms = new ArrayList<>(forms);
2726                    forms.add(form);
2727                    objectClass2NameForms.put(ocOID, forms);
2728                } else {
2729                    forms.add(form);
2730                }
2731            } catch (final SchemaException e) {
2732                removeNameForm(form);
2733                warnings.add(ERR_NAMEFORM_VALIDATION_FAIL
2734                        .get(form.toString(), e.getMessageObject()));
2735            }
2736        }
2737
2738        for (final DITContentRule rule : numericOID2ContentRules.values().toArray(
2739                new DITContentRule[numericOID2ContentRules.values().size()])) {
2740            try {
2741                rule.validate(schema, warnings);
2742            } catch (final SchemaException e) {
2743                removeDITContentRule(rule);
2744                warnings.add(ERR_DCR_VALIDATION_FAIL.get(rule.toString(), e.getMessageObject()));
2745            }
2746        }
2747
2748        // DIT structure rules need special processing because they have
2749        // hierarchical dependencies.
2750        final List<DITStructureRule> invalidStructureRules = new LinkedList<>();
2751        for (final DITStructureRule rule : id2StructureRules.values()) {
2752            rule.validate(schema, invalidStructureRules, warnings);
2753        }
2754
2755        for (final DITStructureRule rule : invalidStructureRules) {
2756            removeDITStructureRule(rule);
2757        }
2758
2759        for (final DITStructureRule rule : id2StructureRules.values()) {
2760            // build the nameForm2StructureRules map
2761            final String ocOID = rule.getNameForm().getOID();
2762            List<DITStructureRule> rules = nameForm2StructureRules.get(ocOID);
2763            if (rules == null) {
2764                nameForm2StructureRules.put(ocOID, Collections.singletonList(rule));
2765            } else if (rules.size() == 1) {
2766                rules = new ArrayList<>(rules);
2767                rules.add(rule);
2768                nameForm2StructureRules.put(ocOID, rules);
2769            } else {
2770                rules.add(rule);
2771            }
2772        }
2773    }
2774}