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