sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1478868 [1/2] - in /sis/trunk: ./ sis-metadata/src/main/java/org/apache/sis/metadata/ sis-metadata/src/test/java/org/apache/sis/metadata/ sis-metadata/src/test/java/org/apache/sis/metadata/iso/citation/ sis-metadata/src/test/java/org/apach...
Date Fri, 03 May 2013 16:33:26 GMT
Author: desruisseaux
Date: Fri May  3 16:33:25 2013
New Revision: 1478868

URL: http://svn.apache.org/r1478868
Log:
Merge from the JDK6 branch.

Added:
    sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTreeTableTest.java
      - copied unchanged from r1478867, sis/branches/JDK6/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTreeTableTest.java
Modified:
    sis/trunk/   (props changed)
    sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/AbstractMetadata.java
    sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java
    sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataTreeChildren.java
    sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataTreeNode.java
    sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/PropertyAccessor.java
    sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/StandardImplementation.java
    sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/ValueMap.java
    sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataStandardTest.java
    sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTestCase.java
    sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTreeChildrenTest.java
    sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTreeNodeTest.java
    sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/PropertyAccessorTest.java
    sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/iso/citation/DefaultCitationDateTest.java
    sis/trunk/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java
    sis/trunk/sis-utility/src/main/java/org/apache/sis/util/ObjectConverters.java
    sis/trunk/sis-utility/src/main/java/org/apache/sis/util/collection/DefaultTreeTable.java
    sis/trunk/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
    sis/trunk/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
    sis/trunk/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties

Propchange: sis/trunk/
------------------------------------------------------------------------------
  Merged /sis/branches/JDK7:r1478203-1478864
  Merged /sis/branches/JDK6:r1478217-1478867

Modified: sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/AbstractMetadata.java
URL: http://svn.apache.org/viewvc/sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/AbstractMetadata.java?rev=1478868&r1=1478867&r2=1478868&view=diff
==============================================================================
--- sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/AbstractMetadata.java [UTF-8] (original)
+++ sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/AbstractMetadata.java [UTF-8] Fri May  3 16:33:25 2013
@@ -23,8 +23,6 @@ import org.apache.sis.util.LenientCompar
 import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.logging.Logging;
 
-import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
-
 
 /**
  * Provides basic operations using Java reflection for metadata implementations.
@@ -45,7 +43,6 @@ import static org.apache.sis.util.Argume
  *
  * <ul>
  *   <li>{@link #prune()}</li>
- *   <li>{@link #shallowCopy(Object)}</li>
  *   <li>{@link #asMap()} with {@code put} operations</li>
  * </ul>
  *
@@ -154,36 +151,27 @@ public abstract class AbstractMetadata i
     }
 
     /**
-     * Copies the values from the specified metadata. The {@code source} metadata must implements
-     * the same metadata interface (defined by the {@linkplain #getStandard() standard}) than this
-     * class, but doesn't need to be the same implementation class.
-     * The default implementation performs the copy using Java reflections.
-     *
-     * {@note This method is intended to provide the functionality of a <cite>copy constructor</cite>.
-     * We do not provide copy constructor directly because usage of Java reflection in this context
-     * is unsafe (we could invoke subclass methods before the subclasses construction is completed).}
-     *
-     * @param  source The metadata to copy values from.
-     * @throws ClassCastException if the specified metadata doesn't implements the expected
-     *         metadata interface.
-     * @throws UnmodifiableMetadataException if this class doesn't define {@code set*(…)} methods
-     *         corresponding to the {@code get*()} methods found in the implemented interface, or
-     *         if this instance is not modifiable for some other reason.
-     */
-    public void shallowCopy(final Object source) throws ClassCastException, UnmodifiableMetadataException {
-        ensureNonNull("source", source);
-        getStandard().shallowCopy(source, this);
-    }
-
-    /**
      * Returns a view of the property values in a {@link Map}. The map is backed by this metadata
      * object, so changes in the underlying metadata object are immediately reflected in the map
      * and conversely.
      *
      * <p>The map supports the {@link Map#put(Object, Object) put(…)} and {@link Map#remove(Object)
      * remove(…)} operations if the underlying metadata object contains setter methods.
-     * The keys are case-insensitive and can be either the JavaBeans property name or
-     * the UML identifier.</p>
+     * The {@code remove(…)} method is implemented by a call to {@code put(…, null)}.</p>
+     *
+     * <p>The keys are case-insensitive and can be either the JavaBeans property name, the getter method name
+     * or the {@linkplain org.opengis.annotation.UML#identifier() UML identifier}. The value given to a call
+     * to the {@code put(…)} method shall be an instance of the type expected by the corresponding setter method,
+     * or an instance of a type {@linkplain org.apache.sis.util.ObjectConverters#find(Class, Class) convertible}
+     * to the expected type.</p>
+     *
+     * <p>Calls to {@code put(…)} replace the previous value, with one noticeable exception: if the metadata
+     * property associated to the given key is a {@link java.util.Collection} but the given value is a single
+     * element (not a collection), then the given value is {@linkplain java.util.Collection#add(Object) added}
+     * to the existing collection. In other words, the returned map behaves as a <cite>multi-values map</cite>
+     * for the properties that allow multiple values. If the intend is to unconditionally discard all previous
+     * values, then make sure that the given value is a collection when the associated metadata property expects
+     * such collection.</p>
      *
      * <p>The default implementation is equivalent to the following method call:</p>
      *

Modified: sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java
URL: http://svn.apache.org/viewvc/sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java?rev=1478868&r1=1478867&r2=1478868&view=diff
==============================================================================
--- sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java [UTF-8] (original)
+++ sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java [UTF-8] Fri May  3 16:33:25 2013
@@ -22,6 +22,10 @@ import java.util.IdentityHashMap;
 import java.util.LinkedHashSet;
 import java.util.Collection;
 import java.util.Iterator;
+import java.io.IOException;
+import java.io.Serializable;
+import java.io.ObjectInputStream;
+import java.lang.reflect.Field;
 import net.jcip.annotations.ThreadSafe;
 import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.ExtendedElementInformation;
@@ -81,7 +85,12 @@ import static org.apache.sis.util.Argume
  * @module
  */
 @ThreadSafe
-public class MetadataStandard {
+public class MetadataStandard implements Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 7549790450195184843L;
+
     /**
      * Metadata instances defined in this class. The current implementation does not yet
      * contains the user-defined instances. However this may be something we will need to
@@ -115,6 +124,8 @@ public class MetadataStandard {
     static {
         final String[] prefix = {"Default", "Abstract"};
         final String[] acronyms = {"CoordinateSystem", "CS", "CoordinateReferenceSystem", "CRS"};
+
+        // If new StandardImplementation instances are added below, please update StandardImplementation.readResolve().
         ISO_19111 = new StandardImplementation("ISO 19111", "org.opengis.referencing.", "org.apache.sis.referencing.", prefix, acronyms);
         ISO_19115 = new StandardImplementation("ISO 19115", "org.opengis.metadata.", "org.apache.sis.metadata.iso.", prefix, null);
         ISO_19119 = new MetadataStandard      ("ISO 19119", "org.opengis.service.");
@@ -137,7 +148,7 @@ public class MetadataStandard {
      *
      * @see #getCitation()
      */
-    private final Citation citation;
+    final Citation citation;
 
     /**
      * The root packages for metadata interfaces. Must have a trailing {@code '.'}.
@@ -154,7 +165,7 @@ public class MetadataStandard {
      *   <li>{@link PropertyAccessor} otherwise.</li>
      * </ul>
      */
-    private final Map<Class<?>, Object> accessors;
+    private final transient Map<Class<?>, Object> accessors; // written by reflection on deserialization.
 
     /**
      * Creates a new instance working on implementation of interfaces defined in the specified package.
@@ -170,7 +181,7 @@ public class MetadataStandard {
         ensureNonNull("interfacePackage", interfacePackage);
         this.citation         = citation;
         this.interfacePackage = interfacePackage.getName() + '.';
-        this.accessors        = new IdentityHashMap<Class<?>,Object>();
+        this.accessors        = new IdentityHashMap<Class<?>,Object>(); // Also defined in readObject(…)
     }
 
     /**
@@ -571,8 +582,23 @@ public class MetadataStandard {
      *
      * <p>The map supports the {@link Map#put(Object, Object) put(…)} and {@link Map#remove(Object)
      * remove(…)} operations if the underlying metadata object contains setter methods.
-     * The keys are case-insensitive and can be either the JavaBeans property name or
-     * the UML identifier.</p>
+     * The {@code remove(…)} method is implemented by a call to {@code put(…, null)}.
+     * Note that whether the entry appears as effectively removed from the map or just cleared
+     * (i.e. associated to a null value) depends on the {@code valuePolicy} argument.</p>
+     *
+     * <p>The keys are case-insensitive and can be either the JavaBeans property name, the getter method name
+     * or the {@linkplain org.opengis.annotation.UML#identifier() UML identifier}. The value given to a call
+     * to the {@code put(…)} method shall be an instance of the type expected by the corresponding setter method,
+     * or an instance of a type {@linkplain org.apache.sis.util.ObjectConverters#find(Class, Class) convertible}
+     * to the expected type.</p>
+     *
+     * <p>Calls to {@code put(…)} replace the previous value, with one noticeable exception: if the metadata
+     * property associated to the given key is a {@link java.util.Collection} but the given value is a single
+     * element (not a collection), then the given value is {@linkplain java.util.Collection#add(Object) added}
+     * to the existing collection. In other words, the returned map behaves as a <cite>multi-values map</cite>
+     * for the properties that allow multiple values. If the intend is to unconditionally discard all previous
+     * values, then make sure that the given value is a collection when the associated metadata property expects
+     * such collection.</p>
      *
      * @param  metadata The metadata object to view as a map.
      * @param  keyPolicy Determines the string representation of map keys.
@@ -629,37 +655,6 @@ public class MetadataStandard {
     }
 
     /**
-     * Copies all metadata from source to target.
-     * The source must implements the same metadata interface than the target.
-     *
-     * <p>If the source contains any null or empty properties, then those properties will
-     * <strong>not</strong> overwrite the corresponding properties in the destination metadata.</p>
-     *
-     * @param  source The metadata to copy.
-     * @param  target The target metadata.
-     * @throws ClassCastException if the source or target object don't
-     *         implements a metadata interface of the expected package.
-     * @throws UnmodifiableMetadataException if the target metadata is unmodifiable,
-     *         or if at least one setter method was required but not found.
-     *
-     * @see ModifiableMetadata#clone()
-     */
-    public void shallowCopy(final Object source, final Object target)
-            throws ClassCastException, UnmodifiableMetadataException
-    {
-        ensureNonNull("target", target);
-        final PropertyAccessor accessor = getAccessor(target.getClass(), true);
-        if (!accessor.type.isInstance(source)) {
-            ensureNonNull("source", source);
-            throw new ClassCastException(Errors.format(Errors.Keys.IllegalArgumentClass_3,
-                    "source", accessor.type, source.getClass()));
-        }
-        if (!accessor.shallowCopy(source, target)) {
-            throw new UnmodifiableMetadataException(Errors.format(Errors.Keys.UnmodifiableMetadata));
-        }
-    }
-
-    /**
      * Compares the two specified metadata objects.
      * The two metadata arguments shall be implementations of a metadata interface defined by
      * this {@code MetadataStandard}, otherwise an exception will be thrown. However the two
@@ -723,4 +718,26 @@ public class MetadataStandard {
     public String toString() {
         return Classes.getShortClassName(this) + '[' + citation.getTitle() + ']';
     }
+
+    /**
+     * Assigns an {@link IdentityHashMap} instance to the given field.
+     * Used on deserialization only.
+     */
+    final void setMapForField(final Class<?> classe, final String name) {
+        try {
+            final Field field = classe.getDeclaredField(name);
+            field.setAccessible(true);
+            field.set(this, new IdentityHashMap());
+        } catch (Exception e) { // (ReflectiveOperationException) on JDK7 branch.
+            throw new AssertionError(e); // Should never happen (tested by MetadataStandardTest).
+        }
+    }
+
+    /**
+     * Invoked during deserialization for restoring the transient fields.
+     */
+    private void readObject(final ObjectInputStream in) throws IOException, ClassNotFoundException {
+        in.defaultReadObject();
+        setMapForField(MetadataStandard.class, "accessors");
+    }
 }

Modified: sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataTreeChildren.java
URL: http://svn.apache.org/viewvc/sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataTreeChildren.java?rev=1478868&r1=1478867&r2=1478868&view=diff
==============================================================================
--- sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataTreeChildren.java [UTF-8] (original)
+++ sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataTreeChildren.java [UTF-8] Fri May  3 16:33:25 2013
@@ -21,7 +21,9 @@ import java.util.Collections;
 import java.util.AbstractCollection;
 import java.util.NoSuchElementException;
 import java.util.ConcurrentModificationException;
+import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.Debug;
 
 // Related to JDK7
@@ -86,7 +88,7 @@ final class MetadataTreeChildren extends
      *     accessor = parent.table.standard.getAccessor(metadata.getClass(), true);
      * }
      */
-    private final PropertyAccessor accessor;
+    final PropertyAccessor accessor;
 
     /**
      * The children to be returned by this collection. All elements in this collection are
@@ -136,7 +138,7 @@ final class MetadataTreeChildren extends
      * @param index The index in the accessor (<em>not</em> the index in this collection).
      */
     final void clearAt(final int index) {
-        accessor.set(index, metadata, null, false);
+        accessor.set(index, metadata, null, PropertyAccessor.RETURN_NULL);
     }
 
     /**
@@ -467,6 +469,60 @@ final class MetadataTreeChildren extends
     }
 
     /**
+     * Adds the given node to this list. This method fetches the object from {@link TableColumn#VALUE}
+     * and assigns it to the property identified by {@link TableColumn#IDENTIFIER}. All other columns
+     * are ignored.
+     *
+     * <p>If the identified property is a collection, then this method adds the value to that collection.
+     * Otherwise the new value will be set only if the previous value is null or empty.</p>
+     *
+     * <p>This method does not iterate explicitly through the children list, because adding a metadata
+     * object implicitly adds all its children.</p>
+     *
+     * @param  node The node from which to get the values.
+     * @return {@code true} if the metadata changed as a result of this method call.
+     * @throws NullPointerException if the given node is null.
+     * @throws IllegalArgumentException if this list does not have a property for the node identifier.
+     * @throws IllegalStateException if a value already exists and no more value can be added for the node identifier.
+     * @throws UnmodifiableMetadataException if the property for the node identifier is read-only.
+     * @throws ClassCastException if the node value can not be converted to the expected type.
+     * @throws BackingStoreException if the metadata implementation threw a checked exception.
+     */
+    @Override
+    public boolean add(final TreeTable.Node node) throws IllegalStateException {
+        final String identifier = node.getValue(TableColumn.IDENTIFIER);
+        if (identifier == null) {
+            throw new IllegalArgumentException(Errors.format(
+                    Errors.Keys.MissingValueInColumn_1, TableColumn.IDENTIFIER.getHeader()));
+        }
+        return add(accessor.indexOf(identifier, true), node.getValue(TableColumn.VALUE));
+    }
+
+    /**
+     * Implementation of {@link #add(TreeTable.Node)}, also invoked by {@link MetadataTreeNode.NewChild}.
+     * This method will attempt to convert the given {@code value} to the expected type.
+     *
+     * @param  index The index in the accessor (<em>not</em> the index in this collection).
+     * @param  value The property value to add.
+     * @return {@code true} if the metadata changed as a result of this method call.
+     */
+    final boolean add(final int index, final Object value) throws IllegalStateException {
+        if (ValueExistencePolicy.isNullOrEmpty(value)) {
+            return false;
+        }
+        // Conversion attempt happen in the PropertyAccessor.set(…) method.
+        final Boolean changed = (Boolean) accessor.set(index, metadata, value, PropertyAccessor.APPEND);
+        if (changed == null) {
+            throw new IllegalStateException(Errors.format(Errors.Keys.ValueAlreadyDefined_1,
+                    accessor.name(index, KeyNamePolicy.UML_IDENTIFIER)));
+        }
+        if (changed) {
+            modCount++;
+        }
+        return changed;
+    }
+
+    /**
      * Returns a string representation of this collection for debugging purpose.
      * This string representation uses one line per element instead of formatting
      * everything on a single line.

Modified: sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataTreeNode.java
URL: http://svn.apache.org/viewvc/sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataTreeNode.java?rev=1478868&r1=1478867&r2=1478868&view=diff
==============================================================================
--- sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataTreeNode.java [UTF-8] (original)
+++ sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataTreeNode.java [UTF-8] Fri May  3 16:33:25 2013
@@ -28,8 +28,10 @@ import org.apache.sis.util.Classes;
 import org.apache.sis.util.iso.Types;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.ObjectConverters;
 import org.apache.sis.util.collection.TableColumn;
+import org.apache.sis.util.collection.TreeTable.Node;
+import org.apache.sis.util.collection.CheckedContainer;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Vocabulary;
 
@@ -45,12 +47,16 @@ import org.apache.sis.util.resources.Voc
  * If a metadata property is a collection, then there is an instance of the {@link CollectionElement}
  * subclass for each element in the collection.</p>
  *
+ * <p>The {@link #newChild()} operation is supported if the node is not a leaf. The user shall
+ * set the identifier and the value, in that order, before any other operation on the new child.
+ * See {@code newChild()} javadpc for an example.</p>
+ *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.3
  * @version 0.3
  * @module
  */
-class MetadataTreeNode implements TreeTable.Node, Serializable {
+class MetadataTreeNode implements Node, Serializable {
     /**
      * For cross-version compatibility.
      */
@@ -64,7 +70,7 @@ class MetadataTreeNode implements TreeTa
      * does not implement the {@link List} interface. So we are better to never give to the user
      * a collection implementing {@code List} in order to signal incorrect casts sooner.</p>
      */
-    private static final Collection<TreeTable.Node> LEAF = Collections.emptySet();
+    private static final Collection<Node> LEAF = Collections.emptySet();
 
     /**
      * A sentinel value meaning that the node is known to allow {@linkplain #children}, but
@@ -74,7 +80,7 @@ class MetadataTreeNode implements TreeTa
      * <p>Any value distinct than {@link #LEAF} is okay. This value will never be visible
      * to the user.</p>
      */
-    private static final Collection<TreeTable.Node> PENDING = Collections.emptyList();
+    private static final Collection<Node> PENDING = Collections.emptyList();
 
     /**
      * The table for which this node is an element. Contains information like
@@ -126,7 +132,7 @@ class MetadataTreeNode implements TreeTa
      *
      * @see #getChildren()
      */
-    private transient Collection<TreeTable.Node> children;
+    private transient Collection<Node> children;
 
     /**
      * Creates the root node of a new metadata tree table.
@@ -310,11 +316,11 @@ class MetadataTreeNode implements TreeTa
         }
 
         /**
-         * Sets the metadata value for this node.
+         * Sets the property value for this node.
          */
         @Override
         void setUserObject(final Object value) {
-            accessor.set(indexInData, metadata, value, false);
+            accessor.set(indexInData, metadata, value, PropertyAccessor.RETURN_NULL);
         }
 
         /**
@@ -423,6 +429,41 @@ class MetadataTreeNode implements TreeTa
                 throw new ConcurrentModificationException();
             }
         }
+
+        /**
+         * Sets the property value for this node.
+         */
+        @Override
+        void setUserObject(Object value) {
+            final Collection<?> values = (Collection<?>) super.getUserObject();
+            if (!(values instanceof List<?>)) {
+                // 'setValue' is the public method which invoked this one.
+                throw new UnsupportedOperationException(Errors.format(
+                        Errors.Keys.UnsupportedOperation_1, "setValue"));
+            }
+            final Class<?> targetType;
+            if (values instanceof CheckedContainer<?>) {
+                // Typically the same than getElementType(), but let be safe
+                // in case some implementations have stricter requirements.
+                targetType = ((CheckedContainer<?>) values).getElementType();
+            } else {
+                targetType = getElementType();
+            }
+            value = ObjectConverters.convert(value, targetType);
+            try {
+                /*
+                 * Unsafe addition into a collection. In SIS implementation, the collection is
+                 * actually an instance of CheckedCollection, so the check will be performed at
+                 * runtime. However other implementations could use unchecked collection. We have
+                 * done our best for converting the type above, there is not much more we can do...
+                 */
+                // No @SuppressWarnings because this is a real hole.
+                ((List) values).set(indexInList, value);
+            } catch (IndexOutOfBoundsException e) {
+                // Same rational than in the getUserObject() method.
+                throw new ConcurrentModificationException();
+            }
+        }
     }
 
 
@@ -433,7 +474,7 @@ class MetadataTreeNode implements TreeTa
      * Returns the parent node, or {@code null} if this node is the root of the tree.
      */
     @Override
-    public final TreeTable.Node getParent() {
+    public final Node getParent() {
         return parent;
     }
 
@@ -461,7 +502,7 @@ class MetadataTreeNode implements TreeTa
      * Only metadata object can have children.
      */
     @Override
-    public final Collection<TreeTable.Node> getChildren() {
+    public final Collection<Node> getChildren() {
         /*
          * 'children' is set to LEAF if an only if the node *can not* have children,
          * in which case we do not need to check for changes in the underlying metadata.
@@ -496,12 +537,127 @@ class MetadataTreeNode implements TreeTa
     }
 
     /**
-     * Unconditionally throws {@link UnsupportedOperationException}, because there is no
-     * way we can safely determine which metadata property a new child would be for.
+     * Returns a proxy for a new property to be defined in the metadata object.
+     * The user shall set the identifier and the value, in that order, before
+     * any other operation on the new child. Example:
+     *
+     * {@preformat java
+     *     TreeTable.Node node = ...;
+     *     TreeTable.Node child = node.newChild();
+     *     child.setValue(TableColumn.IDENTIFIER, "title");
+     *     child.setValue(TableColumn.VALUE, "Le petit prince");
+     *     // Nothing else to do - node has been added.
+     * }
+     *
+     * Do not keep a reference to the returned node for a long time, since it is only
+     * a proxy toward the real node to be created once the identifier is known.
+     *
+     * @throws UnsupportedOperationException If this node {@linkplain #isLeaf() is a leaf}.
      */
     @Override
-    public final TreeTable.Node newChild() {
-        throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnsupportedOperation_1, "newChild"));
+    public final Node newChild() throws UnsupportedOperationException {
+        if (isLeaf()) {
+            throw new UnsupportedOperationException(Errors.format(Errors.Keys.NodeIsLeaf_1, this));
+        }
+        return new NewChild();
+    }
+
+    /**
+     * The proxy to be returned by {@link MetadataTreeNode#newChild()}.
+     * User shall not keep a reference to this proxy for a long time.
+     */
+    private final class NewChild implements Node {
+        /**
+         * Index in the {@link PropertyAccessor} for the property to be set.
+         * This index is known only after a value has been specified for the
+         * {@link TableColumn#IDENTIFIER}.
+         */
+        private int indexInData = -1;
+
+        /**
+         * The real node created after the identifier and the value have been specified.
+         * All operations will be delegated to that node after it has been determined.
+         */
+        private MetadataTreeNode delegate;
+
+        /**
+         * Returns the {@link #delegate} node if non-null, or throw an exception otherwise.
+         *
+         * @throws IllegalStateException if the identifier and value columns have not yet been defined.
+         */
+        private MetadataTreeNode delegate() throws IllegalStateException {
+            if (delegate != null) {
+                return delegate;
+            }
+            throw new IllegalStateException(Errors.format(Errors.Keys.MissingValueInColumn_1,
+                    (indexInData < 0 ? TableColumn.IDENTIFIER : TableColumn.VALUE).getHeader()));
+        }
+
+        /**
+         * Returns all children of the parent node. The new child will be added to that list.
+         */
+        private MetadataTreeChildren getSiblings() {
+            return (MetadataTreeChildren) MetadataTreeNode.this.getChildren();
+        }
+
+        /**
+         * If the {@link #delegate} is not yet known, set the identifier or the value.
+         * After the identifier and value have been specified, delegates to the real node.
+         */
+        @Override
+        public <V> void setValue(final TableColumn<V> column, final V value) {
+            if (delegate == null) {
+                /*
+                 * For the given identifier, get the index in the property accessor.
+                 * This can be done only before the 'delegate' is found - after that
+                 * point, the identifier will become unmodifiable.
+                 */
+                if (column == TableColumn.IDENTIFIER) {
+                    ArgumentChecks.ensureNonNull("value", value);
+                    indexInData = getSiblings().accessor.indexOf((String) value, true);
+                    return;
+                }
+                /*
+                 * Set the value for the property specified by the above identifier,
+                 * then get the 'delegate' on the assumption that the new value will
+                 * be added at the end of collection (if the property is a collection).
+                 */
+                if (column == TableColumn.VALUE) {
+                    ArgumentChecks.ensureNonNull("value", value);
+                    if (indexInData < 0) {
+                        throw new IllegalStateException(Errors.format(Errors.Keys.MissingValueInColumn_1,
+                                TableColumn.IDENTIFIER.getHeader()));
+                    }
+                    final MetadataTreeChildren siblings = getSiblings();
+                    final int indexInList;
+                    if (siblings.isCollection(indexInData)) {
+                        indexInList = ((Collection<?>) siblings.valueAt(indexInData)).size();
+                    } else {
+                        indexInList = -1;
+                    }
+                    if (!siblings.add(indexInData, value)) {
+                        throw new IllegalArgumentException(Errors.format(Errors.Keys.ElementAlreadyPresent_1, value));
+                    }
+                    delegate = siblings.childAt(indexInData, indexInList);
+                    return;
+                }
+            }
+            delegate().setValue(column, value);
+        }
+
+        /**
+         * For all operations other than {@code setValue(…)}, delegates to the {@link #delegate} node
+         * or to some code functionally equivalent.
+         *
+         * @throws IllegalStateException if the identifier and value columns have not yet been defined.
+         */
+        @Override public Node             getParent()                       {return MetadataTreeNode.this;}
+        @Override public boolean          isLeaf()                          {return delegate().isLeaf();}
+        @Override public Collection<Node> getChildren()                     {return delegate().getChildren();}
+        @Override public Node             newChild()                        {return delegate().newChild();}
+        @Override public <V> V            getValue(TableColumn<V> column)   {return delegate().getValue(column);}
+        @Override public boolean          isEditable(TableColumn<?> column) {return delegate().isEditable(column);}
+        @Override public Object           getUserObject()                   {return delegate().getUserObject();}
     }
 
     /**
@@ -533,11 +689,17 @@ class MetadataTreeNode implements TreeTa
     /**
      * Sets the value if the given column is {@link TableColumn#VALUE}. This method verifies
      * the {@code column} argument, then delegates to {@link #setUserObject(Object)}.
+     *
+     * <p>This method does not accept null value, because setting a singleton property to null
+     * with {@link ValueExistencePolicy#NON_EMPTY} is equivalent to removing the property, and
+     * setting a collection element to null is not allowed. Those various behavior are at risk
+     * of causing confusion, so we are better to never allow null.</p>
      */
     @Override
     public final <V> void setValue(final TableColumn<V> column, final V value) throws UnsupportedOperationException {
         ArgumentChecks.ensureNonNull("column", column);
         if (column == TableColumn.VALUE) {
+            ArgumentChecks.ensureNonNull("value", value);
             setUserObject(value);
         } else if (MetadataTreeTable.COLUMNS.contains(column)) {
             throw new UnsupportedOperationException(unmodifiableCellValue(column));

Modified: sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/PropertyAccessor.java
URL: http://svn.apache.org/viewvc/sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/PropertyAccessor.java?rev=1478868&r1=1478867&r2=1478868&view=diff
==============================================================================
--- sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/PropertyAccessor.java [UTF-8] (original)
+++ sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/PropertyAccessor.java [UTF-8] Fri May  3 16:33:25 2013
@@ -67,8 +67,8 @@ import static org.apache.sis.util.collec
  * <ul>
  *   <li>The standard properties defined by the GeoAPI (or other standard) interfaces.
  *       Those properties are the only one accessible by most methods in this class,
- *       except {@link #equals(Object, Object, ComparisonMode, boolean)},
- *       {@link #shallowCopy(Object, Object)} and {@link #freeze(Object)}.</li>
+ *       except {@link #equals(Object, Object, ComparisonMode, boolean)} and
+ *       {@link #freeze(Object)}.</li>
  *
  *   <li>Extra properties defined by the {@link IdentifiedObject} interface. Those properties
  *       invisible in the ISO 19115 model, but appears in ISO 19139 XML marshalling. So we do
@@ -104,6 +104,12 @@ final class PropertyAccessor {
     static final int COUNT_FIRST=0, COUNT_SHALLOW=1, COUNT_DEEP=2;
 
     /**
+     * Enumeration constants for the {@code mode} argument
+     * in the {@link #set(int, Object, Object, int)} method.
+     */
+    static final int RETURN_NULL=0, RETURN_PREVIOUS=1, APPEND=2;
+
+    /**
      * Additional getter to declare in every list of getter methods that do not already provide
      * their own {@code getIdentifiers()} method. We handle this method specially because it is
      * needed for XML marshalling in ISO 19139 compliant document, while not part of abstract
@@ -207,9 +213,9 @@ final class PropertyAccessor {
      * looking for a converter, and also reduce thread contention since it reduces the number
      * of calls to the synchronized {@link ObjectConverters#find(Class, Class)} method.
      *
-     * @see #set(int, Object, Object, boolean)
+     * @see #convert(Object[], Class)
      */
-    private transient volatile ObjectConverter<?,?> converter;
+    private transient volatile ObjectConverter<?,?> lastConverter;
 
     /**
      * The property information, including the name and restrictions on valid values.
@@ -298,7 +304,7 @@ final class PropertyAccessor {
                  * If we found no setter method expecting an argument of the same type than the
                  * argument returned by the GeoAPI method,  try again with the type returned by
                  * the implementation class. It is typically the same type, but sometime it may
-                 * be a subtype.
+                 * be a parent type.
                  *
                  * It is a necessary condition that the type returned by the getter is assignable
                  * to the type expected by the setter.  This contract is required by the 'freeze'
@@ -663,10 +669,24 @@ final class PropertyAccessor {
     }
 
     /**
-     * Sets a value for the specified metadata and returns the old value if {@code getOld} is
-     * {@code true}. If the old value was a collection or a map, then this value is copied in
-     * a new collection or map before the new value is set, because the setter methods typically
-     * copy the new collection in their existing instance.
+     * Sets a value for the specified metadata and returns the old value if {@code mode} is
+     * {@link #RETURN_PREVIOUS}. The {@code mode} argument can be one of the following:
+     *
+     * <ul>
+     *   <li>RETURN_NULL:     Set the value and returns {@code null}.</li>
+     *   <li>RETURN_PREVIOUS: Set the value and returns the previous value. If the previous value was a
+     *                        collection or a map, then that value is copied in a new collection or map
+     *                        before the new value is set because the setter methods typically copy the
+     *                        new collection in their existing instance.</li>
+     *   <li>APPEND:          Set the value only if it doesn't overwrite an existing value, then returns
+     *                        {@link Boolean#TRUE} if the metadata changed as a result of this method call,
+     *                        {@code Boolean#FALSE} if the metadata didn't changed or {@code null} if the
+     *                        value can not be set because an other value already exists.</li>
+     * </ul>
+     *
+     * <p>The {@code APPEND} mode has an additional side effect: it sets the {@code append} argument to
+     * {@code true} in the call to the {@link #convert(Method, Object, Object, Object[], Class, boolean)}
+     * method. See the {@code convert} javadoc for more information.</p>
      *
      * <p>If the given index is out of bounds, then this method does nothing and return {@code null}.
      * We do that because the {@link ValueMap#remove(Object)} method may invoke this method with
@@ -674,16 +694,17 @@ final class PropertyAccessor {
      * However the given value will be silently discarded, so index out-of-bounds shall be used only
      * in the context of {@code remove} operations (this is not verified).</p>
      *
-     * @param  index    The index of the property to set.
-     * @param  metadata The metadata object on which to set the value.
-     * @param  value    The new value.
-     * @param  getOld   {@code true} if this method should first fetches the old value.
-     * @return The old value, or {@code null} if {@code getOld} was {@code false}.
+     * @param  index       The index of the property to set.
+     * @param  metadata    The metadata object on which to set the value.
+     * @param  value       The new value.
+     * @param  mode        Whether this method should first fetches the old value,
+     *                     as one of the {@code RETURN_*} constants.
+     * @return The old value, or {@code null} if {@code returnValue} was {@code RETURN_NULL}.
      * @throws UnmodifiableMetadataException if the property for the given key is read-only.
      * @throws ClassCastException if the given value is not of the expected type.
      * @throws BackingStoreException if the implementation threw a checked exception.
      */
-    final Object set(final int index, final Object metadata, final Object value, final boolean getOld)
+    final Object set(final int index, final Object metadata, final Object value, final int mode)
             throws UnmodifiableMetadataException, ClassCastException, BackingStoreException
     {
         if (index < 0 || index >= standardCount) {
@@ -693,28 +714,58 @@ final class PropertyAccessor {
             final Method getter = getters[index];
             final Method setter = setters[index];
             if (setter != null) {
-                final Object old;
-                final Object copy;
-                if (getOld) {
-                    old = get(getter, metadata);
-                    if (old instanceof Collection<?>) {
-                        if (old instanceof List<?>) {
-                            copy = snapshot((List<?>) old);
+                final Object oldValue;
+                final Object snapshot; // Copy of oldValue before modification.
+                switch (mode) {
+                    case RETURN_NULL: {
+                        oldValue = null;
+                        snapshot = null;
+                        break;
+                    }
+                    case APPEND: {
+                        oldValue = get(getter, metadata);
+                        snapshot = null;
+                        break;
+                    }
+                    case RETURN_PREVIOUS: {
+                        oldValue = get(getter, metadata);
+                        if (oldValue instanceof Collection<?>) {
+                            if (oldValue instanceof List<?>) {
+                                snapshot = snapshot((List<?>) oldValue);
+                            } else {
+                                snapshot = modifiableCopy((Collection<?>) oldValue);
+                            }
+                        } else if (oldValue instanceof Map<?,?>) {
+                            snapshot = modifiableCopy((Map<?,?>) oldValue);
                         } else {
-                            copy = modifiableCopy((Collection<?>) old);
+                            snapshot = oldValue;
                         }
-                    } else if (old instanceof Map<?,?>) {
-                        copy = modifiableCopy((Map<?,?>) old);
-                    } else {
-                        copy = old;
+                        break;
                     }
-                } else {
-                    copy = old = null;
+                    default: throw new AssertionError(mode);
                 }
+                /*
+                 * Converts the new value to a type acceptable for the setter method (if possible).
+                 * If the new value is a singleton while the expected type is a collection, then the 'convert'
+                 * method added the singleton in the existing collection, which may result in no change if the
+                 * collection is a Set and the new value already exists in that Set. If we detect that there is
+                 * no change, then we don't need to invoke the setter method. Note that we conservatively assume
+                 * that there is always a change in RETURN_NULL mode since we don't know the previous value.
+                 */
                 final Object[] newValues = new Object[] {value};
-                converter = convert(getter, metadata, old, newValues, elementTypes[index], converter);
-                set(setter, metadata, newValues);
-                return copy;
+                Boolean changed = convert(getter, metadata, oldValue, newValues, elementTypes[index], mode == APPEND);
+                if (changed == null) {
+                    changed = (mode == RETURN_NULL) || (newValues[0] != oldValue);
+                    if (changed && mode == APPEND && !ValueExistencePolicy.isNullOrEmpty(oldValue)) {
+                        // If 'convert' did not added the value in a collection and if a value already
+                        // exists, do not modify the existing value. Exit now with "no change" status.
+                        return null;
+                    }
+                }
+                if (changed) {
+                    set(setter, metadata, newValues);
+                }
+                return (mode == APPEND) ? changed : snapshot;
             }
         }
         throw new UnmodifiableMetadataException(Errors.format(Errors.Keys.CanNotSetPropertyValue_1, names[index]));
@@ -759,149 +810,176 @@ final class PropertyAccessor {
 
     /**
      * Converts a value to the type required by a setter method.
+     * The values are converted in-place in the {@code newValues} array. We use an array instead
+     * of a single argument and return value because an array will be needed anyway for invoking
+     * the {@link #convert(Object[], Class)} and {@link Method#invoke(Object, Object[])} methods.
+     *
+     * {@section The collection special case}
+     * If the metadata property is a collection, then there is a choice:
+     *
+     * <ul>
+     *   <li>If {@code append} is {@code true}, then the new value (which may itself be a collection)
+     *       is unconditionally added to the previous collection.</li>
+     *   <li>If {@code append} is {@code false} and the new value is <strong>not</strong> a collection,
+     *       then the new value is added to the existing collection. In other words, we behave as a
+     *       <cite>multi-values map</cite> for the properties that allow multi-values.</li>
+     *   <li>Otherwise the new collection replaces the previous collection. All previous values
+     *       are discarded.</li>
+     * </ul>
+     *
+     * Adding new values to the previous collection may or may not change the original metadata
+     * depending on whether those collections are live or not. In Apache SIS implementation,
+     * those collections are live. However this method can be though as if the collections were
+     * not live, since the caller will invoke the setter method with the collection anyway.
      *
      * @param getter      The method to use for fetching the previous value.
-     * @param metadata    The metadata object to query.
+     * @param metadata    The metadata object to query and modify.
      * @param oldValue    The value returned by {@code get(getter, metadata)}, or {@code null} if unknown.
      *                    This parameter is only an optimization for avoiding to invoke the getter method
      *                    twice if the value is already known.
-     * @param newValues   The argument to convert. It must be an array of length 1.
-     *                    The content of this array will be modified in-place.
-     * @param elementType The type required by the setter method.
-     * @param converter   The last converter used, or {@code null} if none.
-     *                    This converter is provided only as a hint and doesn't need to be accurate.
-     * @return The last converter used, or {@code null}.
+     * @param newValues   The argument to convert. The content of this array will be modified in-place.
+     *                    Current implementation requires an array of length 1, however this restriction
+     *                    may be relaxed in a future SIS version if needed.
+     * @param elementType The target type (if singleton) or the type of elements in the collection.
+     * @param append      If {@code true} and the value is a collection, then that collection will be added
+     *                    to any previously existing collection instead of replacing it.
+     * @return If the given value has been added to an existing collection, then whether that existing
+     *         collection has been modified as a result of this method call. Otherwise {@code null}.
      * @throws ClassCastException if the element of the {@code arguments} array is not of the expected type.
      * @throws BackingStoreException If the implementation threw a checked exception.
      */
-    private static ObjectConverter<?,?> convert(final Method getter, final Object metadata,
-            final Object oldValue, final Object[] newValues, Class<?> elementType,
-            ObjectConverter<?,?> converter) throws ClassCastException, BackingStoreException
+    private Boolean convert(final Method getter, final Object metadata, Object oldValue, final Object[] newValues,
+            Class<?> elementType, final boolean append) throws ClassCastException, BackingStoreException
     {
         assert newValues.length == 1;
         Object newValue = newValues[0];
+        Class<?> targetType = getter.getReturnType();
         if (newValue == null) {
             // Can't test elementType, because it has been converted to the wrapper class.
-            final Class<?> type = getter.getReturnType();
-            if (type.isPrimitive()) {
-                newValues[0] = Numbers.valueOfNil(type);
+            if (targetType.isPrimitive()) {
+                newValues[0] = Numbers.valueOfNil(targetType);
             }
+            return null;
+        }
+        Boolean changed = null;
+        if (!Collection.class.isAssignableFrom(targetType)) {
+            /*
+             * We do not expect a collection. The provided argument should not be a
+             * collection neither. It should be some class convertible to targetType.
+             *
+             * If nevertheless the user provided a collection and this collection contains
+             * no more than 1 element, then as a convenience we will extract the singleton
+             * element and process it as if it had been directly provided in argument.
+             */
+            if (newValue instanceof Collection<?>) {
+                final Iterator<?> it = ((Collection<?>) newValue).iterator();
+                if (!it.hasNext()) { // If empty, process like null argument.
+                    newValues[0] = null;
+                    return null;
+                }
+                final Object next = it.next();
+                if (!it.hasNext()) { // Singleton
+                    newValue = next;
+                }
+                // Other cases: let the collection unchanged. It is likely to
+                // cause an exception later. The message should be appropriate.
+            }
+            // Getter type (targetType) shall be the same than the setter type (elementType).
+            assert elementType == Numbers.primitiveToWrapper(targetType) : elementType;
+            targetType = elementType; // Ensure that we use primitive wrapper.
         } else {
-            Class<?> targetType = getter.getReturnType();
-            if (!Collection.class.isAssignableFrom(targetType)) {
-                /*
-                 * We do not expect a collection. The provided argument should not be a
-                 * collection neither. It should be some class convertible to targetType.
-                 *
-                 * If nevertheless the user provided a collection and this collection contains
-                 * no more than 1 element, then as a convenience we will extract the singleton
-                 * element and process it as if it had been directly provided in argument.
-                 */
-                if (newValue instanceof Collection<?>) {
-                    final Iterator<?> it = ((Collection<?>) newValue).iterator();
-                    if (!it.hasNext()) { // If empty, process like null argument.
-                        newValues[0] = null;
-                        return converter;
-                    }
-                    final Object next = it.next();
-                    if (!it.hasNext()) { // Singleton
-                        newValue = next;
-                    }
-                    // Other cases: let the collection unchanged. It is likely to
-                    // cause an exception later. The message should be appropriate.
-                }
-                // Getter type (targetType) shall be the same than the setter type (elementType).
-                assert elementType == Numbers.primitiveToWrapper(targetType) : elementType;
-                targetType = elementType; // Ensure that we use primitive wrapper.
-            } else {
-                /*
-                 * We expect a collection. Collections are handled in one of the two ways below:
-                 *
-                 *   - If the user gives a collection, the user's collection replaces any
-                 *     previous one. The content of the previous collection is discarded.
-                 *
-                 *   - If the user gives a single value, it will be added to the existing
-                 *     collection (if any). The previous values are not discarded. This
-                 *     allow for incremental filling of a property.
-                 *
-                 * The code below prepares an array of elements to be converted and wraps that
-                 * array in a List (to be converted to a Set after this block if required). It
-                 * is okay to convert the elements after the List creation since the list is a
-                 * wrapper.
-                 */
-                final Collection<?> addTo;
-                final Object[] elements;
-                if (newValue instanceof Collection<?>) {
-                    elements = ((Collection<?>) newValue).toArray();
-                    newValue = Arrays.asList(elements); // Content will be converted later.
-                    addTo = null;
-                } else {
-                    elements = new Object[] {newValue};
-                    newValue = addTo = (Collection<?>) (oldValue != null ? oldValue : get(getter, metadata));
-                    if (addTo == null) {
-                        // No previous collection. Create one.
-                        newValue = Arrays.asList(elements);
-                    } else if (addTo instanceof CheckedContainer<?>) {
+            /*
+             * We expect a collection. Collections are handled in one of the two ways below:
+             *
+             *   - If the user gives a collection, the user's collection replaces any
+             *     previous one. The content of the previous collection is discarded.
+             *
+             *   - If the user gives a single value, it will be added to the existing
+             *     collection (if any). The previous values are not discarded. This
+             *     allow for incremental filling of a property.
+             *
+             * The code below prepares an array of elements to be converted and wraps that
+             * array in a List (to be converted to a Set after this block if required). It
+             * is okay to convert the elements after the List creation since the list is a
+             * wrapper.
+             */
+            final boolean isCollection = (newValue instanceof Collection<?>);
+            final Object[] elements = isCollection ? ((Collection<?>) newValue).toArray() : new Object[] {newValue};
+            final List<Object> elementList = Arrays.asList(elements); // Converted later (see above comment).
+            newValue = elementList; // Still contains the same values, but now guaranteed to be a collection.
+            Collection<?> addTo = null;
+            if (!isCollection || append) {
+                if (oldValue == null) {
+                    oldValue = get(getter, metadata);
+                }
+                if (oldValue != null) {
+                    addTo = (Collection<?>) oldValue;
+                    if (addTo instanceof CheckedContainer<?>) {
                         // Get the explicitly-specified element type.
                         elementType = ((CheckedContainer<?>) addTo).getElementType();
                     }
+                    newValue = addTo;
                 }
-                if (elementType != null) {
-                    converter = convert(elements, elementType, converter);
-                }
-                /*
-                 * We now have objects of the appropriate type. If we have a singleton to be added
-                 * in an existing collection, add it now. In that case the 'newValue' should refer
-                 * to the 'addTo' collection. We rely on the ModifiableMetadata.writeCollection(…)
-                 * optimization for detecting that the new collection is the same instance than
-                 * the old one so there is nothing to do. We could exit from the method, but let
-                 * it continues in case the user override the 'setFoo(...)' method.
-                 */
-                if (addTo != null) {
-                    /*
-                     * Unsafe addition into a collection. In SIS implementation, the collection is
-                     * actually an instance of CheckedCollection, so the check will be performed at
-                     * runtime. However other implementations could use unchecked collection.
-                     * There is not much we can do...
-                     */
-                    // No @SuppressWarnings because this is a real hole.
-                    ((Collection<Object>) addTo).add(elements[0]);
-                }
+            }
+            if (elementType != null) {
+                convert(elements, elementType);
             }
             /*
-             * If the expected type was not a collection, the conversion of user value happen
-             * here. Otherwise conversion from List to Set (if needed) happen here.
+             * We now have objects of the appropriate type. If we have a singleton to be added
+             * in an existing collection, add it now. In that case the 'newValue' should refer
+             * to the 'addTo' collection. We rely on the ModifiableMetadata.writeCollection(…)
+             * optimization for detecting that the new collection is the same instance than
+             * the old one so there is nothing to do. We could exit from the method, but let
+             * it continues in case the user override the 'setFoo(…)' method.
              */
-            newValues[0] = newValue;
-            converter = convert(newValues, targetType, converter);
+            if (addTo != null) {
+                /*
+                 * Unsafe addition into a collection. In SIS implementation, the collection is
+                 * actually an instance of CheckedCollection, so the check will be performed at
+                 * runtime. However other implementations could use unchecked collection.
+                 * There is not much we can do...
+                 */
+                // No @SuppressWarnings because this is a real hole.
+                changed = ((Collection) addTo).addAll(elementList);
+            }
         }
-        return converter;
+        /*
+         * If the expected type was not a collection, the conversion of user value happen
+         * here. Otherwise conversion from List to Set (if needed) happen here.
+         */
+        newValues[0] = newValue;
+        convert(newValues, targetType);
+        return changed;
     }
 
     /**
      * Converts values in the specified array to the given type.
-     * The given converter will be used if suitable, or a new one fetched otherwise.
+     * The array content is modified in-place. This method accepts an array instead than
+     * a single value because the values to convert may be the content of a collection.
      *
      * @param  elements   The array which contains element to convert.
      * @param  targetType The base type of target elements.
-     * @param  converter  The proposed converter, or {@code null}.
-     * @return The last converter used, or {@code null}.
      * @throws ClassCastException If an element can't be converted.
      */
     @SuppressWarnings({"unchecked","rawtypes"})
-    private static ObjectConverter<?,?> convert(final Object[] elements, final Class<?> targetType,
-            ObjectConverter<?,?> converter) throws ClassCastException
-    {
+    private void convert(final Object[] elements, final Class<?> targetType) throws ClassCastException {
+        boolean hasNewConverter = false;
+        ObjectConverter<?,?> converter = null;
         for (int i=0; i<elements.length; i++) {
             final Object value = elements[i];
             if (value != null) {
                 final Class<?> sourceType = value.getClass();
                 if (!targetType.isAssignableFrom(sourceType)) try {
-                    if (converter == null
-                            || !converter.getSourceClass().isAssignableFrom(sourceType)
-                            || !targetType.isAssignableFrom(converter.getTargetClass()))
+                    if (converter == null) {
+                        converter = lastConverter; // Volatile field - read only if needed.
+                    }
+                    // Require the exact same classes, not parent or subclass,
+                    // otherwise the converter could be stricter than necessary.
+                    if (converter == null || converter.getSourceClass() != sourceType
+                                          || converter.getTargetClass() != targetType)
                     {
                         converter = ObjectConverters.find(sourceType, targetType);
+                        hasNewConverter = true;
                     }
                     elements[i] = ((ObjectConverter) converter).convert(value);
                 } catch (UnconvertibleObjectException cause) {
@@ -912,7 +990,9 @@ final class PropertyAccessor {
                 }
             }
         }
-        return converter;
+        if (hasNewConverter) {
+            lastConverter = converter; // Volatile field - store only if needed.
+        }
     }
 
     /**
@@ -1005,27 +1085,26 @@ final class PropertyAccessor {
     }
 
     /**
-     * Copies all non-empty metadata from source to target. The source can be any implementation
+     * Appends all non-empty metadata from source to target. The source can be any implementation
      * of the metadata interface, but the target must be the implementation expected by this class.
      *
      * <p>If the source contains any null or empty properties, then those properties will
-     * <strong>not</strong> overwrite the corresponding properties in the destination metadata.</p>
+     * not overwrite the corresponding properties in the destination metadata.</p>
      *
      * @param  source The metadata to copy.
-     * @param  target The target metadata.
+     * @param  target The target metadata where to append.
      * @return {@code true} in case of success, or {@code false} if at least
      *         one setter method was not found.
      * @throws UnmodifiableMetadataException if the target metadata is unmodifiable.
      * @throws BackingStoreException If the implementation threw a checked exception.
      */
-    public boolean shallowCopy(final Object source, final Object target)
+    public boolean append(final Object source, final Object target)
             throws UnmodifiableMetadataException, BackingStoreException
     {
         // Because this PropertyAccesssor is designed for the target, we must
         // check if the extra methods are suitable for the source object.
-        ObjectConverter<?,?> converter = this.converter;
-        boolean success = true;
         assert type.isInstance(source) : Classes.getClass(source);
+        boolean success = true;
         final Object[] arguments = new Object[1];
         for (int i=0; i<standardCount; i++) {
             final Method getter = getters[i];
@@ -1036,14 +1115,13 @@ final class PropertyAccessor {
                 }
                 final Method setter = setters[i];
                 if (setter != null) {
-                    converter = convert(getter, target, null, arguments, elementTypes[i], converter);
+                    convert(getter, target, null, arguments, elementTypes[i], true);
                     set(setter, target, arguments);
                 } else {
                     success = false;
                 }
             }
         }
-        this.converter = converter;
         return success;
     }
 

Modified: sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/StandardImplementation.java
URL: http://svn.apache.org/viewvc/sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/StandardImplementation.java?rev=1478868&r1=1478867&r2=1478868&view=diff
==============================================================================
--- sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/StandardImplementation.java [UTF-8] (original)
+++ sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/StandardImplementation.java [UTF-8] Fri May  3 16:33:25 2013
@@ -18,6 +18,7 @@ package org.apache.sis.metadata;
 
 import java.util.Map;
 import java.util.IdentityHashMap;
+import java.io.ObjectStreamException;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.logging.Logging;
 
@@ -32,6 +33,11 @@ import org.apache.sis.util.logging.Loggi
  */
 final class StandardImplementation extends MetadataStandard {
     /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 855786625369724248L;
+
+    /**
      * The root packages for metadata implementations, or {@code null} if none.
      * If non-null, then this string must ends with a trailing {@code "."}.
      */
@@ -63,7 +69,7 @@ final class StandardImplementation exten
      *        less objects since <code>IdentityHashMap</code> implementation doesn't need the chain
      *        of objects created by <code>HashMap</code>.}
      */
-    private final Map<Class<?>,Class<?>> implementations;
+    private final transient Map<Class<?>,Class<?>> implementations; // written by reflection on deserialization.
 
     /**
      * Creates a new instance working on implementation of interfaces defined in the
@@ -151,4 +157,19 @@ final class StandardImplementation exten
         }
         return null;
     }
+
+    /**
+     * Invoked on deserialization. Returns one of the pre-existing constants if possible.
+     */
+    Object readResolve() throws ObjectStreamException {
+        if (ISO_19111.citation.equals(citation)) return ISO_19111;
+        if (ISO_19115.citation.equals(citation)) return ISO_19115;
+        /*
+         * Following should not occurs, unless we are deserializing an instance created by a
+         * newer version of the Apache SIS library. The newer version could contains constants
+         * not yet declared in this older SIS version, so we have to use this instance.
+         */
+        setMapForField(StandardImplementation.class, "implementations");
+        return this;
+    }
 }

Modified: sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/ValueMap.java
URL: http://svn.apache.org/viewvc/sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/ValueMap.java?rev=1478868&r1=1478867&r2=1478868&view=diff
==============================================================================
--- sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/ValueMap.java [UTF-8] (original)
+++ sis/trunk/sis-metadata/src/main/java/org/apache/sis/metadata/ValueMap.java [UTF-8] Fri May  3 16:33:25 2013
@@ -22,6 +22,9 @@ import java.util.Iterator;
 import java.util.NoSuchElementException;
 import org.apache.sis.util.CharSequences;
 
+import static org.apache.sis.metadata.PropertyAccessor.RETURN_NULL;
+import static org.apache.sis.metadata.PropertyAccessor.RETURN_PREVIOUS;
+
 // Related to JDK7
 import org.apache.sis.internal.jdk7.Objects;
 
@@ -112,7 +115,7 @@ final class ValueMap extends PropertyMap
      */
     @Override
     public Object put(final String key, final Object value) {
-        final Object old = accessor.set(accessor.indexOf(key, true), metadata, value, true);
+        final Object old = accessor.set(accessor.indexOf(key, true), metadata, value, RETURN_PREVIOUS);
         return valuePolicy.isSkipped(old) ? null : old;
     }
 
@@ -127,7 +130,7 @@ final class ValueMap extends PropertyMap
     @Override
     public void putAll(final Map<? extends String, ?> map) {
         for (final Map.Entry<? extends String, ?> e : map.entrySet()) {
-            accessor.set(accessor.indexOf(e.getKey(), true), metadata, e.getValue(), false);
+            accessor.set(accessor.indexOf(e.getKey(), true), metadata, e.getValue(), RETURN_NULL);
         }
     }
 
@@ -139,7 +142,7 @@ final class ValueMap extends PropertyMap
     @Override
     public Object remove(final Object key) throws UnsupportedOperationException {
         if (key instanceof String) {
-            final Object old = accessor.set(accessor.indexOf((String) key, false), metadata, null, true);
+            final Object old = accessor.set(accessor.indexOf((String) key, false), metadata, null, RETURN_PREVIOUS);
             if (!valuePolicy.isSkipped(old)) {
                 return old;
             }
@@ -223,7 +226,7 @@ final class ValueMap extends PropertyMap
          */
         @Override
         public Object setValue(final Object value) {
-            return accessor.set(index, metadata, value, true);
+            return accessor.set(index, metadata, value, RETURN_PREVIOUS);
         }
 
         /**

Modified: sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataStandardTest.java
URL: http://svn.apache.org/viewvc/sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataStandardTest.java?rev=1478868&r1=1478867&r2=1478868&view=diff
==============================================================================
--- sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataStandardTest.java [UTF-8] (original)
+++ sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataStandardTest.java [UTF-8] Fri May  3 16:33:25 2013
@@ -32,7 +32,7 @@ import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
-import static org.opengis.test.Assert.*;
+import static org.apache.sis.test.Assert.*;
 
 
 /**
@@ -87,26 +87,6 @@ public final strictfp class MetadataStan
     }
 
     /**
-     * Tests the shallow copy. For this test, we need to use a class that doesn't have any {@code getIdentifiers()}
-     * method inherited from GeoAPI interfaces. The class will inherit the {@code getIdentifiers()} method defined
-     * by SIS in the parent class, which doesn't have corresponding {@code setIdentifiers(...)} method.
-     */
-    @Test
-    public void testShallowCopy() {
-        final AbstractCompleteness source = new AbstractCompleteness();
-        final AbstractCompleteness target = new AbstractCompleteness();
-        source.setMeasureDescription(new SimpleInternationalString("Some description"));
-        target.getStandard().shallowCopy(source, target);
-        assertEquals("Copy of measureDescription:", "Some description", target.getMeasureDescription().toString());
-        assertEquals("Copy of measureDescription:", source, target);
-
-        source.setMeasureDescription(null);
-        target.getStandard().shallowCopy(source, target);
-        assertEquals("Measure description should not have been removed, since we skipped null values.",
-                "Some description", target.getMeasureDescription().toString());
-    }
-
-    /**
      * Tests the {@link MetadataStandard#equals(Object, Object, ComparisonMode)} method.
      */
     @Test
@@ -182,4 +162,13 @@ public final strictfp class MetadataStan
         assertEquals("hashCode()", new HashSet<Object>(map.values()).hashCode() + Citation.class.hashCode(),
                 std.hashCode(instance));
     }
+
+    /**
+     * Tests serialization of pre-defined constants.
+     */
+    @Test
+    public void testSerialization() {
+        assertSame(MetadataStandard.ISO_19111, assertSerializedEquals(MetadataStandard.ISO_19111));
+        assertSame(MetadataStandard.ISO_19115, assertSerializedEquals(MetadataStandard.ISO_19115));
+    }
 }

Modified: sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTestCase.java
URL: http://svn.apache.org/viewvc/sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTestCase.java?rev=1478868&r1=1478867&r2=1478868&view=diff
==============================================================================
--- sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTestCase.java [UTF-8] (original)
+++ sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTestCase.java [UTF-8] Fri May  3 16:33:25 2013
@@ -251,7 +251,7 @@ public abstract strictfp class MetadataT
             assertEquals("isWritable", isWritable, accessor.isWritable(i));
             if (isWritable) {
                 final Object newValue = valueFor(property, elementType);
-                final Object oldValue = accessor.set(i, instance, newValue, true);
+                final Object oldValue = accessor.set(i, instance, newValue, PropertyAccessor.RETURN_PREVIOUS);
                 assertEquals("PropertyAccessor.set(…) shall return the value previously returned by get(…).", value, oldValue);
                 value = accessor.get(i, instance);
                 if (isCollection) {

Modified: sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTreeChildrenTest.java
URL: http://svn.apache.org/viewvc/sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTreeChildrenTest.java?rev=1478868&r1=1478867&r2=1478868&view=diff
==============================================================================
--- sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTreeChildrenTest.java [UTF-8] (original)
+++ sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTreeChildrenTest.java [UTF-8] Fri May  3 16:33:25 2013
@@ -23,6 +23,8 @@ import java.util.ArrayList;
 import org.opengis.metadata.citation.PresentationForm;
 import org.apache.sis.metadata.iso.citation.DefaultCitation;
 import org.apache.sis.util.iso.SimpleInternationalString;
+import org.apache.sis.util.collection.DefaultTreeTable;
+import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.test.DependsOn;
@@ -182,6 +184,55 @@ public final strictfp class MetadataTree
     }
 
     /**
+     * Tests the {@link MetadataTreeChildren#add(TreeTable.Node)} method.
+     */
+    @Test
+    @DependsOnMethod("testReadOnlyWithMultiOccurrences")
+    public void testAdd() {
+        final DefaultCitation      citation = metadataWithMultiOccurrences();
+        final MetadataTreeChildren children = create(citation, ValueExistencePolicy.NON_EMPTY);
+        final DefaultTreeTable.Node   toAdd = new DefaultTreeTable.Node(new DefaultTreeTable(
+                TableColumn.IDENTIFIER,
+                TableColumn.VALUE));
+        final String[] expected = {
+            "Some title",
+            "First alternate title",
+            "Second alternate title",
+            "Third alternate title",  // After addition
+            "New edition", // After "addition" (actually change).
+            "PresentationForm[MAP_DIGITAL]",
+            "PresentationForm[MAP_HARDCOPY]",
+            "PresentationForm[IMAGE_DIGITAL]", // After addition
+            "Some other details"
+        };
+        toAdd.setValue(TableColumn.IDENTIFIER, "edition");
+        toAdd.setValue(TableColumn.VALUE, citation.getEdition());
+        assertFalse("Adding the same value shall be a no-op.", children.add(toAdd));
+        toAdd.setValue(TableColumn.VALUE, "New edition");
+        try {
+            children.add(toAdd);
+            fail("Setting a different value shall be refused.");
+        } catch (IllegalStateException e) {
+            assertTrue(e.getMessage().contains("edition"));
+        }
+        citation.setEdition(null); // Clears so we are allowed to add.
+        assertTrue("Setting a new value shall be a change.", children.add(toAdd));
+
+        toAdd.setValue(TableColumn.IDENTIFIER, "presentationForm");
+        toAdd.setValue(TableColumn.VALUE, PresentationForm.MAP_DIGITAL);
+        assertFalse("Adding the same value shall be a no-op.", children.add(toAdd));
+        toAdd.setValue(TableColumn.VALUE, PresentationForm.IMAGE_DIGITAL);
+        assertTrue("Adding a new value shall be a change.", children.add(toAdd));
+
+        toAdd.setValue(TableColumn.IDENTIFIER, "alternateTitle");
+        toAdd.setValue(TableColumn.VALUE, "Third alternate title");
+        assertTrue("Adding a new value shall be a change.", children.add(toAdd));
+
+        assertEquals("size()", expected.length, children.size());
+        assertAllNextEqual(expected, children.iterator());
+    }
+
+    /**
      * Tests the {@link Iterator#remove()} operation on a list of properties without collections.
      */
     @Test
@@ -236,6 +287,36 @@ public final strictfp class MetadataTree
         assertTrue(citation.getAlternateTitles().isEmpty());
     }
 
+    /**
+     * Tests the children list with the {@link ValueExistencePolicy#ALL}.
+     */
+    @Test
+    @DependsOnMethod("testReadOnlyWithMultiOccurrences")
+    public void testShowAll() {
+        final DefaultCitation      citation = metadataWithMultiOccurrences();
+        final MetadataTreeChildren children = create(citation, ValueExistencePolicy.ALL);
+        final String[] expected = {
+            "Some title",
+            "First alternate title",
+            "Second alternate title",
+            null, // dates (collection)
+            "Some edition",
+            null, // edition date
+            null, // identifiers (collection)
+            null, // cited responsibly parties (collection)
+            "PresentationForm[MAP_DIGITAL]",
+            "PresentationForm[MAP_HARDCOPY]",
+            null, // series
+            "Some other details",
+            null, // collective title
+            null, // ISBN
+            null  // ISSN
+        };
+        assertFalse ("isEmpty()", children.isEmpty());
+        assertEquals("size()", expected.length, children.size());
+        assertAllNextEqual(expected, children.iterator());
+    }
+
 
     // ------------------------ Support methods for the above tests ------------------------
 
@@ -247,7 +328,8 @@ public final strictfp class MetadataTree
      * because the purpose of this class is not to test {@link MetadataTreeNode}.</p>
      */
     private static String valueOf(final TreeTable.Node node) {
-        return String.valueOf(node.getUserObject());
+        final Object value = node.getUserObject();
+        return (value != null) ? value.toString() : null;
     }
 
     /**

Modified: sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTreeNodeTest.java
URL: http://svn.apache.org/viewvc/sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTreeNodeTest.java?rev=1478868&r1=1478867&r2=1478868&view=diff
==============================================================================
--- sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTreeNodeTest.java [UTF-8] (original)
+++ sis/trunk/sis-metadata/src/test/java/org/apache/sis/metadata/MetadataTreeNodeTest.java [UTF-8] Fri May  3 16:33:25 2013
@@ -130,7 +130,7 @@ public final strictfp class MetadataTree
     @DependsOnMethod("testRootNode") // Because tested more basic methods than 'getValue(TableColumn)'.
     public void testGetNameForSingleton() {
         final DefaultCitation citation = MetadataTreeChildrenTest.metadataWithSingletonInCollections();
-        assertColumnEquals(create(citation, ValueExistencePolicy.NON_EMPTY), TableColumn.NAME,
+        assertColumnContentEquals(create(citation, ValueExistencePolicy.NON_EMPTY), TableColumn.NAME,
             "DefaultCitation",
               "Title",
               "Alternate title",
@@ -147,7 +147,7 @@ public final strictfp class MetadataTree
     @DependsOnMethod("testGetNameForSingleton")
     public void testGetNameForMultiOccurrences() {
         final DefaultCitation citation = MetadataTreeChildrenTest.metadataWithMultiOccurrences();
-        assertColumnEquals(create(citation, ValueExistencePolicy.NON_EMPTY), TableColumn.NAME,
+        assertColumnContentEquals(create(citation, ValueExistencePolicy.NON_EMPTY), TableColumn.NAME,
             "DefaultCitation",
               "Title",
               "Alternate title (1 of 2)",
@@ -165,7 +165,7 @@ public final strictfp class MetadataTree
     @DependsOnMethod("testGetNameForMultiOccurrences")
     public void testGetNameForHierarchy() {
         final DefaultCitation citation = metadataWithHierarchy();
-        assertColumnEquals(create(citation, ValueExistencePolicy.NON_EMPTY), TableColumn.NAME,
+        assertColumnContentEquals(create(citation, ValueExistencePolicy.NON_EMPTY), TableColumn.NAME,
             "DefaultCitation",
               "Title",
               "Alternate title (1 of 2)",
@@ -195,7 +195,7 @@ public final strictfp class MetadataTree
     @DependsOnMethod("testGetNameForMultiOccurrences") // Because similar to names, which were tested progressively.
     public void testGetIdentifier() {
         final DefaultCitation citation = metadataWithHierarchy();
-        assertColumnEquals(create(citation, ValueExistencePolicy.NON_EMPTY), TableColumn.IDENTIFIER,
+        assertColumnContentEquals(create(citation, ValueExistencePolicy.NON_EMPTY), TableColumn.IDENTIFIER,
             "CI_Citation",
               "title",
               "alternateTitle",
@@ -222,7 +222,7 @@ public final strictfp class MetadataTree
     @DependsOnMethod("testGetIdentifier") // Because if identifiers are wrong, we are looking at wrong properties.
     public void testGetElementType() {
         final DefaultCitation citation = metadataWithHierarchy();
-        assertColumnEquals(create(citation, ValueExistencePolicy.NON_EMPTY), TableColumn.TYPE,
+        assertColumnContentEquals(create(citation, ValueExistencePolicy.NON_EMPTY), TableColumn.TYPE,
             Citation.class,
               InternationalString.class,
               InternationalString.class,
@@ -249,7 +249,7 @@ public final strictfp class MetadataTree
     @DependsOnMethod("testGetIdentifier") // Because if identifiers are wrong, we are looking at wrong properties.
     public void testGetValue() {
         final DefaultCitation citation = metadataWithHierarchy();
-        assertColumnEquals(create(citation, ValueExistencePolicy.NON_EMPTY), TableColumn.VALUE,
+        assertColumnContentEquals(create(citation, ValueExistencePolicy.NON_EMPTY), TableColumn.VALUE,
             null, // Citation
               "Some title",
               "First alternate title",
@@ -270,6 +270,45 @@ public final strictfp class MetadataTree
     }
 
     /**
+     * Tests {@link MetadataTreeNode#newChild()}.
+     */
+    @Test
+    @DependsOnMethod("testGetValue")
+    public void testNewChild() {
+        final DefaultCitation citation = metadataWithHierarchy();
+        final MetadataTreeNode node = create(citation, ValueExistencePolicy.NON_EMPTY);
+        /*
+         * Ensure that we can not overwrite existing nodes.
+         */
+        TreeTable.Node child = node.newChild();
+        child.setValue(TableColumn.IDENTIFIER, "title");
+        try {
+            child.setValue(TableColumn.VALUE, "A new title");
+            fail("Attemps to overwrite an existing value shall fail.");
+        } catch (IllegalStateException e) {
+            assertTrue(e.getMessage().contains("title"));
+        }
+        /*
+         * Clear the title and try again. This time, it shall work.
+         */
+        citation.setTitle(null);
+        child = node.newChild();
+        child.setValue(TableColumn.IDENTIFIER, "title");
+        child.setValue(TableColumn.VALUE, "A new title");
+        assertEquals("A new title", citation.getTitle().toString());
+        assertSame(citation.getTitle(), child.getValue(TableColumn.VALUE));
+        /*
+         * Try adding a new element in a collection.
+         * Note that the code below imply a conversion from String to InternationalString.
+         */
+        child = node.newChild();
+        child.setValue(TableColumn.IDENTIFIER, "alternateTitle");
+        child.setValue(TableColumn.VALUE, "Third alternate title");
+        assertEquals(3, citation.getAlternateTitles().size());
+        assertEquals("Third alternate title", child.getValue(TableColumn.VALUE).toString());
+    }
+
+    /**
      * Compares the result of the given getter method invoked on the given node, then invoked
      * on all children of that given. In the particular case of the {@link #NAME} method,
      * international strings are replaced by unlocalized strings before comparisons.
@@ -280,17 +319,17 @@ public final strictfp class MetadataTree
      *                 applied on the given node, and all other values are the result of the
      *                 getter method applied on the children, in iteration order.
      */
-    private static void assertColumnEquals(final MetadataTreeNode node,
+    private static void assertColumnContentEquals(final MetadataTreeNode node,
             final TableColumn<?> column, final Object... values)
     {
         assertEquals("Missing values in the tested metadata.", values.length,
-                assertColumnEquals(node, column, values, 0));
+                assertColumnContentEquals(node, column, values, 0));
     }
 
     /**
      * Implementation of the above {@code assertGetterReturns}, to be invoked recursively.
      */
-    private static int assertColumnEquals(final TreeTable.Node node, final TableColumn<?> column,
+    private static int assertColumnContentEquals(final TreeTable.Node node, final TableColumn<?> column,
             final Object[] values, int index)
     {
         Object actual = node.getValue(column);
@@ -299,7 +338,7 @@ public final strictfp class MetadataTree
         }
         assertEquals("values[" + index + ']', values[index++], actual);
         for (final TreeTable.Node child : node.getChildren()) {
-            index = assertColumnEquals(child, column, values, index);
+            index = assertColumnContentEquals(child, column, values, index);
         }
         return index;
     }



Mime
View raw message