sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject svn commit: r1707278 [1/2] - in /sis/branches/JDK8/core: sis-metadata/src/main/java/org/apache/sis/metadata/ sis-metadata/src/main/java/org/apache/sis/metadata/iso/ sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/ sis-utility/src/...
Date Wed, 07 Oct 2015 12:37:17 GMT
Author: desruisseaux
Date: Wed Oct  7 12:37:16 2015
New Revision: 1707278

URL: http://svn.apache.org/viewvc?rev=1707278&view=rev
Log:
Refactor the IdentifierMap internal implementations in order to allow construction of unmodifiable map (SIS-107).

Added:
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMap.java
      - copied, changed from r1706793, sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java
    sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMapTest.java
      - copied, changed from r1706793, sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/IdentifierMapAdapterTest.java
Removed:
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapWithSpecialCases.java
    sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/IdentifierMapWithSpecialCasesTest.java
Modified:
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/ISOMetadata.java
    sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/gco/PropertyType.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/package-info.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/xml/IdentifierMap.java
    sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/xml/NilObjectHandler.java
    sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/IdentifierMapAdapterTest.java
    sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/gco/PropertyTypeMock.java
    sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/test/suite/UtilityTestSuite.java

Modified: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java?rev=1707278&r1=1707277&r2=1707278&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java [UTF-8] Wed Oct  7 12:37:16 2015
@@ -656,12 +656,12 @@ public abstract class ModifiableMetadata
      */
     @SuppressWarnings({"rawtypes","unchecked"})
     protected <E> Class<? extends Collection<E>> collectionType(final Class<E> elementType) {
-        return (Class) (CodeList.class.isAssignableFrom(elementType) ||
-                            Enum.class.isAssignableFrom(elementType) ||
-                         Charset.class.isAssignableFrom(elementType) ||
-                          String.class ==               elementType  ||
-                          Locale.class ==               elementType  ||
-                        Currency.class ==               elementType
+        return (Class) (CodeList.class.isAssignableFrom(elementType)
+                ||          Enum.class.isAssignableFrom(elementType)
+                ||       Charset.class.isAssignableFrom(elementType)
+                ||        String.class ==               elementType
+                ||        Locale.class ==               elementType
+                ||      Currency.class ==               elementType
                 ? Set.class : List.class);
     }
 

Modified: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/ISOMetadata.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/ISOMetadata.java?rev=1707278&r1=1707277&r2=1707278&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/ISOMetadata.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/ISOMetadata.java [UTF-8] Wed Oct  7 12:37:16 2015
@@ -29,7 +29,8 @@ import org.apache.sis.xml.IdentifierSpac
 import org.apache.sis.xml.IdentifiedObject;
 import org.apache.sis.metadata.MetadataStandard;
 import org.apache.sis.metadata.ModifiableMetadata;
-import org.apache.sis.internal.jaxb.IdentifierMapWithSpecialCases;
+import org.apache.sis.internal.jaxb.IdentifierMapAdapter;
+import org.apache.sis.internal.jaxb.ModifiableIdentifierMap;
 import org.apache.sis.internal.util.Utilities;
 import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.CharSequences;
@@ -52,7 +53,7 @@ import static org.apache.sis.util.collec
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.3
- * @version 0.3
+ * @version 0.7
  * @module
  */
 @XmlTransient
@@ -151,13 +152,14 @@ public class ISOMetadata extends Modifia
          */
         identifiers = nonNullCollection(identifiers, Identifier.class);
         if (identifiers == null) {
-            return IdentifierMapWithSpecialCases.EMPTY;
+            return IdentifierMapAdapter.EMPTY;
         }
         /*
          * We do not cache (for now) the IdentifierMap because it is cheap to create, and if we were
          * caching it we would need anyway to check if 'identifiers' still references the same list.
          */
-        return new IdentifierMapWithSpecialCases(identifiers);
+        return isModifiable() ? new ModifiableIdentifierMap(identifiers)
+                              : new IdentifierMapAdapter(identifiers);
     }
 
 
@@ -204,7 +206,7 @@ public class ISOMetadata extends Modifia
     @XmlJavaTypeAdapter(CollapsedStringAdapter.class)
     private String getUUID() {
         /*
-         * IdentifierMapWithSpecialCases will take care of converting UUID to String,
+         * IdentifierMapAdapter will take care of converting UUID to String,
          * or to return a previously stored String if it was an unparsable UUID.
          */
         return isNullOrEmpty(identifiers) ? null : getIdentifierMap().get(IdentifierSpace.UUID);
@@ -216,9 +218,9 @@ public class ISOMetadata extends Modifia
      */
     private void setUUID(final String id) {
         /*
-         * IdentifierMapWithSpecialCases will take care of converting the String to UUID if possible,
-         * or will store the value as a plain String if it can not be converted. In the later case, a
-         * warning will be emitted (logged or processed by listeners).
+         * IdentifierMapAdapter will take care of converting the String to UUID if possible, or
+         * will store the value as a plain String if it can not be converted. In the later case,
+         * a warning will be emitted (logged or processed by listeners).
          */
         getIdentifierMap().put(IdentifierSpace.UUID, id);
     }

Modified: sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java?rev=1707278&r1=1707277&r2=1707278&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java [UTF-8] Wed Oct  7 12:37:16 2015
@@ -26,7 +26,7 @@ import javax.xml.bind.annotation.adapter
 import javax.xml.bind.annotation.adapters.CollapsedStringAdapter;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.identification.RepresentativeFraction;
-import org.apache.sis.internal.jaxb.IdentifierMapWithSpecialCases;
+import org.apache.sis.internal.jaxb.ModifiableIdentifierMap;
 import org.apache.sis.internal.jaxb.gco.GO_Integer64;
 import org.apache.sis.internal.util.CheckedArrayList;
 import org.apache.sis.measure.ValueRange;
@@ -326,7 +326,7 @@ public class DefaultRepresentativeFracti
      */
     @Override
     public IdentifierMap getIdentifierMap() {
-        return new IdentifierMapWithSpecialCases(getIdentifiers());
+        return new ModifiableIdentifierMap(getIdentifiers());
     }
 
 

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java?rev=1707278&r1=1707277&r2=1707278&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java [UTF-8] Wed Oct  7 12:37:16 2015
@@ -16,8 +16,10 @@
  */
 package org.apache.sis.internal.jaxb;
 
+import java.net.URI;
 import java.util.Set;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.Collection;
 import java.util.Collections;
@@ -28,7 +30,7 @@ import java.io.Serializable;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.citation.Citation;
 import org.apache.sis.util.Debug;
-import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.xml.XLink;
 import org.apache.sis.xml.IdentifierMap;
 import org.apache.sis.xml.IdentifierSpace;
 
@@ -39,14 +41,21 @@ import java.util.Objects;
 
 
 /**
- * A map of identifiers which can be used as a helper class for
- * {@link org.apache.sis.xml.IdentifiedObject} implementations.
+ * Implementation of the map of identifiers associated to {@link org.apache.sis.xml.IdentifiedObject} instances.
+ * This base class implements an unmodifiable map, but the {@link ModifiableIdentifierMap} subclass add write
+ * capabilities.
  *
  * <p>This class works as a wrapper around a collection of identifiers. Because all operations
  * are performed by an iteration over the collection elements, this implementation is suitable
  * only for small maps (less than 10 elements). Given that objects typically have only one or
  * two identifiers, this is considered acceptable.</p>
  *
+ * <div class="section">Special cases</div>
+ * The identifiers for the following authorities are handled in a special way:
+ * <ul>
+ *   <li>{@link IdentifierSpace#HREF}: handled as a shortcut to {@link XLink#getHRef()}.</li>
+ * </ul>
+ *
  * <div class="section">Handling of duplicated authorities</div>
  * The collection shall not contain more than one identifier for the same
  * {@linkplain Identifier#getAuthority() authority}. However duplications may happen if the user
@@ -65,22 +74,17 @@ import java.util.Objects;
  * </ul>
  *
  * <div class="section">Handling of null identifiers</div>
- * The collection of identifiers shall not contains any null element. This is normally ensured by
+ * The collection of identifiers shall not contain any null element. This is normally ensured by
  * the {@link org.apache.sis.metadata.ModifiableMetadata} internal collection implementations.
- * This class performs opportunist null checks as an additional safety. However because we perform
- * those checks only in opportunist ways, the following inconsistencies remain:
- *
- * <ul>
- *   <li>{@link #isEmpty()} may return {@code false} when the more accurate {@link #size()}
- *       method returns 0.</li>
- * </ul>
+ * This class performs opportunist null checks as an additional safety, but consistency is not
+ * guaranteed. See {@link #size()} for more information.
  *
  * <div class="section">Thread safety</div>
  * This class is thread safe if the underlying identifier collection is thread safe.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.3
- * @version 0.3
+ * @version 0.7
  * @module
  *
  * @see org.apache.sis.xml.IdentifiedObject
@@ -106,46 +110,113 @@ public class IdentifierMapAdapter extend
      *
      * @param identifiers The identifiers to wrap in a map view.
      */
-    IdentifierMapAdapter(final Collection<Identifier> identifiers) {
+    public IdentifierMapAdapter(final Collection<Identifier> identifiers) {
         this.identifiers = identifiers;
     }
 
     /**
-     * Removes every entries in the underlying collection.
+     * If the given authority is a special case, returns its {@link NonMarshalledAuthority} integer enum.
+     * Otherwise returns -1. See javadoc for more information about special cases.
      *
-     * @throws UnsupportedOperationException If the collection of identifiers is unmodifiable.
+     * @param authority A {@link Citation} constant. The type is relaxed to {@code Object}
+     *        because the signature of some {@code Map} methods are that way.
      */
-    @Override
-    public void clear() throws UnsupportedOperationException {
-        identifiers.clear();
+    static int specialCase(final Object authority) {
+        if (authority == IdentifierSpace.HREF) return NonMarshalledAuthority.HREF;
+        // A future Apache SIS version may add more special cases here.
+        return -1;
+    }
+
+    /**
+     * Extracts the {@code xlink:href} value from the {@link XLink} if presents.
+     * This method does not test if an explicit {@code xlink:href} identifier exists;
+     * this check must be done by the caller <strong>before</strong> to invoke this method.
+     *
+     * @see ModifiableIdentifierMap#setHRef(URI)
+     */
+    private URI getHRef() {
+        final Identifier identifier = getIdentifier(IdentifierSpace.XLINK);
+        if (identifier instanceof SpecializedIdentifier<?>) {
+            final Object link = ((SpecializedIdentifier<?>) identifier).value;
+            if (link instanceof XLink) {
+                return ((XLink) link).getHRef();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the string representation of the given value, or {@code null} if none.
+     *
+     * @param value The value returned be one of the above {@code getFoo()} methods.
+     */
+    private static String toString(final Object value) {
+        return (value != null) ? value.toString() : null;
+    }
+
+
+
+
+    ////////////////////////////////////////////////////////////////////////////////////////
+    ////////                                                                        ////////
+    ////////    END OF SPECIAL CASES.                                               ////////
+    ////////                                                                        ////////
+    ////////    Implementation of IdentifierMap methods follow. Each method may     ////////
+    ////////    have a switch statement over the special cases declared above.      ////////
+    ////////                                                                        ////////
+    ////////////////////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Whether this map support {@code put} and {@code remove} operations.
+     */
+    boolean isModifiable() {
+        return false;
     }
 
     /**
      * Returns {@code true} if the collection of identifiers contains at least one element.
      * This method does not verify if the collection contains null element (it should not).
-     * Consequently, this method may return {@code false} even if the {@link #size()} method
-     * returns 0.
      */
     @Override
-    public boolean isEmpty() {
+    public final boolean isEmpty() {
         return identifiers.isEmpty();
     }
 
     /**
-     * Returns {@code true} if at least one identifier declares the given
-     * {@linkplain Identifier#getCode() code}.
+     * Counts the number of entries, ignoring null elements and duplicated authorities.
+     *
+     * <p>Because {@code null} elements are ignored, this method may return 0 even if {@link #isEmpty()}
+     * returns {@code false}. However this inconsistency should not happen in practice because
+     * {@link org.apache.sis.metadata.ModifiableMetadata} internal collection implementations
+     * do not allow null values.</p>
+     */
+    @Override
+    public final int size() {
+        final HashSet<Citation> done = new HashSet<>(hashMapCapacity(identifiers.size()));
+        for (final Identifier identifier : identifiers) {
+            if (identifier != null) {
+                done.add(identifier.getAuthority());
+            }
+        }
+        return done.size();
+    }
+
+    /**
+     * Returns {@code true} if at least one identifier declares the given {@linkplain Identifier#getCode() code}.
      *
      * @param  code The code to search, which should be an instance of {@link String}.
      * @return {@code true} if at least one identifier uses the given code.
      */
     @Override
-    public boolean containsValue(final Object code) {
+    public final boolean containsValue(final Object code) {
         if (code instanceof String) {
             for (final Identifier identifier : identifiers) {
                 if (identifier != null && code.equals(identifier.getCode())) {
                     return true;
                 }
             }
+            return code.equals(toString(getHRef()));
+            // A future Apache SIS version may add more special cases here.
         }
         return false;
     }
@@ -158,14 +229,23 @@ public class IdentifierMapAdapter extend
      * @return {@code true} if at least one identifier uses the given authority.
      */
     @Override
-    public boolean containsKey(final Object authority) {
-        return (authority instanceof Citation) && getIdentifier((Citation) authority) != null;
+    public final boolean containsKey(final Object authority) {
+        if (authority instanceof Citation) {
+            if (getIdentifier((Citation) authority) != null) {
+                return true;
+            }
+            switch (specialCase(authority)) {
+                case NonMarshalledAuthority.HREF: return getHRef() != null;
+                // A future Apache SIS version may add more special cases here.
+            }
+        }
+        return false;
     }
 
     /**
      * Returns the identifier for the given key, or {@code null} if none.
      */
-    private Identifier getIdentifier(final Citation authority) {
+    final Identifier getIdentifier(final Citation authority) {
         for (final Identifier identifier : identifiers) {
             if (identifier != null && Objects.equals(authority, identifier.getAuthority())) {
                 return identifier;
@@ -180,148 +260,94 @@ public class IdentifierMapAdapter extend
      */
     @Override
     @SuppressWarnings("unchecked")
-    public <T> T getSpecialized(final IdentifierSpace<T> authority) {
-        final Identifier identifier = getIdentifier(authority);
-        return (identifier instanceof SpecializedIdentifier<?>) ? ((SpecializedIdentifier<T>) identifier).value : null;
-    }
-
-    /**
-     * Returns the code of the first identifier associated with the given authority only if
-     * if is <strong>not</strong> a specialized identifier. Otherwise returns {@code null}.
-     *
-     * <p>This is a helper method for {@link IdentifierMapWithSpecialCases#put(Citation, String)},
-     * in order to be able to return the old value if that value was a {@link String} rather than
-     * the specialized type. We do not return the string for the specialized case in order to avoid
-     * the cost of invoking {@code toString()} on the specialized object (some may be costly). Such
-     * call would be useless because {@code IdentifierMapWithSpecialCase} discard the value of this
-     * method when it found a specialized type.</p>
-     */
-    final String getUnspecialized(final Citation authority) {
+    public final <T> T getSpecialized(final IdentifierSpace<T> authority) {
         final Identifier identifier = getIdentifier(authority);
-        if (identifier != null && !(identifier instanceof SpecializedIdentifier<?>)) {
-            return identifier.getCode();
+        if (identifier instanceof SpecializedIdentifier<?>) {
+            return ((SpecializedIdentifier<T>) identifier).value;
+        }
+        switch (specialCase(authority)) {
+            case NonMarshalledAuthority.HREF: return (T) getHRef();
+            // A future Apache SIS version may add more special cases here.
         }
         return null;
     }
 
     /**
      * Returns the code of the first identifier associated with the given
-     * {@linkplain Identifier#getAuthority() authority}, or {@code null}
-     * if no identifier was found.
+     * {@linkplain Identifier#getAuthority() authority}, or {@code null} if no identifier was found.
      *
      * @param  authority The authority to search, which should be an instance of {@link Citation}.
      * @return The code of the identifier for the given authority, or {@code null} if none.
      */
     @Override
-    public String get(final Object authority) {
+    public final String get(final Object authority) {
         if (authority instanceof Citation) {
             final Identifier identifier = getIdentifier((Citation) authority);
             if (identifier != null) {
                 return identifier.getCode();
             }
+            switch (specialCase(authority)) {
+                case NonMarshalledAuthority.HREF: return toString(getHRef());
+                // A future Apache SIS version may add more special cases here.
+            }
         }
         return null;
     }
 
     /**
      * Removes all identifiers associated with the given {@linkplain Identifier#getAuthority() authority}.
-     * The default implementation delegates to {@link #put(Citation, String)} with a {@code null} value.
      *
      * @param  authority The authority to search, which should be an instance of {@link Citation}.
      * @return The code of the identifier for the given authority, or {@code null} if none.
+     * @throws UnsupportedOperationException if the collection of identifiers is unmodifiable.
      */
     @Override
-    public String remove(final Object authority) {
-        return (authority instanceof Citation) ? put((Citation) authority, null) : null;
+    public String remove(Object authority) throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Removes every entries in the underlying collection.
+     *
+     * @throws UnsupportedOperationException if the collection of identifiers is unmodifiable.
+     */
+    @Override
+    public void clear() throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
     }
 
     /**
      * Sets the code of the identifier having the given authority to the given value.
-     * If no identifier is found for the given authority, a new one is created. If
-     * more than one identifier is found for the given authority, then all previous
-     * identifiers may be removed in order to ensure that the new entry will be the
-     * first entry, so it can be find by the {@code get} method.
+     * If no identifier is found for the given authority, a new one is created.
+     * If more than one identifier is found for the given authority, then all previous identifiers may be removed
+     * in order to ensure that the new entry will be the first entry, so it can be find by the {@code get} method.
      *
      * @param  authority The authority for which to set the code.
      * @param  code The new code for the given authority, or {@code null} for removing the entry.
      * @return The previous code for the given authority, or {@code null} if none.
+     * @throws UnsupportedOperationException if the collection of identifiers is unmodifiable.
      */
     @Override
-    public String put(final Citation authority, final String code)
-            throws UnsupportedOperationException
-    {
-        ArgumentChecks.ensureNonNull("authority", authority);
-        String old = null;
-        final Iterator<? extends Identifier> it = identifiers.iterator();
-        while (it.hasNext()) {
-            final Identifier identifier = it.next();
-            if (identifier == null) {
-                it.remove(); // Opportunist cleaning, but should not happen.
-            } else if (Objects.equals(authority, identifier.getAuthority())) {
-                if (code != null && identifier instanceof IdentifierMapEntry) {
-                    return ((IdentifierMapEntry) identifier).setValue(code);
-                    // No need to suppress other occurrences of the key (if any)
-                    // because we made a replacement in the first entry, so the
-                    // new value will be visible by the getter methods.
-                }
-                if (old == null) {
-                    old = identifier.getCode();
-                }
-                it.remove();
-                // Continue the iteration in order to remove all other occurrences,
-                // in order to ensure that the getter methods will see the new value.
-            }
-        }
-        if (code != null) {
-            identifiers.add(SpecializedIdentifier.parse(authority, code));
-        }
-        return old;
+    public String put(Citation authority, String code) throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
     }
 
     /**
      * Sets the identifier associated with the given authority, and returns the previous value.
      */
     @Override
-    public <T> T putSpecialized(final IdentifierSpace<T> authority, final T value)
-            throws UnsupportedOperationException
-    {
-        ArgumentChecks.ensureNonNull("authority", authority);
-        T old = null;
-        final Iterator<? extends Identifier> it = identifiers.iterator();
-        while (it.hasNext()) {
-            final Identifier identifier = it.next();
-            if (identifier == null) {
-                it.remove(); // Opportunist cleaning, but should not happen.
-            } else if (Objects.equals(authority, identifier.getAuthority())) {
-                if (identifier instanceof SpecializedIdentifier<?>) {
-                    @SuppressWarnings("unchecked")
-                    final SpecializedIdentifier<T> id = (SpecializedIdentifier<T>) identifier;
-                    if (old == null) {
-                        old = id.value;
-                    }
-                    if (value != null) {
-                        id.value = value;
-                        return old;
-                        // No need to suppress other occurrences of the key (if any)
-                        // because we made a replacement in the first entry, so the
-                        // new value will be visible by the getter methods.
-                    }
-                }
-                it.remove();
-                // Continue the iteration in order to remove all other occurrences,
-                // in order to ensure that the getter methods will see the new value.
-            }
-        }
-        if (value != null) {
-            identifiers.add(new SpecializedIdentifier<>(authority, value));
-        }
-        return old;
+    public <T> T putSpecialized(IdentifierSpace<T> authority, T value) throws UnsupportedOperationException {
+        throw new UnsupportedOperationException();
     }
 
     /**
      * Returns a view over the collection of identifiers. This view supports removal operation
      * if the underlying collection of identifiers supports the {@link Iterator#remove()} method.
      *
+     * <p>If the backing identifier collection contains null entries, those entries will be ignored.
+     * If the backing collection contains many entries for the same authority, then only the first
+     * occurrence is included.</p>
+     *
      * @return A view over the collection of identifiers.
      */
     @Override
@@ -332,83 +358,35 @@ public class IdentifierMapAdapter extend
          * fields if the underlying list is thread-safe. Furthermore, IdentifierMapAdapter are temporary
          * objects anyway in the current ISOMetadata implementation.
          */
-        return new Entries(identifiers);
-    }
-
-    /**
-     * The view returned by {@link IdentifierMapAdapter#entrySet()}. If the backing identifier collection
-     * contains null entries, those entries will be ignored. If the backing collection contains many entries
-     * for the same authority, then only the first occurrence is retained.
-     *
-     * @author  Martin Desruisseaux (Geomatys)
-     * @since   0.3
-     * @version 0.3
-     * @module
-     */
-    private static final class Entries extends AbstractSet<Entry<Citation,String>> {
-        /**
-         * The identifiers to wrap in a set of entries view. This is a reference
-         * to the same collection than {@link IdentifierMapAdapter#identifiers}.
-         */
-        private final Collection<? extends Identifier> identifiers;
-
-        /**
-         * Creates a new view over the collection of identifiers.
-         *
-         * @param identifiers The identifiers to wrap in a set of entries view.
-         */
-        Entries(final Collection<? extends Identifier> identifiers) {
-            this.identifiers = identifiers;
-        }
-
-        /**
-         * Same implementation than {@link IdentifierMapAdapter#clear()}.
-         */
-        @Override
-        public void clear() throws UnsupportedOperationException {
-            identifiers.clear();
-        }
+        return new AbstractSet<Entry<Citation,String>>() {
+            /** Delegates to the enclosing class. */
+            @Override public void clear() throws UnsupportedOperationException {
+                IdentifierMapAdapter.this.clear();
+            }
 
-        /**
-         * Same implementation than {@link IdentifierMapAdapter#isEmpty()}.
-         */
-        @Override
-        public boolean isEmpty() {
-            return identifiers.isEmpty();
-        }
+            /** Delegates to the enclosing class. */
+            @Override public boolean isEmpty() {
+                return IdentifierMapAdapter.this.isEmpty();
+            }
 
-        /**
-         * Counts the number of entries, ignoring null elements and duplicated authorities.
-         * Because {@code null} elements are ignored, this method may return 0 even if
-         * {@link #isEmpty()} returns {@code false}.
-         */
-        @Override
-        public int size() {
-            final HashMap<Citation,Boolean> done = new HashMap<>(hashMapCapacity(identifiers.size()));
-            for (final Identifier identifier : identifiers) {
-                if (identifier != null) {
-                    done.put(identifier.getAuthority(), null);
-                }
+            /** Delegates to the enclosing class. */
+            @Override public int size() {
+                return IdentifierMapAdapter.this.size();
             }
-            return done.size();
-        }
 
-        /**
-         * Returns an iterator over the (<var>citation</var>, <var>code</var>) entries.
-         */
-        @Override
-        public Iterator<Entry<Citation, String>> iterator() {
-            return new Iter(identifiers);
-        }
+            /** Returns an iterator over the (<var>citation</var>, <var>code</var>) entries. */
+            @Override public Iterator<Entry<Citation, String>> iterator() {
+                return new Iter(identifiers, isModifiable());
+            }
+        };
     }
 
     /**
      * The iterator over the (<var>citation</var>, <var>code</var>) entries. This iterator is created by
      * the {@link IdentifierMapAdapter.Entries} collection. It extends {@link HashMap} as an opportunist
-     * implementation strategy, but users don't need to know this detail.
+     * implementation strategy, but users does not need to know this detail.
      *
-     * <p>This iterator supports the {@link #remove()} operation if the underlying collection
-     * supports it.</p>
+     * <p>This iterator supports the {@link #remove()} operation if the underlying collection supports it.</p>
      *
      * <p>The map entries are used as a safety against duplicated authority values. The map values
      * are non-null only after we iterated over an authority. Then the value is {@link Boolean#TRUE}
@@ -416,7 +394,7 @@ public class IdentifierMapAdapter extend
      *
      * @author  Martin Desruisseaux (Geomatys)
      * @since   0.3
-     * @version 0.3
+     * @version 0.7
      * @module
      */
     @SuppressWarnings("serial") // Not intended to be serialized.
@@ -439,11 +417,17 @@ public class IdentifierMapAdapter extend
         private transient Citation authority;
 
         /**
+         * {@code true} if the iterator should support the {@link #remove()} operation.
+         */
+        private final boolean isModifiable;
+
+        /**
          * Creates a new iterator for the given collection of identifiers.
          */
-        Iter(final Collection<? extends Identifier> identifiers) {
+        Iter(final Collection<? extends Identifier> identifiers, final boolean isModifiable) {
             super(hashMapCapacity(identifiers.size()));
             this.identifiers = identifiers.iterator();
+            this.isModifiable = isModifiable;
         }
 
         /**
@@ -518,6 +502,9 @@ public class IdentifierMapAdapter extend
          */
         @Override
         public void remove() throws IllegalStateException {
+            if (!isModifiable) {
+                throw new UnsupportedOperationException();
+            }
             final Iterator<? extends Identifier> it = identifiers;
             if (it == null || next != null) {
                 throw new IllegalStateException();

Copied: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMap.java (from r1706793, sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java)
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMap.java?p2=sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMap.java&p1=sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java&r1=1706793&r2=1707278&rev=1707278&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/IdentifierMapAdapter.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMap.java [UTF-8] Wed Oct  7 12:37:16 2015
@@ -16,211 +16,108 @@
  */
 package org.apache.sis.internal.jaxb;
 
-import java.util.Set;
-import java.util.HashMap;
+import java.net.URI;
 import java.util.Iterator;
 import java.util.Collection;
-import java.util.Collections;
-import java.util.AbstractSet;
-import java.util.AbstractMap;
-import java.util.NoSuchElementException;
-import java.io.Serializable;
+import java.net.URISyntaxException;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.citation.Citation;
-import org.apache.sis.util.Debug;
 import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.xml.IdentifierMap;
 import org.apache.sis.xml.IdentifierSpace;
-
-import static org.apache.sis.util.collection.Containers.hashMapCapacity;
+import org.apache.sis.xml.ValueConverter;
+import org.apache.sis.xml.XLink;
 
 // Branch-dependent imports
 import java.util.Objects;
 
 
 /**
- * A map of identifiers which can be used as a helper class for
- * {@link org.apache.sis.xml.IdentifiedObject} implementations.
- *
- * <p>This class works as a wrapper around a collection of identifiers. Because all operations
- * are performed by an iteration over the collection elements, this implementation is suitable
- * only for small maps (less than 10 elements). Given that objects typically have only one or
- * two identifiers, this is considered acceptable.</p>
- *
- * <div class="section">Handling of duplicated authorities</div>
- * The collection shall not contain more than one identifier for the same
- * {@linkplain Identifier#getAuthority() authority}. However duplications may happen if the user
- * has direct access to the list, for example through {@link Citation#getIdentifiers()}. If such
- * duplication is found, then this map implementation applies the following rules:
- *
- * <ul>
- *   <li>All getter methods (including the iterators and the values returned by the {@code put}
- *       and {@code remove} methods) return only the identifier code associated to the first
- *       occurrence of each authority. Any subsequent occurrences of the same authorities are
- *       silently ignored.</li>
- *   <li>All setter methods <em>may</em> affect <em>all</em> identifiers previously associated to
- *       the given authority, not just the first occurrence. The only guarantee is that the list
- *       is update in such a way that the effect of setter methods are visible to subsequent calls
- *       to getter methods.</li>
- * </ul>
- *
- * <div class="section">Handling of null identifiers</div>
- * The collection of identifiers shall not contains any null element. This is normally ensured by
- * the {@link org.apache.sis.metadata.ModifiableMetadata} internal collection implementations.
- * This class performs opportunist null checks as an additional safety. However because we perform
- * those checks only in opportunist ways, the following inconsistencies remain:
- *
- * <ul>
- *   <li>{@link #isEmpty()} may return {@code false} when the more accurate {@link #size()}
- *       method returns 0.</li>
- * </ul>
+ * A map of identifiers which support {@code put} and {@code remove} operations.
  *
  * <div class="section">Thread safety</div>
  * This class is thread safe if the underlying identifier collection is thread safe.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @since   0.3
- * @version 0.3
+ * @since   0.7
+ * @version 0.7
  * @module
  *
  * @see org.apache.sis.xml.IdentifiedObject
  */
-public class IdentifierMapAdapter extends AbstractMap<Citation,String> implements IdentifierMap, Serializable {
+public final class ModifiableIdentifierMap extends IdentifierMapAdapter {
     /**
      * For cross-version compatibility.
      */
-    private static final long serialVersionUID = -1445849218952061605L;
-
-    /**
-     * An immutable empty instance.
-     */
-    public static final IdentifierMap EMPTY = new IdentifierMapAdapter(Collections.<Identifier>emptySet());
-
-    /**
-     * The identifiers to wrap in a map view.
-     */
-    public final Collection<Identifier> identifiers;
+    private static final long serialVersionUID = -80325787192055778L;
 
     /**
      * Creates a new map which will be a view over the given identifiers.
      *
      * @param identifiers The identifiers to wrap in a map view.
      */
-    IdentifierMapAdapter(final Collection<Identifier> identifiers) {
-        this.identifiers = identifiers;
+    public ModifiableIdentifierMap(final Collection<Identifier> identifiers) {
+        super(identifiers);
     }
 
     /**
-     * Removes every entries in the underlying collection.
+     * Sets the {@code xlink:href} value, which may be null. If an explicit {@code xlink:href} identifier exists,
+     * then it will removed before to set the new {@code href} in the {@link XLink} object. The intend is to give
+     * precedence to the {@link XLink#getHRef()} property in every cases where the {@code href} is parsable as a
+     * {@link URI}, and use the value associated to the {@code HREF} key only as a fallback when the string can not
+     * be parsed.
      *
-     * @throws UnsupportedOperationException If the collection of identifiers is unmodifiable.
-     */
-    @Override
-    public void clear() throws UnsupportedOperationException {
-        identifiers.clear();
-    }
-
-    /**
-     * Returns {@code true} if the collection of identifiers contains at least one element.
-     * This method does not verify if the collection contains null element (it should not).
-     * Consequently, this method may return {@code false} even if the {@link #size()} method
-     * returns 0.
-     */
-    @Override
-    public boolean isEmpty() {
-        return identifiers.isEmpty();
-    }
-
-    /**
-     * Returns {@code true} if at least one identifier declares the given
-     * {@linkplain Identifier#getCode() code}.
+     * @param  href The new value, or {@code null} for removing the value.
+     * @return The previous value, or {@code null} if none.
      *
-     * @param  code The code to search, which should be an instance of {@link String}.
-     * @return {@code true} if at least one identifier uses the given code.
+     * @see #getHRef()
      */
-    @Override
-    public boolean containsValue(final Object code) {
-        if (code instanceof String) {
-            for (final Identifier identifier : identifiers) {
-                if (identifier != null && code.equals(identifier.getCode())) {
-                    return true;
+    private URI setHRef(final URI href) {
+        URI old = store(IdentifierSpace.HREF, null);
+        final Identifier identifier = getIdentifier(IdentifierSpace.XLINK);
+        if (identifier instanceof SpecializedIdentifier<?>) {
+            final Object link = ((SpecializedIdentifier<?>) identifier).value;
+            if (link instanceof XLink) {
+                if (old == null) {
+                    old = ((XLink) link).getHRef();
                 }
+                ((XLink) link).setHRef(href);
+                return old;
             }
         }
-        return false;
+        if (href != null) {
+            final XLink link = new XLink();
+            link.setHRef(href);
+            store(IdentifierSpace.XLINK, link);
+        }
+        return old;
     }
 
-    /**
-     * Returns {@code true} if at least one identifier declares the given
-     * {@linkplain Identifier#getAuthority() authority}.
-     *
-     * @param  authority The authority to search, which should be an instance of {@link Citation}.
-     * @return {@code true} if at least one identifier uses the given authority.
-     */
-    @Override
-    public boolean containsKey(final Object authority) {
-        return (authority instanceof Citation) && getIdentifier((Citation) authority) != null;
-    }
 
-    /**
-     * Returns the identifier for the given key, or {@code null} if none.
-     */
-    private Identifier getIdentifier(final Citation authority) {
-        for (final Identifier identifier : identifiers) {
-            if (identifier != null && Objects.equals(authority, identifier.getAuthority())) {
-                return identifier;
-            }
-        }
-        return null;
-    }
 
-    /**
-     * Returns the identifier associated with the given authority,
-     * or {@code null} if no specialized identifier was found.
-     */
-    @Override
-    @SuppressWarnings("unchecked")
-    public <T> T getSpecialized(final IdentifierSpace<T> authority) {
-        final Identifier identifier = getIdentifier(authority);
-        return (identifier instanceof SpecializedIdentifier<?>) ? ((SpecializedIdentifier<T>) identifier).value : null;
-    }
+
+    ////////////////////////////////////////////////////////////////////////////////////////
+    ////////                                                                        ////////
+    ////////    END OF SPECIAL CASES.                                               ////////
+    ////////                                                                        ////////
+    ////////    Implementation of IdentifierMap methods follow. Each method may     ////////
+    ////////    have a switch statement over the special cases declared above.      ////////
+    ////////                                                                        ////////
+    ////////////////////////////////////////////////////////////////////////////////////////
 
     /**
-     * Returns the code of the first identifier associated with the given authority only if
-     * if is <strong>not</strong> a specialized identifier. Otherwise returns {@code null}.
-     *
-     * <p>This is a helper method for {@link IdentifierMapWithSpecialCases#put(Citation, String)},
-     * in order to be able to return the old value if that value was a {@link String} rather than
-     * the specialized type. We do not return the string for the specialized case in order to avoid
-     * the cost of invoking {@code toString()} on the specialized object (some may be costly). Such
-     * call would be useless because {@code IdentifierMapWithSpecialCase} discard the value of this
-     * method when it found a specialized type.</p>
+     * Returns {@code true} since this map support {@code put} and {@code remove} operations.
      */
-    final String getUnspecialized(final Citation authority) {
-        final Identifier identifier = getIdentifier(authority);
-        if (identifier != null && !(identifier instanceof SpecializedIdentifier<?>)) {
-            return identifier.getCode();
-        }
-        return null;
+    @Override
+    final boolean isModifiable() {
+        return true;
     }
 
     /**
-     * Returns the code of the first identifier associated with the given
-     * {@linkplain Identifier#getAuthority() authority}, or {@code null}
-     * if no identifier was found.
-     *
-     * @param  authority The authority to search, which should be an instance of {@link Citation}.
-     * @return The code of the identifier for the given authority, or {@code null} if none.
+     * Removes every entries in the underlying collection.
      */
     @Override
-    public String get(final Object authority) {
-        if (authority instanceof Citation) {
-            final Identifier identifier = getIdentifier((Citation) authority);
-            if (identifier != null) {
-                return identifier.getCode();
-            }
-        }
-        return null;
+    public void clear() {
+        identifiers.clear();
     }
 
     /**
@@ -237,21 +134,53 @@ public class IdentifierMapAdapter extend
 
     /**
      * Sets the code of the identifier having the given authority to the given value.
-     * If no identifier is found for the given authority, a new one is created. If
-     * more than one identifier is found for the given authority, then all previous
-     * identifiers may be removed in order to ensure that the new entry will be the
-     * first entry, so it can be find by the {@code get} method.
+     * If no identifier is found for the given authority, a new one is created.
+     * If more than one identifier is found for the given authority, then all previous identifiers may be removed
+     * in order to ensure that the new entry will be the first entry, so it can be find by the {@code get} method.
+     *
+     * <p>If the given {@code authority} is {@code HREF} and if the given string is parsable as a {@link URI},
+     * then this method will actually store the value as the {@link XLink#getHRef()} property of the {@code XLink}
+     * associated to the {@code XLINK} key. Only if the given string can not be parsed, then the value is stored
+     * <cite>as-is</cite> under the {@code HREF} key.</p>
      *
      * @param  authority The authority for which to set the code.
      * @param  code The new code for the given authority, or {@code null} for removing the entry.
      * @return The previous code for the given authority, or {@code null} if none.
      */
     @Override
-    public String put(final Citation authority, final String code)
-            throws UnsupportedOperationException
-    {
+    public String put(final Citation authority, final String code) {
         ArgumentChecks.ensureNonNull("authority", authority);
-        String old = null;
+        String previous  = null;
+        Object discarded = null;
+        switch (specialCase(authority)) {
+            case NonMarshalledAuthority.HREF: {
+                URI uri = null;
+                if (code != null) {
+                    final Context context = Context.current();
+                    final ValueConverter converter = Context.converter(context);
+                    try {
+                        uri = converter.toURI(context, code);
+                    } catch (URISyntaxException e) {
+                        SpecializedIdentifier.parseFailure(context, code, URI.class, e);
+                        discarded = setHRef(null);
+                        break;  // Fallback on generic code below.
+                    }
+                }
+                final Identifier identifier = getIdentifier(authority);
+                uri = setHRef(uri);
+                if (uri != null) {
+                    previous = uri.toString();
+                } else if (identifier != null) {
+                    previous = identifier.getCode();
+                }
+                return previous;
+            }
+            // A future Apache SIS version may add more special cases here.
+        }
+        /*
+         * Generic code to be executed when the given authority is not one of the special case,
+         * or when it was a special case but parsing of the given string failed.
+         */
         final Iterator<? extends Identifier> it = identifiers.iterator();
         while (it.hasNext()) {
             final Identifier identifier = it.next();
@@ -264,8 +193,8 @@ public class IdentifierMapAdapter extend
                     // because we made a replacement in the first entry, so the
                     // new value will be visible by the getter methods.
                 }
-                if (old == null) {
-                    old = identifier.getCode();
+                if (previous == null) {
+                    previous = identifier.getCode();
                 }
                 it.remove();
                 // Continue the iteration in order to remove all other occurrences,
@@ -275,16 +204,45 @@ public class IdentifierMapAdapter extend
         if (code != null) {
             identifiers.add(SpecializedIdentifier.parse(authority, code));
         }
-        return old;
+        if (previous == null && discarded != null) {
+            previous = discarded.toString();
+        }
+        return previous;
     }
 
     /**
      * Sets the identifier associated with the given authority, and returns the previous value.
+     *
+     * <p>If the given {@code authority} is {@code HREF}, then this method will actually store the value
+     * as the {@link XLink#getHRef()} property of the {@code XLink} associated to the {@code XLINK} key.
+     * The previous {@code HREF} value, if any, is discarded.</p>
+     *
+     * @param  <T> The identifier type.
+     * @param  authority The namespace with which the given identifier is to be associated.
+     * @param  value The identifier to be associated with the given namespace.
+     * @return The previous identifier associated with {@code authority}, or {@code null}
+     *         if there was no mapping of the specialized type for {@code authority}.
      */
     @Override
-    public <T> T putSpecialized(final IdentifierSpace<T> authority, final T value)
-            throws UnsupportedOperationException
-    {
+    @SuppressWarnings("unchecked")
+    public <T> T putSpecialized(final IdentifierSpace<T> authority, final T value) {
+        switch (specialCase(authority)) {
+            default: return store(authority, value);
+            case NonMarshalledAuthority.HREF: return (T) setHRef((URI) value);
+            // A future Apache SIS version may add more special cases here.
+        }
+    }
+
+    /**
+     * Sets the identifier associated with the given authority, without processing for special cases.
+     *
+     * @param  <T> The identifier type.
+     * @param  authority The namespace with which the given identifier is to be associated.
+     * @param  value The identifier to be associated with the given namespace.
+     * @return The previous identifier associated with {@code authority}, or {@code null}
+     *         if there was no mapping of the specialized type for {@code authority}.
+     */
+    private <T> T store(final IdentifierSpace<T> authority, final T value) {
         ArgumentChecks.ensureNonNull("authority", authority);
         T old = null;
         final Iterator<? extends Identifier> it = identifiers.iterator();
@@ -317,240 +275,4 @@ public class IdentifierMapAdapter extend
         }
         return old;
     }
-
-    /**
-     * Returns a view over the collection of identifiers. This view supports removal operation
-     * if the underlying collection of identifiers supports the {@link Iterator#remove()} method.
-     *
-     * @return A view over the collection of identifiers.
-     */
-    @Override
-    public Set<Entry<Citation,String>> entrySet() {
-        /*
-         * Do not cache the entries set because if is very cheap to create and not needed very often.
-         * Not caching allows this implementation to be thread-safe without synchronization or volatile
-         * fields if the underlying list is thread-safe. Furthermore, IdentifierMapAdapter are temporary
-         * objects anyway in the current ISOMetadata implementation.
-         */
-        return new Entries(identifiers);
-    }
-
-    /**
-     * The view returned by {@link IdentifierMapAdapter#entrySet()}. If the backing identifier collection
-     * contains null entries, those entries will be ignored. If the backing collection contains many entries
-     * for the same authority, then only the first occurrence is retained.
-     *
-     * @author  Martin Desruisseaux (Geomatys)
-     * @since   0.3
-     * @version 0.3
-     * @module
-     */
-    private static final class Entries extends AbstractSet<Entry<Citation,String>> {
-        /**
-         * The identifiers to wrap in a set of entries view. This is a reference
-         * to the same collection than {@link IdentifierMapAdapter#identifiers}.
-         */
-        private final Collection<? extends Identifier> identifiers;
-
-        /**
-         * Creates a new view over the collection of identifiers.
-         *
-         * @param identifiers The identifiers to wrap in a set of entries view.
-         */
-        Entries(final Collection<? extends Identifier> identifiers) {
-            this.identifiers = identifiers;
-        }
-
-        /**
-         * Same implementation than {@link IdentifierMapAdapter#clear()}.
-         */
-        @Override
-        public void clear() throws UnsupportedOperationException {
-            identifiers.clear();
-        }
-
-        /**
-         * Same implementation than {@link IdentifierMapAdapter#isEmpty()}.
-         */
-        @Override
-        public boolean isEmpty() {
-            return identifiers.isEmpty();
-        }
-
-        /**
-         * Counts the number of entries, ignoring null elements and duplicated authorities.
-         * Because {@code null} elements are ignored, this method may return 0 even if
-         * {@link #isEmpty()} returns {@code false}.
-         */
-        @Override
-        public int size() {
-            final HashMap<Citation,Boolean> done = new HashMap<>(hashMapCapacity(identifiers.size()));
-            for (final Identifier identifier : identifiers) {
-                if (identifier != null) {
-                    done.put(identifier.getAuthority(), null);
-                }
-            }
-            return done.size();
-        }
-
-        /**
-         * Returns an iterator over the (<var>citation</var>, <var>code</var>) entries.
-         */
-        @Override
-        public Iterator<Entry<Citation, String>> iterator() {
-            return new Iter(identifiers);
-        }
-    }
-
-    /**
-     * The iterator over the (<var>citation</var>, <var>code</var>) entries. This iterator is created by
-     * the {@link IdentifierMapAdapter.Entries} collection. It extends {@link HashMap} as an opportunist
-     * implementation strategy, but users don't need to know this detail.
-     *
-     * <p>This iterator supports the {@link #remove()} operation if the underlying collection
-     * supports it.</p>
-     *
-     * <p>The map entries are used as a safety against duplicated authority values. The map values
-     * are non-null only after we iterated over an authority. Then the value is {@link Boolean#TRUE}
-     * if the identifier has been removed, of {@code Boolean#FALSE} otherwise.</p>
-     *
-     * @author  Martin Desruisseaux (Geomatys)
-     * @since   0.3
-     * @version 0.3
-     * @module
-     */
-    @SuppressWarnings("serial") // Not intended to be serialized.
-    private static final class Iter extends HashMap<Citation,Boolean> implements Iterator<Entry<Citation,String>> {
-        /**
-         * An iterator over the {@link IdentifierMapAdapter#identifiers} collection,
-         * or (@code null} if we have reached the iteration end.
-         */
-        private Iterator<? extends Identifier> identifiers;
-
-        /**
-         * The next entry to be returned by {@link #next()}, or {@code null} if not yet computed.
-         * This field will be computed only when {@link #next()} or {@link #hasNext()} is invoked.
-         */
-        private transient Entry<Citation,String> next;
-
-        /**
-         * The current authority. Used only for removal operations.
-         */
-        private transient Citation authority;
-
-        /**
-         * Creates a new iterator for the given collection of identifiers.
-         */
-        Iter(final Collection<? extends Identifier> identifiers) {
-            super(hashMapCapacity(identifiers.size()));
-            this.identifiers = identifiers.iterator();
-        }
-
-        /**
-         * Advances to the next non-null identifier, skips duplicated authorities, wraps the
-         * identifier in an entry if needed and stores the result in the {@link #next} field.
-         * If we reach the iteration end, then this method set the {@link #identifiers}
-         * iterator to {@code null}.
-         */
-        private void toNext() {
-            final Iterator<? extends Identifier> it = identifiers;
-            if (it != null) {
-                while (it.hasNext()) {
-                    final Identifier identifier = it.next();
-                    if (identifier != null) {
-                        final Citation authority = identifier.getAuthority();
-                        final Boolean state = put(authority, Boolean.FALSE);
-                        if (state == null) {
-                            if (identifier instanceof IdentifierMapEntry) {
-                                next = (IdentifierMapEntry) identifier;
-                            } else {
-                                next = new IdentifierMapEntry.Immutable(authority, identifier.getCode());
-                            }
-                            this.authority = authority;
-                            return;
-                        }
-                        if (state) {
-                            // Found a duplicated entry, and user asked for the
-                            // removal of that authority.
-                            it.remove();
-                        }
-                    }
-                }
-                identifiers = null;
-            }
-        }
-
-        /**
-         * If we need to search for the next element, fetches it now.
-         * Then returns {@code true} if we didn't reached the iteration end.
-         */
-        @Override
-        public boolean hasNext() {
-            if (next == null) {
-                toNext();
-            }
-            return identifiers != null;
-        }
-
-        /**
-         * If we need to search for the next element, searches it now. Then set {@link #next}
-         * to {@code null} as a flag meaning that next invocations will need to search again
-         * for an element, and returns the element that we got.
-         */
-        @Override
-        public Entry<Citation,String> next() throws NoSuchElementException {
-            Entry<Citation,String> entry = next;
-            if (entry == null) {
-                toNext();
-                entry = next;
-            }
-            next = null;
-            if (identifiers == null) {
-                throw new NoSuchElementException();
-            }
-            return entry;
-        }
-
-        /**
-         * Removes the last element returned by {@link #next()}. Note that if the {@link #next}
-         * field is non-null, that would mean that the iteration has moved since the last call
-         * to the {@link #next()} method, in which case the iterator is invalid.
-         */
-        @Override
-        public void remove() throws IllegalStateException {
-            final Iterator<? extends Identifier> it = identifiers;
-            if (it == null || next != null) {
-                throw new IllegalStateException();
-            }
-            it.remove();
-            put(authority, Boolean.TRUE);
-        }
-    }
-
-    /**
-     * Overrides the string representation in order to use only the authority title as keys.
-     * We do that because the string representations of {@code DefaultCitation} objects are
-     * very big.
-     *
-     * <p>String examples:</p>
-     * <ul>
-     *   <li>{gml:id=“myID”}</li>
-     *   <li>{gco:uuid=“42924124-032a-4dfe-b06e-113e3cb81cf0”}</li>
-     *   <li>{xlink:href=“http://www.mydomain.org/myHREF”}</li>
-     * </ul>
-     *
-     * @see SpecializedIdentifier#toString()
-     */
-    @Debug
-    @Override
-    public String toString() {
-    final StringBuilder buffer = new StringBuilder(50).append('{');
-    for (final Entry<Citation,String> entry : entrySet()) {
-        if (buffer.length() != 1) {
-                buffer.append(", ");
-            }
-            SpecializedIdentifier.format(buffer, entry.getKey(), entry.getValue());
-        }
-        return buffer.append('}').toString();
-    }
 }

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/gco/PropertyType.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/gco/PropertyType.java?rev=1707278&r1=1707277&r2=1707278&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/gco/PropertyType.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/gco/PropertyType.java [UTF-8] Wed Oct  7 12:37:16 2015
@@ -206,7 +206,7 @@ public abstract class PropertyType<Value
              *   </gmd:CI_Citation>
              *
              * We do not try to parse UUID or XLink objects from String because it should be the job of
-             * org.apache.sis.internal.jaxb.IdentifierMapWithSpecialCases.put(Citation, String).
+             * org.apache.sis.internal.jaxb.ModifiableIdentifierMap.put(Citation, String).
              */
             final IdentifierMap map = ((IdentifiedObject) value).getIdentifierMap();
             XLink  link = map.getSpecialized(IdentifierSpace.XLINK);

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/package-info.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/package-info.java?rev=1707278&r1=1707277&r2=1707278&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/package-info.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/internal/jaxb/package-info.java [UTF-8] Wed Oct  7 12:37:16 2015
@@ -26,7 +26,7 @@
  * <div class="section">Main content</div>
  * {@link org.apache.sis.internal.jaxb.IdentifierMapAdapter} is our internal implementation of
  * the public {@link org.apache.sis.xml.IdentifierMap} interface. The actual implementation is
- * usually the {@code IdentifierMapWithSpecialCases} subclass.
+ * usually the {@code ModifiableIdentifierMap} subclass.
  *
  * <p>{@link org.apache.sis.internal.jaxb.SpecializedIdentifier} wraps {@link org.apache.sis.xml.XLink},
  * {@link java.net.URI} and {@link java.util.UUID} as {@link org.opengis.metadata.Identifier} instances.

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/xml/IdentifierMap.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/xml/IdentifierMap.java?rev=1707278&r1=1707277&r2=1707278&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/xml/IdentifierMap.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/xml/IdentifierMap.java [UTF-8] Wed Oct  7 12:37:16 2015
@@ -66,6 +66,5 @@ public interface IdentifierMap extends M
      *         if there was no mapping of the specialized type for {@code authority}.
      * @throws UnsupportedOperationException If the identifier map is unmodifiable.
      */
-    <T> T putSpecialized(IdentifierSpace<T> authority, T value)
-            throws UnsupportedOperationException;
+    <T> T putSpecialized(IdentifierSpace<T> authority, T value) throws UnsupportedOperationException;
 }

Modified: sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/xml/NilObjectHandler.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/xml/NilObjectHandler.java?rev=1707278&r1=1707277&r2=1707278&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/xml/NilObjectHandler.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/main/java/org/apache/sis/xml/NilObjectHandler.java [UTF-8] Wed Oct  7 12:37:16 2015
@@ -31,7 +31,7 @@ import org.apache.sis.util.ComparisonMod
 import org.apache.sis.util.LenientComparable;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.internal.jaxb.IdentifierMapAdapter;
-import org.apache.sis.internal.jaxb.IdentifierMapWithSpecialCases;
+import org.apache.sis.internal.jaxb.ModifiableIdentifierMap;
 
 // Branch-dependent imports
 import java.util.Objects;
@@ -73,7 +73,7 @@ final class NilObjectHandler implements
                 asList.add(identifier);
             }
         }
-        attribute = new IdentifierMapWithSpecialCases(asList);
+        attribute = new ModifiableIdentifierMap(asList);
     }
 
     /**

Modified: sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/IdentifierMapAdapterTest.java
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/IdentifierMapAdapterTest.java?rev=1707278&r1=1707277&r2=1707278&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/IdentifierMapAdapterTest.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/IdentifierMapAdapterTest.java [UTF-8] Wed Oct  7 12:37:16 2015
@@ -16,19 +16,14 @@
  */
 package org.apache.sis.internal.jaxb;
 
-import java.net.URI;
 import java.util.Map;
 import java.util.List;
 import java.util.ArrayList;
-import java.util.Iterator;
-import java.util.Collection;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.citation.Citation;
-import org.apache.sis.xml.IdentifierMap;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
 
-import static java.util.UUID.fromString;
 import static org.apache.sis.test.Assert.*;
 import static org.apache.sis.xml.IdentifierSpace.*;
 
@@ -38,198 +33,11 @@ import static org.apache.sis.xml.Identif
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @since   0.3
- * @version 0.3
+ * @version 0.7
  * @module
  */
 public strictfp class IdentifierMapAdapterTest extends TestCase {
     /**
-     * Creates the {@link IdentifierMapAdapter} instance to test for the given identifiers.
-     * This {@code IdentifierMapAdapterTest} class creates {@link IdentifierMapAdapter} instances.
-     * Subclasses will override this method in order to create instances of the class to test.
-     *
-     * @param  identifiers The identifiers to wrap in an {@code IdentifierMapAdapter}.
-     * @return The {@code IdentifierMapAdapter} to test.
-     */
-    IdentifierMapAdapter create(final Collection<Identifier> identifiers) {
-        return new IdentifierMapAdapter(identifiers);
-    }
-
-    /**
-     * Asserts that the content of the given map is equals to the given content, represented
-     * as a string. Subclasses can override this method in order to alter the expected string.
-     * This is needed when, using the "special case rules", {@code "href"} have been replaced
-     * be {@code "xlink:href"}.
-     *
-     * @param  expected The expected content.
-     * @return The map to compare with the expected content.
-     */
-    void assertMapEquals(final String expected, final Map<Citation,String> map) {
-        assertEquals(expected, map.toString());
-    }
-
-    /**
-     * Returns a string representation of the given {@code href} value.
-     * The default implementation returns the value unchanged.
-     */
-    String toHRefString(final String href) {
-        return href;
-    }
-
-    /**
-     * Tests read and write operations on an {@link IdentifierMapAdapter}, using a well-formed
-     * identifier collection (no null values, no duplicated authorities).
-     *
-     * <p>This test does not use the {@link IdentifierMap}-specific API.</p>
-     */
-    @Test
-    public void testGetAndPut() {
-        final List<Identifier> identifiers = new ArrayList<>();
-        final Map<Citation,String> map = create(identifiers);
-        assertTrue  ("Newly created map shall be empty.", map.isEmpty());
-        assertEquals("Newly created map shall be empty.", 0, map.size());
-        /*
-         * Add two entries, then verify the map content.
-         */
-        assertTrue(identifiers.add(new IdentifierMapEntry(ID,   "myID")));
-        assertTrue(identifiers.add(new IdentifierMapEntry(UUID, "myUUID")));
-        assertFalse ("After add, map shall not be empty.", map.isEmpty());
-        assertEquals("After add, map shall not be empty.", 2, map.size());
-        assertEquals("After add, map shall not be empty.", 2, identifiers.size());
-        assertTrue  ("Shall contain the entry we added.",        map.containsKey(ID));
-        assertTrue  ("Shall contain the entry we added.",        map.containsKey(UUID));
-        assertFalse ("Shall not contain entry we didn't added.", map.containsKey(HREF));
-        assertTrue  ("Shall contain the entry we added.",        map.containsValue("myID"));
-        assertTrue  ("Shall contain the entry we added.",        map.containsValue("myUUID"));
-        assertFalse ("Shall not contain entry we didn't added.", map.containsValue("myHREF"));
-        assertEquals("Shall contain the entry we added.",        "myID",   map.get(ID));
-        assertEquals("Shall contain the entry we added.",        "myUUID", map.get(UUID));
-        assertNull  ("Shall not contain entry we didn't added.",           map.get(HREF));
-        assertMapEquals("{gml:id=“myID”, gco:uuid=“myUUID”}", map);
-        /*
-         * Alter one entry (no new entry added).
-         */
-        assertEquals("Shall get the old value.",       "myUUID", map.put(UUID, "myNewUUID"));
-        assertFalse ("Shall not contain anymore the old value.", map.containsValue("myUUID"));
-        assertTrue  ("Shall contain the new value.",             map.containsValue("myNewUUID"));
-        assertMapEquals("{gml:id=“myID”, gco:uuid=“myNewUUID”}", map);
-        assertEquals("Map size shall be unchanged.", 2, map.size());
-        assertEquals("Map size shall be unchanged.", 2, identifiers.size());
-        /*
-         * Add a third identifier.
-         */
-        assertNull  ("Shall not contain entry we didn't added.", map.put(HREF, "myHREF"));
-        assertTrue  ("Shall contain the entry we added.",        map.containsValue("myHREF"));
-        assertTrue  ("Shall contain the entry we added.",        map.containsKey(HREF));
-        assertMapEquals("{gml:id=“myID”, gco:uuid=“myNewUUID”, xlink:href=“myHREF”}", map);
-        assertEquals("Map size shall be updated.", 3, map.size());
-        assertEquals("Map size shall be updated.", 3, identifiers.size());
-        /*
-         * Remove an identifier using the Map.remove(…) API.
-         */
-        assertEquals("Shall get the old value.",   "myNewUUID", map.remove(UUID));
-        assertFalse ("Shall not contain the entry we removed.", map.containsValue("myNewUUID"));
-        assertFalse ("Shall not contain the entry we removed.", map.containsKey(UUID));
-        assertMapEquals("{gml:id=“myID”, xlink:href=“myHREF”}", map);
-        assertEquals("Map size shall be updated.", 2, map.size());
-        assertEquals("Map size shall be updated.", 2, identifiers.size());
-        /*
-         * Remove an identifier using the Set.remove(…) API on values.
-         */
-        assertTrue  (map.values().remove(toHRefString("myHREF")));
-        assertFalse ("Shall not contain the entry we removed.", map.containsValue("myHREF"));
-        assertFalse ("Shall not contain the entry we removed.", map.containsKey(HREF));
-        assertMapEquals("{gml:id=“myID”}", map);
-        assertEquals("Map size shall be updated.", 1, map.size());
-        assertEquals("Map size shall be updated.", 1, identifiers.size());
-        /*
-         * Remove an identifier using the Set.remove(…) API on keys.
-         */
-        assertTrue  (map.keySet().remove(ID));
-        assertFalse ("Shall not contain the entry we removed.", map.containsValue("myID"));
-        assertFalse ("Shall not contain the entry we removed.", map.containsKey(ID));
-        assertMapEquals("{}", map);
-        assertEquals("Map size shall be updated.", 0, map.size());
-        assertEquals("Map size shall be updated.", 0, identifiers.size());
-    }
-
-    /**
-     * Tests write operations on an {@link IdentifierMap} using specific API.
-     */
-    @Test
-    public void testPutSpecialized() {
-        final List<Identifier> identifiers = new ArrayList<>();
-        final IdentifierMap map = create(identifiers);
-        final String myID = "myID";
-        final java.util.UUID myUUID = fromString("a1eb6e53-93db-4942-84a6-d9e7fb9db2c7");
-        final URI myURI = URI.create("http://mylink");
-
-        assertNull(map.putSpecialized(ID,   myID));
-        assertNull(map.putSpecialized(UUID, myUUID));
-        assertNull(map.putSpecialized(HREF, myURI));
-        assertMapEquals("{gml:id=“myID”,"
-                + " gco:uuid=“a1eb6e53-93db-4942-84a6-d9e7fb9db2c7”,"
-                + " xlink:href=“http://mylink”}", map);
-
-        assertSame(myID,   map.getSpecialized(ID));
-        assertSame(myUUID, map.getSpecialized(UUID));
-        assertSame(myURI,  map.getSpecialized(HREF));
-        assertEquals("myID",                                 map.get(ID));
-        assertEquals("a1eb6e53-93db-4942-84a6-d9e7fb9db2c7", map.get(UUID));
-        assertEquals("http://mylink",                        map.get(HREF));
-    }
-
-    /**
-     * Tests read operations on an {@link IdentifierMap} using specific API.
-     */
-    @Test
-    public void testGetSpecialized() {
-        final List<Identifier> identifiers = new ArrayList<>();
-        final IdentifierMap map = create(identifiers);
-
-        assertNull(map.put(ID,   "myID"));
-        assertNull(map.put(UUID, "a1eb6e53-93db-4942-84a6-d9e7fb9db2c7"));
-        assertNull(map.put(HREF, "http://mylink"));
-        assertMapEquals("{gml:id=“myID”,"
-                + " gco:uuid=“a1eb6e53-93db-4942-84a6-d9e7fb9db2c7”,"
-                + " xlink:href=“http://mylink”}", map);
-
-        assertEquals("myID",                                             map.get           (ID));
-        assertEquals("a1eb6e53-93db-4942-84a6-d9e7fb9db2c7",             map.get           (UUID));
-        assertEquals("http://mylink",                                    map.get           (HREF));
-        assertEquals("myID",                                             map.getSpecialized(ID));
-        assertEquals(URI.create("http://mylink"),                        map.getSpecialized(HREF));
-        assertEquals(fromString("a1eb6e53-93db-4942-84a6-d9e7fb9db2c7"), map.getSpecialized(UUID));
-    }
-
-    /**
-     * Tests the handling of duplicated authorities.
-     */
-    @Test
-    public void testDuplicatedAuthorities() {
-        final List<Identifier> identifiers = new ArrayList<>();
-        assertTrue(identifiers.add(new IdentifierMapEntry(ID,   "myID1")));
-        assertTrue(identifiers.add(new IdentifierMapEntry(UUID, "myUUID")));
-        assertTrue(identifiers.add(new IdentifierMapEntry(ID,   "myID2")));
-
-        final IdentifierMap map = create(identifiers);
-        assertEquals("Duplicated authorities shall be filtered.", 2, map.size());
-        assertEquals("Duplicated authorities shall still exist.", 3, identifiers.size());
-        assertEquals("myID1",  map.get(ID));
-        assertEquals("myUUID", map.get(UUID));
-
-        final Iterator<Citation> it = map.keySet().iterator();
-        assertTrue(it.hasNext());
-        assertSame(ID, it.next());
-        it.remove();
-        assertTrue(it.hasNext());
-        assertSame(UUID, it.next());
-        assertFalse("Duplicated authority shall have been removed.", it.hasNext());
-
-        assertEquals(1, identifiers.size());
-        assertEquals(1, map.size());
-    }
-
-    /**
      * Tests serialization.
      */
     @Test
@@ -239,7 +47,7 @@ public strictfp class IdentifierMapAdapt
         assertSame(HREF, assertSerializedEquals(HREF));
 
         final List<Identifier> identifiers = new ArrayList<>();
-        final Map<Citation,String> map = create(identifiers);
+        final Map<Citation,String> map = new IdentifierMapAdapter(identifiers);
         assertTrue(identifiers.add(new IdentifierMapEntry(ID,   "myID")));
         assertTrue(identifiers.add(new IdentifierMapEntry(UUID, "myUUID")));
 

Copied: sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMapTest.java (from r1706793, sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/IdentifierMapAdapterTest.java)
URL: http://svn.apache.org/viewvc/sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMapTest.java?p2=sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMapTest.java&p1=sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/IdentifierMapAdapterTest.java&r1=1706793&r2=1707278&rev=1707278&view=diff
==============================================================================
--- sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/IdentifierMapAdapterTest.java [UTF-8] (original)
+++ sis/branches/JDK8/core/sis-utility/src/test/java/org/apache/sis/internal/jaxb/ModifiableIdentifierMapTest.java [UTF-8] Wed Oct  7 12:37:16 2015
@@ -21,61 +21,59 @@ import java.util.Map;
 import java.util.List;
 import java.util.ArrayList;
 import java.util.Iterator;
-import java.util.Collection;
+import java.util.UUID;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.citation.Citation;
 import org.apache.sis.xml.IdentifierMap;
+import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.TestCase;
+import org.apache.sis.xml.XLink;
 import org.junit.Test;
 
 import static java.util.UUID.fromString;
 import static org.apache.sis.test.Assert.*;
+import static org.apache.sis.test.TestUtilities.getSingleton;
 import static org.apache.sis.xml.IdentifierSpace.*;
 
 
 /**
- * Tests {@link IdentifierMapAdapter}.
+ * Tests {@link ModifiableIdentifierMap}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @since   0.3
- * @version 0.3
+ * @since   0.7
+ * @version 0.7
  * @module
  */
-public strictfp class IdentifierMapAdapterTest extends TestCase {
+@DependsOn(IdentifierMapAdapterTest.class)
+public final strictfp class ModifiableIdentifierMapTest extends TestCase {
     /**
-     * Creates the {@link IdentifierMapAdapter} instance to test for the given identifiers.
-     * This {@code IdentifierMapAdapterTest} class creates {@link IdentifierMapAdapter} instances.
-     * Subclasses will override this method in order to create instances of the class to test.
-     *
-     * @param  identifiers The identifiers to wrap in an {@code IdentifierMapAdapter}.
-     * @return The {@code IdentifierMapAdapter} to test.
+     * The HREF string to replace by {@link XLink#toString()}.
      */
-    IdentifierMapAdapter create(final Collection<Identifier> identifiers) {
-        return new IdentifierMapAdapter(identifiers);
-    }
+    private static final String TO_REPLACE = "xlink:href=“";
 
     /**
-     * Asserts that the content of the given map is equals to the given content, represented
-     * as a string. Subclasses can override this method in order to alter the expected string.
-     * This is needed when, using the "special case rules", {@code "href"} have been replaced
-     * be {@code "xlink:href"}.
+     * Asserts that the content of the given map is equals to the given content, represented as a string.
+     * This method replaces the {@code xlink:href} value by the {@link XLink#toString()} value before to
+     * compare with the map content. This is needed because the "special case rules" cause the {@code "href"}
+     * identifier to be replaced by {@code "xlink:href"}.
      *
      * @param  expected The expected content.
      * @return The map to compare with the expected content.
      */
-    void assertMapEquals(final String expected, final Map<Citation,String> map) {
+    private static void assertMapEquals(String expected, final Map<Citation,String> map) {
+        final int start = expected.indexOf(TO_REPLACE);
+        if (start >= 0) {
+            final int end = start + TO_REPLACE.length();
+            final int close = expected.indexOf('”', end);
+            final StringBuilder buffer = new StringBuilder(expected);
+            buffer.replace(close, close+1, "\"]");
+            buffer.replace(start, end, "xlink=XLink[href=\"");
+            expected = buffer.toString();
+        }
         assertEquals(expected, map.toString());
     }
 
     /**
-     * Returns a string representation of the given {@code href} value.
-     * The default implementation returns the value unchanged.
-     */
-    String toHRefString(final String href) {
-        return href;
-    }
-
-    /**
      * Tests read and write operations on an {@link IdentifierMapAdapter}, using a well-formed
      * identifier collection (no null values, no duplicated authorities).
      *
@@ -84,7 +82,7 @@ public strictfp class IdentifierMapAdapt
     @Test
     public void testGetAndPut() {
         final List<Identifier> identifiers = new ArrayList<>();
-        final Map<Citation,String> map = create(identifiers);
+        final Map<Citation,String> map = new ModifiableIdentifierMap(identifiers);
         assertTrue  ("Newly created map shall be empty.", map.isEmpty());
         assertEquals("Newly created map shall be empty.", 0, map.size());
         /*
@@ -135,7 +133,7 @@ public strictfp class IdentifierMapAdapt
         /*
          * Remove an identifier using the Set.remove(…) API on values.
          */
-        assertTrue  (map.values().remove(toHRefString("myHREF")));
+        assertTrue  (map.values().remove("XLink[href=\"myHREF\"]"));
         assertFalse ("Shall not contain the entry we removed.", map.containsValue("myHREF"));
         assertFalse ("Shall not contain the entry we removed.", map.containsKey(HREF));
         assertMapEquals("{gml:id=“myID”}", map);
@@ -158,7 +156,7 @@ public strictfp class IdentifierMapAdapt
     @Test
     public void testPutSpecialized() {
         final List<Identifier> identifiers = new ArrayList<>();
-        final IdentifierMap map = create(identifiers);
+        final IdentifierMap map = new ModifiableIdentifierMap(identifiers);
         final String myID = "myID";
         final java.util.UUID myUUID = fromString("a1eb6e53-93db-4942-84a6-d9e7fb9db2c7");
         final URI myURI = URI.create("http://mylink");
@@ -184,7 +182,7 @@ public strictfp class IdentifierMapAdapt
     @Test
     public void testGetSpecialized() {
         final List<Identifier> identifiers = new ArrayList<>();
-        final IdentifierMap map = create(identifiers);
+        final IdentifierMap map = new ModifiableIdentifierMap(identifiers);
 
         assertNull(map.put(ID,   "myID"));
         assertNull(map.put(UUID, "a1eb6e53-93db-4942-84a6-d9e7fb9db2c7"));
@@ -211,7 +209,7 @@ public strictfp class IdentifierMapAdapt
         assertTrue(identifiers.add(new IdentifierMapEntry(UUID, "myUUID")));
         assertTrue(identifiers.add(new IdentifierMapEntry(ID,   "myID2")));
 
-        final IdentifierMap map = create(identifiers);
+        final IdentifierMap map = new ModifiableIdentifierMap(identifiers);
         assertEquals("Duplicated authorities shall be filtered.", 2, map.size());
         assertEquals("Duplicated authorities shall still exist.", 3, identifiers.size());
         assertEquals("myID1",  map.get(ID));
@@ -230,21 +228,39 @@ public strictfp class IdentifierMapAdapt
     }
 
     /**
-     * Tests serialization.
+     * Tests explicitely the special handling of {@code href} values.
      */
     @Test
-    public void testSerialization() {
-        assertSame(ID,   assertSerializedEquals(ID));
-        assertSame(UUID, assertSerializedEquals(UUID));
-        assertSame(HREF, assertSerializedEquals(HREF));
+    public void testHRefSubstitution() {
+        final List<Identifier> identifiers = new ArrayList<>();
+        final IdentifierMap map = new ModifiableIdentifierMap(identifiers);
+        assertNull(map.put(HREF, "myHREF"));
+        assertEquals("Shall contain the entry we added.", "myHREF", map.get(HREF));
+
+        // Check the XLink object
+        final XLink link = map.getSpecialized(XLINK);
+        assertEquals("Added href shall be stored as XLink attribute.", "myHREF", String.valueOf(link.getHRef()));
+        assertEquals("Identifier list shall contain the XLink.", link.toString(), getSingleton(identifiers).getCode());
 
+        // Modidfy the XLink object directly
+        link.setHRef(URI.create("myNewHREF"));
+        assertEquals("Change in XLink shall be reflected in href.", "myNewHREF", map.get(HREF));
+    }
+
+    /**
+     * Tests with UUIDs.
+     */
+    @Test
+    public void testUUIDs() {
         final List<Identifier> identifiers = new ArrayList<>();
-        final Map<Citation,String> map = create(identifiers);
-        assertTrue(identifiers.add(new IdentifierMapEntry(ID,   "myID")));
-        assertTrue(identifiers.add(new IdentifierMapEntry(UUID, "myUUID")));
+        final IdentifierMap map = new ModifiableIdentifierMap(identifiers);
+        final java.util.UUID id1 = fromString("434f3107-c6d2-4c8c-bb25-553f68641c5c");
+        final java.util.UUID id2 = fromString("42924124-032a-4dfe-b06e-113e3cb81cf0");
+
+        // Add first UUID.
+        assertNull(map.putSpecialized(UUID, id1));
 
-        final Map<Citation,String> copy = assertSerializedEquals(map);
-        assertNotSame(map, copy);
-        assertEquals(2, copy.size());
+        // Replace UUID by a new one.
+        assertSame(id1, map.putSpecialized(UUID, id2));
     }
 }




Mime
View raw message