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 49818875864c8f3d94acfb2309c33eaa1aac0b7e
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sat Nov 18 16:18:32 2023 +0100

    Add "Obligation" and "Nil reason" columns in metadata `TreeTableView`.
---
 .../org/apache/sis/metadata/AbstractMetadata.java  |  33 ++---
 .../org/apache/sis/metadata/MetadataColumn.java    |  67 +++++++++
 .../org/apache/sis/metadata/MetadataStandard.java  |  39 ++++--
 .../org/apache/sis/metadata/PropertyAccessor.java  |  17 +++
 .../main/org/apache/sis/metadata/TreeNode.java     | 155 +++++++++++++++------
 .../org/apache/sis/metadata/TreeNodeChildren.java  |  64 +++++----
 .../org/apache/sis/metadata/TreeTableView.java     |  47 ++++---
 .../apache/sis/metadata/ValueExistencePolicy.java  |  12 ++
 .../main/org/apache/sis/metadata/package-info.java |  23 +--
 .../org/apache/sis/metadata/TreeTableViewTest.java |  79 ++++++++++-
 .../apache/sis/util/collection/TableColumn.java    |  29 ++--
 .../org/apache/sis/util/resources/Vocabulary.java  |   5 +
 .../sis/util/resources/Vocabulary.properties       |   1 +
 .../sis/util/resources/Vocabulary_fr.properties    |   1 +
 netbeans-project/nbproject/project.xml             |   3 +
 15 files changed, 435 insertions(+), 140 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 49517f0c0c..5e5ac1ffcc 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
@@ -69,7 +69,7 @@ import org.apache.sis.util.collection.TreeTable;
  * use a single lock for the whole metadata tree (including children).
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see MetadataStandard
  *
@@ -150,13 +150,10 @@ public abstract class AbstractMetadata implements 
LenientComparable, Emptiable {
     }
 
     /**
-     * Returns a view of the property values in a {@link Map}. The map is 
backed by this metadata
-     * object, so changes in the underlying metadata object are immediately 
reflected in the map
-     * and conversely.
-     *
-     * <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.
+     * Returns a view of the property values in a {@link Map}. The map is 
backed by this metadata object,
+     * so changes in the underlying metadata object are immediately reflected 
in the map and conversely.
+     * 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)}.
      *
      * <h4>Keys and values</h4>
@@ -179,9 +176,7 @@ public abstract class AbstractMetadata implements 
LenientComparable, Emptiable {
      * The default implementation is equivalent to the following:
      *
      * {@snippet lang="java" :
-     *     return getStandard().asValueMap(this, null,
-     *             KeyNamePolicy.JAVABEANS_PROPERTY,
-     *             ValueExistencePolicy.NON_EMPTY);
+     *     return getStandard().asValueMap(this, null, 
KeyNamePolicy.JAVABEANS_PROPERTY, ValueExistencePolicy.NON_EMPTY);
      *     }
      *
      * @return a view of this metadata object as a map.
@@ -197,7 +192,11 @@ public abstract class AbstractMetadata implements 
LenientComparable, Emptiable {
      * The tree table is backed by the metadata object using Java reflection, 
so changes in the
      * underlying metadata object are immediately reflected in the tree table 
and conversely.
      *
-     * <p>The returned {@code TreeTable} instance contains the following 
columns:</p>
+     * <p>The returned {@code TreeTable} instance contains the columns listed 
below.
+     * The {@code (IDENTIFIER, INDEX)} pair of columns can be used as a 
primary key for uniquely identifying
+     * a node in a list of children. That uniqueness is guaranteed only for 
the children of a given node.
+     * The same keys may appear in the children of any other nodes.</p>
+     *
      * <ul class="verbose">
      *   <li>{@link org.apache.sis.util.collection.TableColumn#IDENTIFIER}<br>
      *       The {@linkplain org.opengis.annotation.UML#identifier() UML 
identifier} if any,
@@ -209,12 +208,7 @@ public abstract class AbstractMetadata implements 
LenientComparable, Emptiable {
      *       If the metadata property is a collection, then the zero-based 
index of the element in that collection.
      *       Otherwise {@code null}. For example, in a tree table view of 
{@code DefaultCitation}, if the
      *       {@code "alternateTitle"} collection contains two elements, then 
there is a node with index 0
-     *       for the first element and another node with index 1 for the 
second element.
-     *
-     *       <div class="note"><b>Note:</b>
-     *       The {@code (IDENTIFIER, INDEX)} pair can be used as a primary key 
for uniquely identifying a node
-     *       in a list of children. That uniqueness is guaranteed only for the 
children of a given node;
-     *       the same keys may appear in the children of any other 
nodes.</div></li>
+     *       for the first element and another node with index 1 for the 
second element.</li>
      *
      *   <li>{@link org.apache.sis.util.collection.TableColumn#NAME}<br>
      *       A human-readable name for the node, derived from the identifier 
and the index.
@@ -224,6 +218,9 @@ public abstract class AbstractMetadata implements 
LenientComparable, Emptiable {
      *   <li>{@link org.apache.sis.util.collection.TableColumn#TYPE}<br>
      *       The base type of the value (usually an interface).</li>
      *
+     *   <li>{@link org.apache.sis.util.collection.TableColumn#OBLIGATION}<br>
+     *       Whether the property is mandatory, optional or conditional.</li>
+     *
      *   <li>{@link org.apache.sis.util.collection.TableColumn#VALUE}<br>
      *       The metadata value for the node. Values in this column are 
writable if the underlying
      *       metadata class have a setter method for the property represented 
by the node.</li>
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataColumn.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataColumn.java
new file mode 100644
index 0000000000..1264826941
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataColumn.java
@@ -0,0 +1,67 @@
+/*
+ * 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.io.Serializable;
+import java.io.ObjectStreamException;
+import org.apache.sis.xml.NilReason;
+import org.apache.sis.util.resources.Vocabulary;
+import org.apache.sis.util.collection.TableColumn;
+
+
+/**
+ * A tree table column specific to the metadata module.
+ * Defined as a class for allowing serialization.
+ *
+ * @param <V>  base type of all values in the column identified by this 
instance.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see TreeTableView
+ */
+final class MetadataColumn<V> extends TableColumn<V> implements Serializable {
+    /**
+     * For cross-version compatibility.
+     */
+    private static final long serialVersionUID = 8256073324266678871L;
+
+    /**
+     * Table column for the reason why a mandatory property value is absent.
+     */
+    public static final MetadataColumn<NilReason> NIL_REASON =
+            new MetadataColumn<>(NilReason.class, Vocabulary.Keys.NilReason);
+
+    /**
+     * Creates a new column header.
+     *
+     * @param type  base type of all values in the column identified by this 
instance.
+     * @param key   resource key of the localized text to use as the column 
header.
+     */
+    private MetadataColumn(final Class<V> type, final short key) {
+        super(type, Vocabulary.formatInternational(key));
+    }
+
+    /**
+     * Invoked on deserialization for resolving this instance to one of the 
predefined constants.
+     *
+     * @return one of the predefined constants.
+     * @throws InvalidObjectException if this instance cannot be resolved.
+     */
+    private Object readResolve() throws ObjectStreamException {
+        return NIL_REASON;      // For now this is the only column.
+    }
+}
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 43952c8d15..cd0d10aeea 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
@@ -32,13 +32,15 @@ import org.opengis.metadata.citation.Citation;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.collection.CheckedContainer;
+import org.apache.sis.util.internal.Strings;
 import org.apache.sis.system.Configuration;
 import org.apache.sis.system.Modules;
 import org.apache.sis.system.Semaphores;
 import org.apache.sis.system.SystemListener;
 import org.apache.sis.metadata.simple.SimpleCitation;
-import org.apache.sis.util.internal.Strings;
+import org.apache.sis.xml.NilReason;
 
 import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
 import static org.apache.sis.util.ArgumentChecks.ensureNonNullElement;
@@ -89,7 +91,7 @@ import static 
org.apache.sis.util.ArgumentChecks.ensureNonNullElement;
  * by a large amount of {@link ModifiableMetadata}.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see AbstractMetadata
  *
@@ -898,38 +900,45 @@ public class MetadataStandard implements Serializable {
      * The tree table is backed by the metadata object using Java reflection, 
so changes in the
      * underlying metadata object are immediately reflected in the tree table 
and conversely.
      *
-     * <p>The returned {@code TreeTable} instance contains the following 
columns:</p>
+     * <p>The returned {@code TreeTable} instance contains the columns listed 
below.
+     * The {@code (IDENTIFIER, INDEX)} pair of columns can be used as a 
primary key for uniquely identifying
+     * a node in a list of children. That uniqueness is guaranteed only for 
the children of a given node.
+     * The same keys may appear in the children of any other nodes.</p>
+     *
      * <ul class="verbose">
-     *   <li>{@link org.apache.sis.util.collection.TableColumn#IDENTIFIER}<br>
+     *   <li>{@link TableColumn#IDENTIFIER}<br>
      *       The {@linkplain org.opengis.annotation.UML#identifier() UML 
identifier} if any,
      *       or the Java Beans property name otherwise, of a metadata 
property. For example
      *       in a tree table view of {@link 
org.apache.sis.metadata.iso.citation.DefaultCitation},
      *       there is a node having the {@code "title"} identifier.</li>
      *
-     *   <li>{@link org.apache.sis.util.collection.TableColumn#INDEX}<br>
+     *   <li>{@link TableColumn#INDEX}<br>
      *       If the metadata property is a collection, then the zero-based 
index of the element in that collection.
      *       Otherwise {@code null}. For example, in a tree table view of 
{@code DefaultCitation}, if the
      *       {@code "alternateTitle"} collection contains two elements, then 
there is a node with index 0
-     *       for the first element and another node with index 1 for the 
second element.
+     *       for the first element and another node with index 1 for the 
second element.</li>
      *
-     *       <div class="note"><b>Note:</b>
-     *       The {@code (IDENTIFIER, INDEX)} pair can be used as a primary key 
for uniquely identifying a node
-     *       in a list of children. That uniqueness is guaranteed only for the 
children of a given node;
-     *       the same keys may appear in the children of any other 
nodes.</div></li>
-     *
-     *   <li>{@link org.apache.sis.util.collection.TableColumn#NAME}<br>
+     *   <li>{@link TableColumn#NAME}<br>
      *       A human-readable name for the node, derived from the identifier 
and the index.
      *       This is the column shown in the default {@link #toString()} 
implementation and
      *       may be localizable.</li>
      *
-     *   <li>{@link org.apache.sis.util.collection.TableColumn#TYPE}<br>
+     *   <li>{@link TableColumn#TYPE}<br>
      *       The base type of the value (usually an interface).</li>
      *
-     *   <li>{@link org.apache.sis.util.collection.TableColumn#VALUE}<br>
+     *   <li>{@link TableColumn#OBLIGATION}<br>
+     *       Whether the property is mandatory, optional or conditional.</li>
+     *
+     *   <li>{@link TableColumn#VALUE}<br>
      *       The metadata value for the node. Values in this column are 
writable if the underlying
      *       metadata class have a setter method for the property represented 
by the node.</li>
      *
-     *   <li>{@link org.apache.sis.util.collection.TableColumn#REMARKS}<br>
+     *   <li>{@code NIL_REASON}<br>
+     *       If the property is mandatory and nevertheless absent, the reason 
why.
+     *       This column is included only if {@code valuePolicy} accepts nil 
values.
+     *       Values are instances of {@link NilReason}.</li>
+     *
+     *   <li>{@link TableColumn#REMARKS}<br>
      *       Remarks or warning on the property value. This is rarely present.
      *       It is provided when the value may look surprising, for example 
the longitude values
      *       in a geographic bounding box crossing the anti-meridian.</li>
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 5f30786d4e..f753bcbd3b 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
@@ -26,6 +26,7 @@ import java.util.Collection;
 import java.lang.reflect.Method;
 import java.lang.reflect.InvocationTargetException;
 import org.opengis.annotation.UML;
+import org.opengis.annotation.Obligation;
 import org.opengis.metadata.ExtendedElementInformation;
 import org.opengis.metadata.citation.Citation;
 import org.apache.sis.util.Classes;
@@ -518,6 +519,22 @@ class PropertyAccessor {
         return index;
     }
 
+    /**
+     * Returns whether the property at the given index is mandatory, optional 
or conditional.
+     *
+     * @param  index  the index of the property for which to get the 
obligation.
+     * @return the obligation at the given index, or {@code null} if none or 
if the index is out of bounds.
+     */
+    final Obligation obligation(final int index) {
+        if (index >= 0 && index < names.length) {
+            final UML uml = getters[index].getAnnotation(UML.class);
+            if (uml != null) {
+                return uml.obligation();
+            }
+        }
+        return null;
+    }
+
     /**
      * Returns the name of the property at the given index, or {@code null} if 
none.
      *
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TreeNode.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TreeNode.java
index b6c29f80f6..41c4b0c3f8 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TreeNode.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TreeNode.java
@@ -25,7 +25,10 @@ import java.util.Objects;
 import java.util.NoSuchElementException;
 import java.util.ConcurrentModificationException;
 import java.util.function.Function;
+import org.opengis.annotation.Obligation;
+import org.apache.sis.xml.NilReason;
 import org.apache.sis.xml.bind.lan.LocaleAndCharset;
+import org.apache.sis.util.Debug;
 import org.apache.sis.util.Classes;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArgumentChecks;
@@ -40,13 +43,12 @@ import org.apache.sis.util.resources.Vocabulary;
 
 
 /**
- * A node in a {@link TreeTableView} view. The {@code TreeTableView} class is 
used directly
- * only for the root node, or for nodes containing a fixed value instead of a 
value fetched from
- * the metadata object. For all other nodes, the actual node class shall be 
either {@link Element}
- * or {@link CollectionElement}.
+ * A node in a {@link TreeTableView} view. The {@code TreeNode} base class is 
used directly only for the root node,
+ * or for nodes containing a fixed value instead of a value fetched from the 
metadata object. For all other nodes,
+ * the actual node class shall be either {@link Element} or {@link 
CollectionElement}.
  *
  * <p>The value of a node is extracted from the {@linkplain #metadata} object 
by {@link #getUserObject()}.
- * For each instance of {@code TreeTableView}, that value is always a 
singleton, never a collection.
+ * For each instance of {@code TreeNode}, that value is always a singleton, 
never a collection.
  * If a metadata property is a collection, then there is an instance of the 
{@link CollectionElement}
  * subclass for each element in the collection.</p>
  *
@@ -54,7 +56,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * set the identifier and the value, in that order, before any other operation 
on the new child.
  * See {@code newChild()} javadoc for an example.</p>
  *
- * <div class="note"><b>API note:</b>
+ * <h2>API note</h2>
  * This class is not serializable because the values of the {@link 
Element#indexInData}
  * and {@link CollectionElement#indexInList} fields may not be stable.
  * The former may be invalid if the node is serialized and deserialized by two 
different versions of Apache SIS
@@ -66,8 +68,8 @@ import org.apache.sis.util.resources.Vocabulary;
  */
 class TreeNode implements Node {
     /**
-     * The collection of {@linkplain #children} to return when the node does 
not allow children
-     * (i.e. is a leaf). This constant is also used as a sentinel value by 
{@link #isLeaf()}.
+     * The collection of {@linkplain #children} to return when the node does 
not allow children.
+     * This constant is also used as a sentinel value by {@link #isLeaf()}.
      *
      * <p>We choose an empty set instead of an empty list because {@link 
TreeNodeChildren}
      * does not implement the {@link List} interface. So we are better to 
never give to the user
@@ -76,8 +78,8 @@ class TreeNode implements Node {
     private static final Collection<Node> LEAF = Set.of();
 
     /**
-     * The table for which this node is an element. Contains information like
-     * the metadata standard and the value existence policy.
+     * The table for which this node is an element.
+     * Contains information like the metadata standard and the value existence 
policy.
      *
      * <p>All {@code TreeNode} instances in the same tree have
      * a reference to the same {@code TreeTableView} instance.</p>
@@ -148,9 +150,20 @@ class TreeNode implements Node {
      * twice in common situations like the {@link TreeTableView#toString()} 
implementation or in
      * Graphical User Interface. However, we may remove this field in any 
future SIS version if
      * experience shows that it is more problematic than helpful.</p>
+     *
+     * @see #getNonNilValue()
      */
     transient Object cachedValue;
 
+    /**
+     * Whether {@link #cachedValue} can be used for the value of {@link 
TableColumn#VALUE}.
+     * This flag is set to {@code true} only by the {@link TreeNodeChildren} 
iterator,
+     * thus allowing the use of cached value in the {@code VALUE} column only 
after
+     * a call to {@link Iterator#next()} (for opportunistic reason), and only 
once.
+     * This restriction does not apply to {@link MetadataColumn#NIL_REASON}.
+     */
+    transient boolean canUseCache;
+
     /**
      * Creates the root node of a new metadata tree table.
      *
@@ -200,6 +213,20 @@ class TreeNode implements Node {
         return new CacheKey(metadata.getClass(), baseType);
     }
 
+    /**
+     * Appends an identifier for this node in the given buffer, for {@link 
#toString()} implementation.
+     * The appended value is similar to the value returned by {@link 
#getIdentifier()} (except for the
+     * root node), but may contains additional information like the index in a 
collection.
+     *
+     * <p>The default implementation is suitable only for the root node - 
subclasses must override.</p>
+     *
+     * @param  buffer  the buffer where to complete the {@link #toString()} 
representation.
+     */
+    @Debug
+    void appendIdentifier(final StringBuilder buffer) {
+        buffer.append(Classes.getShortClassName(metadata));
+    }
+
     /**
      * Returns the UML identifier defined by the standard. The default 
implementation is suitable
      * only for the root node, since it returns the class identifier. 
Subclasses must override in
@@ -232,6 +259,13 @@ class TreeNode implements Node {
                 table.standard.getInterface(key()))).toString();
     }
 
+    /**
+     * Gets whether the property is mandatory, optional or conditional, or 
{@code null} if unspecified.
+     */
+    Obligation getObligation() {
+        return null;
+    }
+
     /**
      * Gets remarks about the value in this node, or {@code null} if none.
      */
@@ -240,14 +274,36 @@ class TreeNode implements Node {
     }
 
     /**
-     * Appends an identifier for this node in the given buffer, for {@link 
#toString()} implementation.
-     * The appended value is similar to the value returned by {@link 
#getIdentifier()} (except for the
-     * root node), but may contains additional information like the index in a 
collection.
-     *
-     * <p>The default implementation is suitable only for the root node - 
subclasses must override.</p>
+     * Gets the reason why the value is missing, or {@code null} if 
unspecified.
+     * Note that this method is expected to always return {@code null} if
+     * {@link ValueExistencePolicy#acceptNilValues()} is {@code false}.
      */
-    void appendIdentifier(final StringBuilder buffer) {
-        buffer.append(Classes.getShortClassName(metadata));
+    private NilReason getNilReason() {
+        // Do not check `canUseCache` because it applies to TableColumn.VALUE.
+        if (cachedValue == null) {
+            cachedValue = getUserObject();
+        }
+        return NilReason.forObject(cachedValue);
+    }
+
+    /**
+     * Returns the property value, excluding nil value and using the cached 
value if available.
+     * Nil value are excluded because the reason why they are nil is reported 
in a separated column.
+     *
+     * <h4>Caching</h4>
+     * The cached value is set by {@link TreeNodeChildren} iterator and used 
only once for
+     * the value in {@link TableColumn#VALUE}. However, the cached value may 
be reused for
+     * the value in {@link MetadataColumn#NIL_REASON}.
+     */
+    private Object getNonNilValue() {
+        if (!canUseCache) {
+            cachedValue = getUserObject();
+        }
+        canUseCache = false;                // Use the cached value only once 
after iteration.
+        if (table.valuePolicy.acceptNilValues() && 
NilReason.forObject(cachedValue) != null) {
+            return null;
+        }
+        return cachedValue;
     }
 
     /**
@@ -356,20 +412,22 @@ class TreeNode implements Node {
         }
 
         /**
-         * The property identifier to be returned in the {@link 
TableColumn#IDENTIFIER} cells.
+         * Appends an identifier for this node in the given buffer, for {@link 
#toString()} implementation.
+         * This method is mostly for debugging purposes and is not used for 
the tree table node values.
          */
+        @Debug
         @Override
-        final String getIdentifier() {
-            return accessor.name(indexInData, KeyNamePolicy.UML_IDENTIFIER);
+        void appendIdentifier(final StringBuilder buffer) {
+            super.appendIdentifier(buffer);
+            buffer.append('.').append(accessor.name(indexInData, 
KeyNamePolicy.JAVABEANS_PROPERTY));
         }
 
         /**
-         * Appends an identifier for this node in the given buffer, for {@link 
#toString()} implementation.
+         * The property identifier to be returned in the {@link 
TableColumn#IDENTIFIER} cells.
          */
         @Override
-        void appendIdentifier(final StringBuilder buffer) {
-            super.appendIdentifier(buffer);
-            buffer.append('.').append(accessor.name(indexInData, 
KeyNamePolicy.JAVABEANS_PROPERTY));
+        final String getIdentifier() {
+            return accessor.name(indexInData, KeyNamePolicy.UML_IDENTIFIER);
         }
 
         /**
@@ -434,6 +492,14 @@ class TreeNode implements Node {
             return type;
         }
 
+        /**
+         * Gets whether the property is mandatory, optional or conditional, or 
{@code null} if unspecified.
+         */
+        @Override
+        Obligation getObligation() {
+            return accessor.obligation(indexInData);
+        }
+
         /**
          * Gets remarks about the value in this node, or {@code null} if none.
          */
@@ -514,7 +580,9 @@ class TreeNode implements Node {
 
         /**
          * Appends an identifier for this node in the given buffer, for {@link 
#toString()} implementation.
+         * This method is mostly for debugging purposes and is not used for 
the tree table node values.
          */
+        @Debug
         @Override
         void appendIdentifier(final StringBuilder buffer) {
             super.appendIdentifier(buffer);
@@ -675,25 +743,21 @@ class TreeNode implements Node {
          * in which case we do not need to check for changes in the underlying 
metadata.
          */
         if (!isLeaf()) {
-            Object value = cachedValue;
+            Object value = getNonNilValue();
             if (value == null) {
-                value = getUserObject();
-                if (value == null) {
-                    /*
-                     * If there is no value, returns an empty set but *do not* 
set `children`
-                     * to that set, in order to allow this method to check 
again the next time
-                     * that this method is invoked.
-                     */
-                    children = null;                                    // Let 
GC do its work.
-                    return LEAF;
-                }
+                /*
+                 * If there is no value, returns an empty set but *do not* set 
`children`
+                 * to that set, in order to allow this method to check again 
the next time
+                 * that this method is invoked.
+                 */
+                children = null;                                    // Let GC 
do its work.
+                return LEAF;
             }
-            cachedValue = null;             // Use the cached value only once 
after iteration.
             /*
              * If there is a value, check if the cached collection is still 
applicable.
              * We verify that the collection is a wrapper for the same 
metadata object.
              * If we need to create a new collection, we know that the 
property accessor
-             * exists otherwise the call to `isLeaf()` above would have 
returned 'true'.
+             * exists otherwise the call to `isLeaf()` above would have 
returned `true`.
              */
             if (children == null || ((TreeNodeChildren) children).metadata != 
value) {
                 PropertyAccessor accessor = table.standard.getAccessor(new 
CacheKey(value.getClass(), baseType), true);
@@ -807,7 +871,7 @@ class TreeNode implements Node {
                     }
                     delegate = siblings.childAt(indexInData, indexInList);
                     /*
-                     * Do not set 'delegate.cachedValue = value', since 
`value` may
+                     * Do not set `delegate.cachedValue = value`, since 
`value` may
                      * have been converted by the setter method to another 
value.
                      */
                     return;
@@ -869,17 +933,17 @@ class TreeNode implements Node {
             }
         } else if (column == TableColumn.VALUE) {
             if (isLeaf()) {
-                value = cachedValue;
-                cachedValue = null;                 // Use the cached value 
only once after iteration.
-                if (value == null) {
-                    value = getUserObject();
-                }
+                value = getNonNilValue();
             } else {
                 final TreeNodeChildren children = getCompactChildren();
                 if (children != null) {
                     value = children.getParentTitle();
                 }
             }
+        } else if (column == MetadataColumn.OBLIGATION) {
+            value = getObligation();
+        } else if (column == MetadataColumn.NIL_REASON) {
+            value = getNilReason();
         } else if (column == TableColumn.REMARKS) {
             value = getRemarks();
         }
@@ -901,11 +965,12 @@ class TreeNode implements Node {
         if (column == TableColumn.VALUE) {
             ArgumentChecks.ensureNonNull("value", value);                      
 // See javadoc.
             cachedValue = null;
+            canUseCache = false;
             final TreeNodeChildren children = getCompactChildren();
             if (children == null || !(children.setParentTitle(value))) {
                 setUserObject(value);
             }
-        } else if (TreeTableView.COLUMNS.contains(column)) {
+        } else if (table.getColumns().contains(column)) {
             throw new 
UnsupportedOperationException(unmodifiableCellValue(column));
         } else {
             throw new 
IllegalArgumentException(Errors.format(Errors.Keys.IllegalArgumentValue_2, 
"column", column));
@@ -942,7 +1007,9 @@ class TreeNode implements Node {
 
     /**
      * Implementation of {@link #toString()} appending the string 
representation in the given buffer.
+     * This method is mostly for debugging purposes and is not used for the 
tree table node values.
      */
+    @Debug
     final void appendStringTo(final StringBuilder buffer) {
         appendIdentifier(buffer.append("Node["));
         buffer.append(" : 
").append(Classes.getShortName(baseType)).append(']');
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TreeNodeChildren.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TreeNodeChildren.java
index 2a30fe6987..98e45d8108 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TreeNodeChildren.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TreeNodeChildren.java
@@ -66,18 +66,17 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
      * This is typically an {@link AbstractMetadata} instance, but not 
necessarily.
      * Any type for which {@link MetadataStandard#isMetadata(Class)} returns 
{@code true} is okay.
      *
-     * <p>This field is a snapshot of the {@linkplain #parent} {@link 
TreeNode#getUserObject()} at
-     * creation time. This collection does not track changes in the reference 
returned by the above-cited
-     * {@code getUserObject()}. In other words, changes in the {@code 
metadata} object will be reflected
-     * in this collection, but if {@code parent.getUserObject()} returns a 
reference to another object,
-     * this change will not be reflected in this collection.
+     * <p>This field is a snapshot of the {@linkplain #parent} {@link 
TreeNode#getUserObject()} at creation time.
+     * This collection does not track changes in the reference returned by the 
above-cited {@code getUserObject()}.
+     * In other words, changes in the {@code metadata} object will be 
reflected in this collection,
+     * but if {@code parent.getUserObject()} returns a reference to another 
object,
+     * then this change will not be reflected in this collection.
      */
     final Object metadata;
 
     /**
-     * The accessor to use for accessing the property names, types and values 
of the
-     * {@link #metadata} object. This is given at construction time and shall 
be the
-     * same than the following code:
+     * The accessor to use for accessing the property names, types and values 
of the {@link #metadata} object.
+     * This is given at construction time and shall be the same than the 
following code:
      *
      * {@snippet lang="java" :
      *     accessor = parent.table.standard.getAccessor(metadata.getClass(), 
true);
@@ -121,7 +120,7 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
      * is done on a <cite>best effort basis</cite> only, since we cannot not 
track the changes which
      * are done independently in the {@linkplain #metadata} object.
      */
-    int modCount;
+    private int modCount;
 
     /**
      * Creates a collection of children for the specified metadata.
@@ -200,8 +199,8 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
      * This method sets the property to {@code null}. This is not strictly 
correct for collections,
      * since we should rather set the property to an empty collection. 
However, this approach would
      * force us to check if the expected collection type is actually a list, a 
set or any other type.
-     * Passing null avoid the type check and is safe at least with SIS 
implementation. We may revisit
-     * later if this appears to be a problem with other implementations.
+     * Passing null avoid the type check and is safe at least with SIS 
implementation.
+     * We may revisit later if this appears to be a problem with other 
implementations.
      *
      * @param  index  the index in the accessor (<em>not</em> the index in 
this collection).
      */
@@ -374,8 +373,8 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
 
         /**
          * The value of the node to be returned by the {@link #next()} method. 
This value is computed
-         * ahead of time by {@link #hasNext()} since we need that information 
in order to determine
-         * if the value needs to be skipped or not.
+         * ahead of time by {@link #hasNext()} because we need that 
information in order to determine
+         * if the value should be skipped or not.
          *
          * <h4>Implementation note</h4>
          * Actually we don't really need to keep this value, since it is not 
used outside the {@link #hasNext()}
@@ -384,6 +383,12 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
          */
         private Object nextValue;
 
+        /**
+         * The node returned by the last call to {@link #next()}. This is used 
for clearing the
+         * {@link TreeNode#cachedValue} field when the iterator moves to the 
next element.
+         */
+        private TreeNode current;
+
         /**
          * If the call to {@link #next()} found a collection, the iterator 
over the elements in that collection.
          * Otherwise {@code null}. A non-null value (even if that sub-iterator 
has no next elements) means that
@@ -417,8 +422,15 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
 
         /**
          * Throws {@link ConcurrentModificationException} if an unexpected 
change has been detected.
+         * Also opportunistically clears the cached value of the previous 
node, since this method is
+         * invoked either before moving to the next node or for removing the 
current node.
          */
-        final void checkConcurrentModification() {
+        private void checkConcurrentModification() {
+            if (current != null) {
+                current.canUseCache = false;
+                current.cachedValue = null;
+                current = null;
+            }
             if (modCountCheck != modCount) {
                 throw new ConcurrentModificationException();
             }
@@ -438,7 +450,7 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
             }
             /*
              * If we were iterating over the elements of a sub-collection, 
move to the next element
-             * in that iteration. We do not check for 'isSkipped(value)' here 
because null or empty
+             * in that iteration. We do not check for `isSkipped(value)` here 
because null or empty
              * elements in collections are probably mistakes, and we want to 
see them.
              */
             if (subIterator != null) {
@@ -477,8 +489,8 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
                             /*
                              * If the property is a collection, 
unconditionally get the first element
                              * even if absent (null) in order to comply with 
the ValueExistencePolicy.
-                             * if we were expected to ignore empty 
collections, 'isSkipped(nextValue)'
-                             * would have returned 'true'.
+                             * if we were expected to ignore empty 
collections, `isSkipped(nextValue)`
+                             * would have returned `true`.
                              */
                             subIndex = 0;
                             if (subIterator.hasNext()) {
@@ -486,7 +498,7 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
                             } else {
                                 nextValue = null;
                                 /*
-                                 * Do not set 'childIterator' to null, since 
the above 'nextValue'
+                                 * Do not set `childIterator` to null, because 
the above `nextValue`
                                  * is considered as part of the child 
iteration.
                                  */
                             }
@@ -502,26 +514,28 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
 
         /**
          * Returns the node for the metadata property at the current {@link 
#nextInAccessor}.
-         * The value of this property is initially {@link #nextValue}, but 
this may change at
-         * any time if the user modifies the underlying metadata object.
+         * The value in `TableColumn.VALUE` is initially set to {@link 
#nextValue},
+         * but may change later if the user modifies the underlying metadata 
object.
          */
         @Override
         public TreeTable.Node next() {
             if (hasNext()) {
                 final TreeNode.Element node = childAt(nextInAccessor, 
subIndex);
-                node.cachedValue = nextValue;
+                node.canUseCache   = true;
+                node.cachedValue   = nextValue;
                 previousInAccessor = nextInAccessor;
                 if (subIterator == null) {
                     /*
                      * If we are iterating over the elements in a collection, 
the PropertyAccessor index
-                     * still the same and will be incremented by 'hasNext()' 
only when the iteration is
+                     * still the same and will be incremented by `hasNext()` 
only when the iteration is
                      * over. Otherwise (not iterating in a collection), move 
to the next property. The
-                     * 'hasNext()' method will determine later if this 
property is non-empty, or if we
+                     * `hasNext()` method will determine later if this 
property is non-empty, or if we
                      * need to move forward again.
                      */
                     nextInAccessor++;
                 }
                 isNextVerified = false;
+                current = node;
                 return (node.decorator == null) ? node : 
node.decorator.apply(node);
             }
             throw new NoSuchElementException();
@@ -530,8 +544,8 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
         /**
          * Clears the element returned by the last call to {@link #next()}.
          * Whether the cleared element is considered removed or not depends
-         * on the value policy and on the element type. With the default
-         * {@code NON_EMPTY} policy, the effect is a removal.
+         * on the value policy and on the element type.
+         * With the default {@code NON_EMPTY} policy, the effect is a removal.
          */
         @Override
         public void remove() {
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TreeTableView.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TreeTableView.java
index c1ce0f9c6a..5a1e0d5d71 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TreeTableView.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TreeTableView.java
@@ -23,6 +23,7 @@ import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.util.function.Predicate;
 import org.opengis.metadata.citation.Citation;
+import org.apache.sis.util.ArraysExt;
 import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.collection.TreeTableFormat;
@@ -38,12 +39,14 @@ import org.apache.sis.system.Semaphores;
  * The tree table is made of the following columns:
  *
  * <ul>
- *   <li>{@link TableColumn#IDENTIFIER} - the property identifier as defined 
by the UML (if any).</li>
- *   <li>{@link TableColumn#INDEX}      - the index in the collection, or null 
if the property is not a collection.</li>
- *   <li>{@link TableColumn#NAME}       - the human-readable property name, 
inferred from the identifier and index.</li>
- *   <li>{@link TableColumn#TYPE}       - the base interface of property 
values.</li>
- *   <li>{@link TableColumn#VALUE}      - the property value.</li>
- *   <li>{@link TableColumn#REMARKS}    - remarks on the property value.</li>
+ *   <li>{@link MetadataColumn#IDENTIFIER} - the property identifier as 
defined by the UML (if any).</li>
+ *   <li>{@link MetadataColumn#INDEX}      - the index in the collection, or 
null if the property is not a collection.</li>
+ *   <li>{@link MetadataColumn#NAME}       - the human-readable property name, 
inferred from the identifier and index.</li>
+ *   <li>{@link MetadataColumn#TYPE}       - the base interface of property 
values.</li>
+ *   <li>{@link MetadataColumn#OBLIGATION} - whether the property is 
mandatory, optional or conditional.</li>
+ *   <li>{@link MetadataColumn#VALUE}      - the property value.</li>
+ *   <li>{@link MetadataColumn#NIL_REASON} - if the property is mandatory and 
nevertheless absent, the reason why.</li>
+ *   <li>{@link MetadataColumn#REMARKS}    - remarks on the property 
value.</li>
  * </ul>
  *
  * @author  Martin Desruisseaux (Geomatys)
@@ -56,15 +59,25 @@ final class TreeTableView implements TreeTable, 
TreeFormatCustomization, Seriali
 
     /**
      * The columns to be returned by {@link #getColumns()}.
+     * The filtered columns are the columns without the nil reason.
+     * The latter column is useless if {@link ValueExistencePolicy} is 
excluding nil values.
      */
-    static final List<TableColumn<?>> COLUMNS = UnmodifiableArrayList.wrap(new 
TableColumn<?>[] {
-        TableColumn.IDENTIFIER,
-        TableColumn.INDEX,
-        TableColumn.NAME,
-        TableColumn.TYPE,
-        TableColumn.VALUE,
-        TableColumn.REMARKS
-    });
+    private static final List<TableColumn<?>> COLUMNS, FILTERED_COLUMNS;
+    static {
+        var columns = new TableColumn<?>[] {
+            MetadataColumn.IDENTIFIER,
+            MetadataColumn.INDEX,
+            MetadataColumn.NAME,
+            MetadataColumn.TYPE,
+            MetadataColumn.OBLIGATION,
+            MetadataColumn.VALUE,
+            MetadataColumn.NIL_REASON,
+            MetadataColumn.REMARKS
+        };
+        COLUMNS = UnmodifiableArrayList.wrap(columns);
+        columns = ArraysExt.remove(columns, 6, 1);
+        FILTERED_COLUMNS = UnmodifiableArrayList.wrap(columns);
+    }
 
     /**
      * The root of the metadata tree.
@@ -100,12 +113,12 @@ final class TreeTableView implements TreeTable, 
TreeFormatCustomization, Seriali
     }
 
     /**
-     * Returns the columns included in this tree table.
+     * {@return the columns included in this tree table}.
      */
     @Override
-    @SuppressWarnings("ReturnOfCollectionOrArrayField")
+    @SuppressWarnings("ReturnOfCollectionOrArrayField")     // Because the 
returned collection is unmodifiable.
     public List<TableColumn<?>> getColumns() {
-        return COLUMNS;                                 // Unmodifiable
+        return valuePolicy.acceptNilValues() ? COLUMNS : FILTERED_COLUMNS;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ValueExistencePolicy.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ValueExistencePolicy.java
index 677381447a..0939e7e1bc 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ValueExistencePolicy.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ValueExistencePolicy.java
@@ -45,6 +45,11 @@ import org.apache.sis.xml.NilReason;
  * @since 0.3
  */
 public enum ValueExistencePolicy {
+    /*
+     * Implementation note: enumeration order matter.
+     * The `acceptNilValues()` method relies on it.
+     */
+
     /**
      * Includes all entries in the map, including those having a null value or 
an empty collection.
      */
@@ -180,6 +185,13 @@ public enum ValueExistencePolicy {
         }
     };
 
+    /**
+     * {@return whether this policy accepts nil values}.
+     */
+    final boolean acceptNilValues() {
+        return ordinal() < NON_NIL.ordinal();
+    }
+
     /**
      * Returns {@code true} if the given value shall be skipped for this 
policy.
      */
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/package-info.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/package-info.java
index 24c207efe6..2b5308b6f6 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/package-info.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/package-info.java
@@ -19,21 +19,22 @@
  * Root package for various metadata implementations.
  *
  * <h2>Foreword</h2>
- * Many metadata standards exist, including <cite>Dublin core</cite>, 
<cite>ISO 19115</cite> and the Image I/O
- * metadata defined in {@link javax.imageio.metadata}. The SIS implementation 
focuses on ISO 19115 (including
- * its ISO 19115-2 extension), but the classes are designed in a way that 
allow the usage of different standards.
+ * Many metadata standards exist, including Dublin core, ISO 19115
+ * and the Image I/O metadata defined in {@link javax.imageio.metadata} 
package.
+ * The SIS implementation focuses on the ISO 19115 series of standards,
+ * but the classes are designed in a way that allow the usage of different 
standards.
  * This genericity goal should be keep in mind in the discussion below.
  *
  * <h2>How Metadata are defined</h2>
- * A metadata standard is defined by a set of Java interfaces belonging to a 
specific package and its sub-packages.
- * For example, the ISO 19115 standard is defined by the <a 
href="http://www.geoapi.org";>GeoAPI</a> interfaces
+ * A metadata standard is reified by a set of Java interfaces belonging to a 
specific package and its sub-packages.
+ * For example, the ISO 19115 standard is reified by the <a 
href="http://www.geoapi.org";>GeoAPI</a> interfaces
  * defined in the {@link org.opengis.metadata} package and sub-packages. That 
standard is identified in SIS by the
  * {@link org.apache.sis.metadata.MetadataStandard#ISO_19115} constant. Other 
standards are defined as well,
  * for example the {@link org.apache.sis.metadata.MetadataStandard#ISO_19123} 
constant stands for the standards
  * defined by the interfaces in the {@link org.opengis.coverage} package and 
sub-packages.
  *
- * <p>For each interface, the collection of declared getter methods defines 
its <cite>properties</cite>
- * (or <cite>attributes</cite>). If a {@link org.opengis.annotation.UML} 
annotation is attached to the getter method,
+ * <p>For each interface, the collection of declared getter methods defines 
its <dfn>properties</dfn>
+ * (or <dfn>attributes</dfn>). If a {@link org.opengis.annotation.UML} 
annotation is attached to the getter method,
  * the identifier declared in that annotation is taken as the property name. 
This is typically the name defined by the
  * International Standard from which the interface is derived. Otherwise (if 
there is no {@code UML} annotation)
  * the property name is inferred from the method name like what the <cite>Java 
Beans</cite> framework does.</p>
@@ -48,7 +49,7 @@
  * <ul class="verbose">
  *   <li>The {@code Abstract} prefix means that the class is abstract in the 
sense of the implemented standard.
  *       It it not necessarily abstract in the sense of Java. Because 
incomplete metadata are common in practice,
- *       sometimes we wish to instantiate an "abstract" class despite the lack 
of knowledge about the exact sub-type.</li>
+ *       sometimes we wish to instantiate an "abstract" class because of the 
lack of knowledge about the exact sub-type.</li>
  *   <li>The properties are determined by the getter methods declared in the 
interfaces.
  *       Getter methods declared in the implementation classes are 
ignored.</li>
  *   <li>Setter methods, if any, can be declared in the implementation classes 
without the need for declarations
@@ -66,10 +67,10 @@
  *
  * <p>In addition, the metadata modules provide support methods for handling 
the metadata objects through Java Reflection.
  * This is an approach similar to <cite>Java Beans</cite>, in that users are 
encouraged to use directly the API of
- * <cite>Plain Old Java</cite> objects (actually interfaces) every time their 
type is known at compile time,
+ * Plain Old Java objects (actually interfaces) every time their type is known 
at compile time,
  * and fallback on the reflection technic when the type is known only at 
runtime.</p>
  *
- * <p>Using Java reflection, a metadata can be viewed in many different 
ways:</p>
+ * <p>Using Java reflection, a metadata can be viewed in different ways:</p>
  * <ul class="verbose">
  *   <li><b>As a {@link java.util.Map}</b><br>
  *       The {@link org.apache.sis.metadata.MetadataStandard} class provides 
various methods returning a view
@@ -119,7 +120,7 @@
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Adrian Custer (Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   0.3
  */
 package org.apache.sis.metadata;
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TreeTableViewTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TreeTableViewTest.java
index 644acb683a..b129fc86f9 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TreeTableViewTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TreeTableViewTest.java
@@ -16,12 +16,20 @@
  */
 package org.apache.sis.metadata;
 
+import java.util.Locale;
+import java.util.Iterator;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
+import org.opengis.annotation.Obligation;
+import org.opengis.util.InternationalString;
 import org.opengis.metadata.citation.Citation;
+import org.apache.sis.util.collection.TreeTable;
+import org.apache.sis.util.collection.TableColumn;
+import org.apache.sis.metadata.iso.citation.DefaultCitation;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
+import org.apache.sis.xml.NilReason;
 
 // Test dependencies
 import org.junit.Test;
@@ -29,7 +37,7 @@ import org.apache.sis.test.DependsOnMethod;
 import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.TestCase;
 
-import static org.junit.Assert.*;
+import static org.junit.jupiter.api.Assertions.*;
 import static org.apache.sis.test.Assertions.assertMultilinesEquals;
 import static org.apache.sis.test.TestUtilities.toTreeStructure;
 import static org.apache.sis.test.TestUtilities.formatMetadata;
@@ -86,10 +94,79 @@ public final class TreeTableViewTest extends TestCase {
     @Test
     public void testToString() {
         final TreeTableView metadata = create(ValueExistencePolicy.COMPACT);
+        assertFalse(metadata.getColumns().contains(MetadataColumn.NIL_REASON));
         assertMultilinesEquals(EXPECTED, formatMetadata(metadata));            
                 // Locale-independent
         assertArrayEquals(toTreeStructure(EXPECTED), 
toTreeStructure(metadata.toString()));     // Locale-dependent.
     }
 
+    /**
+     * Verifies most columns in the tree table. All nil reasons are null.
+     */
+    @Test
+    public void testGetValues() {
+        final TreeTableView metadata = create(ValueExistencePolicy.NON_NULL);
+        assertTrue(metadata.getColumns().contains(MetadataColumn.NIL_REASON));
+        verify(metadata.getRoot(), "Some title", null);
+    }
+
+    /**
+     * Verifies columns in the tree table with some non-null nil reasons.
+     */
+    @Test
+    public void testNilReasons() {
+        final TreeTableView metadata = create(ValueExistencePolicy.NON_NULL);
+        assertTrue(metadata.getColumns().contains(MetadataColumn.NIL_REASON));
+        final var citation = (DefaultCitation) 
metadata.getRoot().getUserObject();
+        
citation.setTitle(NilReason.TEMPLATE.createNilObject(InternationalString.class));
+        verify(metadata.getRoot(), null, NilReason.TEMPLATE);
+    }
+
+    /**
+     * Verifies the values of the given root node and some of its children.
+     *
+     * @param  node     root node to verify.
+     * @param  title    expected citation title, or {@code null} if it is 
expected to be missing.
+     * @param  titleNR  if the title is missing, the expected reason why.
+     */
+    private void verify(TreeTableView.Node node, final String title, final 
NilReason titleNR) {
+        assertEquals("CI_Citation",  node.getValue(TableColumn.IDENTIFIER));
+        assertNull  (                node.getValue(TableColumn.INDEX));
+        assertEquals("Citation",     node.getValue(TableColumn.NAME));
+        assertEquals(Citation.class, node.getValue(TableColumn.TYPE));
+        assertNull  (                node.getValue(TableColumn.OBLIGATION));
+        assertNull  (                node.getValue(TableColumn.VALUE));
+        assertNull  (                node.getValue(MetadataColumn.NIL_REASON));
+
+        Iterator<TreeTable.Node> it = node.getChildren().iterator();
+        node = it.next();
+        assertEquals("title",                      
node.getValue(TableColumn.IDENTIFIER));
+        assertNull  (                              
node.getValue(TableColumn.INDEX));
+        assertEquals("Title",                      
node.getValue(TableColumn.NAME));
+        assertEquals(InternationalString.class,    
node.getValue(TableColumn.TYPE));
+        assertEquals(Obligation.MANDATORY,         
node.getValue(TableColumn.OBLIGATION));
+        assertI18nEq(title,                        
node.getValue(TableColumn.VALUE));
+        assertEquals(titleNR,                      
node.getValue(MetadataColumn.NIL_REASON));
+
+        node = it.next();
+        assertEquals("alternateTitle",              
node.getValue(TableColumn.IDENTIFIER));
+        assertEquals(0,                             
node.getValue(TableColumn.INDEX));
+        assertI18nEq("Alternate title (1 of 2)",    
node.getValue(TableColumn.NAME));
+        assertEquals(InternationalString.class,     
node.getValue(TableColumn.TYPE));
+        assertEquals(Obligation.OPTIONAL,           
node.getValue(TableColumn.OBLIGATION));
+        assertI18nEq("First alternate title",       
node.getValue(TableColumn.VALUE));
+        assertNull  (                               
node.getValue(MetadataColumn.NIL_REASON));
+    }
+
+    /**
+     * Verifies the value of the given international string in English.
+     */
+    private static void assertI18nEq(final String expected, Object text) {
+        if (text instanceof InternationalString) {
+            text = ((InternationalString) text).toString(Locale.ENGLISH);
+        }
+        assertEquals(expected, text);
+    }
+
     /**
      * Tests serialization.
      *
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/TableColumn.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/TableColumn.java
index b3e441ddfa..f4d8418d51 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/TableColumn.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/collection/TableColumn.java
@@ -20,6 +20,7 @@ import java.util.Map;
 import java.io.Serializable;
 import java.io.ObjectStreamException;
 import java.io.InvalidObjectException;
+import org.opengis.annotation.Obligation;
 import org.opengis.util.InternationalString;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.SimpleInternationalString;
@@ -91,7 +92,7 @@ import org.apache.sis.util.resources.Vocabulary;
  * The constants defined in this class use a similar approach for providing 
serialization support.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.5
  *
  * @param <V>  base type of all values in the column identified by this 
instance.
  *
@@ -99,7 +100,7 @@ import org.apache.sis.util.resources.Vocabulary;
  */
 public class TableColumn<V> implements CheckedContainer<V> {
     /**
-     * Frequently-used constant for a column of object names.
+     * Predefined constant for a column of object names.
      * The column {@linkplain #getHeader() header} is <q>Name</q> (eventually 
localized) and
      * the column elements are typically instances of {@link String} or {@link 
InternationalString},
      * depending on whether the data provide localization support or not.
@@ -108,7 +109,7 @@ public class TableColumn<V> implements CheckedContainer<V> {
             CharSequence.class, Vocabulary.Keys.Name);
 
     /**
-     * Frequently-used constant for a column of object identifiers.
+     * Predefined constant for a column of object identifiers.
      * The column {@linkplain #getHeader() header} is <q>Identifier</q> 
(eventually localized)
      * and the column elements are instances of {@link String}.
      */
@@ -116,7 +117,7 @@ public class TableColumn<V> implements CheckedContainer<V> {
             String.class, Vocabulary.Keys.Identifier);
 
     /**
-     * Frequently-used constant for a column of index values.
+     * Predefined constant for a column of index values.
      * The column {@linkplain #getHeader() header} is <q>Index</q> (eventually 
localized)
      * and the column elements are instances of {@link Integer}.
      */
@@ -124,7 +125,7 @@ public class TableColumn<V> implements CheckedContainer<V> {
             Integer.class, Vocabulary.Keys.Index);
 
     /**
-     * Frequently-used constant for a column of object types.
+     * Predefined constant for a column of object types.
      * The column {@linkplain #getHeader() header} is <q>Type</q> (eventually 
localized).
      */
     @SuppressWarnings("unchecked")
@@ -132,7 +133,17 @@ public class TableColumn<V> implements CheckedContainer<V> 
{
             (Class) Class.class, Vocabulary.Keys.Type);
 
     /**
-     * Frequently-used constant for a column of object values.
+     * Predefined constant for a column of obligation (mandatory, optional, 
conditional).
+     * The column {@linkplain #getHeader() header} is <q>Obligation</q> 
(eventually localized)
+     * and the column elements are instances of {@link Obligation}.
+     *
+     * @since 1.5
+     */
+    public static final TableColumn<Obligation> OBLIGATION = new 
Constant<>("OBLIGATION",
+            Obligation.class, Vocabulary.Keys.Obligation);
+
+    /**
+     * Predefined constant for a column of object values.
      * The column {@linkplain #getHeader() header} is <q>Value</q> (eventually 
localized) and
      * the column elements can be instance of any kind of objects.
      *
@@ -143,7 +154,7 @@ public class TableColumn<V> implements CheckedContainer<V> {
             Object.class, Vocabulary.Keys.Value);
 
     /**
-     * Frequently-used constant for a column of object textual values.
+     * Predefined constant for a column of object textual values.
      * The column {@linkplain #getHeader() header} is <q>Value</q> (eventually 
localized) and
      * the column elements are typically instances of {@link String} or {@link 
InternationalString},
      * depending on whether the data provide localization support or not.
@@ -152,14 +163,14 @@ public class TableColumn<V> implements 
CheckedContainer<V> {
             CharSequence.class, Vocabulary.Keys.Value);
 
     /**
-     * Frequently-used constant for a column of object numerical values.
+     * Predefined constant for a column of object numerical values.
      * The column {@linkplain #getHeader() header} is <q>Value</q> (eventually 
localized).
      */
     public static final TableColumn<Number> VALUE_AS_NUMBER = new 
Constant<>("VALUE_AS_NUMBER",
             Number.class, Vocabulary.Keys.Value);
 
     /**
-     * Frequently-used constant for a column of remarks.
+     * Predefined constant for a column of remarks.
      * The column {@linkplain #getHeader() header} is <q>Remarks</q> 
(eventually localized) and
      * the column elements are typically instances of {@link String} or {@link 
InternationalString},
      * depending on whether the data provide localization support or not.
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
index c56c3ff649..9a581e9c82 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.java
@@ -869,6 +869,11 @@ public class Vocabulary extends IndexedResourceBundle {
          */
         public static final short NearestNeighbor = 232;
 
+        /**
+         * Nil reason
+         */
+        public static final short NilReason = 274;
+
         /**
          * No data
          */
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
index 3c760931aa..4e60a35b3c 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary.properties
@@ -178,6 +178,7 @@ More_1                  = \u2026 {0} more\u2026
 Multiplicity            = Multiplicity
 Name                    = Name
 NearestNeighbor         = Nearest neighbor
+NilReason               = Nil reason
 Nodata                  = No data
 None                    = None
 Note                    = Note
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
index 0061fa151a..5c2178784e 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Vocabulary_fr.properties
@@ -185,6 +185,7 @@ More_1                  = \u2026 {0} de plus\u2026
 Multiplicity            = Multiplicit\u00e9
 Name                    = Nom
 NearestNeighbor         = Plus proche voisin
+NilReason               = Raison de l\u2019absence
 Nodata                  = Absence de donn\u00e9es
 None                    = Aucun
 Note                    = Note
diff --git a/netbeans-project/nbproject/project.xml 
b/netbeans-project/nbproject/project.xml
index 7c5ceb94b7..8fce63d8e7 100644
--- a/netbeans-project/nbproject/project.xml
+++ b/netbeans-project/nbproject/project.xml
@@ -27,5 +27,8 @@
                 <root id="incubator.test.dir" 
pathref="incubator.test.dir.path"/>
             </test-roots>
         </data>
+        <spellchecker-wordlist 
xmlns="http://www.netbeans.org/ns/spellchecker-wordlist/1";>
+            <word>genericity</word>
+        </spellchecker-wordlist>
     </configuration>
 </project>

Reply via email to