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

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new 130aab7172 Apply to ISO-19115 metadata the same mechanism used by 
GeoHEIF for summarizing metadata.
130aab7172 is described below

commit 130aab7172a1255cf952021be7dd1e0988ba9117
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Mon May 18 01:07:13 2026 +0200

    Apply to ISO-19115 metadata the same mechanism used by GeoHEIF for 
summarizing metadata.
---
 .../org/apache/sis/metadata/MetadataStandard.java  |   2 +-
 .../org/apache/sis/metadata/TitleProperty.java     |   2 +-
 .../main/org/apache/sis/metadata/TreeNode.java     |   6 +-
 .../org/apache/sis/metadata/TreeNodeChildren.java  |  35 +++++--
 .../org/apache/sis/metadata/TreeTableView.java     |  14 ++-
 .../apache/sis/metadata/ValueExistencePolicy.java  | 104 +++++++++++++--------
 .../main/org/apache/sis/metadata/package-info.java |   2 +-
 .../main/org/apache/sis/xml/bind/gcx/Anchor.java   |   9 +-
 .../apache/sis/metadata/TreeNodeChildrenTest.java  |  14 ++-
 .../test/org/apache/sis/metadata/TreeNodeTest.java |  11 ++-
 .../org/apache/sis/metadata/TreeTableViewTest.java |   3 +-
 .../apache/sis/storage/metadata/NodeSummary.java   |  18 +++-
 .../sis/util/AbstractInternationalString.java      |  22 ++---
 .../sis/util/internal/shared/TreeTableForGUI.java  |  42 +++++++++
 .../org/apache/sis/storage/isobmff/TreeNode.java   |  22 +++--
 .../org/apache/sis/gui/metadata/MetadataTree.java  |  26 ++++--
 .../sis/gui/metadata/StandardMetadataTree.java     |   8 +-
 17 files changed, 237 insertions(+), 103 deletions(-)

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 f7ec708fae..ddc62e51ca 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
@@ -669,7 +669,7 @@ public class MetadataStandard implements Serializable {
      * @return the title property value of the given metadata, or {@code null} 
if none.
      *
      * @see TitleProperty
-     * @see ValueExistencePolicy#COMPACT
+     * @see ValueExistencePolicy#TITLED
      */
     final Object getTitle(final Object metadata) {
         if (metadata != null) {
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TitleProperty.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TitleProperty.java
index 8efa67e88b..42e15f4433 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TitleProperty.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/TitleProperty.java
@@ -61,7 +61,7 @@ import java.lang.annotation.Documented;
  * @version 0.8
  * @since   0.8
  *
- * @see ValueExistencePolicy#COMPACT
+ * @see ValueExistencePolicy#TITLED
  */
 @Documented
 @Inherited
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 a4abb46eba..2261a1b70d 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
@@ -1026,10 +1026,12 @@ class TreeNode implements Node {
     }
 
     /**
-     * Returns the children if the value policy is {@link 
ValueExistencePolicy#COMPACT}, or {@code null} otherwise.
+     * Returns the children if the value policy puts a title on metadata 
objects, or {@code null} otherwise.
+     * The callers are not interested in the collection of children, but 
rather in specialized methods such
+     * as {@link TreeNodeChildren#getParentTitle()}.
      */
     private TreeNodeChildren getCompactChildren() {
-        if (table.valuePolicy == ValueExistencePolicy.COMPACT) {
+        if (table.valuePolicy.isTitled()) {
             @SuppressWarnings("LocalVariableHidesMemberVariable")
             final Collection<Node> children = getChildren();
             if (children instanceof TreeNodeChildren) {
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 95d0fce1ab..92dca2a1f9 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
@@ -96,8 +96,9 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
 
     /**
      * Index of the property to write in the parent node instead of as a child.
-     * If a property has the same name as the parent property that contains it,
-     * we write its value in that parent property. For example, instead of:
+     * If a property is specified in a {@link TitleProperty} class annotation,
+     * we write the property value value in the parent node.
+     * For example, instead of:
      *
      * <pre class="text">
      *   Citation
@@ -116,6 +117,13 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
      */
     final int titleProperty;
 
+    /**
+     * Index of the property to hide, or -1 if none.
+     * This is positive only in the {@link ValueExistencePolicy#COMPACT} case,
+     * in which case this value is the same as {@link #titleProperty}.
+     */
+    private final int hiddenProperty;
+
     /**
      * Modification count, incremented when the content of this collection is 
modified. This check
      * is done on a <em>best effort basis</em> only, since we cannot not track 
the changes which
@@ -137,10 +145,10 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
         this.children = new TreeNode.Element[accessor.count()];
         /*
          * Search for something that looks like the main property, to be 
associated with the parent node
-         * instead of provided as a child. The intent is to have more compact 
and easy to read trees.
+         * instead of provided as a child. The intent is to make trees more 
compact and easier to read.
          * That property shall be a singleton for a simple value (not another 
metadata object).
          */
-        if (parent.table.valuePolicy == ValueExistencePolicy.COMPACT) {
+        if (parent.table.valuePolicy.isTitled()) {
             TitleProperty an = 
accessor.implementation.getAnnotation(TitleProperty.class);
             if (an == null) {
                 Class<?> implementation = 
parent.table.standard.getImplementation(accessor.type);
@@ -152,12 +160,21 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
                 final int index = accessor.indexOf(an.name(), false);
                 final Class<?> type = accessor.type(index, 
TypeValuePolicy.ELEMENT_TYPE);
                 if (type != null && !parent.isMetadata(type) && type == 
accessor.type(index, TypeValuePolicy.PROPERTY_TYPE)) {
+                    hiddenProperty = (parent.table.valuePolicy == 
ValueExistencePolicy.COMPACT) ? index : -1;
                     titleProperty = index;
                     return;
                 }
             }
         }
-        titleProperty = -1;
+        titleProperty  = -1;
+        hiddenProperty = -1;
+    }
+
+    /**
+     * Returns whether the value returned by this node is a title fetched from 
another property.
+     */
+    final boolean isNodeTitle() {
+        return titleProperty >= 0;
     }
 
     /**
@@ -178,7 +195,7 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
 
     /**
      * Sets the value associated to the parent node, if possible.
-     * This returned boolean tells whether the value has been written.
+     * The returned Boolean tells whether the value has been written.
      */
     final boolean setParentTitle(final Object value) {
         if (titleProperty < 0) {
@@ -306,7 +323,7 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
     @Override
     public int size() {
         int count = accessor.count(metadata, parent.table.valuePolicy, 
PropertyAccessor.COUNT_DEEP);
-        if (titleProperty >= 0 && !isSkipped(valueAt(titleProperty))) count--;
+        if (hiddenProperty >= 0 && !isSkipped(valueAt(hiddenProperty))) 
count--;
         return count;
     }
 
@@ -316,7 +333,7 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
      */
     @Override
     public boolean isEmpty() {
-        if (titleProperty >= 0) return size() == 0;     // COUNT_FIRST is not 
reliable in this case.
+        if (hiddenProperty >= 0) return size() == 0;     // COUNT_FIRST is not 
reliable in this case.
         return accessor.count(metadata, parent.table.valuePolicy, 
PropertyAccessor.COUNT_FIRST) == 0;
     }
 
@@ -475,7 +492,7 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
              */
             final int count = childCount();
             while (nextInAccessor < count) {
-                if (nextInAccessor != titleProperty) {
+                if (nextInAccessor != hiddenProperty) {
                     nextValue = valueAt(nextInAccessor);
                     if (!isSkipped(nextValue)) {
                         if (isCollectionOrMap(nextInAccessor)) {
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 681e8c7f8e..56fbb43c45 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
@@ -17,15 +17,16 @@
 package org.apache.sis.metadata;
 
 import java.util.List;
+import java.util.Collection;
 import java.io.Serializable;
 import java.io.IOException;
 import java.io.ObjectInputStream;
 import java.io.ObjectOutputStream;
 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;
 import org.apache.sis.util.collection.Containers;
+import org.apache.sis.util.internal.shared.TreeTableForGUI;
 import org.apache.sis.system.Semaphores;
 
 
@@ -46,7 +47,7 @@ import org.apache.sis.system.Semaphores;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class TreeTableView implements TreeTable, Serializable {
+final class TreeTableView implements TreeTableForGUI, Serializable {
     /**
      * For cross-version compatibility.
      */
@@ -124,6 +125,15 @@ final class TreeTableView implements TreeTable, 
Serializable {
         return root;
     }
 
+    /**
+     * Returns whether the given value produces by the given node is a title.
+     */
+    @Override
+    public boolean isNodeTitle(final Node node, final Object value) {
+        final Collection<Node> children = node.getChildren();
+        return (children instanceof TreeNodeChildren) && ((TreeNodeChildren) 
children).isNodeTitle();
+    }
+
     /**
      * Returns a string representation of this tree table.
      * The current implementation uses a shared instance of {@link 
TreeTableFormat}.
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 e050b230f8..c283cef3c3 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
@@ -38,7 +38,7 @@ import org.apache.sis.xml.NilReason;
  * Those explanations can be obtained by calls to the {@link 
org.apache.sis.xml.NilReason#forObject(Object)} method.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.8
+ * @version 1.7
  *
  * @see MetadataStandard#asValueMap(Object, Class, KeyNamePolicy, 
ValueExistencePolicy)
  *
@@ -47,13 +47,13 @@ import org.apache.sis.xml.NilReason;
 public enum ValueExistencePolicy {
     /*
      * Implementation note: enumeration order matter.
-     * The `acceptNilValues()` method relies on it.
+     * The `acceptNilValues()` and `isTitled()` methods rely on this order.
      */
 
     /**
      * Includes all entries in the map, including those having a null value or 
an empty collection.
      */
-    ALL() {
+    ALL {
         /** Never skip values. */
         @Override boolean isSkipped(final Object value) {
             return false;
@@ -73,7 +73,7 @@ public enum ValueExistencePolicy {
      *
      * <p>The set of {@code NON_NULL} properties is a subset of {@link #ALL} 
properties.</p>
      */
-    NON_NULL() {
+    NON_NULL {
         /** Skips all null values. */
         @Override boolean isSkipped(final Object value) {
             return (value == null);
@@ -94,7 +94,7 @@ public enum ValueExistencePolicy {
      *
      * @since 0.4
      */
-    NON_NIL() {
+    NON_NIL {
         /** Skips all null or nil values. */
         @Override boolean isSkipped(final Object value) {
             return (value == null) || (value instanceof NilObject) || 
NilReason.forObject(value) != null;
@@ -122,34 +122,47 @@ public enum ValueExistencePolicy {
      *
      * <p>The set of {@code NON_EMPTY} properties is a subset of {@link 
#NON_NIL} properties.</p>
      */
-    NON_EMPTY() {
-        /** Skips all null or empty values. */
-        @Override boolean isSkipped(final Object value) {
-            return isNullOrEmpty(value);
-        }
+    NON_EMPTY,
 
-        /** Never substitute null or empty collections since they should be 
skipped. */
-        @Override boolean substituteByNullElement(final Collection<?> values) {
-            return false;
-        }
-    },
+    /**
+     * Same as {@code NON_EMPTY}, but with the addition of titles associated 
to metadata objects.
+     * Metadata objects such as {@link Citation} are normally associated to no 
textual value.
+     * Texts are rather associated to <em>properties</em> of the metadata 
object.
+     * But some metadata classes have a property which can summarize the whole 
object.
+     * For example, for {@link org.opengis.metadata.citation.Citation} 
objects, this property
+     * is the {@linkplain org.opengis.metadata.citation.Citation#getTitle() 
citation title}.
+     *
+     * <p>The property to use as the title of a metadata object is specified by
+     * the {@link TitleProperty} annotation on the metadata implementation 
class
+     * (for example, {@link 
org.apache.sis.metadata.iso.citation.DefaultCitation}).
+     * When using this {@code TITLED} or the {@link #COMPACT} policy, the 
values of
+     * the properties identified by the {@link TitleProperty} annotations are 
copied
+     * in the root of the metadata sub-tree.
+     * See {@link #COMPACT} Javadoc for an example.</p>
+     *
+     * @see TitleProperty
+     *
+     * @since 1.7
+     */
+    TITLED,
 
     /**
-     * Includes non-empty properties but omits {@linkplain TitleProperty title 
properties}.
-     * Values associated to title properties are instead associated with the 
parent node.
-     * This policy is relevant for metadata classes annotated with {@link 
TitleProperty};
-     * for all other classes, this policy is identical to {@link #NON_EMPTY}.
+     * Same as {@code TITLED} but omitting the properties which have been used 
as titles.
+     * This omission removes a redundancy, thus making the tree a little bit 
more compact,
+     * at the expanse of a slight departure from the metadata model. For 
metadata classes
+     * without {@link TitleProperty} annotation, this policy is equivalent to 
{@link #NON_EMPTY}.
      *
      * <h4>Example</h4>
-     * the {@link org.apache.sis.metadata.iso.citation.DefaultCitation} and
+     * The {@link org.apache.sis.metadata.iso.citation.DefaultCitation} and
      * {@link org.apache.sis.metadata.iso.citation.DefaultCitationDate} 
classes are annotated with
      * <code>&#64;TitleProperty(name="title")</code> and 
<code>&#64;TitleProperty(name="date")</code>
-     * respectively. The following table compares the trees produced by two 
policies:
+     * respectively. The following table compares the trees produced by three 
policies:
      *
      * <table class="sis">
-     *   <caption>Comparison of "non-empty" and "compact" policy on the same 
metadata</caption>
+     *   <caption>Comparison of policies on the same metadata</caption>
      *   <tr>
      *     <th>{@code NON_EMPTY}</th>
+     *     <th class="sep">{@code TITLED}</th>
      *     <th class="sep">{@code COMPACT}</th>
      *   </tr><tr><td>
      *     <pre class="text">
@@ -161,29 +174,33 @@ public enum ValueExistencePolicy {
      *   </td><td class="sep">
      *     <pre class="text">
      * Citation……………………… My document
+     *  ├─Title……………………… My document
+     *  └─Date………………………… 2012/01/01
+     *     ├─Date………………… 2012/01/01
+     *     └─Date type…… Creation</pre>
+     *   </td><td class="sep">
+     *     <pre class="text">
+     * Citation……………………… My document
      *  └─Date………………………… 2012/01/01
      *     └─Date type…… Creation</pre>
      *   </td></tr>
      * </table>
      *
-     * This policy is the default behavior of {@link 
AbstractMetadata#asTreeTable()},
-     * and consequently defines the default rendering of {@link 
AbstractMetadata#toString()}.
+     * This {@code COMPACT} policy is the default behavior of {@link 
AbstractMetadata#asTreeTable()}.
+     * Therefore, this policy defines the default rendering of {@link 
AbstractMetadata#toString()}.
      *
      * @see TitleProperty
      *
      * @since 0.8
      */
-    COMPACT() {
-        /** Skips all null or empty values. */
-        @Override boolean isSkipped(final Object value) {
-            return isNullOrEmpty(value);
-        }
+    COMPACT;
 
-        /** Never substitute null or empty collections since they should be 
skipped. */
-        @Override boolean substituteByNullElement(final Collection<?> values) {
-            return false;
-        }
-    };
+    /**
+     * Returns whether this policy add a title in metadata objects.
+     */
+    final boolean isTitled() {
+        return ordinal() >= TITLED.ordinal();
+    }
 
     /**
      * Returns whether this policy accepts nil values.
@@ -194,18 +211,29 @@ public enum ValueExistencePolicy {
 
     /**
      * Returns {@code true} if the given value shall be skipped for this 
policy.
+     * By default, this method implements the {@link #NON_EMPTY} policy
+     * because that policy is reused in more than one enumeration values.
      */
-    abstract boolean isSkipped(Object value);
+    boolean isSkipped(final Object value) {
+        return isNullOrEmpty(value);
+    }
 
     /**
      * Returns {@code true} if {@link TreeNode} shall substitute the given 
collection by
      * a singleton containing only a null element.
      *
-     * <h4>Purpose</h4>
+     * <p><b>Purpose:</b>
      * When a collection is null or empty, while not excluded according this 
{@code ValueExistencePolicy},
-     * we need an empty space for making the metadata property visible in 
{@code TreeNode}.
+     * we need an empty space for making the metadata property visible in 
{@code TreeNode}.</p>
+     *
+     * <p>By default, this method implements the {@link #NON_EMPTY} policy
+     * because that policy is reused in more than one enumeration values.
+     * This default never substitute null or empty collections since they 
should be
+     * skipped according the default implementation of {@link 
#isSkipped(Object)}.</p>
      */
-    abstract boolean substituteByNullElement(Collection<?> values);
+    boolean substituteByNullElement(Collection<?> values) {
+        return false;
+    }
 
     /**
      * Returns {@code true} if the specified object is null or an empty 
collection, array or string.
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 53851b5ce2..28ea1288ae 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
@@ -120,7 +120,7 @@
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Adrian Custer (Geomatys)
- * @version 1.6
+ * @version 1.7
  * @since   0.3
  */
 @XmlAccessorType(XmlAccessType.NONE)
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gcx/Anchor.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gcx/Anchor.java
index 0bc9fe8bcf..744468fba6 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gcx/Anchor.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/gcx/Anchor.java
@@ -86,6 +86,8 @@ public final class Anchor extends XLink implements 
InternationalString {
     /**
      * Returns the text as a string, or {@code null} if none. The null value 
is needed for proper
      * working of {@link 
org.apache.sis.xml.bind.gco.GO_CharacterString#toString()} method.
+     *
+     * @return the string in the default locale.
      */
     @Override
     public String toString() {
@@ -126,13 +128,14 @@ public final class Anchor extends XLink implements 
InternationalString {
      * appropriate for the sub-sequence.
      */
     @Override
+    @SuppressWarnings("StringEquality")
     public CharSequence subSequence(final int start, final int end) {
         String original = value;
         if (original == null) {
             original = "";
         }
         final String substring = original.substring(start, end);
-        if (substring == original) {                                // 
Identity comparison is ok here.
+        if (substring == original) {        // Identity comparison is ok here.
             return this;
         }
         return new Anchor(this, substring);
@@ -164,7 +167,7 @@ public final class Anchor extends XLink implements 
InternationalString {
             return true;
         }
         if (super.equals(object)) {
-            final Anchor that = (Anchor) object;
+            final var that = (Anchor) object;
             return Objects.equals(this.value, that.value);
         }
         return false;
@@ -172,6 +175,8 @@ public final class Anchor extends XLink implements 
InternationalString {
 
     /**
      * Returns a hash code value for this anchor type.
+     *
+     * @return an arbitrary hash code value.
      */
     @Override
     public int hashCode() {
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TreeNodeChildrenTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TreeNodeChildrenTest.java
index 5ab3297d0f..b38e966ce3 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TreeNodeChildrenTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TreeNodeChildrenTest.java
@@ -72,7 +72,7 @@ public final class TreeNodeChildrenTest extends TestCase {
      *     └─Other citation details…… Some other details</pre>
      */
     static DefaultCitation metadataWithoutCollections() {
-        final DefaultCitation citation = new DefaultCitation("Some title");
+        final var citation = new DefaultCitation("Some title");
         citation.setEdition(new SimpleInternationalString("Some edition"));
         citation.setOtherCitationDetails(Set.of(new 
SimpleInternationalString("Some other details")));
         return citation;
@@ -138,8 +138,8 @@ public final class TreeNodeChildrenTest extends TestCase {
      * @see <a href="https://issues.apache.org/jira/browse/SIS-298";>SIS-298</a>
      */
     static DefaultCitation metadataSimplifiable() {
-        final DefaultCitation citation = new DefaultCitation();
-        final DefaultCitationDate date = new 
DefaultCitationDate(LocalDate.of(2012, 1, 1), DateType.CREATION);
+        final var citation = new DefaultCitation();
+        final var date = new DefaultCitationDate(LocalDate.of(2012, 1, 1), 
DateType.CREATION);
         assertTrue(citation.getDates().add(date));
         return citation;
     }
@@ -232,8 +232,8 @@ public final class TreeNodeChildrenTest extends TestCase {
          * We need to perform the tests on the "Date" node, not on the 
"DefaultCitation" node.
          */
         final TreeTable.Node node = assertSingleton(create(citation, 
ValueExistencePolicy.COMPACT));
-        assertEquals(15340, ((LocalDate) 
node.getValue(TableColumn.VALUE)).toEpochDay());
-        final TreeNodeChildren children = (TreeNodeChildren) 
node.getChildren();
+        assertEquals(15340, assertInstanceOf(LocalDate.class, 
node.getValue(TableColumn.VALUE)).toEpochDay());
+        final TreeNodeChildren children = 
assertInstanceOf(TreeNodeChildren.class, node.getChildren());
         final String[] expected = {
             // The "Date" node should be omitted because merged with the 
parent "Date" node.
             "DateType.CREATION"
@@ -253,9 +253,7 @@ public final class TreeNodeChildrenTest extends TestCase {
         final TreeNodeChildren children = create(citation, 
ValueExistencePolicy.NON_EMPTY);
         assertEquals(-1, children.titleProperty);
 
-        final DefaultTreeTable.Node toAdd = new DefaultTreeTable.Node(new 
DefaultTreeTable(
-                TableColumn.IDENTIFIER,
-                TableColumn.VALUE));
+        final var toAdd = new DefaultTreeTable.Node(new 
DefaultTreeTable(TableColumn.IDENTIFIER, TableColumn.VALUE));
         final String[] expected = {
             "Some title",
             "First alternate title",
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TreeNodeTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TreeNodeTest.java
index e5325ecaad..f2529ec333 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TreeNodeTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TreeNodeTest.java
@@ -55,6 +55,7 @@ import 
org.apache.sis.metadata.iso.citation.DefaultResponsibility;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class TreeNodeTest extends TestCase {
     /**
      * Creates a new test case.
@@ -94,8 +95,8 @@ public final class TreeNodeTest extends TestCase {
         assertTrue(citation.getCitedResponsibleParties().add(responsibility));
 
         // Add a second responsible party with deeper hierarchy.
-        final DefaultContact contact = new DefaultContact();
-        final DefaultAddress address = new DefaultAddress();
+        final var contact = new DefaultContact();
+        final var address = new DefaultAddress();
         address.setElectronicMailAddresses(Set.of("Some email"));
         contact.setAddresses(Set.of(address));
         party = new DefaultIndividual("Some person of contact", null, contact);
@@ -114,8 +115,8 @@ public final class TreeNodeTest extends TestCase {
      */
     private <T extends AbstractMetadata> TreeNode create(final T metadata, 
final Class<? super T> baseType) {
         final MetadataStandard  standard = MetadataStandard.ISO_19115;
-        final TreeTableView table = new TreeTableView(standard, metadata, 
baseType, valuePolicy);
-        return (TreeNode) table.getRoot();
+        final var table = new TreeTableView(standard, metadata, baseType, 
valuePolicy);
+        return assertInstanceOf(TreeNode.class, table.getRoot());
     }
 
     /**
@@ -133,7 +134,7 @@ public final class TreeNodeTest extends TestCase {
         assertNull  (                node.getParent());
         assertFalse (                node.isLeaf());
 
-        final TreeNodeChildren children = (TreeNodeChildren) 
node.getChildren();
+        final TreeNodeChildren children = 
assertInstanceOf(TreeNodeChildren.class, node.getChildren());
         assertEquals(-1, children.titleProperty);
         assertSame  (citation, children.metadata);
         assertFalse (node.getChildren().isEmpty());
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 0fb5d71398..64e6570ae4 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
@@ -63,7 +63,8 @@ public final class TreeTableViewTest extends TestCase {
 
     /**
      * The expected string representation of the tree created by {@link 
#create(ValueExistencePolicy)}
-     * with {@link ValueExistencePolicy#NON_EMPTY}.
+     * with {@link ValueExistencePolicy#COMPACT}. The citation title property 
is omitted, replaced by
+     * the title at the level of the {@code Citation} object.
      */
     private static final String EXPECTED =
             "Citation………………………………………………………………………………………………… Some title\n" +
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/metadata/NodeSummary.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/metadata/NodeSummary.java
index 0f4cd2963f..7634c498ad 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/metadata/NodeSummary.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/metadata/NodeSummary.java
@@ -25,6 +25,8 @@ import org.apache.sis.util.SimpleInternationalString;
  * node is collapsed and hide this text when the node is expanded.
  *
  * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see org.apache.sis.metadata.TitleProperty
  */
 public final class NodeSummary extends SimpleInternationalString {
     /**
@@ -37,7 +39,21 @@ public final class NodeSummary extends 
SimpleInternationalString {
      *
      * @param text the string for all locales.
      */
-    public NodeSummary(final String text) {
+    private NodeSummary(final String text) {
         super(text);
     }
+
+    /**
+     * Returns the given text as a {@code NodeSummary} instance.
+     *
+     * @param  text  the text to wrap, or {@code null}.
+     * @return the wrapped text, or {@code null} if the given text was null.
+     */
+    public static NodeSummary of(final CharSequence text) {
+        if (text == null || text instanceof NodeSummary) {
+            return (NodeSummary) text;
+        } else {
+            return new NodeSummary(text.toString());
+        }
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/AbstractInternationalString.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/AbstractInternationalString.java
index 85ae05d991..9f99decd65 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/AbstractInternationalString.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/AbstractInternationalString.java
@@ -34,7 +34,7 @@ import org.apache.sis.util.internal.shared.Strings;
  * The {@linkplain Comparable natural ordering} is defined by the value 
returned by {@link #toString()}.</p>
  *
  * <h2>Substituting a free text by a code list</h2>
- * The ISO standard allows to substitute some character strings in the <q>free 
text</q> domain
+ * The <abbr>ISO</abbr> standard allows to substitute some character strings 
in the <q>free text</q> domain
  * by a {@link org.opengis.util.CodeList} value. This can be done with:
  *
  * <ul>
@@ -56,8 +56,7 @@ public abstract class AbstractInternationalString implements 
InternationalString
      *
      * <h4>Thread safety</h4>
      * For thread safety this field shall either be read and written in a 
synchronized block,
-     * or be fixed at construction time and never changed after than point. 
All other usages
-     * are prohibited.
+     * or be fixed at construction time and never changed after than point.
      *
      * <h4>Serialization</h4>
      * This field is not serialized because serialization is often used for 
data transmission
@@ -73,7 +72,7 @@ public abstract class AbstractInternationalString implements 
InternationalString
     }
 
     /**
-     * Returns the length of the string in the {@linkplain Locale#getDefault() 
default locale}.
+     * Returns the length of the string in the default locale.
      * This is the length of the string returned by {@link #toString()}.
      *
      * @return length of the string in the default locale.
@@ -84,8 +83,8 @@ public abstract class AbstractInternationalString implements 
InternationalString
     }
 
     /**
-     * Returns the character of the string in the {@linkplain 
Locale#getDefault() default locale}
-     * at the specified index. This is a character of the string returned by 
{@link #toString()}.
+     * Returns the character of the string in the default locale at the 
specified index.
+     * This is a character of the string returned by {@link #toString()}.
      *
      * @param  index  the index of the character.
      * @return the character at the specified index.
@@ -97,7 +96,7 @@ public abstract class AbstractInternationalString implements 
InternationalString
     }
 
     /**
-     * Returns a subsequence of the string in the {@linkplain 
Locale#getDefault() default locale}.
+     * Returns a subsequence of the string in the default locale.
      * The subsequence is a {@link String} object starting with the character 
value at the specified
      * index and ending with the character value at index {@code end - 1}.
      *
@@ -116,14 +115,14 @@ public abstract class AbstractInternationalString 
implements InternationalString
      * then some fallback locale is used. The fallback locale is 
implementation-dependent, and
      * is not necessarily the same as the default locale used by the {@link 
#toString()} method.
      *
-     * <h4>Handling of <code>Locale.ROOT</code> argument value</h4>
+     * <h4>Handling of {@code Locale.ROOT} argument value</h4>
      * {@link Locale#ROOT} can be given to this method for requesting a 
"unlocalized" string,
      * typically some programmatic values like enumerations or identifiers. 
While identifiers
      * often look like English words, {@code Locale.ROOT} is not considered 
synonymous to
      * {@link Locale#ENGLISH} because the values may differ in the way numbers 
and dates are
      * formatted (e.g. using the ISO 8601 standard for dates instead of 
English conventions).
      *
-     * <h4>Handling of <code>null</code> argument value</h4>
+     * <h4>Handling of {@code null} argument value</h4>
      * The {@code Locale.ROOT} constant is new in Java 6. Some other libraries 
designed for Java 5
      * use the {@code null} value for "unlocalized" strings. Apache SIS 
accepts {@code null} value
      * for inter-operability with those libraries. However, the behavior is 
implementation dependent:
@@ -183,9 +182,8 @@ public abstract class AbstractInternationalString 
implements InternationalString
     }
 
     /**
-     * Compares this string with the specified object for order. This method 
compares
-     * the string in the {@linkplain Locale#getDefault() default locale}, as 
returned
-     * by {@link #toString()}.
+     * Compares this string with the specified object for order.
+     * This method compares the strings in the default locale, as returned by 
{@link #toString()}.
      *
      * @param  object  the string to compare with this string.
      * @return a negative number if this string is before the given string,
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/TreeTableForGUI.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/TreeTableForGUI.java
new file mode 100644
index 0000000000..9a7f40eb96
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/TreeTableForGUI.java
@@ -0,0 +1,42 @@
+/*
+ * 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.util.internal.shared;
+
+import org.apache.sis.util.collection.TreeTable;
+
+
+/**
+ * An extension of the {@code TreeTable} interface for use with the 
<abbr>GUI</abbr> module.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public interface TreeTableForGUI extends TreeTable {
+    /**
+     * Returns whether the given value produces by the given node is a title.
+     * Title are a short description of the node, typically copied from one of 
the children.
+     * For example for the code of a {@code Citation} object, this is the 
{@code title} property.
+     *
+     * <p>This information is used by <abbr>GUI</abbr> for showing the value 
when the node is collapsed,
+     * and hiding the value when the node is expanded for avoiding redundancy 
with the child that really
+     * provides the value.</p>
+     *
+     * @param  node   the node where the value come from.
+     * @param  value  the value provided by the node.
+     * @return whether the given value is a title.
+     */
+    boolean isNodeTitle(Node node, Object value);
+}
diff --git 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/TreeNode.java
 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/TreeNode.java
index 0bd120a61d..510c2125ba 100644
--- 
a/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/TreeNode.java
+++ 
b/incubator/src/org.apache.sis.storage.geoheif/main/org/apache/sis/storage/isobmff/TreeNode.java
@@ -42,6 +42,7 @@ import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.collection.DefaultTreeTable;
 import org.apache.sis.util.collection.TreeTableFormat;
 import org.apache.sis.util.internal.shared.PropertyFormat;
+import org.apache.sis.util.internal.shared.TreeTableForGUI;
 import org.apache.sis.storage.isobmff.base.ItemInfoEntry;
 import org.apache.sis.storage.geoheif.GeoHeifStore;
 import org.apache.sis.storage.metadata.NodeSummary;
@@ -123,7 +124,7 @@ public abstract class TreeNode {
             @Override CharSequence summary(final TreeBuilder tree, final 
Number value, final String text) {
                 final String name = tree.getItemName(value);
                 if (name != null) {
-                    // Do not wrap in `NodeSummary` for keeping the name 
always visible.
+                    // Do not wrap in `NodeSummary` because we want to keep 
that name always visible.
                     return name;
                 }
                 return super.summary(tree, value, text);
@@ -157,7 +158,7 @@ public abstract class TreeNode {
                     }
                     break;
                 }
-            };
+            }
             return tree.integerFormat.format(value);
         }
 
@@ -170,7 +171,7 @@ public abstract class TreeNode {
          * @return text to show as a summary of a collapsed node.
          */
         CharSequence summary(final TreeBuilder tree, final Number value, final 
String text) {
-            return new NodeSummary(text);
+            return NodeSummary.of(text);
         }
 
         /**
@@ -258,7 +259,7 @@ public abstract class TreeNode {
      * The value column is replaced by the {@code VALUE_AS_TEXT} column at 
{@link #toString()} time.
      */
     @SuppressWarnings("serial")
-    private static final class Tree extends DefaultTreeTable implements 
Localized {
+    private static final class Tree extends DefaultTreeTable implements 
TreeTableForGUI, Localized {
         /**
          * The locale to use, or {@code null} for the default.
          */
@@ -284,6 +285,14 @@ public abstract class TreeNode {
             return locale;
         }
 
+        /**
+         * Returns whether the given value produces by the given node is a 
title.
+         */
+        @Override
+        public boolean isNodeTitle(final TreeTable.Node node, final Object 
value) {
+            return (value instanceof NodeSummary);
+        }
+
         /**
          * Returns a string representation of the tree table.
          */
@@ -511,10 +520,7 @@ public abstract class TreeNode {
             if (summary == null) {
                 final TreeTable.Node child = 
Containers.peekIfSingleton(target.getChildren());
                 if (child != null) {
-                    summary = child.getValue(TableColumn.VALUE_AS_TEXT);
-                    if (summary != null && !(summary instanceof NodeSummary)) {
-                        summary = new NodeSummary(summary.toString());
-                    }
+                    summary = 
NodeSummary.of(child.getValue(TableColumn.VALUE_AS_TEXT));
                 }
             }
             if (summary != null) {
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/metadata/MetadataTree.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/metadata/MetadataTree.java
index d68ec03aef..54f27b5786 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/metadata/MetadataTree.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/metadata/MetadataTree.java
@@ -52,7 +52,7 @@ import org.apache.sis.gui.internal.ExceptionReporter;
 import org.apache.sis.util.collection.TreeTable;
 import org.apache.sis.util.collection.TableColumn;
 import org.apache.sis.util.resources.Vocabulary;
-import org.apache.sis.storage.metadata.NodeSummary;
+import org.apache.sis.util.internal.shared.TreeTableForGUI;
 
 
 /**
@@ -150,7 +150,9 @@ check:      if (data != null) {
                 if (columns.contains(TableColumn.NAME)) {
                     for (final TableColumn<?> value : VALUES_COLUMNS) {
                         if (columns.contains(value)) {
-                            ((MetadataTree) getBean()).valueSourceColumn = 
value;
+                            final var tree = (MetadataTree) getBean();
+                            tree.valueSourceColumn = value;
+                            tree.formatter.setTree(data);
                             break check;
                         }
                     }
@@ -345,6 +347,11 @@ check:      if (data != null) {
     private static final class Formatter extends PropertyValueFormatter
             implements Callback<CellDataFeatures<TreeTable.Node, String>, 
ObservableValue<String>>
     {
+        /**
+         * The tree table if it is an instance of {@code TreeTableForGUI}, or 
{@code null} otherwise.
+         */
+        private TreeTableForGUI treeIfGUI;
+
         /**
          * The observable properties created for each tree table node.
          * We need to reuse the instances created for each node in order to 
keep listeners.
@@ -361,6 +368,13 @@ check:      if (data != null) {
             observables = new WeakHashMap<>();
         }
 
+        /**
+         * Sets the source of the tree nodes to be formatted.
+         */
+        final void setTree(final TreeTable data) {
+            treeIfGUI = (data instanceof TreeTableForGUI c) ? c : null;
+        }
+
         /**
          * Returns the value of the metadata property wrapped by the given 
argument.
          * This method is invoked by JavaFX when a new cell needs to be 
rendered.
@@ -379,7 +393,7 @@ check:      if (data != null) {
                 } else {
                     text = formatUsingStringBuilder(value);
                 }
-                if (value instanceof NodeSummary) {
+                if (treeIfGUI != null && treeIfGUI.isNodeTitle(node, value)) {
                     final var summary = new SummaryProperty(text);
                     item.expandedProperty().addListener(new 
WeakChangeListener<>(summary));
                     property = summary;
@@ -394,9 +408,9 @@ check:      if (data != null) {
 
     /**
      * A property with a text which is shown or hidden depending on whether 
the node is collapsed or expanded.
-     * This is used for content of {@link NodeSummary}. We want to hide this 
content when the node is expanded
-     * because it become redundant with the children, and this redundancy is 
distracting when the user wants
-     * to look for the child that contains this information.
+     * This is used for content of node titles. We want to hide this content 
when the node is expanded because
+     * it become redundant with the children, and this redundancy is 
distracting when the user wants to look
+     * for the child that contains this information.
      */
     private static final class SummaryProperty extends SimpleStringProperty 
implements ChangeListener<Boolean> {
         /**
diff --git 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/metadata/StandardMetadataTree.java
 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/metadata/StandardMetadataTree.java
index 6dfd5f0b2e..7248bfe09a 100644
--- 
a/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/metadata/StandardMetadataTree.java
+++ 
b/optional/src/org.apache.sis.gui/main/org/apache/sis/gui/metadata/StandardMetadataTree.java
@@ -27,7 +27,6 @@ import javafx.scene.input.Clipboard;
 import javafx.scene.input.ClipboardContent;
 import org.opengis.metadata.Metadata;
 import org.opengis.referencing.IdentifiedObject;
-import org.apache.sis.metadata.AbstractMetadata;
 import org.apache.sis.metadata.MetadataStandard;
 import org.apache.sis.metadata.ValueExistencePolicy;
 import org.apache.sis.xml.XML;
@@ -64,7 +63,7 @@ import org.apache.sis.io.wkt.WKTFormat;
  *
  * @author  Siddhesh Rane (GSoC)
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.2
+ * @version 1.7
  * @since   1.1
  */
 public class StandardMetadataTree extends MetadataTree {
@@ -102,11 +101,8 @@ public class StandardMetadataTree extends MetadataTree {
         final TreeTable tree;
         if (metadata == null) {
             tree = null;
-        } else if (metadata instanceof AbstractMetadata md) {
-            tree = md.asTreeTable();
         } else {
-            // `COMPACT` is the default policy of 
`AbstractMetadata.asTreeTable()`.
-            tree = MetadataStandard.ISO_19115.asTreeTable(metadata, 
Metadata.class, ValueExistencePolicy.COMPACT);
+            tree = MetadataStandard.ISO_19115.asTreeTable(metadata, 
Metadata.class, ValueExistencePolicy.TITLED);
         }
         setContent(tree);
     }


Reply via email to