add a mechanism for catching invalid mods to some of the global collections

git-svn-id: https://svn.apache.org/repos/asf/webservices/commons/trunk/modules/XmlSchema@1022772 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index b8606aa..e38b7e8 100644
--- a/pom.xml
+++ b/pom.xml
@@ -214,11 +214,15 @@
         </plugin>
         <plugin>
           <artifactId>maven-surefire-plugin</artifactId>
+	  <version>2.6</version>
           <configuration>
 	    <forkMode>never</forkMode>
 	    <additionalClasspathElements>
 	      <additionalClasspathElement>w3c-testcases</additionalClasspathElement>
 	    </additionalClasspathElements>
+	    <systemPropertyVariables>
+	      <org.apache.ws.commons.schema.protectReadOnlyCollections>true</org.apache.ws.commons.schema.protectReadOnlyCollections>
+	    </systemPropertyVariables>
           </configuration>
         </plugin>
         <plugin>
@@ -386,7 +390,7 @@
         <dependency>
           <groupId>xalan</groupId>
           <artifactId>xalan</artifactId>
-          <version>2.7.0</version>
+          <version>2.7.1</version>
         </dependency>
       </dependencies>
       <build>
@@ -394,23 +398,15 @@
           <plugin>
             <artifactId>maven-surefire-plugin</artifactId>
             <configuration>
-              <systemProperties>
-<!--
-									The default xalan TransformerFactory on the ibm jdk is
-									org.apache.xalan.processor.TransformerFactoryImpl which has a
-									known issue with implicit namespaces. Set this property to use
-									the xsltc TransformerFactory (which the sun jdk seems to
-									default to).
-								-->
-                <property>
-                  <name>
-										javax.xml.transform.TransformerFactory
-        							</name>
-                  <value>
-										org.apache.xalan.xsltc.trax.TransformerFactoryImpl
-        							</value>
-                </property>
-              </systemProperties>
+              <systemPropertyVariables>
+		<!--
+		    The default xalan TransformerFactory on the ibm jdk is
+		    org.apache.xalan.processor.TransformerFactoryImpl which has a
+		    known issue with implicit namespaces. Set this property to use
+		    the xsltc TransformerFactory (which the sun jdk seems to
+		    default to). -->
+		<javax.xml.transform.TransformerFactory>org.apache.xalan.xsltc.trax.TransformerFactoryImpl</javax.xml.transform.TransformerFactory>
+              </systemPropertyVariables>
             </configuration>
           </plugin>
         </plugins>
diff --git a/src/main/java/org/apache/ws/commons/schema/XmlSchema.java b/src/main/java/org/apache/ws/commons/schema/XmlSchema.java
index 772fa48..e6d91b0 100644
--- a/src/main/java/org/apache/ws/commons/schema/XmlSchema.java
+++ b/src/main/java/org/apache/ws/commons/schema/XmlSchema.java
@@ -45,6 +45,7 @@
 import org.w3c.dom.Document;
 
 import org.apache.ws.commons.schema.XmlSchemaSerializer.XmlSchemaSerializerException;
+import org.apache.ws.commons.schema.utils.CollectionFactory;
 import org.apache.ws.commons.schema.utils.NamespaceContextOwner;
 import org.apache.ws.commons.schema.utils.NamespacePrefixList;
 
@@ -201,22 +202,28 @@
      * Return a map containing all the defined attribute groups of this schema. The keys are QNames, where the
      * namespace will always be the target namespace of this schema. This makes it easier to look up items for
      * cross-schema references.
+     * <br/>
+     * If org.apache.ws.commons.schema.protectReadOnlyCollections
+     * is 'true', this will return a map that checks at runtime.
      *
      * @return the map of attribute groups.
      */
     public Map<QName, XmlSchemaAttributeGroup> getAttributeGroups() {
-        return attributeGroups;
+        return CollectionFactory.getProtectedMap(attributeGroups);
     }
 
     /**
      * Return a map containing all the defined attributes of this schema. The keys are QNames, where the
      * namespace will always be the target namespace of this schema. This makes it easier to look up items for
      * cross-schema references.
+     * <br/>
+     * If org.apache.ws.commons.schema.protectReadOnlyCollections
+     * is 'true', this will return a map that checks at runtime.
      *
      * @return the map of attributes.
      */
     public Map<QName, XmlSchemaAttribute> getAttributes() {
-        return attributes;
+        return CollectionFactory.getProtectedMap(attributes);
     }
 
     /**
@@ -260,20 +267,26 @@
      * Return a map containing all the defined elements of this schema. The keys are QNames, where the
      * namespace will always be the target namespace of this schema. This makes it easier to look up items for
      * cross-schema references.
+     * <br/>
+     * If org.apache.ws.commons.schema.protectReadOnlyCollections
+     * is 'true', this will return a map that checks at runtime
      *
      * @return the map of elements.
      */
     public Map<QName, XmlSchemaElement> getElements() {
-        return elements;
+        return CollectionFactory.getProtectedMap(elements);
     }
 
     /**
      * Return all of the includes, imports, and redefines for this schema.
+     * <br/>
+     * If org.apache.ws.commons.schema.protectReadOnlyCollections
+     * is 'true', this will return a list that checks at runtime
      *
      * @return a list of the objects representing includes, imports, and redefines.
      */
     public List<XmlSchemaExternal> getExternals() {
-        return externals;
+        return CollectionFactory.getProtectedList(externals);
     }
 
     /**
@@ -296,12 +309,14 @@
     /**
      * Return a map containing all the defined groups of this schema. The keys are QNames, where the namespace
      * will always be the target namespace of this schema. This makes it easier to look up items for
-     * cross-schema references.
+     * cross-schema references.<br/>
+     * If org.apache.ws.commons.schema.protectReadOnlyCollections
+     * is 'true', this will return a map that checks at runtime
      *
      * @return the map of groups.
      */
     public Map<QName, XmlSchemaGroup> getGroups() {
-        return groups;
+        return CollectionFactory.getProtectedMap(groups);
     }
 
     /**
@@ -315,10 +330,14 @@
     }
 
     /**
+     * Return all of the global items in this schema.<br/>
+     * If org.apache.ws.commons.schema.protectReadOnlyCollections
+     * is 'true', this will return a map that checks at runtime.
      * @return <strong>all</strong> of the global items from this schema.
+     *
      */
     public List<XmlSchemaObject> getItems() {
-        return items;
+        return CollectionFactory.getProtectedList(items);
     }
 
     /**
@@ -353,11 +372,14 @@
      * Return a map containing all the defined notations of this schema. The keys are QNames, where the
      * namespace will always be the target namespace of this schema. This makes it easier to look up items for
      * cross-schema references.
+     * <br/>
+     * If org.apache.ws.commons.schema.protectReadOnlyCollections
+     * is 'true', this will return a map that checks at runtime.
      *
      * @return the map of notations.
      */
     public Map<QName, XmlSchemaNotation> getNotations() {
-        return notations;
+        return CollectionFactory.getProtectedMap(notations);
     }
 
     /**
diff --git a/src/main/java/org/apache/ws/commons/schema/XmlSchemaAttribute.java b/src/main/java/org/apache/ws/commons/schema/XmlSchemaAttribute.java
index c466111..3d99040 100644
--- a/src/main/java/org/apache/ws/commons/schema/XmlSchemaAttribute.java
+++ b/src/main/java/org/apache/ws/commons/schema/XmlSchemaAttribute.java
@@ -21,6 +21,7 @@
 

 import javax.xml.namespace.QName;

 

+import org.apache.ws.commons.schema.utils.CollectionFactory;

 import org.apache.ws.commons.schema.utils.XmlSchemaNamedWithForm;

 import org.apache.ws.commons.schema.utils.XmlSchemaNamedWithFormImpl;

 import org.apache.ws.commons.schema.utils.XmlSchemaRef;

@@ -53,8 +54,13 @@
         namedDelegate.setRefObject(ref);

         ref.setNamedObject(namedDelegate);

         use = XmlSchemaUse.NONE;

+        final XmlSchema fSchema = schema;

         if (topLevel) {

-            schema.getItems().add(this);

+            CollectionFactory.withSchemaModifiable(new Runnable() {

+                public void run() {

+                    fSchema.getItems().add(XmlSchemaAttribute.this);

+                }

+            });

         }

     }

 

@@ -130,17 +136,23 @@
     }

 

     public void setName(String name) {

+        final String fName = name;

+        CollectionFactory.withSchemaModifiable(new Runnable() {

 

-        if (namedDelegate.isTopLevel() && namedDelegate.getName() != null) {

-            namedDelegate.getParent().getAttributes().remove(getQName());

-        }

-        namedDelegate.setName(name);

-        if (namedDelegate.isTopLevel()) {

-            if (name == null) {

-                throw new XmlSchemaException("Top-level attributes may not be anonymous");

+            public void run() {

+                if (namedDelegate.isTopLevel() && namedDelegate.getName() != null) {

+                    namedDelegate.getParent().getAttributes().remove(getQName());

+                }

+                namedDelegate.setName(fName);

+                if (namedDelegate.isTopLevel()) {

+                    if (fName == null) {

+                        throw new XmlSchemaException("Top-level attributes may not be anonymous");

+                    }

+                    namedDelegate.getParent().getAttributes().put(getQName(), XmlSchemaAttribute.this);

+                }

             }

-            namedDelegate.getParent().getAttributes().put(getQName(), this);

-        }

+

+        });

     }

 

     public boolean isFormSpecified() {

diff --git a/src/main/java/org/apache/ws/commons/schema/XmlSchemaAttributeGroup.java b/src/main/java/org/apache/ws/commons/schema/XmlSchemaAttributeGroup.java
index ac18697..a41100b 100644
--- a/src/main/java/org/apache/ws/commons/schema/XmlSchemaAttributeGroup.java
+++ b/src/main/java/org/apache/ws/commons/schema/XmlSchemaAttributeGroup.java
@@ -19,20 +19,20 @@
 

 package org.apache.ws.commons.schema;

 

-import java.util.ArrayList;

 import java.util.List;

 

 import javax.xml.namespace.QName;

 

+import org.apache.ws.commons.schema.utils.CollectionFactory;

 import org.apache.ws.commons.schema.utils.XmlSchemaNamed;

 import org.apache.ws.commons.schema.utils.XmlSchemaNamedImpl;

 

 /**

- * Class for attribute groups. Groups a set of attribute declarations so that 

+ * Class for attribute groups. Groups a set of attribute declarations so that

  * they can be incorporated as a

- * group into complex type definitions. Represents the World Wide Web 

+ * group into complex type definitions. Represents the World Wide Web

  * consortium (W3C) attributeGroup element when it does <i>not</i> have a 'ref='

- * attribute. 

+ * attribute.

  */

 

 public class XmlSchemaAttributeGroup extends XmlSchemaAnnotated implements XmlSchemaNamed,

@@ -45,10 +45,16 @@
      * Creates new XmlSchemaAttributeGroup

      */

     public XmlSchemaAttributeGroup(XmlSchema parent) {

+        final XmlSchema fParent = parent;

         namedDelegate = new XmlSchemaNamedImpl(parent, true);

-        parent.getItems().add(this);

+        CollectionFactory.withSchemaModifiable(new Runnable() {

+            public void run() {

+                fParent.getItems().add(XmlSchemaAttributeGroup.this);

+            }

+        });

+

         // we can't be put in the map until we have a name. Perhaps we should be forced to have a name ?

-        attributes = new ArrayList<XmlSchemaAttributeGroupMember>();

+        attributes = CollectionFactory.getList(XmlSchemaAttributeGroupMember.class);

     }

 

     public XmlSchemaAnyAttribute getAnyAttribute() {

@@ -84,10 +90,15 @@
     }

 

     public void setName(String name) {

-        if (name != null) {

-            namedDelegate.getParent().getAttributeGroups().remove(getQName());

-        }

-        namedDelegate.setName(name);

-        namedDelegate.getParent().getAttributeGroups().put(getQName(), this);

+        final String fName = name;

+        CollectionFactory.withSchemaModifiable(new Runnable() {

+            public void run() {

+                if (fName != null) {

+                    namedDelegate.getParent().getAttributeGroups().remove(getQName());

+                }

+                namedDelegate.setName(fName);

+                namedDelegate.getParent().getAttributeGroups().put(getQName(), XmlSchemaAttributeGroup.this);

+            }

+        });

     }

 }

diff --git a/src/main/java/org/apache/ws/commons/schema/XmlSchemaElement.java b/src/main/java/org/apache/ws/commons/schema/XmlSchemaElement.java
index cc0d7c9..288b79f 100644
--- a/src/main/java/org/apache/ws/commons/schema/XmlSchemaElement.java
+++ b/src/main/java/org/apache/ws/commons/schema/XmlSchemaElement.java
@@ -25,6 +25,7 @@
 

 import javax.xml.namespace.QName;

 

+import org.apache.ws.commons.schema.utils.CollectionFactory;

 import org.apache.ws.commons.schema.utils.XmlSchemaNamedWithForm;

 import org.apache.ws.commons.schema.utils.XmlSchemaNamedWithFormImpl;

 import org.apache.ws.commons.schema.utils.XmlSchemaRef;

@@ -93,8 +94,13 @@
         nillable = false;

         finalDerivation = XmlSchemaDerivationMethod.NONE;

         block = XmlSchemaDerivationMethod.NONE;

+        final XmlSchema fParentSchema = parentSchema;

         if (topLevel) {

-            parentSchema.getItems().add(this);

+            CollectionFactory.withSchemaModifiable(new Runnable() {

+                public void run() {

+                    fParentSchema.getItems().add(XmlSchemaElement.this);

+                }

+            });

         }

     }

 

@@ -215,13 +221,18 @@
     }

 

     public void setName(String name) {

-        if (namedDelegate.isTopLevel() && namedDelegate.getName() != null) {

-            namedDelegate.getParent().getElements().remove(getQName());

-        }

-        namedDelegate.setName(name);

-        if (namedDelegate.isTopLevel()) {

-            namedDelegate.getParent().getElements().put(getQName(), this);

-        }

+        final String fName = name;

+        CollectionFactory.withSchemaModifiable(new Runnable() {

+            public void run() {

+                if (namedDelegate.isTopLevel() && namedDelegate.getName() != null) {

+                    namedDelegate.getParent().getElements().remove(getQName());

+                }

+                namedDelegate.setName(fName);

+                if (namedDelegate.isTopLevel()) {

+                    namedDelegate.getParent().getElements().put(getQName(), XmlSchemaElement.this);

+                }

+            }

+        });

     }

 

     public XmlSchemaForm getForm() {

diff --git a/src/main/java/org/apache/ws/commons/schema/XmlSchemaExternal.java b/src/main/java/org/apache/ws/commons/schema/XmlSchemaExternal.java
index 7089c9d..e3ca7a0 100644
--- a/src/main/java/org/apache/ws/commons/schema/XmlSchemaExternal.java
+++ b/src/main/java/org/apache/ws/commons/schema/XmlSchemaExternal.java
@@ -19,6 +19,8 @@
 

 package org.apache.ws.commons.schema;

 

+import org.apache.ws.commons.schema.utils.CollectionFactory;

+

 /**

  * Common class for include, import, and redefine. All have in common two items:

  * the location of the referenced schema (required) and an optional

@@ -33,8 +35,14 @@
      * Creates new XmlSchemaExternal

      */

     protected XmlSchemaExternal(XmlSchema parent) {

-        parent.getExternals().add(this);

-        parent.getItems().add(this);

+        final XmlSchema fParent = parent;

+        CollectionFactory.withSchemaModifiable(new Runnable() {

+

+            public void run() {

+                fParent.getExternals().add(XmlSchemaExternal.this);

+                fParent.getItems().add(XmlSchemaExternal.this);

+            }

+        });

     }

 

     public XmlSchema getSchema() {

diff --git a/src/main/java/org/apache/ws/commons/schema/XmlSchemaGroup.java b/src/main/java/org/apache/ws/commons/schema/XmlSchemaGroup.java
index 37d0436..ed2257f 100644
--- a/src/main/java/org/apache/ws/commons/schema/XmlSchemaGroup.java
+++ b/src/main/java/org/apache/ws/commons/schema/XmlSchemaGroup.java
@@ -22,6 +22,7 @@
 

 import javax.xml.namespace.QName;

 

+import org.apache.ws.commons.schema.utils.CollectionFactory;

 import org.apache.ws.commons.schema.utils.XmlSchemaNamed;

 import org.apache.ws.commons.schema.utils.XmlSchemaNamedImpl;

 

@@ -31,17 +32,22 @@
  * the World Wide Web Consortium (W3C) group element.

  */

 

-public class XmlSchemaGroup extends XmlSchemaAnnotated implements XmlSchemaNamed, 

+public class XmlSchemaGroup extends XmlSchemaAnnotated implements XmlSchemaNamed,

     XmlSchemaChoiceMember, XmlSchemaSequenceMember {

 

     private XmlSchemaGroupParticle particle;

     private XmlSchemaNamedImpl namedDelegate;

-    

+

     public XmlSchemaGroup(XmlSchema parent) {

         namedDelegate = new XmlSchemaNamedImpl(parent, true);

-        parent.getItems().add(this);

+        final XmlSchema fParent = parent;

+        CollectionFactory.withSchemaModifiable(new Runnable() {

+            public void run() {

+                fParent.getItems().add(XmlSchemaGroup.this);

+            }

+        });

     }

-    

+

 

     public XmlSchemaGroupParticle getParticle() {

         return particle;

@@ -72,13 +78,17 @@
     }

 

     public void setName(String name) {

-        if (namedDelegate.getQName() != null) {

-            namedDelegate.getParent().getGroups().remove(namedDelegate.getQName());

-        }

-        namedDelegate.setName(name);

-        if (name != null) {

-            namedDelegate.getParent().getGroups().put(namedDelegate.getQName(), this);

-        }

+        final String fName = name;

+        CollectionFactory.withSchemaModifiable(new Runnable() {

+            public void run() {

+                if (namedDelegate.getQName() != null) {

+                    namedDelegate.getParent().getGroups().remove(namedDelegate.getQName());

+                }

+                namedDelegate.setName(fName);

+                if (fName != null) {

+                    namedDelegate.getParent().getGroups().put(namedDelegate.getQName(), XmlSchemaGroup.this);

+                }

+            }

+        });

     }

-

 }

diff --git a/src/main/java/org/apache/ws/commons/schema/XmlSchemaNotation.java b/src/main/java/org/apache/ws/commons/schema/XmlSchemaNotation.java
index 15d3e2a..a715799 100644
--- a/src/main/java/org/apache/ws/commons/schema/XmlSchemaNotation.java
+++ b/src/main/java/org/apache/ws/commons/schema/XmlSchemaNotation.java
@@ -21,6 +21,7 @@
 

 import javax.xml.namespace.QName;

 

+import org.apache.ws.commons.schema.utils.CollectionFactory;

 import org.apache.ws.commons.schema.utils.XmlSchemaNamed;

 import org.apache.ws.commons.schema.utils.XmlSchemaNamedImpl;

 

@@ -41,7 +42,12 @@
      */

     public XmlSchemaNotation(XmlSchema parent) {

         namedDelegate = new XmlSchemaNamedImpl(parent, true);

-        parent.getItems().add(this);

+        final XmlSchema fParent = parent;

+        CollectionFactory.withSchemaModifiable(new Runnable() {

+            public void run() {

+                fParent.getItems().add(XmlSchemaNotation.this);

+            }

+        });

     }

 

     public String getPublic() {

@@ -93,10 +99,15 @@
     }

 

     public void setName(String name) {

-        if (namedDelegate.getName() != null) {

-            namedDelegate.getParent().getNotations().remove(getQName());

-        }

-        namedDelegate.setName(name);

-        namedDelegate.getParent().getNotations().put(getQName(), this);

+        final String fName = name;

+        CollectionFactory.withSchemaModifiable(new Runnable() {

+            public void run() {

+                if (namedDelegate.getName() != null) {

+                    namedDelegate.getParent().getNotations().remove(getQName());

+                }

+                namedDelegate.setName(fName);

+                namedDelegate.getParent().getNotations().put(getQName(), XmlSchemaNotation.this);

+            }

+        });

     }

 }

diff --git a/src/main/java/org/apache/ws/commons/schema/XmlSchemaType.java b/src/main/java/org/apache/ws/commons/schema/XmlSchemaType.java
index 25d7ebf..2017581 100644
--- a/src/main/java/org/apache/ws/commons/schema/XmlSchemaType.java
+++ b/src/main/java/org/apache/ws/commons/schema/XmlSchemaType.java
@@ -21,6 +21,7 @@
 
 import javax.xml.namespace.QName;
 
+import org.apache.ws.commons.schema.utils.CollectionFactory;
 import org.apache.ws.commons.schema.utils.XmlSchemaNamed;
 import org.apache.ws.commons.schema.utils.XmlSchemaNamedImpl;
 
@@ -42,10 +43,16 @@
      * Creates new XmlSchemaType
      */
     protected XmlSchemaType(XmlSchema schema, boolean topLevel) {
+        final XmlSchema fSchema = schema;
         namedDelegate = new XmlSchemaNamedImpl(schema, topLevel);
         finalDerivation = XmlSchemaDerivationMethod.NONE;
         if (topLevel) {
-            schema.getItems().add(this);
+            CollectionFactory.withSchemaModifiable(new Runnable() {
+
+                public void run() {
+                    fSchema.getItems().add(XmlSchemaType.this);
+                }
+            });
         }
     }
 
diff --git a/src/main/java/org/apache/ws/commons/schema/utils/CollectionFactory.java b/src/main/java/org/apache/ws/commons/schema/utils/CollectionFactory.java
index 3adb0d4..88f4b99 100644
--- a/src/main/java/org/apache/ws/commons/schema/utils/CollectionFactory.java
+++ b/src/main/java/org/apache/ws/commons/schema/utils/CollectionFactory.java
@@ -23,24 +23,79 @@
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
- * There are many collections of XML Schema objects inside XmlSchema.
- * This class provides consistent construction to centralize policy
- * for thread synchronization and the like.
+ * There are many collections of XML Schema objects inside XmlSchema. This class provides consistent
+ * construction to centralize policy for thread synchronization and the like.
  */
 public final class CollectionFactory {
-    
+
+    private static final String PROTECT_READ_ONLY_COLLECTIONS_PROP =
+        "org.apache.ws.commons.schema.protectReadOnlyCollections";
+
+    private static final ThreadLocal<Boolean> PROTECT_READ_ONLY_COLLECTIONS = new ThreadLocal<Boolean>() {
+
+        @Override
+        protected Boolean initialValue() {
+            return Boolean.parseBoolean(System.getProperty(PROTECT_READ_ONLY_COLLECTIONS_PROP));
+        }
+    };
+
     private CollectionFactory() {
     }
-    
+
     public static <T> List<T> getList(Class<T> type) {
         return Collections.synchronizedList(new ArrayList<T>());
     }
-    
+
     public static <T> Set<T> getSet(Class<T> type) {
         return Collections.synchronizedSet(new HashSet<T>());
     }
 
+    /**
+     * Call this to obtain a list to return from a public API where the caller is not supposed to modify the
+     * list. If org.apache.ws.commons.schema.protectReadOnlyCollections is 'true', this will return a list
+     * that checks at runtime.
+     *
+     * @param <T> Generic parameter type of the list.
+     * @param list the list.
+     * @return
+     */
+    public static <T> List<T> getProtectedList(List<T> list) {
+        if (PROTECT_READ_ONLY_COLLECTIONS.get().booleanValue()) {
+            return Collections.unmodifiableList(list);
+        } else {
+            return list;
+        }
+    }
+
+    /**
+     * Call this to obtain a map to return from a public API where the caller is not supposed to modify the
+     * map. If org.apache.ws.commons.schema.protectReadOnlyCollections is 'true', this will return a map that
+     * checks at runtime.
+     *
+     * @param <K> key type
+     * @param <V> value type
+     * @param map the map.
+     * @return
+     */
+    public static <K, V> Map<K, V> getProtectedMap(Map<K, V> map) {
+        if (PROTECT_READ_ONLY_COLLECTIONS.get().booleanValue()) {
+            return Collections.unmodifiableMap(map);
+        } else {
+            return map;
+        }
+    }
+
+    public static void withSchemaModifiable(Runnable action) {
+        Boolean saved = PROTECT_READ_ONLY_COLLECTIONS.get();
+        try {
+            PROTECT_READ_ONLY_COLLECTIONS.set(Boolean.FALSE);
+            action.run();
+        } finally {
+            PROTECT_READ_ONLY_COLLECTIONS.set(saved);
+        }
+    }
 }