You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sis.apache.org by de...@apache.org on 2023/11/19 19:13:03 UTC

(sis) branch geoapi-4.0 updated (3d379f9aad -> c0a92a7311)

This is an automated email from the ASF dual-hosted git repository.

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


    from 3d379f9aad Add support for nil date and nil URI in metadata.
     new dfc7aff69f Remove `NilDate`. It was not working because this class is mutable, so metadata make copies of it and those copies were not `NilObject`.
     new c0a92a7311 Add `MetadataStandard.asNilReasonMap(…)` method, together with support for classes that cannot implement the `NilObject` interface. As a side-effect of this work, other map views were refactored for putting more common code in the `PropertyMap` base class.

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/sis/metadata/AbstractMetadata.java  |  30 +++
 .../main/org/apache/sis/metadata/IndexMap.java     |  33 +--
 .../org/apache/sis/metadata/InformationMap.java    |  31 +--
 .../org/apache/sis/metadata/MetadataStandard.java  |  35 +++
 .../main/org/apache/sis/metadata/NameMap.java      |  34 +--
 .../main/org/apache/sis/metadata/NilReasonMap.java | 181 ++++++++++++++
 .../org/apache/sis/metadata/PropertyAccessor.java  |  26 +-
 .../main/org/apache/sis/metadata/PropertyMap.java  | 265 ++++++++++++++++-----
 .../main/org/apache/sis/metadata/TypeMap.java      |  33 +--
 .../main/org/apache/sis/metadata/ValueMap.java     | 228 ++----------------
 .../org/apache/sis/metadata/iso/ISOMetadata.java   |   2 +-
 .../main/org/apache/sis/xml/NilDate.java           |  69 ------
 .../main/org/apache/sis/xml/NilReason.java         |  34 ++-
 .../org/apache/sis/metadata/NilReasonMapTest.java  | 102 ++++++++
 .../test/org/apache/sis/xml/NilReasonTest.java     |  13 -
 15 files changed, 620 insertions(+), 496 deletions(-)
 create mode 100644 endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NilReasonMap.java
 delete mode 100644 endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilDate.java
 create mode 100644 endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/NilReasonMapTest.java


(sis) 02/02: Add `MetadataStandard.asNilReasonMap(…)` method, together with support for classes that cannot implement the `NilObject` interface. As a side-effect of this work, other map views were refactored for putting more common code in the `PropertyMap` base class.

Posted by de...@apache.org.
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 c0a92a731189492536debd2697aca0c841a7e6a5
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Nov 19 20:05:27 2023 +0100

    Add `MetadataStandard.asNilReasonMap(…)` method, together with support for classes that cannot implement the `NilObject` interface.
    As a side-effect of this work, other map views were refactored for putting more common code in the `PropertyMap` base class.
---
 .../org/apache/sis/metadata/AbstractMetadata.java  |  30 +++
 .../main/org/apache/sis/metadata/IndexMap.java     |  33 +--
 .../org/apache/sis/metadata/InformationMap.java    |  31 +--
 .../org/apache/sis/metadata/MetadataStandard.java  |  35 +++
 .../main/org/apache/sis/metadata/NameMap.java      |  34 +--
 .../main/org/apache/sis/metadata/NilReasonMap.java | 181 ++++++++++++++
 .../org/apache/sis/metadata/PropertyAccessor.java  |  26 +-
 .../main/org/apache/sis/metadata/PropertyMap.java  | 265 ++++++++++++++++-----
 .../main/org/apache/sis/metadata/TypeMap.java      |  33 +--
 .../main/org/apache/sis/metadata/ValueMap.java     | 228 ++----------------
 .../org/apache/sis/metadata/iso/ISOMetadata.java   |   2 +-
 .../org/apache/sis/metadata/NilReasonMapTest.java  | 102 ++++++++
 12 files changed, 593 insertions(+), 407 deletions(-)

diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/AbstractMetadata.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/AbstractMetadata.java
index 5e5ac1ffcc..f3333c9a39 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/AbstractMetadata.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/AbstractMetadata.java
@@ -17,7 +17,9 @@
 package org.apache.sis.metadata;
 
 import java.util.Map;
+import java.util.HashMap;
 import jakarta.xml.bind.annotation.XmlTransient;
+import org.apache.sis.xml.NilReason;
 import org.apache.sis.util.Emptiable;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.LenientComparable;
@@ -77,6 +79,14 @@ import org.apache.sis.util.collection.TreeTable;
  */
 @XmlTransient
 public abstract class AbstractMetadata implements LenientComparable, Emptiable {
+    /**
+     * The reasons why some mandatory properties are absent. This map is used only for values of
+     * classes that cannot be represented as instances of {@link org.apache.sis.xml.NilObject}.
+     *
+     * @see NilReasonMap
+     */
+    HashMap<Integer,NilReason> nilReasons;
+
     /**
      * Creates an initially empty metadata.
      */
@@ -332,4 +342,24 @@ public abstract class AbstractMetadata implements LenientComparable, Emptiable {
     public String toString() {
         return asTreeTable().toString();
     }
+
+    /**
+     * Returns a <em>shallow</em> clone of this metadata. The properties values and the children are not cloned.
+     * This method is for internal usage by {@link ModifiableMetadata} and should usually not be invoked directly.
+     *
+     * @return a shallow clone of this metadata.
+     * @throws CloneNotSupportedException if this metadata is not cloneable.
+     *
+     * @see ModifiableMetadata#deepCopy(ModifiableMetadata.State)
+     * @see MetadataCopier
+     */
+    @Override
+    @SuppressWarnings("unchecked")
+    protected AbstractMetadata clone() throws CloneNotSupportedException {
+        final var clone = (AbstractMetadata) super.clone();
+        if (clone.nilReasons != null) {
+            clone.nilReasons = (HashMap<Integer,NilReason>) clone.nilReasons.clone();
+        }
+        return clone;
+    }
 }
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/IndexMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/IndexMap.java
index 379a6d3999..c7602348cf 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/IndexMap.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/IndexMap.java
@@ -16,10 +16,6 @@
  */
 package org.apache.sis.metadata;
 
-import java.util.Map;
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-
 
 /**
  * Map of property indices for a given implementation class. This map is read-only.
@@ -40,33 +36,10 @@ final class IndexMap extends PropertyMap<Integer> {
     }
 
     /**
-     * Returns the value to which the specified key is mapped, or {@code null}
-     * if this map contains no mapping for the key.
-     */
-    @Override
-    public Integer get(final Object key) {
-        if (key instanceof String) {
-            final int i = accessor.indexOf((String) key, false);
-            if (i >= 0) return i;
-        }
-        return null;
-    }
-
-    /**
-     * Returns an iterator over the entries contained in this map.
+     * Returns the index for the property at the specified index.
      */
     @Override
-    final Iterator<Map.Entry<String,Integer>> iterator() {
-        return new Iter() {
-            @Override
-            public Map.Entry<String,Integer> next() {
-                final int i = index++;
-                final String name = accessor.name(i, keyPolicy);
-                if (name != null) {
-                    return new SimpleImmutableEntry<>(name, i);
-                }
-                throw new NoSuchElementException();
-            }
-        };
+    final Integer getReflectively(final int index) {
+        return (index >= 0) ? index : null;
     }
 }
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/InformationMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/InformationMap.java
index 3e621e0777..49fafb0eba 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/InformationMap.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/InformationMap.java
@@ -16,9 +16,6 @@
  */
 package org.apache.sis.metadata;
 
-import java.util.Map;
-import java.util.Iterator;
-import java.util.NoSuchElementException;
 import org.opengis.metadata.ExtendedElementInformation;
 import org.opengis.metadata.citation.Citation;
 
@@ -51,32 +48,10 @@ final class InformationMap extends PropertyMap<ExtendedElementInformation> {
     }
 
     /**
-     * Returns the information to which the specified key is mapped,
-     * or {@code null} if this map contains no mapping for the key.
+     * Returns the information for the property at the specified index.
      */
     @Override
-    public ExtendedElementInformation get(final Object key) {
-        if (key instanceof String) {
-            return accessor.information(standard, accessor.indexOf((String) key, false));
-        }
-        return null;
-    }
-
-    /**
-     * Returns an iterator over the entries contained in this map.
-     */
-    @Override
-    final Iterator<Map.Entry<String,ExtendedElementInformation>> iterator() {
-        return new Iter() {
-            @Override
-            public Map.Entry<String,ExtendedElementInformation> next() {
-                final ExtendedElementInformation value = accessor.information(standard, index);
-                if (value == null) {
-                    // PropertyAccessor.information(int) never return null if the index is valid.
-                    throw new NoSuchElementException();
-                }
-                return new SimpleImmutableEntry<>(accessor.name(index++, keyPolicy), value);
-            }
-        };
+    final ExtendedElementInformation getReflectively(final int index) {
+        return accessor.information(standard, index);
     }
 }
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataStandard.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataStandard.java
index cd0d10aeea..0240680033 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataStandard.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataStandard.java
@@ -895,6 +895,41 @@ public class MetadataStandard implements Serializable {
         return new ValueMap(metadata, getAccessor(new CacheKey(metadata.getClass(), baseType), true), keyPolicy, valuePolicy);
     }
 
+    /**
+     * Returns the reasons for all missing values of mandatory properties.
+     * The map is backed by the metadata object using Java reflection, so changes in the
+     * underlying metadata object are immediately reflected in the map and conversely.
+     * Nil reasons are determined by calls to {@link NilReason#forObject(Object)},
+     * potentially completed by an internal storage for
+     * {@linkplain NilReason#isSupported(Class) unsupported value types}.
+     *
+     * <h4>Mandatory and optional properties</h4>
+     * If a {@linkplain org.opengis.annotation.Obligation#MANDATORY mandatory} property has no value,
+     * then the property will have an entry in the map even if the associated {@link NilReason} is null.
+     * By contrast, {@linkplain org.opengis.annotation.Obligation#OPTIONAL optional} properties have
+     * entries in the map only if they have a non-null {@link NilReason}.
+     *
+     * <h4>Supported operations</h4>
+     * The map supports the {@link Map#put(Object, Object) put(…)} and {@link Map#remove(Object)
+     * remove(…)} operations if the underlying metadata object contains setter methods.
+     * The {@code remove(…)} method is implemented by a call to {@code put(…, null)}.
+     *
+     * @param  metadata     the metadata object to view as a map.
+     * @param  baseType     base type of the metadata of interest, or {@code null} if unspecified.
+     * @param  keyPolicy    determines the string representation of map keys.
+     * @return a map view over the metadata object.
+     * @throws ClassCastException if the metadata object does not implement a metadata interface of the expected package.
+     *
+     * @since 1.5
+     */
+    public Map<String,NilReason> asNilReasonMap(final Object metadata, final Class<?> baseType,
+            final KeyNamePolicy keyPolicy) throws ClassCastException
+    {
+        ensureNonNull("metadata",  metadata);
+        ensureNonNull("keyPolicy", keyPolicy);
+        return new NilReasonMap(metadata, getAccessor(new CacheKey(metadata.getClass(), baseType), true), keyPolicy);
+    }
+
     /**
      * Returns the specified metadata object as a tree table.
      * The tree table is backed by the metadata object using Java reflection, so changes in the
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NameMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NameMap.java
index c76cb17706..1aee26e8e8 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NameMap.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NameMap.java
@@ -16,10 +16,6 @@
  */
 package org.apache.sis.metadata;
 
-import java.util.Map;
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-
 
 /**
  * Map of property names for a given implementation class. This map is read-only.
@@ -32,7 +28,7 @@ final class NameMap extends PropertyMap<String> {
     /**
      * Determines the string representation of values in this map.
      */
-    final KeyNamePolicy valuePolicy;
+    private final KeyNamePolicy valuePolicy;
 
     /**
      * Creates a name map for the specified accessor.
@@ -47,32 +43,10 @@ final class NameMap extends PropertyMap<String> {
     }
 
     /**
-     * Returns the value to which the specified key is mapped, or {@code null}
-     * if this map contains no mapping for the key.
-     */
-    @Override
-    public String get(final Object key) {
-        if (key instanceof String) {
-            return accessor.name(accessor.indexOf((String) key, false), valuePolicy);
-        }
-        return null;
-    }
-
-    /**
-     * Returns an iterator over the entries contained in this map.
+     * Returns the name for the property at the specified index.
      */
     @Override
-    final Iterator<Map.Entry<String,String>> iterator() {
-        return new Iter() {
-            @Override
-            public Map.Entry<String,String> next() {
-                final String value = accessor.name(index, valuePolicy);
-                if (value == null) {
-                    // PropertyAccessor.name(int) never return null if the index is valid.
-                    throw new NoSuchElementException();
-                }
-                return new SimpleImmutableEntry<>(accessor.name(index++, keyPolicy), value);
-            }
-        };
+    final String getReflectively(final int index) {
+        return accessor.name(index, valuePolicy);
     }
 }
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NilReasonMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NilReasonMap.java
new file mode 100644
index 0000000000..9ceed8c7aa
--- /dev/null
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/NilReasonMap.java
@@ -0,0 +1,181 @@
+/*
+ * 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.Map;
+import java.util.HashMap;
+import java.util.Iterator;
+import org.opengis.annotation.Obligation;
+import org.apache.sis.xml.NilReason;
+import org.apache.sis.xml.NilObject;
+
+
+/**
+ * A view of nil reasons as a map. Keys are property names and each value is the reason
+ * why the value returned by the {@code getFoo()} method (using reflection) is missing.
+ * This map contains only the properties that are mandatory.
+ *
+ * <p>Contrarily to other map views, this map may contain entries associated to null values.
+ * It happens when a mandatory property is missing, but nevertheless no reason is provided.
+ * So {@code containsValue(null)} can be used for checking if a metadata is invalid.</p>
+ *
+ * <p>Contrarily to other map views, this map is state-full.
+ * Only one instance should be created per metadata object.</p>
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see MetadataStandard#asNilReasonMap(Object, Class, KeyNamePolicy)
+ */
+final class NilReasonMap extends PropertyMap<NilReason> {
+    /**
+     * The metadata object to wrap.
+     */
+    private final Object metadata;
+
+    /**
+     * The reasons why a mandatory property is missing when that reason cannot be expressed by {@link NilObject}.
+     * This is needed for value objects such as {@link Integer}. This map shall always be checked in last resort,
+     * after we verified that the {@linkplain #metadata} does not have any value for the corresponding property.
+     * If {@link #metadata} changed without updating this {@code reasons} map, the metadata value prevails.
+     */
+    private final Map<Integer,NilReason> nilReasons;
+
+    /**
+     * Creates a map of nil reasons for the specified metadata and accessor.
+     *
+     * @param metadata   the metadata object to wrap.
+     * @param accessor   the accessor to use for the metadata.
+     * @param keyPolicy  determines the string representation of keys in the map.
+     */
+    NilReasonMap(final Object metadata, final PropertyAccessor accessor, final KeyNamePolicy keyPolicy) {
+        super(accessor, keyPolicy);
+        this.metadata = metadata;
+        if (metadata instanceof AbstractMetadata) {
+            final var c = (AbstractMetadata) metadata;
+            synchronized (c) {
+                if (c.nilReasons == null) {
+                    c.nilReasons = new HashMap<>(4);
+                }
+                nilReasons = c.nilReasons;
+            }
+        } else {
+            nilReasons = new HashMap<>(4);
+        }
+    }
+
+    /**
+     * Returns {@code true} if this map contains no key-value mappings.
+     */
+    @Override
+    public boolean isEmpty() {
+        final int count = accessor.count();
+        for (int i=0; i<count; i++) {
+            if (contains(i)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns the number of entries in this map.
+     * Note that some entries may be associated to null values.
+     */
+    @Override
+    public int size() {
+        int n = 0;
+        final int count = accessor.count();
+        for (int i=0; i<count; i++) {
+            if (contains(i)) n++;
+        }
+        return n;
+    }
+
+    /**
+     * Returns whether this map contains an entry for the property at the given index.
+     * The value associated to that entry may be null.
+     *
+     * @param  index   property index, using the numbering for all properties.
+     * @return whether this map contains a value, potentially null, for the specified property.
+     */
+    @Override
+    final boolean contains(final int index) {
+        final Object value = accessor.get(index, metadata);
+        if (value != null) {
+            nilReasons.remove(index);
+            return NilReason.forObject(value) != null;
+        }
+        return nilReasons.containsKey(index) || accessor.obligation(index) == Obligation.MANDATORY;
+    }
+
+    /**
+     * Returns the nil reason for the property at the specified index.
+     */
+    @Override
+    final NilReason getReflectively(final int index) {
+        final Object value = accessor.get(index, metadata);
+        if (value != null) {
+            nilReasons.remove(index);
+            return NilReason.forObject(value);
+        }
+        return nilReasons.get(index);
+    }
+
+    /**
+     * Associates the specified nil reason with the specified key in this map.
+     * The given value will replace any previous value including non-nil ones,
+     * unless the given value is null while the previous value is non-nil.
+     * In the latter case, this method does nothing (i.e. the non-nil value is not discarded).
+     *
+     * <p>Note that the latter exception is not really an exception from the point of view of
+     * a {@code NilReasonMap} user, because non-nil values are returned as {@code null} values.
+     * Consequently, {@code put(key, null)} behaves as if {@code null} has been stored in this
+     * map even if the underlying {@linkplain #metadata} object has a non-nil value.</p>
+     *
+     * @throws IllegalArgumentException if the given key is not the name of a property in the metadata.
+     * @throws ClassCastException if the given value is not of the expected type.
+     * @throws UnmodifiableMetadataException if the property for the given key is read-only.
+     */
+    @Override
+    final NilReason setReflectively(final int index, final NilReason value) {
+        if (value == null) {
+            final Object    oldObject = accessor.get(index, metadata);
+            final NilReason oldReason = NilReason.forObject(oldObject);
+            if (oldReason == null) {
+                NilReason oldStored = nilReasons.remove(index);
+                return (oldObject == null) ? oldStored : null;
+            }
+            accessor.set(index, metadata, null, PropertyAccessor.RETURN_NULL);
+            nilReasons.remove(index);   // Shall do only if `set(…)` succeeded.
+            return oldReason;
+        }
+        final Class<?>  type      = accessor.type(index, TypeValuePolicy.PROPERTY_TYPE);
+        final Object    nilObject = NilReason.isSupported(type) ? value.createNilObject(type) : null;
+        final Object    oldObject = accessor.set(index, metadata, nilObject, PropertyAccessor.RETURN_PREVIOUS);
+        final NilReason oldReason = NilReason.forObject(oldObject);
+        final NilReason oldStored = (nilObject == null) ? nilReasons.put(index, value) : nilReasons.remove(index);
+        return (oldReason != null) ? oldReason : oldStored;
+    }
+
+    /**
+     * Returns an iterator over the entries contained in this map.
+     */
+    @Override
+    final Iterator<Map.Entry<String,NilReason>> iterator() {
+        return new ReflectiveIterator();
+    }
+}
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyAccessor.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyAccessor.java
index f753bcbd3b..248bd3afdd 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyAccessor.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyAccessor.java
@@ -238,8 +238,8 @@ class PropertyAccessor {
         this.type           = type;
         this.implementation = implementation;
         this.getters        = getGetters(type, implementation, standardImpl);
-        int allCount = getters.length;
-        int standardCount = allCount;
+        int allCount        = getters.length;
+        int standardCount   = allCount;
         if (allCount != 0 && getters[allCount-1] == EXTRA_GETTER) {
             if (!EXTRA_GETTER.getDeclaringClass().isAssignableFrom(implementation)) {
                 allCount--;                                 // The extra getter method does not exist.
@@ -311,7 +311,7 @@ class PropertyAccessor {
                  * be a parent type.
                  *
                  * It is a necessary condition that the type returned by the getter is assignable
-                 * to the type expected by the setter.  This contract is required by the 'FINAL'
+                 * to the type expected by the setter.  This contract is required by the `FINAL`
                  * state among others.
                  */
                 try {
@@ -330,7 +330,7 @@ class PropertyAccessor {
                     } catch (NoSuchMethodException ignore) {
                         /*
                          * There is no setter, which may be normal. At this stage
-                         * the 'setter' variable should still have the null value.
+                         * the `setter` variable should still have the null value.
                          */
                     }
                 }
@@ -768,7 +768,7 @@ class PropertyAccessor {
             }
         } catch (IllegalAccessException e) {
             /*
-             * Should never happen since 'getters' should contain only public methods.
+             * Should never happen since `getters` should contain only public methods.
              */
             throw new AssertionError(method.toString(), e);
         } catch (InvocationTargetException e) {
@@ -867,7 +867,7 @@ class PropertyAccessor {
                 }
                 /*
                  * Converts the new value to a type acceptable for the setter method (if possible).
-                 * If the new value is a singleton while the expected type is a collection, then the 'convert'
+                 * If the new value is a singleton while the expected type is a collection, then the `convert`
                  * method added the singleton in the existing collection, which may result in no change if the
                  * collection is a Set and the new value already exists in that Set. If we detect that there is
                  * no change, then we don't need to invoke the setter method. Note that we conservatively assume
@@ -879,7 +879,7 @@ class PropertyAccessor {
                     changed = (mode == RETURN_NULL) || (mode == IGNORE_READ_ONLY) || (newValues[0] != oldValue);
                     if (changed && mode == APPEND && !isNullOrEmpty(oldValue)) {
                         /*
-                         * If 'convert' did not added the value in a collection and if a value already
+                         * If `convert` did not added the value in a collection and if a value already
                          * exists, do not modify the existing value. Exit now with "no change" status.
                          */
                         return null;
@@ -921,7 +921,7 @@ class PropertyAccessor {
         try {
             setter.invoke(metadata, newValues);
         } catch (IllegalAccessException e) {
-            // Should never happen since 'setters' should contain only public methods.
+            // Should never happen since `setters` should contain only public methods.
             throw new AssertionError(e);
         } catch (InvocationTargetException e) {
             final Throwable cause = e.getTargetException();
@@ -1053,11 +1053,11 @@ class PropertyAccessor {
             }
             /*
              * We now have objects of the appropriate type. If we have a singleton to be added
-             * in an existing collection, add it now. In that case the 'newValue' should refer
-             * to the 'addTo' collection. We rely on the ModifiableMetadata.writeCollection(…)
+             * in an existing collection, add it now. In that case the `newValue` should refer
+             * to the `addTo` collection. We rely on the ModifiableMetadata.writeCollection(…)
              * optimization for detecting that the new collection is the same instance than
              * the old one so there is nothing to do. We could exit from the method, but let
-             * it continues in case the user override the 'setFoo(…)' method.
+             * it continues in case the user override the `setFoo(…)` method.
              */
             if (addTo != null) {
                 /*
@@ -1148,7 +1148,7 @@ class PropertyAccessor {
             return count();
         }
         int count = 0;
-        // Use 'standardCount' instead of 'allCount' for ignoring deprecated methods.
+        // Use `standardCount` instead of `allCount` for ignoring deprecated methods.
         for (int i=0; i<standardCount; i++) {
             final Object value = get(getters[i], metadata);
             if (!valuePolicy.isSkipped(value)) {
@@ -1163,7 +1163,7 @@ class PropertyAccessor {
                     case COUNT_DEEP: {
                         /*
                          * Count always at least one element because if the user wanted to skip null or empty
-                         * collections, then 'valuePolicy.isSkipped(value)' above would have returned 'true'.
+                         * collections, then `valuePolicy.isSkipped(value)` above would have returned `true`.
                          */
                         count += isCollectionOrMap(i) ? Math.max(CollectionsExt.size(value), 1) : 1;
                         break;
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyMap.java
index 80ad8c1cc4..90f4fb2042 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyMap.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/PropertyMap.java
@@ -21,6 +21,9 @@ import java.util.Set;
 import java.util.AbstractMap;
 import java.util.AbstractSet;
 import java.util.Iterator;
+import java.util.NoSuchElementException;
+import org.apache.sis.util.internal.AbstractMapEntry;
+import org.apache.sis.util.resources.Errors;
 
 
 /**
@@ -37,6 +40,7 @@ import java.util.Iterator;
  * @see NameMap
  * @see TypeMap
  * @see InformationMap
+ * @see NilReasonMap
  */
 abstract class PropertyMap<V> extends AbstractMap<String,V> {
     /**
@@ -47,12 +51,12 @@ abstract class PropertyMap<V> extends AbstractMap<String,V> {
     /**
      * Determines the string representation of keys in the map.
      */
-    final KeyNamePolicy keyPolicy;
+    protected final KeyNamePolicy keyPolicy;
 
     /**
      * A view of the mappings contained in this map.
      */
-    transient Set<Map.Entry<String,V>> entrySet;
+    private transient Set<Map.Entry<String,V>> entrySet;
 
     /**
      * Creates a new map backed by the given accessor.
@@ -73,24 +77,90 @@ abstract class PropertyMap<V> extends AbstractMap<String,V> {
         return accessor.count();
     }
 
+    /**
+     * Returns {@code true} if this map contains a mapping for the property at the specified index.
+     * The default implementation is okay only if all metadata defined by the standard are included in the map.
+     * Subclasses shall override this method if their map contain only a subset of all possible metadata elements.
+     *
+     * @param  index  index of the property to test, possibly negative.
+     * @return whether this map contains a property for the specified index.
+     */
+    boolean contains(int index) {
+        return index >= 0;
+    }
+
+    /**
+     * Returns the value for the property at the specified index.
+     *
+     * @param  index  index of the property to get, possibly negative.
+     * @return value at the given index, or {@code null} if none.
+     */
+    abstract V getReflectively(int index);
+
+    /**
+     * Sets the value for the property at the specified index.
+     *
+     * @param  index  index of the property to set.
+     * @param  value  new property value to set.
+     * @return old property value, or {@code null} if none.
+     * @throws UnsupportedOperationException if this map is read-only.
+     */
+    V setReflectively(int index, V value) {
+        throw new UnsupportedOperationException(Errors.format(Errors.Keys.UnmodifiableObject_1, Map.class));
+    }
+
     /**
      * Returns {@code true} if this map contains a mapping for the specified key.
-     * The default implementation is okay only if all metadata defined by the standard are included
-     * in the map. Subclasses shall override this method if their map contain only a subset of all
-     * possible metadata elements.
+     * Note that the associated value may be null.
      */
     @Override
-    public boolean containsKey(final Object key) {
-        return (key instanceof String) && accessor.indexOf((String) key, false) >= 0;
+    public final boolean containsKey(final Object key) {
+        return (key instanceof String) && contains(accessor.indexOf((String) key, false));
+    }
+
+    /**
+     * Returns the value to which the specified key is mapped, or {@code null} if none.
+     */
+    @Override
+    public final V get(final Object key) {
+        if (key instanceof String) {
+            return getReflectively(accessor.indexOf((String) key, false));
+        }
+        return null;
+    }
+
+    /**
+     * Associates the specified value with the specified key in this map.
+     *
+     * @throws IllegalArgumentException if the given key is not the name of a property in the metadata.
+     * @throws ClassCastException if the given value is not of the expected type.
+     * @throws UnmodifiableMetadataException if the property for the given key is read-only.
+     */
+    @Override
+    public final V put(final String key, final V value) {
+        return setReflectively(accessor.indexOf(key, true), value);
+    }
+
+    /**
+     * Removes the mapping for a key from this map if it is present.
+     *
+     * @throws UnmodifiableMetadataException if the property for the given key is read-only.
+     */
+    @Override
+    public final V remove(final Object key) throws UnsupportedOperationException {
+        if (key instanceof String) {
+            return setReflectively(accessor.indexOf((String) key, false), null);
+        }
+        return null;
     }
 
     /**
-     * Returns a view of the mappings contained in this map. Subclasses shall override this method
-     * if they define a different entries set class than the default {@link Entries} inner class.
+     * Returns a view of the mappings contained in this map.
+     * The entries are provided by {@link #iterator()}.
      */
     @Override
     @SuppressWarnings("ReturnOfCollectionOrArrayField")         // Intentionally modifiable set.
-    public Set<Map.Entry<String,V>> entrySet() {
+    public final Set<Map.Entry<String,V>> entrySet() {
         if (entrySet == null) {
             entrySet = new Entries();
         }
@@ -98,81 +168,152 @@ abstract class PropertyMap<V> extends AbstractMap<String,V> {
     }
 
     /**
-     * Returns an iterator over the entries in this map.
+     * Views of the entries contained in the map.
+     * The entries are provided by {@link PropertyMap#iterator()}.
      */
-    abstract Iterator<Map.Entry<String,V>> iterator();
+    private final class Entries extends AbstractSet<Map.Entry<String,V>> {
+        /** Creates a new entries set. */
+        Entries() {
+        }
+
+        /** Returns true if this collection contains no elements. */
+        @Override public final boolean isEmpty() {
+            return PropertyMap.this.isEmpty();
+        }
 
+        /** Returns the number of elements in this collection. */
+        @Override public final int size() {
+            return PropertyMap.this.size();
+        }
 
+        /** Returns an iterator over the elements contained in this collection. */
+        @Override public final Iterator<Map.Entry<String,V>> iterator() {
+            return PropertyMap.this.iterator();
+        }
 
+        /** Returns {@code true} if this collection contains the specified element. */
+        @Override public boolean contains(final Object object) {
+            if (object instanceof Map.Entry<?,?>) {
+                final Map.Entry<?,?> entry = (Map.Entry<?,?>) object;
+                final Object key   = entry.getKey();
+                final Object value = entry.getValue();
+                if (value != null) {
+                    return value.equals(get(key));
+                } else {
+                    return containsKey(key);
+                }
+            }
+            return false;
+        }
+    }
 
     /**
-     * The iterator over the elements contained in a {@link Entries} set.
-     *
-     * @author  Martin Desruisseaux (Geomatys)
+     * Returns an iterator over the entries in this map.
+     * The default implementation is okay only if all metadata defined by the standard are included in the map.
+     * Subclasses shall override this method if their map contain only a subset of all possible metadata elements.
+     * The {@link ReflectiveIterator} may be used for those maps.
      */
-    abstract class Iter implements Iterator<Map.Entry<String,V>> {
-        /**
-         * Index of the next element to return.
-         */
-        int index;
+    Iterator<Map.Entry<String,V>> iterator() {
+        return new Iterator<>() {
+            /** Index of the next property to return. */
+            private int index;
 
-        /**
-         * Creates a new iterator.
-         */
-        Iter() {
-        }
+            /** Returns true if there is more entries to return. */
+            @Override public boolean hasNext() {
+                return index < accessor.count();
+            }
+
+            /** Returns the next entry. */
+            @Override public Map.Entry<String,V> next() {
+                final int i = index++;
+                final String name = accessor.name(i, keyPolicy);
+                if (name != null) {
+                    return new SimpleImmutableEntry<>(name, getReflectively(i));
+                }
+                throw new NoSuchElementException();
+            }
+        };
+    }
+
+    /**
+     * The iterator over the {@link ReflectiveEntry} elements contained in an {@link Entries} set.
+     * Can be used as an alternative to the default {@link PropertyMap#iterator()} when the map
+     * contains only a subset of all possible properties.
+     */
+    final class ReflectiveIterator implements Iterator<Map.Entry<String,V>> {
+        /** The current and the next property, or {@code null} if the iteration is over. */
+        private ReflectiveEntry current, next;
 
-        /**
-         * Returns {@code true} if there is more elements to return.
-         */
-        @Override
-        public final boolean hasNext() {
-            return index < accessor.count();
+        /** Creates en iterator. */
+        ReflectiveIterator() {
+            move(0);
         }
 
-        /*
-         * remove() is an unsupported operation since we assume that the underlying map is unmodifiable.
-         * So we inherit the default implementation from Iterator.
-         */
-    }
+        /** Moves to the first property with a valid value, starting at the specified index. */
+        private void move(int index) {
+            final int count = accessor.count();
+            while (index < count) {
+                if (contains(index)) {
+                    next = new ReflectiveEntry(index);
+                    return;
+                }
+                index++;
+            }
+            next = null;
+        }
 
+        /** Returns {@code true} if the iteration has more elements. */
+        @Override public boolean hasNext() {
+            return next != null;
+        }
 
+        /** Returns the next element in the iteration. */
+        @Override public Map.Entry<String,V> next() {
+            if (next != null) {
+                current = next;
+                move(next.index + 1);
+                return current;
+            } else {
+                throw new NoSuchElementException();
+            }
+        }
 
+        /** Removes from the underlying collection the last element returned by the iterator. */
+        @Override public void remove() {
+            if (current != null) {
+                current.setValue(null);
+                current = null;
+            } else {
+                throw new IllegalStateException();
+            }
+        }
+    }
 
     /**
-     * Base class of views of the entries contained in the map.
-     *
-     * @author  Martin Desruisseaux (Geomatys)
+     * A map entry for a property whose key and value are fetched by reflection.
      */
-    class Entries extends AbstractSet<Map.Entry<String,V>> {
-        /**
-         * Creates a new entries set.
-         */
-        Entries() {
+    private final class ReflectiveEntry extends AbstractMapEntry<String,V> {
+        /** The property index. */
+        final int index;
+
+        /** Creates an entry for the property at the given index. */
+        ReflectiveEntry(final int index) {
+            this.index = index;
         }
 
-        /**
-         * Returns true if this collection contains no elements.
-         */
-        @Override
-        public final boolean isEmpty() {
-            return PropertyMap.this.isEmpty();
+        /** Returns the key corresponding to this entry. */
+        @Override public String getKey() {
+            return accessor.name(index, keyPolicy);
         }
 
-        /**
-         * Returns the number of elements in this collection.
-         */
-        @Override
-        public final int size() {
-            return PropertyMap.this.size();
+        /** Returns the value corresponding to this entry. */
+        @Override public V getValue() {
+            return getReflectively(index);
         }
 
-        /**
-         * Returns an iterator over the elements contained in this collection.
-         */
-        @Override
-        public final Iterator<Map.Entry<String,V>> iterator() {
-            return PropertyMap.this.iterator();
+        /** Replaces the value corresponding to this entry with the specified value. */
+        @Override public V setValue(final V value) {
+            return setReflectively(index, value);
         }
     }
 }
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TypeMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TypeMap.java
index 034a58d9ac..4969a02e1a 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TypeMap.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TypeMap.java
@@ -16,10 +16,6 @@
  */
 package org.apache.sis.metadata;
 
-import java.util.Map;
-import java.util.Iterator;
-import java.util.NoSuchElementException;
-
 
 /**
  * Map of property valuePolicy for a given implementation class. This map is read-only.
@@ -32,7 +28,7 @@ final class TypeMap extends PropertyMap<Class<?>> {
     /**
      * The kind of values in this map.
      */
-    final TypeValuePolicy valuePolicy;
+    private final TypeValuePolicy valuePolicy;
 
     /**
      * Creates a type map for the specified accessor.
@@ -47,31 +43,10 @@ final class TypeMap extends PropertyMap<Class<?>> {
     }
 
     /**
-     * Returns the value to which the specified key is mapped, or {@code null}
-     * if this map contains no mapping for the key.
-     */
-    @Override
-    public Class<?> get(final Object key) {
-        if (key instanceof String) {
-            return accessor.type(accessor.indexOf((String) key, false), valuePolicy);
-        }
-        return null;
-    }
-
-    /**
-     * Returns an iterator over the entries contained in this map.
+     * Returns the type for the property at the specified index.
      */
     @Override
-    final Iterator<Map.Entry<String,Class<?>>> iterator() {
-        return new Iter() {
-            @Override
-            public Map.Entry<String,Class<?>> next() {
-                if (index >= accessor.count()) {
-                    throw new NoSuchElementException();
-                }
-                final Class<?> value = accessor.type(index, valuePolicy);
-                return new SimpleImmutableEntry<>(accessor.name(index++, keyPolicy), value);
-            }
-        };
+    final Class<?> getReflectively(final int index) {
+        return accessor.type(index, valuePolicy);
     }
 }
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ValueMap.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ValueMap.java
index 5beb484e89..2605d3eb81 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ValueMap.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ValueMap.java
@@ -17,10 +17,7 @@
 package org.apache.sis.metadata;
 
 import java.util.Map;
-import java.util.Set;
 import java.util.Iterator;
-import java.util.NoSuchElementException;
-import org.apache.sis.util.internal.AbstractMapEntry;
 
 import static org.apache.sis.metadata.PropertyAccessor.RETURN_NULL;
 import static org.apache.sis.metadata.PropertyAccessor.RETURN_PREVIOUS;
@@ -50,7 +47,7 @@ final class ValueMap extends PropertyMap<Object> {
      *
      * @param metadata     the metadata object to wrap.
      * @param accessor     the accessor to use for the metadata.
-     * @param keyPolicy    determines the string representation of keys in the map..
+     * @param keyPolicy    determines the string representation of keys in the map.
      * @param valuePolicy  the behavior of this map toward null or empty values.
      */
     ValueMap(final Object metadata, final PropertyAccessor accessor,
@@ -78,26 +75,23 @@ final class ValueMap extends PropertyMap<Object> {
     }
 
     /**
-     * Returns {@code true} if this map contains a mapping for the specified key.
+     * Returns {@code true} if this map contains a mapping for the property at the specified index.
      */
     @Override
-    public boolean containsKey(final Object key) {
-        return get(key) != null;
+    final boolean contains(final int index) {
+        if (valuePolicy == ValueExistencePolicy.ALL) {
+            return super.contains(index);
+        }
+        return !valuePolicy.isSkipped(accessor.get(index, metadata));
     }
 
     /**
-     * Returns the value to which the specified key is mapped, or {@code null}
-     * if this map contains no mapping for the key.
+     * Returns the value for the property at the specified index.
      */
     @Override
-    public Object get(final Object key) {
-        if (key instanceof String) {
-            final Object value = accessor.get(accessor.indexOf((String) key, false), metadata);
-            if (!valuePolicy.isSkipped(value)) {
-                return value;
-            }
-        }
-        return null;
+    final Object getReflectively(final int index) {
+        final Object value = accessor.get(index, metadata);
+        return valuePolicy.isSkipped(value) ? null : value;
     }
 
     /**
@@ -108,8 +102,8 @@ final class ValueMap extends PropertyMap<Object> {
      * @throws UnmodifiableMetadataException if the property for the given key is read-only.
      */
     @Override
-    public Object put(final String key, final Object value) {
-        final Object old = accessor.set(accessor.indexOf(key, true), metadata, value, RETURN_PREVIOUS);
+    final Object setReflectively(final int index, final Object value) {
+        final Object old = accessor.set(index, metadata, value, RETURN_PREVIOUS);
         return valuePolicy.isSkipped(old) ? null : old;
     }
 
@@ -146,205 +140,11 @@ final class ValueMap extends PropertyMap<Object> {
         }
     }
 
-    /**
-     * Removes the mapping for a key from this map if it is present.
-     *
-     * @throws UnmodifiableMetadataException if the property for the given key is read-only.
-     */
-    @Override
-    public Object remove(final Object key) throws UnsupportedOperationException {
-        if (key instanceof String) {
-            final Object old = accessor.set(accessor.indexOf((String) key, false), metadata, null, RETURN_PREVIOUS);
-            if (!valuePolicy.isSkipped(old)) {
-                return old;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Returns a view of the mappings contained in this map.
-     */
-    @Override
-    public Set<Map.Entry<String,Object>> entrySet() {
-        if (entrySet == null) {
-            entrySet = new Entries();
-        }
-        return entrySet;
-    }
-
     /**
      * Returns an iterator over the entries contained in this map.
      */
     @Override
     final Iterator<Map.Entry<String,Object>> iterator() {
-        return new Iter();
-    }
-
-
-
-
-    /**
-     * A map entry for a given property.
-     *
-     * @author  Martin Desruisseaux (Geomatys)
-     */
-    final class Property extends AbstractMapEntry<String,Object> {
-        /**
-         * The property index.
-         */
-        final int index;
-
-        /**
-         * Creates an entry for the property at the given index.
-         */
-        Property(final int index) {
-            this.index = index;
-        }
-
-        /**
-         * Returns the key corresponding to this entry.
-         */
-        @Override
-        public String getKey() {
-            return accessor.name(index, keyPolicy);
-        }
-
-        /**
-         * Returns value type as declared in the interface method signature.
-         * It may be a primitive type.
-         */
-        public Class<?> getValueType() {
-            return accessor.type(index, TypeValuePolicy.PROPERTY_TYPE);
-        }
-
-        /**
-         * Returns the value corresponding to this entry.
-         */
-        @Override
-        public Object getValue() {
-            final Object value = accessor.get(index, metadata);
-            return valuePolicy.isSkipped(value) ? null : value;
-        }
-
-        /**
-         * Replaces the value corresponding to this entry with the specified value.
-         *
-         * @throws ClassCastException if the given value is not of the expected type.
-         * @throws UnmodifiableMetadataException if the property for this entry is read-only.
-         */
-        @Override
-        public Object setValue(final Object value) {
-            return accessor.set(index, metadata, value, RETURN_PREVIOUS);
-        }
-    }
-
-
-
-
-    /**
-     * The iterator over the {@link Property} elements contained in an {@link Entries} set.
-     *
-     * @author  Martin Desruisseaux (Geomatys)
-     */
-    private final class Iter implements Iterator<Map.Entry<String,Object>> {
-        /**
-         * The current and the next property, or {@code null} if the iteration is over.
-         */
-        private Property current, next;
-
-        /**
-         * Creates en iterator.
-         */
-        Iter() {
-            move(0);
-        }
-
-        /**
-         * Moves {@link #next} to the first property with a valid value,
-         * starting at the specified index.
-         */
-        private void move(int index) {
-            final int count = accessor.count();
-            while (index < count) {
-                if (!valuePolicy.isSkipped(accessor.get(index, metadata))) {
-                    next = new Property(index);
-                    return;
-                }
-                index++;
-            }
-            next = null;
-        }
-
-        /**
-         * Returns {@code true} if the iteration has more elements.
-         */
-        @Override
-        public boolean hasNext() {
-            return next != null;
-        }
-
-        /**
-         * Returns the next element in the iteration.
-         */
-        @Override
-        public Map.Entry<String,Object> next() {
-            if (next != null) {
-                current = next;
-                move(next.index + 1);
-                return current;
-            } else {
-                throw new NoSuchElementException();
-            }
-        }
-
-        /**
-         * Removes from the underlying collection the last element returned by the iterator.
-         *
-         * @throws UnmodifiableMetadataException if the property for this entry is read-only.
-         */
-        @Override
-        public void remove() {
-            if (current != null) {
-                current.setValue(null);
-                current = null;
-            } else {
-                throw new IllegalStateException();
-            }
-        }
-    }
-
-
-
-
-    /**
-     * View of the entries contained in the map.
-     *
-     * @author  Martin Desruisseaux (Geomatys)
-     */
-    private final class Entries extends PropertyMap<Object>.Entries {
-        /**
-         * Creates an entry set.
-         */
-        Entries() {
-        }
-
-        /**
-         * Returns {@code true} if this collection contains the specified element.
-         */
-        @Override
-        public boolean contains(final Object object) {
-            if (object instanceof Map.Entry<?,?>) {
-                final Map.Entry<?,?> entry = (Map.Entry<?,?>) object;
-                final Object key = entry.getKey();
-                if (key instanceof String) {
-                    final int index = accessor.indexOf((String) key, false);
-                    if (index >= 0) {
-                        return new Property(index).equals(entry);
-                    }
-                }
-            }
-            return false;
-        }
+        return new ReflectiveIterator();
     }
 }
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/ISOMetadata.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/ISOMetadata.java
index b92afbecec..bb0b40ad4c 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/ISOMetadata.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/ISOMetadata.java
@@ -220,7 +220,7 @@ public class ISOMetadata extends ModifiableMetadata implements IdentifiedObject,
     // --------------------------------------------------------------------------------------
 
     /**
-     * {@inheritDoc}
+     * @hidden
      */
     @Override
     public boolean transitionTo(final State target) {
diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/NilReasonMapTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/NilReasonMapTest.java
new file mode 100644
index 0000000000..a45f7afb5f
--- /dev/null
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/NilReasonMapTest.java
@@ -0,0 +1,102 @@
+/*
+ * 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.Map;
+import org.opengis.metadata.citation.Citation;
+import org.apache.sis.metadata.iso.citation.DefaultCitation;
+import org.apache.sis.xml.NilReason;
+import org.apache.sis.xml.NilObject;
+
+// Test dependencies
+import org.junit.Test;
+import org.apache.sis.test.TestCase;
+import org.apache.sis.test.DependsOn;
+import org.apache.sis.util.SimpleInternationalString;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.apache.sis.test.TestUtilities.getSingleton;
+
+
+/**
+ * Tests the {@link NilReasonMap} class.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+@DependsOn(PropertyAccessorTest.class)
+public final class NilReasonMapTest extends TestCase {
+    /**
+     * Creates a new test case.
+     */
+    public NilReasonMapTest() {
+    }
+
+    /**
+     * Creates a metadata instance to use for the tests.
+     * This metadata has a mandatory property, which is the citation title.
+     *
+     * @param  title  a value for the mandatory property, of {@code null} if none.
+     */
+    private static DefaultCitation metadata(final String title) {
+        final var citation = new DefaultCitation(title);
+        citation.getAlternateTitles().add(new SimpleInternationalString("Another title"));
+        return citation;
+    }
+
+    /**
+     * Tests a map with no
+     */
+    @Test
+    public void testCitation() {
+        final DefaultCitation citation = metadata("A title");
+        final Map<String,NilReason> map = MetadataStandard.ISO_19115.asNilReasonMap(citation, Citation.class, KeyNamePolicy.UML_IDENTIFIER);
+        assertTrue(map.isEmpty());
+        /*
+         * Sets the edition date to a nil value.
+         */
+        assertNull (map.get        ("editionDate"));
+        assertFalse(map.containsKey("editionDate"));
+        assertNull (map.put        ("editionDate", NilReason.TEMPLATE));
+        assertFalse(map.isEmpty());
+        assertEquals(1, map.size());
+        assertEquals("editionDate",      getSingleton(map.keySet()));
+        assertEquals(NilReason.TEMPLATE, getSingleton(map.values()));
+        assertEquals(NilReason.TEMPLATE, map.put("editionDate", null));
+        /*
+         * Set the title to nil value instead of edition date.
+         */
+        assertTrue  (map.isEmpty());
+        assertFalse (map.containsKey("title"));
+        assertNull  (map.put("title", NilReason.MISSING));
+        assertTrue  (map.containsKey("title"));
+        assertEquals(NilReason.MISSING, map.get("title"));
+        assertEquals(1, map.size());
+        assertEquals("title",           getSingleton(map.keySet()));
+        assertEquals(NilReason.MISSING, getSingleton(map.values()));
+        assertInstanceOf(NilObject.class, citation.getTitle());
+        /*
+         * Even if we clear the citation title, because that property is mandatory,
+         * it should still be present in the map of nil reasons.
+         */
+        assertEquals(NilReason.MISSING, map.put("title", null));
+        assertNull  (citation.getTitle());
+        assertTrue  (map.containsKey("title"));
+        assertEquals(1, map.size());
+        assertEquals("title", getSingleton(map.keySet()));
+        assertNull  (         getSingleton(map.values()));
+    }
+}


(sis) 01/02: Remove `NilDate`. It was not working because this class is mutable, so metadata make copies of it and those copies were not `NilObject`.

Posted by de...@apache.org.
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 dfc7aff69ff1c417cdf6018eaccca8ff53c22b92
Author: Martin Desruisseaux <ma...@geomatys.com>
AuthorDate: Sun Nov 19 20:04:05 2023 +0100

    Remove `NilDate`. It was not working because this class is mutable,
    so metadata make copies of it and those copies were not `NilObject`.
---
 .../main/org/apache/sis/xml/NilDate.java           | 69 ----------------------
 .../main/org/apache/sis/xml/NilReason.java         | 34 ++++++++---
 .../test/org/apache/sis/xml/NilReasonTest.java     | 13 ----
 3 files changed, 27 insertions(+), 89 deletions(-)

diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilDate.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilDate.java
deleted file mode 100644
index 047603685f..0000000000
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilDate.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * 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.xml;
-
-import java.util.Date;
-import java.io.ObjectStreamException;
-
-
-/**
- * An empty {@code Date} which is nil for the given reason.
- *
- * @author  Martin Desruisseaux (Geomatys)
- */
-final class NilDate extends Date implements NilObject {
-    /**
-     * For cross-version compatibility.
-     */
-    private static final long serialVersionUID = 4374532826187673813L;
-
-    /**
-     * The reason why the object is nil.
-     */
-    private final NilReason reason;
-
-    /**
-     * Creates a new international string which is nil for the given reason.
-     */
-    NilDate(final NilReason reason) {
-        super(0);
-        this.reason = reason;
-    }
-
-    /**
-     * Returns the reason why this object is nil.
-     */
-    @Override
-    public NilReason getNilReason() {
-        return reason;
-    }
-
-    /**
-     * Unconditionally returns en empty string.
-     */
-    @Override
-    public String toString() {
-        return "";
-    }
-
-    /**
-     * Invoked on deserialization for replacing the deserialized instance by the unique instance.
-     */
-    private Object readResolve() throws ObjectStreamException {
-        return reason.createNilObject(Date.class);
-    }
-}
diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilReason.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilReason.java
index 25310a03be..39bc587a38 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilReason.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/NilReason.java
@@ -16,8 +16,8 @@
  */
 package org.apache.sis.xml;
 
-import java.util.Date;
 import java.util.Map;
+import java.util.Set;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.io.Serializable;
@@ -413,6 +413,21 @@ public final class NilReason implements Serializable {
         return false;
     }
 
+    /**
+     * Returns whether the given class is accepted by {@code createNilObject(Class)}.
+     * The set of accepted types is implementation-dependent and may change in any future Apache SIS version.
+     *
+     * @param  type  the class or interface to test, or {@code null}.
+     * @return {@code true} if the given type is non-null and is accepted by {@link #createNilObject(Class)}.
+     *
+     * @since 1.5
+     */
+    public static boolean isSupported(final Class<?> type) {
+        if (type == null) return false;
+        if (type.isInterface()) return !NilObjectHandler.isIgnoredInterface(type);
+        return ACCEPTED_CLASSES.contains(type);
+    }
+
     /**
      * Returns an object of the given type which is nil for the reason represented by this instance.
      * The {@code type} argument can be one of the following cases:
@@ -428,7 +443,7 @@ public final class NilReason implements Serializable {
      *             {@code 0} or {@code false}, in this preference order, depending on the method return type.</li>
      *       </ul>
      *   </li>
-     *   <li>One of {@link Float}, {@link Double}, {@link String}, {@link URI} or {@link Date} types.
+     *   <li>One of {@link Float}, {@link Double}, {@link String} or {@link URI} types.
      *       In such case, this method returns an instance which will be recognized as "nil" by the XML marshaller.</li>
      * </ul>
      *
@@ -493,11 +508,18 @@ public final class NilReason implements Serializable {
         return type.cast(object);
     }
 
+    /**
+     * Classes that are handled in a special way by {@link #createNilInstance(Class)}.
+     */
+    private static final Set<Class<?>> ACCEPTED_CLASSES = Set.of(
+            Double.class, Float.class, String.class, URI.class);
+
     /**
      * Returns a new {@code Float}, {@code Double} or {@code String} instance to be considered as a nil value.
      *
-     * <p><b>Reminder:</b> If more special cases are added, do not forget to update the {@link #forObject(Object)}
-     * method and to update the {@link #createNilObject(Class)} and {@link #forObject(Object)} javadoc.</p>
+     * <p><b>Reminder:</b> If more special cases are added,
+     * do not forget to update the {@link #forObject(Object)} method and the {@link #ACCEPTED_CLASSES} set,
+     * then to update the {@link #createNilObject(Class)} and {@link #forObject(Object)} documentation.</p>
      *
      * @throws IllegalArgumentException if the given type is not a supported type.
      *
@@ -512,8 +534,6 @@ public final class NilReason implements Serializable {
             object = new String("");                // REALLY need a new instance.
         } else if (type == URI.class) {
             object = URI.create("");                // Really need a new instance.
-        } else if (type == Date.class) {
-            object = new NilDate(this);
         } else {
             throw new IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, "type", type));
         }
@@ -529,7 +549,7 @@ public final class NilReason implements Serializable {
      *   <li>If the given object implements the {@link NilObject} interface, then this method delegates
      *       to the {@link NilObject#getNilReason()} method.</li>
      *   <li>Otherwise if the given object is one of the {@link Float}, {@link Double}, {@link String},
-     *       {@link URI} or {@link Date} instances
+     *       or {@link URI} instances
      *       returned by {@link #createNilObject(Class)}, then this method returns the associated reason.</li>
      *   <li>Otherwise this method returns {@code null}.</li>
      * </ul>
diff --git a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NilReasonTest.java b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NilReasonTest.java
index 513ded6721..1d86c8b4dc 100644
--- a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NilReasonTest.java
+++ b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/NilReasonTest.java
@@ -17,7 +17,6 @@
 package org.apache.sis.xml;
 
 import java.net.URI;
-import java.util.Date;
 import java.net.URISyntaxException;
 import org.opengis.util.InternationalString;
 import org.opengis.metadata.citation.Citation;
@@ -175,18 +174,6 @@ public final class NilReasonTest extends TestCase {
         assertSame(value, NilReason.MISSING.createNilObject(InternationalString.class), "Expected cached value.");
     }
 
-    /**
-     * Tests {@link NilReason#createNilObject(Class)} for a date.
-     */
-    @Test
-    public void testCreateNilDate() {
-        final Date value = NilReason.TEMPLATE.createNilObject(Date.class);
-        assertEquals("", value.toString());
-        assertInstanceOf(NilObject.class, value);
-        assertSame(NilReason.TEMPLATE, NilReason.forObject(value));
-        assertSame(value, NilReason.TEMPLATE.createNilObject(Date.class), "Expected cached value.");
-    }
-
     /**
      * Tests {@link NilReason#createNilObject(Class)} for an URI.
      */