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