sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] 01/02: Implement and test State.COMPLETABLE.
Date Fri, 27 Jul 2018 15:06:57 GMT
This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch geoapi-4.0
in repository https://gitbox.apache.org/repos/asf/sis.git

commit aa209d9701455d894ac89430cf86fb58f92e11ea
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Fri Jul 27 16:51:01 2018 +0200

    Implement and test State.COMPLETABLE.
---
 .../sis/internal/metadata/MetadataUtilities.java   |  24 +-
 .../apache/sis/internal/metadata/Resources.java    | 128 ++++++++++
 .../sis/internal/metadata/Resources.properties     |  23 ++
 .../sis/internal/metadata/Resources_fr.properties  |  28 +++
 .../org/apache/sis/metadata/MetadataStandard.java  |  68 +++--
 .../apache/sis/metadata/ModifiableMetadata.java    | 278 ++++++++++++---------
 .../java/org/apache/sis/metadata/StateChanger.java |   3 -
 .../apache/sis/metadata/iso/DefaultMetadata.java   |  18 +-
 .../org/apache/sis/metadata/iso/ISOMetadata.java   |  16 +-
 .../iso/acquisition/DefaultRequestedDate.java      |   5 +-
 .../iso/acquisition/DefaultRequirement.java        |   3 +-
 .../sis/metadata/iso/citation/DefaultCitation.java |   3 +-
 .../metadata/iso/citation/DefaultCitationDate.java |   3 +-
 .../iso/citation/DefaultResponsibleParty.java      |   6 +-
 .../iso/content/DefaultCoverageDescription.java    |   6 +-
 .../DefaultFeatureCatalogueDescription.java        |   4 +-
 .../iso/extent/DefaultGeographicBoundingBox.java   |  36 ++-
 .../iso/extent/DefaultSpatialTemporalExtent.java   |   4 +-
 .../DefaultAggregateInformation.java               |   2 +-
 .../DefaultRepresentativeFraction.java             |   5 +-
 .../metadata/iso/identification/DefaultUsage.java  |   3 +-
 .../maintenance/DefaultMaintenanceInformation.java |   8 +-
 .../sis/metadata/iso/quality/AbstractElement.java  |   5 +-
 .../org/apache/sis/metadata/sql/Dispatcher.java    |   9 +-
 .../sis/metadata/ModifiableMetadataTest.java       | 188 ++++++++++++++
 .../sis/metadata/iso/citation/CitationsTest.java   |  18 +-
 .../sis/metadata/sql/MetadataSourceTest.java       |  23 ++
 .../apache/sis/test/suite/MetadataTestSuite.java   |   1 +
 .../apache/sis/internal/util/CollectionsExt.java   |  51 ++--
 .../java/org/apache/sis/util/resources/Errors.java |   7 +-
 .../apache/sis/util/resources/Errors.properties    |   1 -
 .../apache/sis/util/resources/Errors_fr.properties |   1 -
 ide-project/NetBeans/build.xml                     |   3 +
 33 files changed, 726 insertions(+), 255 deletions(-)

diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/MetadataUtilities.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/MetadataUtilities.java
index 3843385..cf17acb 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/MetadataUtilities.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/MetadataUtilities.java
@@ -17,6 +17,7 @@
 package org.apache.sis.internal.metadata;
 
 import java.util.Date;
+import java.util.Collection;
 import org.apache.sis.xml.NilReason;
 import org.apache.sis.xml.IdentifierSpace;
 import org.apache.sis.xml.IdentifiedObject;
@@ -68,25 +69,14 @@ public final class MetadataUtilities extends Static {
     }
 
     /**
-     * Returns {@link Boolean#TRUE} if the given value is a valid date, or {@code null} otherwise.
-     * This method is used for calls to {@code checkWritePermission(Object)}. The use of a boolean
-     * as return type is for avoiding to create new {@link Date} instances.
+     * Returns the given collection if non-null and non-empty, or {@code null} otherwise.
+     * This method is used for calls to {@code checkWritePermission(Object)}.
      *
-     * @param  value  the time in milliseconds.
-     * @return {@code Boolean.TRUE} if the given value is a valid date.
-     */
-    public static Boolean isDateDefined(final long value) {
-        return (value != Long.MIN_VALUE) ? Boolean.TRUE : null;
-    }
-
-    /**
-     * Returns {@link Boolean#TRUE} if the given value is a valid double, or {@code null} if NaN.
-     *
-     * @param  value  the numeric value.
-     * @return {@code Boolean.TRUE} if the given value is non-NaN.
+     * @param  value  the collection.
+     * @return the given collection if non-empty, or {@code null} otherwise.
      */
-    public static Boolean isDefined(final double value) {
-        return Double.isNaN(value) ? null : Boolean.TRUE;
+    public static Collection<?> valueIfDefined(final Collection<?> value) {
+        return (value == null) || value.isEmpty() ? null : value;
     }
 
     /**
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.java b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.java
new file mode 100644
index 0000000..9e4af18
--- /dev/null
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.java
@@ -0,0 +1,128 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.internal.metadata;
+
+import java.net.URL;
+import java.util.Locale;
+import java.util.MissingResourceException;
+import javax.annotation.Generated;
+import org.apache.sis.util.resources.KeyConstants;
+import org.apache.sis.util.resources.IndexedResourceBundle;
+
+
+/**
+ * Warning and error messages that are specific to the {@code sis-metadata} module.
+ * Resources in this file should not be used by any other module. For resources shared by
+ * all modules in the Apache SIS project, see {@link org.apache.sis.util.resources} package.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+public final class Resources extends IndexedResourceBundle {
+    /**
+     * Resource keys. This class is used when compiling sources, but no dependencies to
+     * {@code Keys} should appear in any resulting class files. Since the Java compiler
+     * inlines final integer values, using long identifiers will not bloat the constant
+     * pools of compiled classes.
+     *
+     * @author  Martin Desruisseaux (Geomatys)
+     * @since   1.0
+     * @module
+     */
+    @Generated("org.apache.sis.util.resources.IndexedResourceCompiler")
+    public static final class Keys extends KeyConstants {
+        /**
+         * The unique instance of key constants handler.
+         */
+        static final Keys INSTANCE = new Keys();
+
+        /**
+         * For {@link #INSTANCE} creation only.
+         */
+        private Keys() {
+        }
+
+        /**
+         * This metadata element is already initialized with value “{0}”.
+         */
+        public static final short ElementAlreadyInitialized_1 = 2;
+
+        /**
+         * This metadata is not modifiable.
+         */
+        public static final short UnmodifiableMetadata = 1;
+    }
+
+    /**
+     * Constructs a new resource bundle loading data from the given UTF file.
+     *
+     * @param resources  the path of the binary file containing resources, or {@code null} if
+     *        there is no resources. The resources may be a file or an entry in a JAR file.
+     */
+    public Resources(final URL resources) {
+        super(resources);
+    }
+
+    /**
+     * Returns the handle for the {@code Keys} constants.
+     *
+     * @return a handler for the constants declared in the inner {@code Keys} class.
+     */
+    @Override
+    protected KeyConstants getKeyConstants() {
+        return Keys.INSTANCE;
+    }
+
+    /**
+     * Returns resources in the given locale.
+     *
+     * @param  locale  the locale, or {@code null} for the default locale.
+     * @return resources in the given locale.
+     * @throws MissingResourceException if resources can not be found.
+     */
+    public static Resources forLocale(final Locale locale) throws MissingResourceException {
+        return getBundle(Resources.class, locale);
+    }
+
+    /**
+     * Gets a string for the given key from this resource bundle or one of its parents.
+     *
+     * @param  key  the key for the desired string.
+     * @return the string for the given key.
+     * @throws MissingResourceException if no object for the given key can be found.
+     */
+    public static String format(final short key) throws MissingResourceException {
+        return forLocale(null).getString(key);
+    }
+
+    /**
+     * Gets a string for the given key are replace all occurrence of "{0}"
+     * with values of {@code arg0}.
+     *
+     * @param  key   the key for the desired string.
+     * @param  arg0  value to substitute to "{0}".
+     * @return the formatted string for the given key.
+     * @throws MissingResourceException if no object for the given key can be found.
+     */
+    public static String format(final short  key,
+                                final Object arg0) throws MissingResourceException
+    {
+        return forLocale(null).getString(key, arg0);
+    }
+}
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.properties b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.properties
new file mode 100644
index 0000000..5e5cfe5
--- /dev/null
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources.properties
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Resources in this file are for "sis-metadata" usage only and should not be used by any other module.
+# For resources shared by all modules in the Apache SIS project, see "org.apache.sis.util.resources" package.
+#
+ElementAlreadyInitialized_1       = This metadata element is already initialized with value \u201c{0}\u201d.
+UnmodifiableMetadata              = This metadata is not modifiable.
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources_fr.properties b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources_fr.properties
new file mode 100644
index 0000000..7b917c2
--- /dev/null
+++ b/core/sis-metadata/src/main/java/org/apache/sis/internal/metadata/Resources_fr.properties
@@ -0,0 +1,28 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Resources in this file are for "sis-metadata" usage only and should not be used by any other module.
+# For resources shared by all modules in the Apache SIS project, see "org.apache.sis.util.resources" package.
+#
+# Punctuation rules in French (source: http://unicode.org/udhr/n/notes_fra.html)
+#
+#   U+202F NARROW NO-BREAK SPACE  before  ; ! and ?
+#   U+00A0 NO-BREAK SPACE         before  :
+#
+ElementAlreadyInitialized_1       = Cet \u00e9l\u00e9ment de m\u00e9ta-donn\u00e9e est d\u00e9j\u00e0 initialis\u00e9 avec la valeur \u00ab\u202f{0}\u202f\u00bb.
+UnmodifiableMetadata              = Cette m\u00e9ta-donn\u00e9e n\u2019est pas modifiable.
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java
index 69ca60e..493fd04 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/MetadataStandard.java
@@ -294,6 +294,18 @@ public class MetadataStandard implements Serializable {
     }
 
     /**
+     * Returns a key for use in {@link #getAccessor(CacheKey, boolean)} for the given type.
+     * The type may be an interface (typically a GeoAPI interface) or an implementation class.
+     */
+    private CacheKey createCacheKey(Class<?> type) {
+        final Class<?> implementation = getImplementation(type);
+        if (implementation != null) {
+            type = implementation;
+        }
+        return new CacheKey(type);
+    }
+
+    /**
      * Returns the accessor for the specified implementation class, or {@code null} if none.
      * The given class shall not be the standard interface, unless the metadata is read-only.
      * More specifically, the given {@code type} shall be one of the following:
@@ -607,6 +619,30 @@ public class MetadataStandard implements Serializable {
     }
 
     /**
+     * Returns a value of the "title" property of the given metadata object.
+     * The title property is defined by {@link TitleProperty} annotation on the implementation class.
+     *
+     * @param  metadata  the metadata for which to get the title property, or {@code null}.
+     * @return the title property value of the given metadata, or {@code null} if none.
+     *
+     * @see TitleProperty
+     * @see ValueExistencePolicy#COMPACT
+     */
+    final Object getTitle(final Object metadata) {
+        if (metadata != null) {
+            final Class<?> type = metadata.getClass();
+            final PropertyAccessor accessor = getAccessor(createCacheKey(type), false);
+            if (accessor != null) {
+                TitleProperty an = type.getAnnotation(TitleProperty.class);
+                if (an != null || (an = accessor.implementation.getAnnotation(TitleProperty.class)) != null) {
+                    return accessor.get(accessor.indexOf(an.name(), false), metadata);
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
      * Returns the names of all properties defined in the given metadata type.
      * The property names appears both as keys and as values, but may be written differently.
      * The names may be {@linkplain KeyNamePolicy#UML_IDENTIFIER standard identifiers} (e.g.
@@ -636,17 +672,13 @@ public class MetadataStandard implements Serializable {
      * @throws ClassCastException if the specified interface or implementation class does
      *         not extend or implement a metadata interface of the expected package.
      */
-    public Map<String,String> asNameMap(Class<?> type, final KeyNamePolicy keyPolicy,
+    public Map<String,String> asNameMap(final Class<?> type, final KeyNamePolicy keyPolicy,
             final KeyNamePolicy valuePolicy) throws ClassCastException
     {
         ensureNonNull("type",        type);
         ensureNonNull("keyPolicy",   keyPolicy);
         ensureNonNull("valuePolicy", valuePolicy);
-        final Class<?> implementation = getImplementation(type);
-        if (implementation != null) {
-            type = implementation;
-        }
-        return new NameMap(getAccessor(new CacheKey(type), true), keyPolicy, valuePolicy);
+        return new NameMap(getAccessor(createCacheKey(type), true), keyPolicy, valuePolicy);
     }
 
     /**
@@ -675,17 +707,13 @@ public class MetadataStandard implements Serializable {
      * @throws ClassCastException if the specified interface or implementation class does
      *         not extend or implement a metadata interface of the expected package.
      */
-    public Map<String,Class<?>> asTypeMap(Class<?> type, final KeyNamePolicy keyPolicy,
+    public Map<String,Class<?>> asTypeMap(final Class<?> type, final KeyNamePolicy keyPolicy,
             final TypeValuePolicy valuePolicy) throws ClassCastException
     {
         ensureNonNull("type",        type);
         ensureNonNull("keyPolicy",   keyPolicy);
         ensureNonNull("valuePolicy", valuePolicy);
-        final Class<?> implementation = getImplementation(type);
-        if (implementation != null) {
-            type = implementation;
-        }
-        return new TypeMap(getAccessor(new CacheKey(type), true), keyPolicy, valuePolicy);
+        return new TypeMap(getAccessor(createCacheKey(type), true), keyPolicy, valuePolicy);
     }
 
     /**
@@ -732,16 +760,12 @@ public class MetadataStandard implements Serializable {
      *
      * @see org.apache.sis.metadata.iso.DefaultExtendedElementInformation
      */
-    public Map<String,ExtendedElementInformation> asInformationMap(Class<?> type, final KeyNamePolicy keyPolicy)
+    public Map<String,ExtendedElementInformation> asInformationMap(final Class<?> type, final KeyNamePolicy keyPolicy)
             throws ClassCastException
     {
         ensureNonNull("type",     type);
         ensureNonNull("keyNames", keyPolicy);
-        final Class<?> implementation = getImplementation(type);
-        if (implementation != null) {
-            type = implementation;
-        }
-        return new InformationMap(getAccessor(new CacheKey(type), true), keyPolicy);
+        return new InformationMap(getAccessor(createCacheKey(type), true), keyPolicy);
     }
 
     /**
@@ -761,16 +785,12 @@ public class MetadataStandard implements Serializable {
      * @throws ClassCastException if the specified interface or implementation class does
      *         not extend or implement a metadata interface of the expected package.
      */
-    public Map<String,Integer> asIndexMap(Class<?> type, final KeyNamePolicy keyPolicy)
+    public Map<String,Integer> asIndexMap(final Class<?> type, final KeyNamePolicy keyPolicy)
             throws ClassCastException
     {
         ensureNonNull("type",      type);
         ensureNonNull("keyPolicy", keyPolicy);
-        final Class<?> implementation = getImplementation(type);
-        if (implementation != null) {
-            type = implementation;
-        }
-        return new IndexMap(getAccessor(new CacheKey(type), true), keyPolicy);
+        return new IndexMap(getAccessor(createCacheKey(type), true), keyPolicy);
     }
 
     /**
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java
index 538ba9b..bc7c901 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/ModifiableMetadata.java
@@ -29,12 +29,16 @@ import java.nio.charset.Charset;
 import javax.xml.bind.annotation.XmlTransient;
 import org.opengis.util.CodeList;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.collection.Containers;
 import org.apache.sis.util.collection.CodeListSet;
+import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.internal.util.CheckedHashSet;
 import org.apache.sis.internal.util.CheckedArrayList;
+import org.apache.sis.internal.metadata.Resources;
 import org.apache.sis.internal.system.Semaphores;
 
 import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
+import static org.apache.sis.internal.metadata.MetadataUtilities.valueIfDefined;
 
 
 /**
@@ -86,43 +90,38 @@ import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
 @XmlTransient
 public abstract class ModifiableMetadata extends AbstractMetadata {
     /**
-     * Initial capacity of sets. We use a small value because collections will typically
-     * contain few elements (often just a singleton).
-     */
-    private static final int INITIAL_CAPACITY = 4;
-
-    /**
      * The {@link #state} value meaning that the metadata is modifiable.
      * This is the default state when new {@link ModifiableMetadata} instances are created.
      */
     private static final byte EDITABLE = 0;
 
     /**
-     * The {@link #state} value meaning that missing properties can be set,
-     * but no existing properties can be modified (including collections).
+     * See https://issues.apache.org/jira/browse/SIS-81 - not yet committed.
      */
-    private static final byte COMPLETABLE = 1;
+    private static final byte STAGED = 1;
 
     /**
-     * See https://issues.apache.org/jira/browse/SIS-81 - not yet committed.
+     * A value for {@link #state} meaning that execution of {@code transition(…)} is in progress.
+     * Must be greater than all other values except {@link #COMPLETABLE} and {@link #FINAL}.
      */
-    private static final byte STAGED = 2;
+    private static final byte FREEZING = 2;
 
     /**
-     * A value for {@link #state} meaning that execution of {@code apply(State.FINAL)} is in progress.
-     * Must be greater than all other values except {@link #FINAL}.
+     * The {@link #state} value meaning that missing properties can be set,
+     * but no existing properties can be modified (including collections).
+     * This is a kind of semi-final state.
      */
-    private static final byte FREEZING = 3;
+    private static final byte COMPLETABLE = 3;
 
     /**
-     * A value for {@link #state} meaning that {@code apply(State.FINAL)} has been invoked.
+     * A value for {@link #state} meaning that {@code transition(State.FINAL)} has been invoked.
      * Must be greater than all other values.
      */
     private static final byte FINAL = 4;
 
     /**
      * Whether this metadata has been made unmodifiable, as one of {@link #EDITABLE}, {@link #FREEZING}
-     * or {@link #FINAL} values.
+     * {@link #COMPLETABLE} or {@link #FINAL} values.
      *
      * <p>This field is not yet serialized because we are not sure to keep this information as a byte in
      * the future. We could for example use an {@code int} and use remaining bits for caching hash-code
@@ -168,13 +167,13 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
          * Note that a modifiable metadata instance does <strong>not</strong> imply that all
          * properties contained in that instance are also editable.
          */
-        EDITABLE,
+        EDITABLE(ModifiableMetadata.EDITABLE),
 
         /**
          * The metadata allows missing values to be set, but does not allow existing values to be modified.
          * This state is not appendable, i.e. it does not allow adding elements in a collection.
          */
-        COMPLETABLE,
+        COMPLETABLE(ModifiableMetadata.COMPLETABLE),
 
         /**
          * The metadata is unmodifiable.
@@ -183,7 +182,7 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
          * Invoking any setter method on an unmodifiable metadata cause an
          * {@link UnmodifiableMetadataException} to be thrown.
          */
-        FINAL;
+        FINAL(ModifiableMetadata.FINAL);
 
         /**
          * Mapping from {@link ModifiableMetadata} private flags to {@code State} enumeration.
@@ -193,11 +192,24 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
         private static final State[] VALUES = new State[ModifiableMetadata.FINAL + 1];
         static {
             VALUES[ModifiableMetadata.EDITABLE]    = EDITABLE;
-            VALUES[ModifiableMetadata.COMPLETABLE] = COMPLETABLE;
             VALUES[ModifiableMetadata.STAGED]      = EDITABLE;
             VALUES[ModifiableMetadata.FREEZING]    = FINAL;
+            VALUES[ModifiableMetadata.COMPLETABLE] = COMPLETABLE;
             VALUES[ModifiableMetadata.FINAL]       = FINAL;
         }
+
+        /**
+         * The numerical code associated to this enumeration value. It serves similar purpose to the
+         * {@link #ordinal()} value, but is nevertheless provided for the reasons given in {@link #VALUES}.
+         */
+        final byte code;
+
+        /**
+         * Creates a new state associated to the given code numerical code.
+         */
+        private State(final byte code) {
+            this.code = code;
+        }
     }
 
     /**
@@ -215,7 +227,7 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      * but still have the same metadata content. For this reason, this method does not have {@code get}
      * prefix for avoiding confusion with getter and setter methods of metadata properties.</div>
      *
-     * @return the state (editable or final) of this {@code ModifiableMetadata} instance.
+     * @return the state (editable, completable or final) of this {@code ModifiableMetadata} instance.
      *
      * @since 1.0
      */
@@ -240,11 +252,15 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      *     <td>Does nothing and returns {@code false}.</td>
      *   </tr><tr>
      *     <td>{@link State#EDITABLE}</td>
+     *     <td>{@link State#COMPLETABLE}</td>
+     *     <td>Marks this metadata and all children as completable.</td>
+     *   </tr><tr>
+     *     <td>Any</td>
      *     <td>{@link State#FINAL}</td>
      *     <td>Marks this metadata and all children as unmodifiable.</td>
      *   </tr><tr>
      *     <td>{@link State#FINAL}</td>
-     *     <td>{@link State#EDITABLE}</td>
+     *     <td>Any other</td>
      *     <td>Throws {@link UnmodifiableMetadataException}.</td>
      *   </tr>
      * </table>
@@ -254,30 +270,27 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      *
      * @param  target  the desired new state.
      * @return {@code true} if the state of this {@code ModifiableMetadata} changed as a result of this method call.
-     * @throws UnmodifiableMetadataException if a transition from {@link State#FINAL} to {@link State#EDITABLE} was attempted.
+     * @throws UnmodifiableMetadataException if a transition to a less restrictive state
+     *         (e.g. from {@link State#FINAL} to {@link State#EDITABLE}) was attempted.
      *
      * @since 1.0
      */
     public boolean transition(final State target) {
-        switch (target) {
-            case EDITABLE: {
-                if (state == EDITABLE) break;
-                throw new UnmodifiableMetadataException(Errors.format(Errors.Keys.UnmodifiableMetadata));
-            }
-            case FINAL: {
-                if (state >= FREEZING) break;
-                byte result = state;
-                try {
-                    state = FREEZING;
-                    StateChanger.applyTo(target, this);
-                    result = FINAL;
-                } finally {
-                    state = result;
-                }
-                return true;
-            }
+        if (target.code < state) {
+            throw new UnmodifiableMetadataException(Resources.format(Resources.Keys.UnmodifiableMetadata));
+        }
+        if (target.code == state || state == FREEZING) {
+            return false;
+        }
+        byte result = state;
+        try {
+            state = FREEZING;
+            StateChanger.applyTo(target, this);
+            result = target.code;
+        } finally {
+            state = result;
         }
-        return false;
+        return true;
     }
 
     /**
@@ -294,7 +307,7 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      */
     @Deprecated
     public final boolean isModifiable() {
-        return state < FREEZING;
+        return state <= STAGED;
     }
 
     /**
@@ -328,7 +341,7 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      *
      * @see MetadataCopier
      *
-     * @deprecated Replaced by {@code MetadataCopier.forModifiable(getStandard()).copy(this).apply(State.FINAL)}.
+     * @deprecated Replaced by {@code MetadataCopier.forModifiable(getStandard()).copy(this).transition(State.FINAL)}.
      */
     @Deprecated
     public AbstractMetadata unmodifiable() {
@@ -365,7 +378,7 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      * @see #state()
      * @see #checkWritePermission()
      *
-     * @deprecated Replaced by {@code apply(State.FINAL)}.
+     * @deprecated Replaced by {@code transition(State.FINAL)}.
      */
     @Deprecated
     public void freeze() {
@@ -380,7 +393,7 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
     @Deprecated
     protected void checkWritePermission() throws UnmodifiableMetadataException {
         if (state == FINAL) {
-            throw new UnmodifiableMetadataException(Errors.format(Errors.Keys.UnmodifiableMetadata));
+            throw new UnmodifiableMetadataException(Resources.format(Resources.Keys.UnmodifiableMetadata));
         } else {
             unmodifiable = null;                    // Discard since this metadata is going to change.
         }
@@ -395,12 +408,22 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      * @throws UnmodifiableMetadataException if this metadata is unmodifiable.
      *
      * @see #state()
+     *
+     * @since 1.0
      */
-    protected void checkWritePermission(final Object current) throws UnmodifiableMetadataException {
+    protected void checkWritePermission(Object current) throws UnmodifiableMetadataException {
         if (state != COMPLETABLE) {
             checkWritePermission();
         } else if (current != null) {
-            throw new UnmodifiableMetadataException(Errors.format(Errors.Keys.UnmodifiableMetadata));
+            final MetadataStandard standard;
+            if (current instanceof AbstractMetadata) {
+                standard = ((AbstractMetadata) current).getStandard();
+            } else {
+                standard = getStandard();
+            }
+            final Object c = standard.getTitle(current);
+            if (c != null) current = c;
+            throw new UnmodifiableMetadataException(Resources.format(Resources.Keys.ElementAlreadyInitialized_1, current));
         }
     }
 
@@ -427,28 +450,10 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      * @see #nonNullList(List, Class)
      */
     @SuppressWarnings("unchecked")
-    protected final <E> List<E> writeList(final Collection<? extends E> source,
-            List<E> target, final Class<E> elementType)
-            throws UnmodifiableMetadataException
+    protected final <E> List<E> writeList(Collection<? extends E> source, List<E> target,
+            Class<E> elementType) throws UnmodifiableMetadataException
     {
-        // See the comments in writeCollection(…) for implementation notes.
-        if (source != target) {
-            if (state == FREEZING) {
-                return (List<E>) source;
-            }
-            checkWritePermission(target);
-            if (isNullOrEmpty(source)) {
-                target = null;
-            } else {
-                if (target != null) {
-                    target.clear();
-                } else {
-                    target = new CheckedArrayList<>(elementType, source.size());
-                }
-                target.addAll(source);
-            }
-        }
-        return target;
+        return (List<E>) write(source, target, elementType, Boolean.FALSE);
     }
 
     /**
@@ -473,29 +478,10 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      *
      * @see #nonNullSet(Set, Class)
      */
-    @SuppressWarnings("unchecked")
-    protected final <E> Set<E> writeSet(final Collection<? extends E> source,
-            Set<E> target, final Class<E> elementType)
-            throws UnmodifiableMetadataException
+    protected final <E> Set<E> writeSet(Collection<? extends E> source, Set<E> target,
+            Class<E> elementType) throws UnmodifiableMetadataException
     {
-        // See the comments in writeCollection(…) for implementation notes.
-        if (source != target) {
-            if (state == FREEZING) {
-                return (Set<E>) source;
-            }
-            checkWritePermission(target);
-            if (isNullOrEmpty(source)) {
-                target = null;
-            } else {
-                if (target != null) {
-                    target.clear();
-                } else {
-                    target = new CheckedHashSet<>(elementType, source.size());
-                }
-                target.addAll(source);
-            }
-        }
-        return target;
+        return (Set<E>) write(source, target, elementType, Boolean.TRUE);
     }
 
     /**
@@ -526,10 +512,22 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      *         or {@code null} if the source was null.
      * @throws UnmodifiableMetadataException if this metadata is unmodifiable.
      */
+    protected final <E> Collection<E> writeCollection(Collection<? extends E> source, Collection<E> target,
+            Class<E> elementType) throws UnmodifiableMetadataException
+    {
+        return write(source, target, elementType, null);
+    }
+
+    /**
+     * Writes the content of the {@code source} collection into the {@code target} list or set,
+     * creating it if needed.
+     *
+     * @param  useSet  {@link Boolean#TRUE} for creating a set, {@link Boolean#FALSE} for creating a list,
+     *                 or null for automatic choice.
+     */
     @SuppressWarnings("unchecked")
-    protected final <E> Collection<E> writeCollection(final Collection<? extends E> source,
-            Collection<E> target, final Class<E> elementType)
-            throws UnmodifiableMetadataException
+    private <E> Collection<E> write(final Collection<? extends E> source, Collection<E> target,
+            final Class<E> elementType, Boolean useSet) throws UnmodifiableMetadataException
     {
         /*
          * It is not worth to copy the content if the current and the new instance are the
@@ -540,27 +538,40 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
         if (source != target) {
             if (state == FREEZING) {
                 /*
-                 * apply(State.FINAL) is under progress. The source collection is already
+                 * transition(State.FINAL) is under progress. The source collection is already
                  * an unmodifiable instance created by StageChanger.
                  */
-                assert collectionType(elementType).isInstance(source);
+                assert (useSet != null) || collectionType(elementType).isInstance(source) : elementType;
                 return (Collection<E>) source;
             }
-            checkWritePermission(target);
+            checkWritePermission(valueIfDefined(target));
             if (isNullOrEmpty(source)) {
                 target = null;
             } else {
-                if (target != null) {
+                /*
+                 * Reuse the existing collection if available, except in State.COMPLETABLE case
+                 * since that collection may the Collection.EMPTY_SET or EMPTY_LIST.
+                 */
+                if (target != null && state != COMPLETABLE) {
                     target.clear();
                 } else {
-                    final int capacity = source.size();
-                    if (useSet(elementType)) {
-                        target = createSet(elementType, capacity);
+                    if (useSet == null) {
+                        useSet = useSet(elementType);
+                    }
+                    if (useSet) {
+                        target = createSet(elementType, source);
                     } else {
-                        target = new CheckedArrayList<>(elementType, capacity);
+                        target = createList(elementType, source);
                     }
                 }
                 target.addAll(source);
+                if (state == COMPLETABLE) {
+                    if (useSet) {
+                        target = CollectionsExt.unmodifiableOrCopy((Set<E>) target);
+                    } else {
+                        target = CollectionsExt.unmodifiableOrCopy((List<E>) target);
+                    }
+                }
             }
         }
         return target;
@@ -581,7 +592,7 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
         if (isNullOrEmpty(source)) {
             return null;
         }
-        final List<E> target = new CheckedArrayList<>(elementType, source.size());
+        final List<E> target = createList(elementType, source);
         target.addAll(source);
         return target;
     }
@@ -601,7 +612,7 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
         if (isNullOrEmpty(source)) {
             return null;
         }
-        final Set<E> target = new CheckedHashSet<>(elementType, source.size());
+        final Set<E> target = createSet(elementType, source);
         target.addAll(source);
         return target;
     }
@@ -625,11 +636,10 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
             return null;
         }
         final Collection<E> target;
-        final int capacity = source.size();
         if (useSet(elementType)) {
-            target = createSet(elementType, capacity);
+            target = createSet(elementType, source);
         } else {
-            target = new CheckedArrayList<>(elementType, capacity);
+            target = createList(elementType, source);
         }
         target.addAll(source);
         return target;
@@ -654,7 +664,7 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
         }
         final Collection<E> collection;
         if (useSet(elementType)) {
-            collection = createSet(elementType, INITIAL_CAPACITY);
+            collection = createSet(elementType, null);
         } else {
             collection = new CheckedArrayList<>(elementType, 1);
         }
@@ -678,13 +688,13 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      * This is a convenience method for implementation of {@code getFoo()} methods.
      *
      * @param  <E>          the type represented by the {@code Class} argument.
-     * @param  c            the existing list, or {@code null} if the list has not yet been created.
+     * @param  current      the existing list, or {@code null} if the list has not yet been created.
      * @param  elementType  the element type (used only if {@code c} is null).
      * @return {@code c}, or a new list if {@code c} is null.
      */
-    protected final <E> List<E> nonNullList(final List<E> c, final Class<E> elementType) {
-        if (c != null) {
-            return c.isEmpty() && emptyCollectionAsNull() ? null : c;
+    protected final <E> List<E> nonNullList(final List<E> current, final Class<E> elementType) {
+        if (current != null) {
+            return current.isEmpty() && emptyCollectionAsNull() ? null : current;
         }
         if (emptyCollectionAsNull()) {
             return null;
@@ -707,19 +717,19 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      * This is a convenience method for implementation of {@code getFoo()} methods.
      *
      * @param  <E>          the type represented by the {@code Class} argument.
-     * @param  c            the existing set, or {@code null} if the set has not yet been created.
+     * @param  current      the existing set, or {@code null} if the set has not yet been created.
      * @param  elementType  the element type (used only if {@code c} is null).
      * @return {@code c}, or a new set if {@code c} is null.
      */
-    protected final <E> Set<E> nonNullSet(final Set<E> c, final Class<E> elementType) {
-        if (c != null) {
-            return c.isEmpty() && emptyCollectionAsNull() ? null : c;
+    protected final <E> Set<E> nonNullSet(final Set<E> current, final Class<E> elementType) {
+        if (current != null) {
+            return current.isEmpty() && emptyCollectionAsNull() ? null : current;
         }
         if (emptyCollectionAsNull()) {
             return null;
         }
         if (state < FREEZING) {
-            return createSet(elementType, INITIAL_CAPACITY);
+            return createSet(elementType, null);
         }
         return Collections.emptySet();
     }
@@ -736,14 +746,14 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
      * a {@link List} or a {@link Set} should be used.
      *
      * @param  <E>          the type represented by the {@code Class} argument.
-     * @param  c            the existing collection, or {@code null} if the collection has not yet been created.
+     * @param  current      the existing collection, or {@code null} if the collection has not yet been created.
      * @param  elementType  the element type (used only if {@code c} is null).
      * @return {@code c}, or a new collection if {@code c} is null.
      */
-    protected final <E> Collection<E> nonNullCollection(final Collection<E> c, final Class<E> elementType) {
-        if (c != null) {
-            assert collectionType(elementType).isInstance(c);
-            return c.isEmpty() && emptyCollectionAsNull() ? null : c;
+    protected final <E> Collection<E> nonNullCollection(final Collection<E> current, final Class<E> elementType) {
+        if (current != null) {
+            assert collectionType(elementType).isInstance(current);
+            return current.isEmpty() && emptyCollectionAsNull() ? null : current;
         }
         if (emptyCollectionAsNull()) {
             return null;
@@ -751,7 +761,7 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
         final boolean isModifiable = (state < FREEZING);
         if (useSet(elementType)) {
             if (isModifiable) {
-                return createSet(elementType, INITIAL_CAPACITY);
+                return createSet(elementType, null);
             } else {
                 return Collections.emptySet();
             }
@@ -766,18 +776,38 @@ public abstract class ModifiableMetadata extends AbstractMetadata {
     }
 
     /**
+     * Creates a modifiable list for elements of the given type. This method is defined mostly
+     * for consistency with {@link #createSet(Class, Collection)}.
+     *
+     * @param  source  the collection to be copied in the new list. This method uses this information
+     *                 only for computing initial capacity; it does not perform the actual copy.
+     */
+    private static <E> List<E> createList(final Class<E> elementType, final Collection<?> source) {
+        return new CheckedArrayList<>(elementType, source.size());
+    }
+
+    /**
      * Creates a modifiable set for elements of the given type. This method will create an {@link EnumSet},
      * {@link CodeListSet} or {@link java.util.LinkedHashSet} depending on the {@code elementType} argument.
+     *
+     * @param  source  the collection to be copied in the new set, or {@code null} if unknown.
+     *                 This method uses this information only for computing initial capacity;
+     *                 it does not perform the actual copy.
      */
     @SuppressWarnings({"unchecked","rawtypes"})
-    private <E> Set<E> createSet(final Class<E> elementType, final int capacity) {
+    private static <E> Set<E> createSet(final Class<E> elementType, final Collection<?> source) {
         if (Enum.class.isAssignableFrom(elementType)) {
             return EnumSet.noneOf((Class) elementType);
         }
         if (CodeList.class.isAssignableFrom(elementType) && Modifier.isFinal(elementType.getModifiers())) {
             return new CodeListSet(elementType);
         }
-        return new CheckedHashSet<>(elementType, capacity);
+        /*
+         * If we can not compute an initial capacity from the size of an existing source, use an arbitrary
+         * small value (currently 4). We use a small value because collections will typically contain few
+         * elements (often just a singleton).
+         */
+        return new CheckedHashSet<>(elementType, (source != null) ? Containers.hashMapCapacity(source.size()) : 4);
     }
 
     /**
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/StateChanger.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/StateChanger.java
index 64d8c48..c68d89d 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/StateChanger.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/StateChanger.java
@@ -143,9 +143,6 @@ final class StateChanger extends MetadataVisitor<Boolean> {
             ((ModifiableMetadata) object).transition(target);
             return object;
         }
-        if (target != ModifiableMetadata.State.FINAL) {
-            return object;
-        }
         if (object instanceof DefaultRepresentativeFraction) {
             ((DefaultRepresentativeFraction) object).freeze();
             return object;
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/DefaultMetadata.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/DefaultMetadata.java
index bc0e1f8..5cee8a2 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/DefaultMetadata.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/DefaultMetadata.java
@@ -73,6 +73,8 @@ import org.apache.sis.internal.jaxb.Context;
 import org.apache.sis.internal.jaxb.metadata.CI_Citation;
 import org.apache.sis.internal.jaxb.metadata.MD_Identifier;
 
+import static org.apache.sis.internal.metadata.MetadataUtilities.valueIfDefined;
+
 
 /**
  * Root entity which defines metadata about a resource or resources.
@@ -545,7 +547,7 @@ public class DefaultMetadata extends ISOMetadata implements Metadata {
      */
     @Deprecated
     public void setLanguage(final Locale newValue) {
-        checkWritePermission(languages);
+        checkWritePermission(valueIfDefined(languages));
         setDefaultLocale(newValue);
     }
 
@@ -573,7 +575,7 @@ public class DefaultMetadata extends ISOMetadata implements Metadata {
      */
     @Deprecated
     public void setLocales(final Collection<? extends Locale> newValues) {
-        checkWritePermission(languages);
+        checkWritePermission(valueIfDefined(languages));
         setOtherLocales(newValues);
     }
 
@@ -801,7 +803,7 @@ public class DefaultMetadata extends ISOMetadata implements Metadata {
      */
     @Deprecated
     public void setHierarchyLevels(final Collection<? extends ScopeCode> newValues) {
-        checkWritePermission(metadataScopes);
+        checkWritePermission(valueIfDefined(metadataScopes));
         ((LegacyPropertyAdapter<ScopeCode,?>) getHierarchyLevels()).setValues(newValues);
     }
 
@@ -852,7 +854,7 @@ public class DefaultMetadata extends ISOMetadata implements Metadata {
      */
     @Deprecated
     public void setHierarchyLevelNames(final Collection<? extends String> newValues) {
-        checkWritePermission(metadataScopes);
+        checkWritePermission(valueIfDefined(metadataScopes));
         ((LegacyPropertyAdapter<String,?>) getHierarchyLevelNames()).setValues(newValues);
     }
 
@@ -937,7 +939,7 @@ public class DefaultMetadata extends ISOMetadata implements Metadata {
      */
     @Deprecated
     public void setDateStamp(final Date newValue) {
-        checkWritePermission(dateInfo);
+        checkWritePermission(valueIfDefined(dateInfo));
         Collection<CitationDate> newValues = dateInfo;      // See "Note about deprecated methods implementation"
         if (newValues == null) {
             if (newValue == null) {
@@ -1070,7 +1072,7 @@ public class DefaultMetadata extends ISOMetadata implements Metadata {
      * {@link #setMetadataStandardVersion(String)} methods.
      */
     private void setMetadataStandard(final boolean version, final String newValue) {
-        checkWritePermission(metadataStandards);
+        checkWritePermission(valueIfDefined(metadataStandards));
         final InternationalString i18n = (newValue != null) ? new SimpleInternationalString(newValue) : null;
         final List<Citation> newValues = (metadataStandards != null)
                 ? new ArrayList<>(metadataStandards)
@@ -1228,8 +1230,8 @@ public class DefaultMetadata extends ISOMetadata implements Metadata {
     @Deprecated
     public void setDataSetUri(final String newValue) throws URISyntaxException {
         final URI uri = new URI(newValue);
-        checkWritePermission(identificationInfo);
-        Collection<Identification> info = identificationInfo; // See "Note about deprecated methods implementation"
+        Collection<Identification> info = identificationInfo;   // See "Note about deprecated methods implementation"
+        checkWritePermission(valueIfDefined(info));
         AbstractIdentification firstId = AbstractIdentification.castOrCopy(CollectionsExt.first(info));
         if (firstId == null) {
             firstId = new DefaultDataIdentification();
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/ISOMetadata.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/ISOMetadata.java
index 77408e3..2ae4361 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/ISOMetadata.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/ISOMetadata.java
@@ -16,7 +16,10 @@
  */
 package org.apache.sis.metadata.iso;
 
+import java.util.Set;
+import java.util.List;
 import java.util.Collection;
+import java.util.Collections;
 import java.io.Serializable;
 import javax.xml.bind.annotation.XmlID;
 import javax.xml.bind.annotation.XmlAttribute;
@@ -39,6 +42,7 @@ import org.apache.sis.internal.system.Modules;
 import org.apache.sis.util.collection.Containers;
 
 import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
+import static org.apache.sis.internal.metadata.MetadataUtilities.valueIfDefined;
 
 
 /**
@@ -206,7 +210,7 @@ public class ISOMetadata extends ModifiableMetadata implements IdentifiedObject,
      * @since 1.0
      */
     protected void setIdentifier(final Identifier newValue) {
-        checkWritePermission(identifiers);
+        checkWritePermission(valueIfDefined(identifiers));
         identifiers = nonNullCollection(identifiers, Identifier.class);
         identifiers = writeCollection(NonMarshalledAuthority.setMarshallable(identifiers, newValue), identifiers, Identifier.class);
     }
@@ -228,8 +232,14 @@ public class ISOMetadata extends ModifiableMetadata implements IdentifiedObject,
              * subclass has an "identifiers" property. If this is not the case, then the collection
              * is unchanged (or null) so we have to make it unmodifiable here.
              */
-            if (p == identifiers) {
-                identifiers = CollectionsExt.unmodifiableOrCopy(p);                     // Null safe.
+            if (p != null && p == identifiers) {
+                if (p instanceof Set<?>) {
+                    identifiers = CollectionsExt.unmodifiableOrCopy((Set<Identifier>) p);
+                } else if (p instanceof List<?>) {
+                    identifiers = CollectionsExt.unmodifiableOrCopy((List<Identifier>) p);
+                } else {
+                    identifiers = Collections.unmodifiableCollection(p);
+                }
             }
         }
         return changed;
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/acquisition/DefaultRequestedDate.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/acquisition/DefaultRequestedDate.java
index 661d337..c6a6ede 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/acquisition/DefaultRequestedDate.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/acquisition/DefaultRequestedDate.java
@@ -25,7 +25,6 @@ import org.apache.sis.metadata.iso.ISOMetadata;
 
 import static org.apache.sis.internal.metadata.MetadataUtilities.toDate;
 import static org.apache.sis.internal.metadata.MetadataUtilities.toMilliseconds;
-import static org.apache.sis.internal.metadata.MetadataUtilities.isDateDefined;
 
 
 /**
@@ -139,7 +138,7 @@ public class DefaultRequestedDate extends ISOMetadata implements RequestedDate {
      * @param  newValue  the new requested date of collection value.
      */
     public void setRequestedDateOfCollection(final Date newValue) {
-        checkWritePermission(isDateDefined(requestedDateOfCollection));
+        checkWritePermission(toDate(requestedDateOfCollection));
         requestedDateOfCollection = toMilliseconds(newValue);
     }
 
@@ -160,7 +159,7 @@ public class DefaultRequestedDate extends ISOMetadata implements RequestedDate {
      * @param  newValue  the new latest acceptable data value.
      */
     public void setLatestAcceptableDate(final Date newValue) {
-        checkWritePermission(isDateDefined(latestAcceptableDate));
+        checkWritePermission(toDate(latestAcceptableDate));
         latestAcceptableDate = toMilliseconds(newValue);
     }
 }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/acquisition/DefaultRequirement.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/acquisition/DefaultRequirement.java
index e83040b..ed4edf6 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/acquisition/DefaultRequirement.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/acquisition/DefaultRequirement.java
@@ -32,7 +32,6 @@ import org.apache.sis.metadata.iso.ISOMetadata;
 
 import static org.apache.sis.internal.metadata.MetadataUtilities.toDate;
 import static org.apache.sis.internal.metadata.MetadataUtilities.toMilliseconds;
-import static org.apache.sis.internal.metadata.MetadataUtilities.isDateDefined;
 
 
 /**
@@ -318,7 +317,7 @@ public class DefaultRequirement extends ISOMetadata implements Requirement {
      * @param  newValue  the new expiry date.
      */
     public void setExpiryDate(final Date newValue) {
-        checkWritePermission(isDateDefined(expiryDate));
+        checkWritePermission(toDate(expiryDate));
         expiryDate = toMilliseconds(newValue);
     }
 
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultCitation.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultCitation.java
index 4608e1a..e55901d 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultCitation.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultCitation.java
@@ -42,7 +42,6 @@ import org.apache.sis.xml.IdentifierMap;
 import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
 import static org.apache.sis.internal.metadata.MetadataUtilities.toDate;
 import static org.apache.sis.internal.metadata.MetadataUtilities.toMilliseconds;
-import static org.apache.sis.internal.metadata.MetadataUtilities.isDateDefined;
 
 
 /**
@@ -349,7 +348,7 @@ public class DefaultCitation extends ISOMetadata implements Citation {
      * @param  newValue  the new edition date, or {@code null} if none.
      */
     public void setEditionDate(final Date newValue) {
-        checkWritePermission(isDateDefined(editionDate));
+        checkWritePermission(toDate(editionDate));
         editionDate = toMilliseconds(newValue);
     }
 
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultCitationDate.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultCitationDate.java
index a88c98a..b832a11 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultCitationDate.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultCitationDate.java
@@ -27,7 +27,6 @@ import org.apache.sis.metadata.TitleProperty;
 
 import static org.apache.sis.internal.metadata.MetadataUtilities.toDate;
 import static org.apache.sis.internal.metadata.MetadataUtilities.toMilliseconds;
-import static org.apache.sis.internal.metadata.MetadataUtilities.isDateDefined;
 
 
 /**
@@ -152,7 +151,7 @@ public class DefaultCitationDate extends ISOMetadata implements CitationDate {
      * @param  newValue  the new date.
      */
     public void setDate(final Date newValue) {
-        checkWritePermission(isDateDefined(date));
+        checkWritePermission(toDate(date));
         date = toMilliseconds(newValue);
     }
 
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultResponsibleParty.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultResponsibleParty.java
index cdcf6c6..df98f48 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultResponsibleParty.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/citation/DefaultResponsibleParty.java
@@ -35,6 +35,8 @@ import org.apache.sis.internal.xml.LegacyNamespaces;
 import org.apache.sis.internal.metadata.Dependencies;
 import org.apache.sis.internal.metadata.LegacyPropertyAdapter;
 
+import static org.apache.sis.internal.metadata.MetadataUtilities.valueIfDefined;
+
 
 /**
  * Identification of, and means of communication with, person(s) and
@@ -188,7 +190,7 @@ public class DefaultResponsibleParty extends DefaultResponsibility implements Re
      * @return {@code true} if the name has been set, or {@code false} otherwise.
      */
     private boolean setName(final Class<? extends Party> type, final boolean position, final InternationalString name) {
-        checkWritePermission(null);
+        checkWritePermission(valueIfDefined(super.getParties()));
         final Iterator<Party> it = getParties().iterator();
         while (it.hasNext()) {
             final Party party = it.next();
@@ -370,7 +372,7 @@ public class DefaultResponsibleParty extends DefaultResponsibility implements Re
      */
     @Deprecated
     public void setContactInfo(final Contact newValue) {
-        checkWritePermission(null);
+        checkWritePermission(valueIfDefined(super.getParties()));
         final Iterator<Party> it = getParties().iterator();
         while (it.hasNext()) {
             final Party party = it.next();
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultCoverageDescription.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultCoverageDescription.java
index 13a2fcc..5af5218 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultCoverageDescription.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultCoverageDescription.java
@@ -37,6 +37,8 @@ import org.apache.sis.internal.jaxb.FilterByVersion;
 import org.apache.sis.internal.xml.LegacyNamespaces;
 import org.apache.sis.internal.jaxb.metadata.MD_Identifier;
 
+import static org.apache.sis.internal.metadata.MetadataUtilities.valueIfDefined;
+
 
 /**
  * Information about the content of a grid data cell.
@@ -276,7 +278,7 @@ public class DefaultCoverageDescription extends AbstractContentInformation imple
      */
     @Deprecated
     public void setContentType(final CoverageContentType newValue) {
-        checkWritePermission(attributeGroups);
+        checkWritePermission(valueIfDefined(attributeGroups));
         final Collection<CoverageContentType> newValues = LegacyPropertyAdapter.asCollection(newValue);
         Collection<AttributeGroup> groups = attributeGroups;
         if (groups != null) {
@@ -346,7 +348,7 @@ public class DefaultCoverageDescription extends AbstractContentInformation imple
      */
     @Deprecated
     public void setDimensions(final Collection<? extends RangeDimension> newValues) {
-        checkWritePermission(attributeGroups);
+        checkWritePermission(valueIfDefined(attributeGroups));
         ((LegacyPropertyAdapter<RangeDimension,?>) getDimensions()).setValues(newValues);
     }
 
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultFeatureCatalogueDescription.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultFeatureCatalogueDescription.java
index b4c2023..ea109f9 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultFeatureCatalogueDescription.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/content/DefaultFeatureCatalogueDescription.java
@@ -32,6 +32,8 @@ import org.apache.sis.internal.jaxb.lan.LocaleAdapter;
 import org.apache.sis.internal.metadata.Dependencies;
 import org.apache.sis.internal.metadata.LegacyPropertyAdapter;
 
+import static org.apache.sis.internal.metadata.MetadataUtilities.valueIfDefined;
+
 
 /**
  * Information identifying the feature catalogue or the conceptual schema.
@@ -288,7 +290,7 @@ public class DefaultFeatureCatalogueDescription extends AbstractContentInformati
      */
     @Deprecated
     public void setFeatureTypes(final Collection<? extends GenericName> newValues) {
-        checkWritePermission(featureTypes);
+        checkWritePermission(valueIfDefined(featureTypes));
         ((LegacyPropertyAdapter<GenericName,?>) getFeatureTypes()).setValues(newValues);
     }
 
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBox.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBox.java
index e27b0fb..6a934b0 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBox.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBox.java
@@ -23,6 +23,7 @@ import javax.xml.bind.annotation.XmlType;
 import org.opengis.geometry.Envelope;
 import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.opengis.referencing.operation.TransformException;
+import org.apache.sis.measure.Angle;
 import org.apache.sis.measure.Latitude;
 import org.apache.sis.measure.Longitude;
 import org.apache.sis.measure.ValueRange;
@@ -37,7 +38,6 @@ import org.apache.sis.metadata.InvalidMetadataException;
 import org.apache.sis.xml.NilReason;
 
 import static java.lang.Double.doubleToLongBits;
-import static org.apache.sis.internal.metadata.MetadataUtilities.isDefined;
 
 
 /**
@@ -274,6 +274,17 @@ public class DefaultGeographicBoundingBox extends AbstractGeographicExtent imple
     }
 
     /**
+     * Returns the given value if it defined, or {@code null} if NaN.
+     *
+     * @param  value    the numeric value.
+     * @param  latitude {@code true} if latitude, {@code false} if longitude.
+     * @return the given value if it is non-NaN.
+     */
+    private static Angle valueIfDefined(final double value, final boolean latitude) {
+        return Double.isNaN(value) ? null : (latitude ? new Latitude(value) : new Longitude(value));
+    }
+
+    /**
      * Returns the western-most coordinate of the limit of the dataset extent.
      * The value is expressed in longitude in decimal degrees (positive east).
      *
@@ -299,7 +310,7 @@ public class DefaultGeographicBoundingBox extends AbstractGeographicExtent imple
      *         or {@linkplain Double#NaN NaN} to undefine.
      */
     public void setWestBoundLongitude(double newValue) {
-        checkWritePermission(isDefined(westBoundLongitude));
+        checkWritePermission(valueIfDefined(westBoundLongitude, false));
         if (newValue != Longitude.MAX_VALUE) {                  // Do not normalize +180° to -180°.
             newValue = Longitude.normalize(newValue);
         }
@@ -332,7 +343,7 @@ public class DefaultGeographicBoundingBox extends AbstractGeographicExtent imple
      *         or {@linkplain Double#NaN NaN} to undefine.
      */
     public void setEastBoundLongitude(double newValue) {
-        checkWritePermission(isDefined(eastBoundLongitude));
+        checkWritePermission(valueIfDefined(eastBoundLongitude, false));
         if (newValue != Longitude.MAX_VALUE) {                      // Do not normalize +180° to -180°.
             newValue = Longitude.normalize(newValue);
         }
@@ -364,7 +375,7 @@ public class DefaultGeographicBoundingBox extends AbstractGeographicExtent imple
      *         or {@linkplain Double#NaN NaN} to undefine.
      */
     public void setSouthBoundLatitude(final double newValue) {
-        checkWritePermission(isDefined(southBoundLatitude));
+        checkWritePermission(valueIfDefined(southBoundLatitude, true));
         southBoundLatitude = Latitude.clamp(newValue);
         if (southBoundLatitude > northBoundLatitude) {
             northBoundLatitude = Double.NaN;
@@ -396,7 +407,7 @@ public class DefaultGeographicBoundingBox extends AbstractGeographicExtent imple
      *         or {@linkplain Double#NaN NaN} to undefine.
      */
     public void setNorthBoundLatitude(final double newValue) {
-        checkWritePermission(isDefined(northBoundLatitude));
+        checkWritePermission(valueIfDefined(northBoundLatitude, true));
         northBoundLatitude = Latitude.clamp(newValue);
         if (northBoundLatitude < southBoundLatitude) {
             southBoundLatitude = Double.NaN;
@@ -741,11 +752,11 @@ public class DefaultGeographicBoundingBox extends AbstractGeographicExtent imple
      * The return value can be given to {@link #checkWritePermission(Object)}
      * before to set the bounds of this geographic extent.
      */
-    private Boolean isNonEmpty() {
-        return Double.isNaN(eastBoundLongitude) &&
-               Double.isNaN(westBoundLongitude) &&
-               Double.isNaN(northBoundLatitude) &&
-               Double.isNaN(southBoundLatitude) ? null : Boolean.TRUE;
+    private Angle isNonEmpty() {
+        double value;
+        if (!Double.isNaN(value = eastBoundLongitude) || !Double.isNaN(value = westBoundLongitude)) return new Longitude(value);
+        if (!Double.isNaN(value = northBoundLatitude) || !Double.isNaN(value = southBoundLatitude)) return new  Latitude(value);
+        return null;
     }
 
     /**
@@ -761,7 +772,10 @@ public class DefaultGeographicBoundingBox extends AbstractGeographicExtent imple
      */
     @Override
     public boolean isEmpty() {
-        return isNonEmpty() == null;
+        return Double.isNaN(eastBoundLongitude) &&
+               Double.isNaN(westBoundLongitude) &&
+               Double.isNaN(northBoundLatitude) &&
+               Double.isNaN(southBoundLatitude);
     }
 
     /**
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/DefaultSpatialTemporalExtent.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/DefaultSpatialTemporalExtent.java
index b27b81b..4319e19 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/DefaultSpatialTemporalExtent.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/extent/DefaultSpatialTemporalExtent.java
@@ -29,6 +29,8 @@ import org.opengis.metadata.extent.SpatialTemporalExtent;
 import org.opengis.referencing.operation.TransformException;
 import org.apache.sis.internal.metadata.ReferencingServices;
 
+import static org.apache.sis.internal.metadata.MetadataUtilities.valueIfDefined;
+
 
 /**
  * Extent with respect to date/time and spatial boundaries.
@@ -221,7 +223,7 @@ public class DefaultSpatialTemporalExtent extends DefaultTemporalExtent implemen
      */
     @Override
     public void setBounds(final Envelope envelope) throws TransformException {
-        checkWritePermission(spatialExtent);
+        checkWritePermission(valueIfDefined(spatialExtent));
         ReferencingServices.getInstance().setBounds(envelope, this);
     }
 }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultAggregateInformation.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultAggregateInformation.java
index 0e62d89..376f9aa 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultAggregateInformation.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultAggregateInformation.java
@@ -194,7 +194,7 @@ public class DefaultAggregateInformation extends DefaultAssociatedResource imple
      */
     @Deprecated
     public void setAggregateDataSetIdentifier(final Identifier newValue) {
-        checkWritePermission(null);
+        checkWritePermission(super.getName());
         Citation name = getAggregateDataSetName();
         if (newValue != null) {
             if (!(name instanceof DefaultCitation)) {
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java
index a304d5b..e6cbf94 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultRepresentativeFraction.java
@@ -33,6 +33,7 @@ import org.apache.sis.internal.jaxb.ModifiableIdentifierMap;
 import org.apache.sis.internal.jaxb.IdentifierMapAdapter;
 import org.apache.sis.internal.jaxb.gco.GO_Integer64;
 import org.apache.sis.internal.metadata.MetadataUtilities;
+import org.apache.sis.internal.metadata.Resources;
 import org.apache.sis.internal.util.CheckedArrayList;
 import org.apache.sis.measure.ValueRange;
 import org.apache.sis.xml.IdentifierMap;
@@ -176,7 +177,7 @@ public class DefaultRepresentativeFraction extends Number implements Representat
      */
     public void setDenominator(final long denominator) {
         if (isUnmodifiable) {
-            throw new UnmodifiableMetadataException(Errors.format(Errors.Keys.UnmodifiableMetadata));
+            throw new UnmodifiableMetadataException(Resources.format(Resources.Keys.UnmodifiableMetadata));
         }
         if (ensurePositive(DefaultRepresentativeFraction.class, "denominator", false, denominator)) {
             this.denominator = denominator;
@@ -194,7 +195,7 @@ public class DefaultRepresentativeFraction extends Number implements Representat
      */
     public void setScale(final double scale) {
         if (isUnmodifiable) {
-            throw new UnmodifiableMetadataException(Errors.format(Errors.Keys.UnmodifiableMetadata));
+            throw new UnmodifiableMetadataException(Resources.format(Resources.Keys.UnmodifiableMetadata));
         }
         /*
          * For the following argument check, we do not need to use a Metadatautility method because
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultUsage.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultUsage.java
index 8b53c12..5f67e73 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultUsage.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/identification/DefaultUsage.java
@@ -32,7 +32,6 @@ import org.apache.sis.util.iso.Types;
 
 import static org.apache.sis.internal.metadata.MetadataUtilities.toDate;
 import static org.apache.sis.internal.metadata.MetadataUtilities.toMilliseconds;
-import static org.apache.sis.internal.metadata.MetadataUtilities.isDateDefined;
 
 
 /**
@@ -225,7 +224,7 @@ public class DefaultUsage extends ISOMetadata implements Usage {
      * @param  newValue  the new usage date.
      */
     public void setUsageDate(final Date newValue)  {
-        checkWritePermission(isDateDefined(usageDate));
+        checkWritePermission(toDate(usageDate));
         usageDate = toMilliseconds(newValue);
     }
 
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/maintenance/DefaultMaintenanceInformation.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/maintenance/DefaultMaintenanceInformation.java
index 7f4504f..1760003 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/maintenance/DefaultMaintenanceInformation.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/maintenance/DefaultMaintenanceInformation.java
@@ -40,6 +40,8 @@ import org.apache.sis.internal.metadata.Dependencies;
 import org.apache.sis.internal.jaxb.FilterByVersion;
 import org.apache.sis.internal.xml.LegacyNamespaces;
 
+import static org.apache.sis.internal.metadata.MetadataUtilities.valueIfDefined;
+
 
 /**
  * Information about the scope and frequency of updating.
@@ -262,7 +264,7 @@ public class DefaultMaintenanceInformation extends ISOMetadata implements Mainte
      */
     @Deprecated
     public void setDateOfNextUpdate(final Date newValue) {
-        checkWritePermission(maintenanceDates);
+        checkWritePermission(valueIfDefined(maintenanceDates));
         Collection<CitationDate> dates = maintenanceDates;
         if (dates != null) {
             final Iterator<CitationDate> it = dates.iterator();
@@ -383,7 +385,7 @@ public class DefaultMaintenanceInformation extends ISOMetadata implements Mainte
      */
     @Deprecated
     public void setUpdateScopes(final Collection<? extends ScopeCode> newValues) {
-        checkWritePermission(maintenanceScopes);
+        checkWritePermission(valueIfDefined(maintenanceScopes));
         ((LegacyPropertyAdapter<ScopeCode,?>) getUpdateScopes()).setValues(newValues);
     }
 
@@ -438,7 +440,7 @@ public class DefaultMaintenanceInformation extends ISOMetadata implements Mainte
      */
     @Deprecated
     public void setUpdateScopeDescriptions(final Collection<? extends ScopeDescription> newValues) {
-        checkWritePermission(maintenanceScopes);
+        checkWritePermission(valueIfDefined(maintenanceScopes));
         ((LegacyPropertyAdapter<ScopeDescription,?>) getUpdateScopeDescriptions()).setValues(newValues);
     }
 
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/quality/AbstractElement.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/quality/AbstractElement.java
index 8d11995..ab018cd 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/quality/AbstractElement.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/iso/quality/AbstractElement.java
@@ -46,6 +46,7 @@ import org.apache.sis.util.resources.Errors;
 import org.apache.sis.xml.Namespaces;
 
 import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
+import static org.apache.sis.internal.metadata.MetadataUtilities.valueIfDefined;
 
 
 /**
@@ -128,6 +129,8 @@ public class AbstractElement extends ISOMetadata implements Element {
 
     /**
      * Start time ({@code date1}) and end time ({@code date2}) on which a data quality measure was applied.
+     *
+     * @todo Needs to be made unmodifiable after transition to {@link State#FINAL}.
      */
     private Dates dates;
 
@@ -553,8 +556,8 @@ public class AbstractElement extends ISOMetadata implements Element {
      * @param  newValues  the new dates, or {@code null}.
      */
     public void setDates(final Collection<? extends Date> newValues) {
-        checkWritePermission(dates);
         if (newValues != dates) {               // Mandatory check for avoiding the call to 'dates.clear()'.
+            checkWritePermission(valueIfDefined(dates));
             writeDates(newValues);
         }
     }
diff --git a/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/Dispatcher.java b/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/Dispatcher.java
index e1c7611..6cc95e5 100644
--- a/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/Dispatcher.java
+++ b/core/sis-metadata/src/main/java/org/apache/sis/metadata/sql/Dispatcher.java
@@ -25,6 +25,7 @@ import org.apache.sis.util.Classes;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.collection.BackingStoreException;
 import org.apache.sis.internal.util.CollectionsExt;
+import org.apache.sis.metadata.ModifiableMetadata;
 import org.apache.sis.metadata.MetadataStandard;
 import org.apache.sis.metadata.KeyNamePolicy;
 import org.apache.sis.metadata.ValueExistencePolicy;
@@ -54,7 +55,7 @@ import org.apache.sis.internal.metadata.Dependencies;
  *
  * @author  Touraïvane (IRD)
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 0.8
+ * @version 1.0
  * @since   0.8
  * @module
  */
@@ -230,11 +231,15 @@ final class Dispatcher implements InvocationHandler {
                             if (impl == null) {
                                 return value;
                             }
-                            this.cache = cache = impl.newInstance();
+                            cache = impl.newInstance();
+                            if (cache instanceof ModifiableMetadata) {
+                                ((ModifiableMetadata) cache).transition(ModifiableMetadata.State.COMPLETABLE);
+                            }
                             /*
                              * We do not use AtomicReference because it is okay if the cache is instantiated twice.
                              * It would cause us to query the database twice, but we should get the same information.
                              */
+                            this.cache = cache;
                         }
                         final Map<String, Object> map = source.standard.asValueMap(cache, type,
                                     KeyNamePolicy.METHOD_NAME, ValueExistencePolicy.ALL);
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/metadata/ModifiableMetadataTest.java b/core/sis-metadata/src/test/java/org/apache/sis/metadata/ModifiableMetadataTest.java
new file mode 100644
index 0000000..1bc1cd2
--- /dev/null
+++ b/core/sis-metadata/src/test/java/org/apache/sis/metadata/ModifiableMetadataTest.java
@@ -0,0 +1,188 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.metadata;
+
+import java.util.Arrays;
+import java.util.Collections;
+import org.opengis.metadata.distribution.MediumFormat;
+import org.apache.sis.metadata.iso.DefaultIdentifier;
+import org.apache.sis.metadata.iso.distribution.DefaultMedium;
+import org.apache.sis.util.iso.SimpleInternationalString;
+import org.apache.sis.util.collection.CodeListSet;
+import org.apache.sis.test.DependsOn;
+import org.apache.sis.test.TestCase;
+import org.junit.Test;
+
+import static org.apache.sis.test.Assert.*;
+
+
+/**
+ * Tests {@link ModifiableMetadata}, in particular the state transitions.
+ * This class uses {@link DefaultMedium} as an arbitrary metadata implementation for running the tests.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ * @version 1.0
+ * @since   1.0
+ * @module
+ */
+@DependsOn(AbstractMetadataTest.class)
+public final strictfp class ModifiableMetadataTest extends TestCase {
+    /**
+     * An arbitrary metadata on which to perform the tests.
+     */
+    private final DefaultMedium md;
+
+    /**
+     * Creates an arbitrary metadata for testing purpose.
+     */
+    public ModifiableMetadataTest() {
+        md = new DefaultMedium();
+        md.setMediumNote(new SimpleInternationalString("The original note."));
+        md.setIdentifier(new DefaultIdentifier("A medium identifier"));
+        assertInstanceOf("mediumFormat", CodeListSet.class, md.getMediumFormats());    // Force assignation of a Set in private field.
+    }
+
+    /**
+     * Returns the state of the identifier of the metadata.
+     */
+    private ModifiableMetadata.State identifierState() {
+        return ((DefaultIdentifier) md.getIdentifier()).state();
+    }
+
+    /**
+     * Verifies the metadata properties values.
+     */
+    private void assertPropertiesEqual(final Integer volumes, final String mediumNote, final MediumFormat... formats) {
+        assertEquals("mediumNote", mediumNote, String.valueOf(md.getMediumNote()));
+        assertEquals("volumes", volumes, md.getVolumes());
+        assertSetEquals(Arrays.asList(formats), md.getMediumFormats());
+    }
+
+    /**
+     * Verifies the exception for an unmodifiable metadata.
+     */
+    private static void verifyUnmodifiableException(final UnmodifiableMetadataException e) {
+        assertNotNull("Expected an error message.", e.getMessage());
+    }
+
+    /**
+     * Tests the behavior when state is {@link ModifiableMetadata.State#EDITABLE}.
+     * Setting new values and overwriting existing values are allowed.
+     */
+    @Test
+    public void testStateEditable() {
+        assertFalse("transition", md.transition(ModifiableMetadata.State.EDITABLE));        // Shall be a no-op.
+        assertEquals("state", ModifiableMetadata.State.EDITABLE, md.state());
+        assertEquals("identifier.state", ModifiableMetadata.State.EDITABLE, identifierState());
+        /*
+         * Verify conditions given in Javadoc: allow new values and overwriting.
+         */
+        md.setVolumes(4);                                                                   // New value.
+        md.setMediumNote(new SimpleInternationalString("A new note."));                     // Overwriting.
+        md.getMediumFormats().add(MediumFormat.TAR);
+        md.setMediumFormats(Collections.singleton(MediumFormat.CPIO));                      // Discard TAR.
+        md.getMediumFormats().add(MediumFormat.ISO_9660);
+        assertPropertiesEqual(4, "A new note.", MediumFormat.CPIO, MediumFormat.ISO_9660);
+    }
+
+    /**
+     * Tests the behavior when state is {@link ModifiableMetadata.State#COMPLETABLE}.
+     * Setting new values is allowed but overwriting existing values is not allowed.
+     */
+    @Test
+    public void testStateCompletable() {
+        assertTrue("transition", md.transition(ModifiableMetadata.State.COMPLETABLE));
+        assertEquals("state", ModifiableMetadata.State.COMPLETABLE, md.state());
+        assertEquals("identifier.state", ModifiableMetadata.State.COMPLETABLE, identifierState());
+        try {
+            md.transition(ModifiableMetadata.State.EDITABLE);
+            fail("Shall not be allowed to transition back to editable state.");
+        } catch (UnmodifiableMetadataException e) {
+            verifyUnmodifiableException(e);
+        }
+        /*
+         * Verify conditions given in Javadoc: allow new values but not overwriting.
+         */
+        md.setVolumes(4);
+        try {
+            md.setMediumNote(new SimpleInternationalString("A new note."));
+            fail("Overwriting an existing value shall not be allowed.");
+        } catch (UnmodifiableMetadataException e) {
+            verifyUnmodifiableException(e);
+            assertTrue(e.getMessage().contains("The original note."));
+        }
+        try {
+            md.getMediumFormats().add(MediumFormat.TAR);
+            fail("Adding new value shall not be allowed.");
+        } catch (UnsupportedOperationException e) {
+            // This is the expected exception.
+        }
+        md.setMediumFormats(Collections.singleton(MediumFormat.CPIO));
+        try {
+            md.getMediumFormats().add(MediumFormat.ISO_9660);
+            fail("Adding new value shall not be allowed.");
+        } catch (UnsupportedOperationException e) {
+            // This is the expected exception.
+        }
+        assertPropertiesEqual(4, "The original note.", MediumFormat.CPIO);
+    }
+
+    /**
+     * Tests the behavior when state is {@link ModifiableMetadata.State#FINAL}.
+     * Setting new values and overwriting existing values are <strong>not</strong> allowed.
+     */
+    @Test
+    public void testStateFinal() {
+        assertTrue("transition", md.transition(ModifiableMetadata.State.FINAL));
+        assertEquals("state", ModifiableMetadata.State.FINAL, md.state());
+        assertEquals("identifier.state", ModifiableMetadata.State.FINAL, identifierState());
+        try {
+            md.transition(ModifiableMetadata.State.EDITABLE);
+            fail("Shall not be allowed to transition back to editable state.");
+        } catch (UnmodifiableMetadataException e) {
+            verifyUnmodifiableException(e);
+        }
+        /*
+         * Verify conditions given in Javadoc: new values and overwriting not allowed.
+         */
+        try {
+            md.setVolumes(4);
+            fail("Setting new value shall not be allowed.");
+        } catch (UnmodifiableMetadataException e) {
+            verifyUnmodifiableException(e);
+        }
+        try {
+            md.setMediumNote(new SimpleInternationalString("A new note."));
+            fail("Overwriting an existing value shall not be allowed.");
+        } catch (UnmodifiableMetadataException e) {
+            verifyUnmodifiableException(e);
+        }
+        try {
+            md.getMediumFormats().add(MediumFormat.TAR);
+            fail("Adding new value shall not be allowed.");
+        } catch (UnsupportedOperationException e) {
+            // This is the expected exception.
+        }
+        try {
+            md.setMediumFormats(Collections.singleton(MediumFormat.CPIO));
+            fail("Setting new value shall not be allowed.");
+        } catch (UnmodifiableMetadataException e) {
+            verifyUnmodifiableException(e);
+        }
+        assertPropertiesEqual(null, "The original note.");
+    }
+}
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/citation/CitationsTest.java b/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/citation/CitationsTest.java
index aa32db5..2699b8e 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/citation/CitationsTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/metadata/iso/citation/CitationsTest.java
@@ -22,6 +22,7 @@ import java.util.Locale;
 import java.util.Collections;
 import java.util.IdentityHashMap;
 import java.lang.reflect.Field;
+import java.util.Collection;
 import org.opengis.metadata.Identifier;
 import org.opengis.metadata.citation.Citation;
 import org.apache.sis.internal.simple.CitationConstant;
@@ -248,6 +249,21 @@ public final strictfp class CitationsTest extends TestCase {
     }
 
     /**
+     * Verifies that citation constants are unmodifiable.
+     */
+    @Test
+    public void ensureUnmodifiable() {
+        final Collection<? extends Identifier> identifiers = Citations.EPSG.getIdentifiers();
+        assertNotNull(identifiers);
+        try {
+            identifiers.add(null);
+            fail("Pre-defined metadata shall be unmodifiable.");
+        } catch (UnsupportedOperationException e) {
+            // This is the expected exception.
+        }
+    }
+
+    /**
      * Test serialization.
      *
      * @throws IllegalAccessException should never happen since we asked only for public fields.
@@ -255,7 +271,7 @@ public final strictfp class CitationsTest extends TestCase {
     @Test
     @DependsOnMethod("testFromName")
     public void testSerialization() throws IllegalAccessException {
-        for (final Field field : Citations.class.getFields()) {
+        for (final Field field : Citations.class.getDeclaredFields()) {
             if (CitationConstant.class.isAssignableFrom(field.getType())) {
                 final Object c = field.get(null);
                 assertSame(field.getName(), c, assertSerializedEquals(c));
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/metadata/sql/MetadataSourceTest.java b/core/sis-metadata/src/test/java/org/apache/sis/metadata/sql/MetadataSourceTest.java
index 2b8236b..ca045d9 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/metadata/sql/MetadataSourceTest.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/metadata/sql/MetadataSourceTest.java
@@ -18,6 +18,7 @@ package org.apache.sis.metadata.sql;
 
 import java.util.Collection;
 import java.util.Collections;
+import org.opengis.util.InternationalString;
 import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.distribution.Format;
 import org.apache.sis.metadata.MetadataStandard;
@@ -79,6 +80,7 @@ public final strictfp class MetadataSourceTest extends TestCase {
             verifyFormats(source);
             testSearch(source);
             testEmptyCollection(source);
+            ensureReadOnly(source);
 
             // Opportunistic verification using the database we have at hand.
             MetadataFallbackVerifier.compare(source);
@@ -147,4 +149,25 @@ public final strictfp class MetadataSourceTest extends TestCase {
         assertTrue("Expected an empty collection.", details.isEmpty());
         assertSame("Collection shall be unmodifiable.", Collections.EMPTY_LIST, details);
     }
+
+    /**
+     * Verifies that instances created by {@link MetadataSource} are read-only.
+     * In particular, it should not be possible to add elements in the collection.
+     *
+     * @param  source  the instance to test.
+     * @throws MetadataStoreException if an error occurred while querying the database.
+     */
+    @TestStep
+    public static void ensureReadOnly(final MetadataSource source) throws MetadataStoreException {
+        final Citation c = source.lookup(Citation.class, "SIS");
+        @SuppressWarnings("unchecked")                                  // Cheat or the purpose of this test.
+        final Collection<InternationalString> titles = (Collection<InternationalString>) c.getAlternateTitles();
+        final InternationalString more = new SimpleInternationalString("An open source project.");
+        try {
+            titles.add(more);
+            fail("Pre-defined metadata should be unmodifiable.");
+        } catch (UnsupportedOperationException e) {
+            // This is the expected exception.
+        }
+    }
 }
diff --git a/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java b/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java
index cd803d0..256796a 100644
--- a/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java
+++ b/core/sis-metadata/src/test/java/org/apache/sis/test/suite/MetadataTestSuite.java
@@ -53,6 +53,7 @@ import org.junit.BeforeClass;
     org.apache.sis.metadata.HashCodeTest.class,
     org.apache.sis.metadata.PrunerTest.class,
     org.apache.sis.metadata.AbstractMetadataTest.class,
+    org.apache.sis.metadata.ModifiableMetadataTest.class,
     org.apache.sis.metadata.MetadataCopierTest.class,
     org.apache.sis.internal.metadata.MergerTest.class,
 
diff --git a/core/sis-utility/src/main/java/org/apache/sis/internal/util/CollectionsExt.java b/core/sis-utility/src/main/java/org/apache/sis/internal/util/CollectionsExt.java
index 0a60b8e..492a7cc 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/internal/util/CollectionsExt.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/internal/util/CollectionsExt.java
@@ -428,41 +428,34 @@ public final class CollectionsExt extends Static {
     }
 
     /**
-     * Returns a unmodifiable version of the given collection.
-     * If the given collection is a {@link Set} or a {@link List}, then this method tries to
-     * return a collection of the same type. Other types are not guaranteed to be preserved.
+     * Returns a unmodifiable version of the given list.
      *
-     * <p><em>The collection returned by this method may or may not be a view of the given collection</em>.
-     * Consequently this method shall be used <strong>only</strong> if the given collection will
+     * <p><em>The collection returned by this method may or may not be a view of the given list</em>.
+     * Consequently this method shall be used <strong>only</strong> if the given list will
      * <strong>not</strong> be modified after this method call. In case of doubt, use the
-     * standard {@link Collections#unmodifiableCollection(Collection)} method instead.</p>
+     * standard {@link Collections#unmodifiableList(List)} method instead.</p>
      *
-     * @param  <E>         the type of elements in the collection.
-     * @param  collection  the collection to make unmodifiable, or {@code null}.
-     * @return a unmodifiable version of the given collection, or {@code null} if the given collection was null.
-     *
-     * @since 0.8
+     * @param  <E>   the type of elements in the list.
+     * @param  list  the list to make unmodifiable, or {@code null}.
+     * @return a unmodifiable version of the given list, or {@code null} if the given list was null.
      */
-    public static <E> Collection<E> unmodifiableOrCopy(Collection<E> collection) {
-        if (collection != null) {
-            if (collection instanceof Set<?>) {
-                return unmodifiableOrCopy((Set<E>) collection);
-            }
-            final int length = collection.size();
+    public static <E> List<E> unmodifiableOrCopy(List<E> list) {
+        if (list != null) {
+            final int length = list.size();
             switch (length) {
                 case 0: {
-                    collection = Collections.emptyList();
+                    list = Collections.emptyList();
                     break;
                 }
                 case 1: {
-                    collection = Collections.singletonList(collection.iterator().next());
+                    list = Collections.singletonList(list.get(0));
                     break;
                 }
                 default: {
-                    if (collection instanceof UnmodifiableArrayList<?>) {
+                    if (list instanceof UnmodifiableArrayList<?>) {
                         break;                                              // List is already unmodifiable.
                     }
-                    if (collection instanceof CheckedContainer<?>) {
+                    if (list instanceof CheckedContainer<?>) {
                         /*
                          * We use UnmodifiableArrayList for avoiding one level of indirection. The fact that it
                          * implements CheckedContainer is not a goal here, and is actually unsafe since we have
@@ -472,18 +465,16 @@ public final class CollectionsExt extends Static {
                          * by JDK9 collections.
                          */
                         @SuppressWarnings("unchecked")       // Okay if collection is compliant with CheckedContainer contract.
-                        final E[] array = (E[]) Array.newInstance(((CheckedContainer<E>) collection).getElementType(), length);
-                        collection = UnmodifiableArrayList.wrap(collection.toArray(array));
-                    } else if (collection instanceof List<?>) {
-                        collection = Collections.unmodifiableList((List<E>) collection);
-                    } else {
-                        collection = Collections.unmodifiableCollection(collection);
+                        final E[] array = (E[]) Array.newInstance(((CheckedContainer<E>) list).getElementType(), length);
+                        list = UnmodifiableArrayList.wrap(list.toArray(array));
+                    } else if (list instanceof List<?>) {
+                        list = Collections.unmodifiableList(list);
                     }
                     break;
                 }
             }
         }
-        return collection;
+        return list;
     }
 
     /**
@@ -636,7 +627,7 @@ public final class CollectionsExt extends Static {
 
     /**
      * Returns a more compact representation of the given list. This method is similar to
-     * {@link #unmodifiableOrCopy(Collection)} except that it does not wrap the list in an unmodifiable view.
+     * {@link #unmodifiableOrCopy(List)} except that it does not wrap the list in an unmodifiable view.
      * The intend is to avoid one level of indirection for performance and memory reasons.
      * This is okay only if the list is kept in a private field and never escape outside that class.
      *
@@ -644,7 +635,7 @@ public final class CollectionsExt extends Static {
      * @param  list  the list to compact, or {@code null}.
      * @return a unmodifiable version of the given list, or {@code null} if the given list was null.
      *
-     * @see #unmodifiableOrCopy(Collection)
+     * @see #unmodifiableOrCopy(List)
      */
     public static <E> List<E> compact(final List<E> list) {
         if (list != null) {
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
index cadcaf0..b0cfc8b 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.java
@@ -143,7 +143,7 @@ public final class Errors extends IndexedResourceBundle {
         /**
          * Can not process property “{0}”. The reason is: {1}
          */
-        public static final short CanNotProcessProperty_2 = 185;
+        public static final short CanNotProcessProperty_2 = 152;
 
         /**
          * Can not read property “{1}” in file “{0}”.
@@ -896,11 +896,6 @@ public final class Errors extends IndexedResourceBundle {
         public static final short UnmodifiableCellValue_2 = 151;
 
         /**
-         * This metadata is not modifiable.
-         */
-        public static final short UnmodifiableMetadata = 152;
-
-        /**
          * This instance of ‘{0}’ is not modifiable.
          */
         public static final short UnmodifiableObject_1 = 153;
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
index a3053bf..990dcce 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors.properties
@@ -190,7 +190,6 @@ UnknownOption_1                   = Option \u201c{0}\u201d is not recognized.
 UnknownType_1                     = Type \u2018{0}\u2019 is unknown in this context.
 UnknownUnit_1                     = Unit \u201c{0}\u201d is not recognized.
 UnmodifiableCellValue_2           = The cell at column \u201c{1}\u201d of row \u201c{0}\u201d is unmodifiable.
-UnmodifiableMetadata              = This metadata is not modifiable.
 UnmodifiableObject_1              = This instance of \u2018{0}\u2019 is not modifiable.
 UnparsableStringForClass_2        = Text \u201c{1}\u201d can not be parsed as an object of type \u2018{0}\u2019.
 UnparsableStringForClass_3        = Text \u201c{1}\u201d can not be parsed as an object of type \u2018{0}\u2019, because of the \u201c{2}\u201d characters.
diff --git a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
index bce1748..176b593 100644
--- a/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
+++ b/core/sis-utility/src/main/java/org/apache/sis/util/resources/Errors_fr.properties
@@ -186,7 +186,6 @@ UnknownOption_1                   = L\u2019option \u00ab\u202f{0}\u202f\u00bb n\
 UnknownType_1                     = Le type \u2018{0}\u2019 n\u2019est pas reconnu dans ce contexte.
 UnknownUnit_1                     = Les unit\u00e9s \u00ab\u202f{0}\u202f\u00bb ne sont pas reconnues.
 UnmodifiableCellValue_2           = La cellule \u00e0 la colonne \u00ab\u202f{1}\u202f\u00bb de la ligne \u00ab\u202f{0}\u202f\u00bb n\u2019est pas modifiable.
-UnmodifiableMetadata              = Cette m\u00e9ta-donn\u00e9e n\u2019est pas modifiable.
 UnmodifiableObject_1              = Cette instance de \u2018{0}\u2019 n\u2019est pas modifiable.
 UnspecifiedCRS                    = Le syst\u00e8me de r\u00e9f\u00e9rence des coordonn\u00e9es n\u2019a pas \u00e9t\u00e9 sp\u00e9cifi\u00e9.
 UnspecifiedFormatForClass_1       = Aucun format n\u2019est sp\u00e9cifi\u00e9 pour les objets de classe \u2018{0}\u2019.
diff --git a/ide-project/NetBeans/build.xml b/ide-project/NetBeans/build.xml
index e67120b..1d1f1c8 100644
--- a/ide-project/NetBeans/build.xml
+++ b/ide-project/NetBeans/build.xml
@@ -36,6 +36,9 @@
       <fileset dir="${project.root}/core/sis-utility/target/generated-resources">
         <include name="**/*.utf"/>
       </fileset>
+      <fileset dir="${project.root}/core/sis-metadata/target/generated-resources">
+        <include name="**/*.utf"/>
+      </fileset>
       <fileset dir="${project.root}/core/sis-referencing/target/generated-resources">
         <include name="**/*.utf"/>
       </fileset>


Mime
View raw message