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 <martin.desruisse...@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()));
+    }
+}

Reply via email to