From 8d87254b4576d48cc6cbd69765f9f3d7df95e32c Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Tue, 28 Jun 2011 16:46:41 +0000
Subject: [PATCH] Fix OPENDJ-220: Make sure that SchemaBuilder is re-usable after calling toSchema

---
 opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/schema/SchemaBuilderTest.java |  117 ++++++++++++++
 opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/SchemaBuilder.java     |  345 +++++++++++++++++++++++++++++++-----------
 2 files changed, 369 insertions(+), 93 deletions(-)

diff --git a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/SchemaBuilder.java b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/SchemaBuilder.java
index 187aaea..6535580 100644
--- a/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/SchemaBuilder.java
+++ b/opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/schema/SchemaBuilder.java
@@ -171,9 +171,12 @@
 
   private boolean allowMalformedNamesAndOptions;
 
+  // A schema which should be copied into this builder on any mutation.
+  private Schema copyOnWriteSchema = null;
+
   // A unique ID which can be used to uniquely identify schemas
   // constructed without a name.
-  private final AtomicInteger nextSchemaID = new AtomicInteger();
+  private static final AtomicInteger NEXT_SCHEMA_ID = new AtomicInteger();
 
 
 
@@ -183,7 +186,7 @@
    */
   public SchemaBuilder()
   {
-    initBuilder(null);
+    preLazyInitBuilder(null, null);
   }
 
 
@@ -201,8 +204,7 @@
    */
   public SchemaBuilder(final Entry entry) throws NullPointerException
   {
-    initBuilder(entry.getName().toString());
-
+    preLazyInitBuilder(entry.getName().toString(), null);
     addSchema(entry, true);
   }
 
@@ -219,8 +221,7 @@
    */
   public SchemaBuilder(final Schema schema) throws NullPointerException
   {
-    initBuilder(schema.getSchemaName());
-    addSchema(schema, true);
+    preLazyInitBuilder(schema.getSchemaName(), schema);
   }
 
 
@@ -235,7 +236,7 @@
    */
   public SchemaBuilder(final String schemaName)
   {
-    initBuilder(schemaName);
+    preLazyInitBuilder(schemaName, null);
   }
 
 
@@ -262,6 +263,9 @@
       LocalizedIllegalArgumentException, NullPointerException
   {
     Validator.ensureNotNull(definition);
+
+    lazyInitBuilder();
+
     try
     {
       final SubstringReader reader = new SubstringReader(definition);
@@ -586,6 +590,8 @@
       final Map<String, List<String>> extraProperties, final boolean overwrite)
       throws ConflictingSchemaElementException
   {
+    lazyInitBuilder();
+
     final AttributeType attrType = new AttributeType(oid,
         unmodifiableCopyOfList(names), description, obsolete, superiorType,
         equalityMatchingRule, orderingMatchingRule, substringMatchingRule,
@@ -620,6 +626,9 @@
       LocalizedIllegalArgumentException, NullPointerException
   {
     Validator.ensureNotNull(definition);
+
+    lazyInitBuilder();
+
     try
     {
       final SubstringReader reader = new SubstringReader(definition);
@@ -807,6 +816,8 @@
       final Map<String, List<String>> extraProperties, final boolean overwrite)
       throws ConflictingSchemaElementException
   {
+    lazyInitBuilder();
+
     final DITContentRule rule = new DITContentRule(structuralClass,
         unmodifiableCopyOfList(names), description, obsolete,
         unmodifiableCopyOfSet(auxiliaryClasses),
@@ -854,6 +865,8 @@
       final Map<String, List<String>> extraProperties, final boolean overwrite)
       throws ConflictingSchemaElementException
   {
+    lazyInitBuilder();
+
     final DITStructureRule rule = new DITStructureRule(ruleID,
         unmodifiableCopyOfList(names), description, obsolete, nameForm,
         unmodifiableCopyOfSet(superiorRules),
@@ -887,6 +900,9 @@
       LocalizedIllegalArgumentException, NullPointerException
   {
     Validator.ensureNotNull(definition);
+
+    lazyInitBuilder();
+
     try
     {
       final SubstringReader reader = new SubstringReader(definition);
@@ -1045,6 +1061,8 @@
   {
     Validator.ensureNotNull((Object) enumerations);
 
+    lazyInitBuilder();
+
     final EnumSyntaxImpl enumImpl = new EnumSyntaxImpl(oid,
         Arrays.asList(enumerations));
     final Syntax enumSyntax = new Syntax(oid, description,
@@ -1092,6 +1110,9 @@
       LocalizedIllegalArgumentException, NullPointerException
   {
     Validator.ensureNotNull(definition);
+
+    lazyInitBuilder();
+
     try
     {
       final SubstringReader reader = new SubstringReader(definition);
@@ -1256,6 +1277,9 @@
       throws ConflictingSchemaElementException
   {
     Validator.ensureNotNull(implementation);
+
+    lazyInitBuilder();
+
     final MatchingRule matchingRule = new MatchingRule(oid,
         unmodifiableCopyOfList(names), description, obsolete, assertionSyntax,
         unmodifiableCopyOfExtraProperties(extraProperties), null,
@@ -1288,6 +1312,9 @@
       LocalizedIllegalArgumentException, NullPointerException
   {
     Validator.ensureNotNull(definition);
+
+    lazyInitBuilder();
+
     try
     {
       final SubstringReader reader = new SubstringReader(definition);
@@ -1450,6 +1477,8 @@
       final Map<String, List<String>> extraProperties, final boolean overwrite)
       throws ConflictingSchemaElementException
   {
+    lazyInitBuilder();
+
     final MatchingRuleUse use = new MatchingRuleUse(oid,
         unmodifiableCopyOfList(names), description, obsolete,
         unmodifiableCopyOfSet(attributeOIDs),
@@ -1482,6 +1511,9 @@
       LocalizedIllegalArgumentException, NullPointerException
   {
     Validator.ensureNotNull(definition);
+
+    lazyInitBuilder();
+
     try
     {
       final SubstringReader reader = new SubstringReader(definition);
@@ -1672,6 +1704,8 @@
       final Map<String, List<String>> extraProperties, final boolean overwrite)
       throws ConflictingSchemaElementException
   {
+    lazyInitBuilder();
+
     final NameForm nameForm = new NameForm(oid, unmodifiableCopyOfList(names),
         description, obsolete, structuralClass,
         unmodifiableCopyOfSet(requiredAttributes),
@@ -1705,6 +1739,9 @@
       LocalizedIllegalArgumentException, NullPointerException
   {
     Validator.ensureNotNull(definition);
+
+    lazyInitBuilder();
+
     try
     {
       final SubstringReader reader = new SubstringReader(definition);
@@ -1914,6 +1951,8 @@
       final Map<String, List<String>> extraProperties, final boolean overwrite)
       throws ConflictingSchemaElementException
   {
+    lazyInitBuilder();
+
     if (oid.equals(EXTENSIBLE_OBJECT_OBJECTCLASS_OID))
     {
       addObjectClass(new ObjectClass(description,
@@ -1962,6 +2001,8 @@
   {
     Validator.ensureNotNull(pattern);
 
+    lazyInitBuilder();
+
     addSyntax(
         new Syntax(oid, description, Collections.singletonMap("X-PATTERN",
             Collections.singletonList(pattern.toString())), null, null),
@@ -2003,6 +2044,7 @@
       final boolean overwrite) throws UnsupportedOperationException,
       IllegalStateException, NullPointerException
   {
+    // The call to addSchema will perform copyOnWrite.
     final SearchRequest request = getReadSchemaSearchRequest(name);
 
     final FutureResultTransformer<SearchResultEntry, SchemaBuilder> future =
@@ -2060,6 +2102,7 @@
       InterruptedException, UnsupportedOperationException,
       IllegalStateException, NullPointerException
   {
+    // The call to addSchema will perform copyOnWrite.
     final SearchRequest request = getReadSchemaSearchRequest(name);
     final Entry entry = connection.searchSingleEntry(request);
     return addSchema(entry, overwrite);
@@ -2087,6 +2130,8 @@
   {
     Validator.ensureNotNull(entry);
 
+    lazyInitBuilder();
+
     Attribute attr = entry.getAttribute(Schema.ATTR_LDAP_SYNTAXES);
     if (attr != null)
     {
@@ -2241,50 +2286,9 @@
   {
     Validator.ensureNotNull(schema);
 
-    // All of the schema elements must be duplicated because validation will
-    // cause them to update all their internal references which, although
-    // unlikely, may be different in the new schema.
+    lazyInitBuilder();
 
-    for (final Syntax syntax : schema.getSyntaxes())
-    {
-      addSyntax(syntax.duplicate(), overwrite);
-    }
-
-    for (final MatchingRule matchingRule : schema.getMatchingRules())
-    {
-      addMatchingRule(matchingRule.duplicate(), overwrite);
-    }
-
-    for (final MatchingRuleUse matchingRuleUse : schema.getMatchingRuleUses())
-    {
-      addMatchingRuleUse(matchingRuleUse.duplicate(), overwrite);
-    }
-
-    for (final AttributeType attributeType : schema.getAttributeTypes())
-    {
-      addAttributeType(attributeType.duplicate(), overwrite);
-    }
-
-    for (final ObjectClass objectClass : schema.getObjectClasses())
-    {
-      addObjectClass(objectClass.duplicate(), overwrite);
-    }
-
-    for (final NameForm nameForm : schema.getNameForms())
-    {
-      addNameForm(nameForm.duplicate(), overwrite);
-    }
-
-    for (final DITContentRule contentRule : schema.getDITContentRules())
-    {
-      addDITContentRule(contentRule.duplicate(), overwrite);
-    }
-
-    for (final DITStructureRule structureRule : schema.getDITStuctureRules())
-    {
-      addDITStructureRule(structureRule.duplicate(), overwrite);
-    }
-
+    addSchema0(schema, overwrite);
     return this;
   }
 
@@ -2328,6 +2332,7 @@
       final boolean overwrite) throws UnsupportedOperationException,
       IllegalStateException, NullPointerException
   {
+    // The call to addSchema will perform copyOnWrite.
     final RecursiveFutureResult<SearchResultEntry, SchemaBuilder> future =
       new RecursiveFutureResult<SearchResultEntry, SchemaBuilder>(handler)
     {
@@ -2391,6 +2396,7 @@
       InterruptedException, UnsupportedOperationException,
       IllegalStateException, NullPointerException
   {
+    // The call to addSchema will perform copyOnWrite.
     final SearchRequest request = getReadSchemaForEntrySearchRequest(name);
     final Entry entry = connection.searchSingleEntry(request);
     final DN subschemaDN = getSubschemaSubentryDN(name, entry);
@@ -2422,6 +2428,8 @@
   {
     Validator.ensureNotNull(substituteSyntax);
 
+    lazyInitBuilder();
+
     addSyntax(
         new Syntax(oid, description, Collections.singletonMap("X-SUBST",
             Collections.singletonList(substituteSyntax)), null, null),
@@ -2453,6 +2461,9 @@
       LocalizedIllegalArgumentException, NullPointerException
   {
     Validator.ensureNotNull(definition);
+
+    lazyInitBuilder();
+
     try
     {
       final SubstringReader reader = new SubstringReader(definition);
@@ -2604,6 +2615,8 @@
       final SyntaxImpl implementation, final boolean overwrite)
       throws ConflictingSchemaElementException, NullPointerException
   {
+    lazyInitBuilder();
+
     addSyntax(new Syntax(oid, description,
         unmodifiableCopyOfExtraProperties(extraProperties), null,
         implementation), overwrite);
@@ -2636,6 +2649,8 @@
   public SchemaBuilder allowMalformedNamesAndOptions(
       final boolean allowMalformedNamesAndOptions)
   {
+    lazyInitBuilder();
+
     this.allowMalformedNamesAndOptions = allowMalformedNamesAndOptions;
     return this;
   }
@@ -2657,6 +2672,8 @@
   public SchemaBuilder allowNonStandardTelephoneNumbers(
       final boolean allowNonStandardTelephoneNumbers)
   {
+    lazyInitBuilder();
+
     this.allowNonStandardTelephoneNumbers = allowNonStandardTelephoneNumbers;
     return this;
   }
@@ -2680,6 +2697,8 @@
   public SchemaBuilder allowZeroLengthDirectoryStrings(
       final boolean allowZeroLengthDirectoryStrings)
   {
+    lazyInitBuilder();
+
     this.allowZeroLengthDirectoryStrings = allowZeroLengthDirectoryStrings;
     return this;
   }
@@ -2695,6 +2714,8 @@
    */
   public boolean removeAttributeType(final String name)
   {
+    lazyInitBuilder();
+
     final AttributeType element = numericOID2AttributeTypes.get(name);
     if (element != null)
     {
@@ -2725,6 +2746,8 @@
    */
   public boolean removeDITContentRule(final String name)
   {
+    lazyInitBuilder();
+
     final DITContentRule element = numericOID2ContentRules.get(name);
     if (element != null)
     {
@@ -2755,6 +2778,8 @@
    */
   public boolean removeDITStructureRule(final int ruleID)
   {
+    lazyInitBuilder();
+
     final DITStructureRule element = id2StructureRules.get(ruleID);
     if (element != null)
     {
@@ -2775,6 +2800,8 @@
    */
   public boolean removeMatchingRule(final String name)
   {
+    lazyInitBuilder();
+
     final MatchingRule element = numericOID2MatchingRules.get(name);
     if (element != null)
     {
@@ -2805,6 +2832,8 @@
    */
   public boolean removeMatchingRuleUse(final String name)
   {
+    lazyInitBuilder();
+
     final MatchingRuleUse element = numericOID2MatchingRuleUses.get(name);
     if (element != null)
     {
@@ -2835,6 +2864,8 @@
    */
   public boolean removeNameForm(final String name)
   {
+    lazyInitBuilder();
+
     final NameForm element = numericOID2NameForms.get(name);
     if (element != null)
     {
@@ -2864,6 +2895,8 @@
    */
   public boolean removeObjectClass(final String name)
   {
+    lazyInitBuilder();
+
     final ObjectClass element = numericOID2ObjectClasses.get(name);
     if (element != null)
     {
@@ -2894,6 +2927,8 @@
    */
   public boolean removeSyntax(final String numericOID)
   {
+    lazyInitBuilder();
+
     final Syntax element = numericOID2Syntaxes.get(numericOID);
     if (element != null)
     {
@@ -2910,8 +2945,7 @@
    * contained in this schema builder as well as the same set of schema
    * compatibility options.
    * <p>
-   * When this method returns this schema builder is empty and contains a
-   * default set of compatibility options.
+   * This method does not alter the contents of this schema builder.
    *
    * @return A {@code Schema} containing all of the schema elements contained in
    *         this schema builder as well as the same set of schema compatibility
@@ -2919,18 +2953,45 @@
    */
   public Schema toSchema()
   {
-    final Schema schema = new Schema(schemaName, allowMalformedNamesAndOptions,
-        allowNonStandardTelephoneNumbers, allowZeroLengthDirectoryStrings,
-        numericOID2Syntaxes, numericOID2MatchingRules,
-        numericOID2MatchingRuleUses, numericOID2AttributeTypes,
-        numericOID2ObjectClasses, numericOID2NameForms,
-        numericOID2ContentRules, id2StructureRules, name2MatchingRules,
-        name2MatchingRuleUses, name2AttributeTypes, name2ObjectClasses,
-        name2NameForms, name2ContentRules, name2StructureRules,
-        objectClass2NameForms, nameForm2StructureRules, warnings);
+    // If this schema builder was initialized from another schema and no
+    // modifications have been made since then we can simply return the original
+    // schema.
+    if (copyOnWriteSchema != null)
+    {
+      return copyOnWriteSchema;
+    }
+
+    // We still need to ensure that this builder has been initialized (otherwise
+    // some fields may still be null).
+    lazyInitBuilder();
+
+    final String localSchemaName;
+    if (schemaName != null)
+    {
+      localSchemaName = schemaName;
+    }
+    else
+    {
+      localSchemaName = String.format("Schema#%d",
+          NEXT_SCHEMA_ID.getAndIncrement());
+    }
+
+    final Schema schema = new Schema(localSchemaName,
+        allowMalformedNamesAndOptions, allowNonStandardTelephoneNumbers,
+        allowZeroLengthDirectoryStrings, numericOID2Syntaxes,
+        numericOID2MatchingRules, numericOID2MatchingRuleUses,
+        numericOID2AttributeTypes, numericOID2ObjectClasses,
+        numericOID2NameForms, numericOID2ContentRules, id2StructureRules,
+        name2MatchingRules, name2MatchingRuleUses, name2AttributeTypes,
+        name2ObjectClasses, name2NameForms, name2ContentRules,
+        name2StructureRules, objectClass2NameForms, nameForm2StructureRules,
+        warnings);
 
     validate(schema);
-    initBuilder(null);
+
+    // Re-init this builder so that it can continue to be used afterwards.
+    preLazyInitBuilder(schemaName, schema);
+
     return schema;
   }
 
@@ -3224,6 +3285,55 @@
 
 
 
+  private void addSchema0(final Schema schema, final boolean overwrite)
+  {
+    // All of the schema elements must be duplicated because validation will
+    // cause them to update all their internal references which, although
+    // unlikely, may be different in the new schema.
+
+    for (final Syntax syntax : schema.getSyntaxes())
+    {
+      addSyntax(syntax.duplicate(), overwrite);
+    }
+
+    for (final MatchingRule matchingRule : schema.getMatchingRules())
+    {
+      addMatchingRule(matchingRule.duplicate(), overwrite);
+    }
+
+    for (final MatchingRuleUse matchingRuleUse : schema.getMatchingRuleUses())
+    {
+      addMatchingRuleUse(matchingRuleUse.duplicate(), overwrite);
+    }
+
+    for (final AttributeType attributeType : schema.getAttributeTypes())
+    {
+      addAttributeType(attributeType.duplicate(), overwrite);
+    }
+
+    for (final ObjectClass objectClass : schema.getObjectClasses())
+    {
+      addObjectClass(objectClass.duplicate(), overwrite);
+    }
+
+    for (final NameForm nameForm : schema.getNameForms())
+    {
+      addNameForm(nameForm.duplicate(), overwrite);
+    }
+
+    for (final DITContentRule contentRule : schema.getDITContentRules())
+    {
+      addDITContentRule(contentRule.duplicate(), overwrite);
+    }
+
+    for (final DITStructureRule structureRule : schema.getDITStuctureRules())
+    {
+      addDITStructureRule(structureRule.duplicate(), overwrite);
+    }
+  }
+
+
+
   private void addSyntax(final Syntax syntax, final boolean overwrite)
       throws ConflictingSchemaElementException
   {
@@ -3244,37 +3354,85 @@
 
 
 
-  private void initBuilder(String schemaName)
+  private void lazyInitBuilder()
   {
-    if (schemaName == null)
+    // Lazy initialization.
+    if (numericOID2Syntaxes == null)
     {
-      schemaName = String.format("Schema#%d", nextSchemaID.getAndIncrement());
+      allowMalformedNamesAndOptions = true;
+      allowNonStandardTelephoneNumbers = true;
+      allowZeroLengthDirectoryStrings = false;
+
+      numericOID2Syntaxes = new LinkedHashMap<String, Syntax>();
+      numericOID2MatchingRules = new LinkedHashMap<String, MatchingRule>();
+      numericOID2MatchingRuleUses = new LinkedHashMap<String, MatchingRuleUse>();
+      numericOID2AttributeTypes = new LinkedHashMap<String, AttributeType>();
+      numericOID2ObjectClasses = new LinkedHashMap<String, ObjectClass>();
+      numericOID2NameForms = new LinkedHashMap<String, NameForm>();
+      numericOID2ContentRules = new LinkedHashMap<String, DITContentRule>();
+      id2StructureRules = new LinkedHashMap<Integer, DITStructureRule>();
+
+      name2MatchingRules = new LinkedHashMap<String, List<MatchingRule>>();
+      name2MatchingRuleUses = new LinkedHashMap<String, List<MatchingRuleUse>>();
+      name2AttributeTypes = new LinkedHashMap<String, List<AttributeType>>();
+      name2ObjectClasses = new LinkedHashMap<String, List<ObjectClass>>();
+      name2NameForms = new LinkedHashMap<String, List<NameForm>>();
+      name2ContentRules = new LinkedHashMap<String, List<DITContentRule>>();
+      name2StructureRules = new LinkedHashMap<String, List<DITStructureRule>>();
+
+      objectClass2NameForms = new HashMap<String, List<NameForm>>();
+      nameForm2StructureRules = new HashMap<String, List<DITStructureRule>>();
+      warnings = new LinkedList<LocalizableMessage>();
     }
+
+    if (copyOnWriteSchema != null)
+    {
+      // Copy the schema.
+      addSchema0(copyOnWriteSchema, true);
+
+      allowMalformedNamesAndOptions = copyOnWriteSchema
+          .allowMalformedNamesAndOptions();
+      allowNonStandardTelephoneNumbers = copyOnWriteSchema
+          .allowNonStandardTelephoneNumbers();
+      allowZeroLengthDirectoryStrings = copyOnWriteSchema
+          .allowZeroLengthDirectoryStrings();
+
+      copyOnWriteSchema = null;
+    }
+  }
+
+
+
+  private void preLazyInitBuilder(final String schemaName,
+      final Schema copyOnWriteSchema)
+  {
     this.schemaName = schemaName;
+    this.copyOnWriteSchema = copyOnWriteSchema;
 
-    allowMalformedNamesAndOptions = true;
-    allowNonStandardTelephoneNumbers = true;
-    allowZeroLengthDirectoryStrings = false;
-    numericOID2Syntaxes = new LinkedHashMap<String, Syntax>();
-    numericOID2MatchingRules = new LinkedHashMap<String, MatchingRule>();
-    numericOID2MatchingRuleUses = new LinkedHashMap<String, MatchingRuleUse>();
-    numericOID2AttributeTypes = new LinkedHashMap<String, AttributeType>();
-    numericOID2ObjectClasses = new LinkedHashMap<String, ObjectClass>();
-    numericOID2NameForms = new LinkedHashMap<String, NameForm>();
-    numericOID2ContentRules = new LinkedHashMap<String, DITContentRule>();
-    id2StructureRules = new LinkedHashMap<Integer, DITStructureRule>();
+    this.allowMalformedNamesAndOptions = true;
+    this.allowNonStandardTelephoneNumbers = true;
+    this.allowZeroLengthDirectoryStrings = false;
 
-    name2MatchingRules = new LinkedHashMap<String, List<MatchingRule>>();
-    name2MatchingRuleUses = new LinkedHashMap<String, List<MatchingRuleUse>>();
-    name2AttributeTypes = new LinkedHashMap<String, List<AttributeType>>();
-    name2ObjectClasses = new LinkedHashMap<String, List<ObjectClass>>();
-    name2NameForms = new LinkedHashMap<String, List<NameForm>>();
-    name2ContentRules = new LinkedHashMap<String, List<DITContentRule>>();
-    name2StructureRules = new LinkedHashMap<String, List<DITStructureRule>>();
+    this.numericOID2Syntaxes = null;
+    this.numericOID2MatchingRules = null;
+    this.numericOID2MatchingRuleUses = null;
+    this.numericOID2AttributeTypes = null;
+    this.numericOID2ObjectClasses = null;
+    this.numericOID2NameForms = null;
+    this.numericOID2ContentRules = null;
+    this.id2StructureRules = null;
 
-    objectClass2NameForms = new HashMap<String, List<NameForm>>();
-    nameForm2StructureRules = new HashMap<String, List<DITStructureRule>>();
-    warnings = new LinkedList<LocalizableMessage>();
+    this.name2MatchingRules = null;
+    this.name2MatchingRuleUses = null;
+    this.name2AttributeTypes = null;
+    this.name2ObjectClasses = null;
+    this.name2NameForms = null;
+    this.name2ContentRules = null;
+    this.name2StructureRules = null;
+
+    this.objectClass2NameForms = null;
+    this.nameForm2StructureRules = null;
+    this.warnings = null;
   }
 
 
@@ -3484,26 +3642,26 @@
 
     // Attribute types need special processing because they have hierarchical
     // dependencies.
-    List<AttributeType> invalidAttributeTypes = new LinkedList<AttributeType>();
+    final List<AttributeType> invalidAttributeTypes = new LinkedList<AttributeType>();
     for (final AttributeType attributeType : numericOID2AttributeTypes.values())
     {
       attributeType.validate(schema, invalidAttributeTypes, warnings);
     }
 
-    for (AttributeType attributeType : invalidAttributeTypes)
+    for (final AttributeType attributeType : invalidAttributeTypes)
     {
       removeAttributeType(attributeType);
     }
 
     // Object classes need special processing because they have hierarchical
     // dependencies.
-    List<ObjectClass> invalidObjectClasses = new LinkedList<ObjectClass>();
+    final List<ObjectClass> invalidObjectClasses = new LinkedList<ObjectClass>();
     for (final ObjectClass objectClass : numericOID2ObjectClasses.values())
     {
       objectClass.validate(schema, invalidObjectClasses, warnings);
     }
 
-    for (ObjectClass objectClass : invalidObjectClasses)
+    for (final ObjectClass objectClass : invalidObjectClasses)
     {
       removeObjectClass(objectClass);
     }
@@ -3572,15 +3730,16 @@
       }
     }
 
-    // DIT structure rules need special processing because they have hierarchical
+    // DIT structure rules need special processing because they have
+    // hierarchical
     // dependencies.
-    List<DITStructureRule> invalidStructureRules = new LinkedList<DITStructureRule>();
+    final List<DITStructureRule> invalidStructureRules = new LinkedList<DITStructureRule>();
     for (final DITStructureRule rule : id2StructureRules.values())
     {
       rule.validate(schema, invalidStructureRules, warnings);
     }
 
-    for (DITStructureRule rule : invalidStructureRules)
+    for (final DITStructureRule rule : invalidStructureRules)
     {
       removeDITStructureRule(rule);
     }
diff --git a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/schema/SchemaBuilderTest.java b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/schema/SchemaBuilderTest.java
index e2374cb..3b6125c 100644
--- a/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/schema/SchemaBuilderTest.java
+++ b/opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/schema/SchemaBuilderTest.java
@@ -182,6 +182,98 @@
 
 
   /**
+   * Tests that it is possible to create a schema which is an exact copy of
+   * another and take advantage of copy on write.
+   */
+  @Test
+  public void testCopyOnWriteNoChanges()
+  {
+    final Schema baseSchema = Schema.getCoreSchema();
+    final Schema schema = new SchemaBuilder(baseSchema).toSchema();
+
+    assertThat(schema).isSameAs(baseSchema);
+  }
+
+
+
+  /**
+   * Tests that it is possible to create a schema which is based on another.
+   */
+  @Test
+  public void testCopyOnWriteWithChanges()
+  {
+    final Schema baseSchema = Schema.getCoreSchema();
+    final Schema schema = new SchemaBuilder(baseSchema).addAttributeType(
+        "( testtype-oid NAME 'testtype' SUP name )", false).toSchema();
+    assertThat(schema).isNotSameAs(baseSchema);
+    assertThat(schema.getObjectClasses().containsAll(
+        baseSchema.getObjectClasses()));
+    assertThat(schema.getObjectClasses().size()).isEqualTo(
+        baseSchema.getObjectClasses().size());
+    assertThat(schema.getAttributeTypes().containsAll(
+        baseSchema.getAttributeTypes()));
+    assertThat(schema.getAttributeType("testtype")).isNotNull();
+    assertThat(schema.getSchemaName()).isEqualTo(baseSchema.getSchemaName());
+    assertThat(schema.allowMalformedNamesAndOptions()).isEqualTo(
+        baseSchema.allowMalformedNamesAndOptions());
+  }
+
+
+
+  /**
+   * Tests that it is possible to create an empty schema.
+   */
+  @Test
+  public void testCreateEmptySchema()
+  {
+    final Schema schema = new SchemaBuilder().toSchema();
+    assertThat(schema.getAttributeTypes()).isEmpty();
+    assertThat(schema.getObjectClasses()).isEmpty();
+    assertThat(schema.getSyntaxes()).isEmpty();
+    assertThat(schema.getWarnings()).isEmpty();
+    // Could go on...
+  }
+
+
+
+  /**
+   * Tests that multiple consecutive invocations of toSchema return the exact
+   * same schema.
+   */
+  @Test
+  public void testMultipleToSchema1()
+  {
+    final Schema baseSchema = Schema.getCoreSchema();
+    final SchemaBuilder builder = new SchemaBuilder(baseSchema);
+    final Schema schema1 = builder.toSchema();
+    final Schema schema2 = builder.toSchema();
+    assertThat(schema1).isSameAs(baseSchema);
+    assertThat(schema1).isSameAs(schema2);
+  }
+
+
+
+  /**
+   * Tests that multiple consecutive invocations of toSchema return the exact
+   * same schema.
+   */
+  @Test
+  public void testMultipleToSchema2()
+  {
+    final SchemaBuilder builder = new SchemaBuilder()
+        .addAttributeType(
+            "( testtype-oid NAME 'testtype' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )",
+            false);
+    final Schema schema1 = builder.toSchema();
+    final Schema schema2 = builder.toSchema();
+    assertThat(schema1).isSameAs(schema2);
+    assertThat(schema1.getAttributeType("testtype")).isNotNull();
+    assertThat(schema2.getAttributeType("testtype")).isNotNull();
+  }
+
+
+
+  /**
    * Tests that schema validation resolves dependencies between parent/child
    * object classes regardless of the order in which they were added.
    */
@@ -302,4 +394,29 @@
       // Expected.
     }
   }
+
+
+
+  /**
+   * Tests that a schema builder can be re-used after toSchema has been called.
+   */
+  @Test
+  public void testReuseSchemaBuilder()
+  {
+    final SchemaBuilder builder = new SchemaBuilder();
+    final Schema schema1 = builder
+        .addAttributeType(
+            "( testtype1-oid NAME 'testtype1' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )",
+            false).toSchema();
+
+    final Schema schema2 = builder
+        .addAttributeType(
+            "( testtype2-oid NAME 'testtype2' SYNTAX 1.3.6.1.4.1.1466.115.121.1.40 )",
+            false).toSchema();
+    assertThat(schema1).isNotSameAs(schema2);
+    assertThat(schema1.getAttributeType("testtype1")).isNotNull();
+    assertThat(schema1.hasAttributeType("testtype2")).isFalse();
+    assertThat(schema2.getAttributeType("testtype1")).isNotNull();
+    assertThat(schema2.getAttributeType("testtype2")).isNotNull();
+  }
 }

--
Gitblit v1.10.0