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

commit 69716c5e8fdb09281b3f99f62417deb5e71f9b47
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu May 28 13:02:02 2026 +0200

    Support transitive search of metadata title when metadata are represented 
as a tree.
---
 .../org/apache/sis/metadata/MetadataStandard.java  | 88 ++++++++++++++++++----
 .../apache/sis/metadata/ModifiableMetadata.java    |  8 +-
 .../org/apache/sis/metadata/PropertyAccessor.java  |  2 +-
 .../org/apache/sis/metadata/TitleProperty.java     | 11 +--
 .../main/org/apache/sis/metadata/TreeNode.java     | 35 +++++----
 .../org/apache/sis/metadata/TreeNodeChildren.java  | 74 ++++++++++++------
 .../apache/sis/metadata/ValueExistencePolicy.java  |  1 +
 .../apache/sis/metadata/sql/MetadataSource.java    | 14 ++--
 .../apache/sis/metadata/sql/MetadataWriter.java    | 30 +++++---
 .../sis/metadata/ModifiableMetadataTest.java       |  1 +
 .../sis/metadata/PropertyConsistencyCheck.java     |  8 +-
 11 files changed, 186 insertions(+), 86 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 f485016d8f..5746d44b2f 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
@@ -20,8 +20,10 @@ import java.util.Set;
 import java.util.Map;
 import java.util.HashSet;
 import java.util.LinkedHashMap;
+import java.util.IdentityHashMap;
 import java.util.Iterator;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.io.IOException;
@@ -359,9 +361,7 @@ public class MetadataStandard implements Serializable {
 
     /**
      * Returns the accessor for the specified metadata instance.
-     * The {@code propertyType.isInstance(metadata)} condition should always 
be {@code true},
-     * but this is not verified by this constructor. Instead, the validity can 
be verified
-     * after constructor with {@link CacheKey#isValid()}.
+     * The {@code propertyType.isInstance(metadata)} condition should always 
be {@code true}.
      *
      * <p>A null value for {@code propertyType} is not equivalent to {@code 
Object.class}.
      * If the value is null, this constructor tries to detect the interface 
automatically.
@@ -720,27 +720,83 @@ public class MetadataStandard implements Serializable {
     }
 
     /**
-     * Returns a value of the "title" property of the given metadata object.
-     * The title property is defined by {@link TitleProperty} annotation on 
the implementation class.
+     * Returns the value of the property that summarizes the given metadata 
object.
+     * The property is specified by the {@link TitleProperty} annotation on 
the implementation class.
+     * This method first searches for {@code TitleProperty} in the 
implementation class of the given
+     * {@code metadata}. If the implementation class is not annotated, then 
this method searches in
+     * the {@linkplain #getImplementation(Class) implementation class managed 
by this standard}.
+     *
+     * <p>If the property value is itself another metadata annotated with 
{@code TitleProperty},
+     * then the search for a title continues recursively in the other metadata.
+     * If a cyclic graph is detected, this method returns the last value 
before the cycle.</p>
      *
      * @param  metadata  the metadata for which to get the title property, or 
{@code null}.
-     * @return the title property value of the given metadata, or {@code null} 
if none.
+     * @return the title property value of the given metadata, or empty if 
none.
      *
      * @see TitleProperty
      * @see ValueExistencePolicy#TITLED
+     *
+     * @since 1.7
      */
-    final Object getTitle(final Object metadata) {
-        if (metadata != null) {
-            final Class<?> type = metadata.getClass();
-            final PropertyAccessor accessor = getTypeAccessor(type, false);
-            if (accessor != null) {
-                TitleProperty an = type.getAnnotation(TitleProperty.class);
-                if (an != null || (an = 
accessor.implementation.getAnnotation(TitleProperty.class)) != null) {
-                    return accessor.get(accessor.indexOf(an.name(), false), 
metadata);
+    public Optional<Object> getTitle(final Object metadata) {
+        return Optional.ofNullable(getTitle(metadata, null, false, false, 
false));
+    }
+
+    /**
+     * Implementation of {@link #getTitle(Object)} with the possibility to 
specify the base interface.
+     * The {@code metadata} argument should be an instance of {@code 
propertyType}.
+     *
+     * <p>Note that a return value of {@code null} may be either because the 
metadata has no
+     * {@link TitleProperty} property, or because the property exists but has 
a {@code null} value.
+     * The {@code isStarted} parameter is required for callers that need to 
distinguish these two cases.</p>
+     *
+     * @param  metadata      the metadata for which to get the title property, 
or {@code null}.
+     * @param  propertyType  the interface which should be implemented to the 
metadata.
+     * @param  isTypeOnly    {@code false} if {@code metadata} is an instance, 
or {@code true} if a {@code Class}.
+     * @param  wantTypeOnly  whether to return the property type (as a {@link 
Class}) rather than the value.
+     * @param  isStarted     if this method is invoked for a search that 
already started on the first annotated property.
+     * @return the title property value or type of the given metadata, or 
{@code null} if none.
+     */
+    final Object getTitle(Object   metadata,
+                          Class<?> propertyType,
+                          boolean  isTypeOnly,
+                          final boolean wantTypeOnly,
+                          final boolean isStarted)
+    {
+        final var done = new IdentityHashMap<Object, Boolean>();
+        while (metadata != null) {
+            final Class<?> type;
+            final PropertyAccessor accessor;
+            if (isTypeOnly) {
+                type     = (Class<?>) metadata;
+                accessor = getTypeAccessor(type, false);
+            } else {
+                type     = metadata.getClass();
+                accessor = getInstanceAccessor(metadata, propertyType, false);
+            }
+            if (accessor == null) break;    // Not a metadata.
+            TitleProperty a = type.getAnnotation(TitleProperty.class);
+            if (a == null) {
+                if (accessor.implementation == type) break;
+                a = accessor.implementation.getAnnotation(TitleProperty.class);
+                if (a == null) break;       // No `TitleProperty` annotation.
+            }
+            final int index = accessor.indexOf(a.name(), false);
+            if (index < 0 || accessor.isCollectionOrMap(index)) break;      // 
Illegal property in the annotation.
+            if (done.put(metadata, Boolean.TRUE) != null) break;            // 
Safety against never-ending loop.
+            propertyType = accessor.type(index, TypeValuePolicy.ELEMENT_TYPE);
+            if (isTypeOnly) {
+                metadata = propertyType;
+            } else {
+                metadata = accessor.get(index, metadata);
+                if (metadata == null && wantTypeOnly) {
+                    metadata = propertyType;
+                    isTypeOnly = true;
                 }
             }
+            // Check recursively if the property value is another metadata.
         }
-        return null;
+        return (!isStarted && done.isEmpty()) ? null : wantTypeOnly ? 
propertyType : metadata;
     }
 
     /**
@@ -790,7 +846,7 @@ public class MetadataStandard implements Serializable {
      * the following code prints the {@link 
org.opengis.util.InternationalString} class name:
      *
      * {@snippet lang="java" :
-     *     MetadataStandard  standard = MetadataStandard.ISO_19115;
+     *     MetadataStandard standard = MetadataStandard.ISO_19115;
      *     Map<String, Class<?>> types = standard.asTypeMap(Citation.class, 
UML_IDENTIFIER, ELEMENT_TYPE);
      *     Class<?> value = types.get("alternateTitle");
      *     System.out.println(value);                       // class 
org.opengis.util.InternationalString
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ModifiableMetadata.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ModifiableMetadata.java
index b21e83387b..f91f1d7001 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ModifiableMetadata.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/ModifiableMetadata.java
@@ -371,7 +371,7 @@ public abstract class ModifiableMetadata extends 
AbstractMetadata {
      *
      * @since 1.0
      */
-    protected void checkWritePermission(Object current) throws 
UnmodifiableMetadataException {
+    protected void checkWritePermission(final Object current) throws 
UnmodifiableMetadataException {
         if (state != COMPLETABLE) {
             if (state == FINAL) {
                 throw new 
UnmodifiableMetadataException(Resources.format(Resources.Keys.UnmodifiableMetadata));
@@ -383,9 +383,9 @@ public abstract class ModifiableMetadata extends 
AbstractMetadata {
             } else {
                 standard = getStandard();
             }
-            final Object c = standard.getTitle(current);
-            if (c != null) current = c;
-            throw new 
UnmodifiableMetadataException(Resources.format(Resources.Keys.ElementAlreadyInitialized_1,
 current));
+            throw new UnmodifiableMetadataException(Resources.format(
+                    Resources.Keys.ElementAlreadyInitialized_1,
+                    standard.getTitle(current).orElse(current)));
         }
     }
 
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 b9f4278176..09d9eba38f 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
@@ -621,7 +621,7 @@ class PropertyAccessor {
     }
 
     /**
-     * Returns {@code true} if the type at the given index is {@link 
Collection} or {@link Map}.
+     * Returns {@code true} if the type at the given index is assignable to 
{@link Collection} or {@link Map}.
      */
     final boolean isCollectionOrMap(final int index) {
         if (index >= 0 && index < allCount) {
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 42e15f4433..ff7c2d301b 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
@@ -32,7 +32,7 @@ import java.lang.annotation.Documented;
  * especially when there is redundant node names.
  *
  * <h2>Example</h2>
- * the {@code Citation} type contains a {@linkplain 
org.apache.sis.metadata.iso.citation.DefaultCitation#getDates() date}
+ * The {@code Citation} type contains a {@linkplain 
org.apache.sis.metadata.iso.citation.DefaultCitation#getDates() date}
  * property which itself contains another {@linkplain 
org.apache.sis.metadata.iso.citation.DefaultCitationDate#getDate()
  * date} property. They form a tree like below:
  *
@@ -45,7 +45,7 @@ import java.lang.annotation.Documented;
  *
  * With <code>&#64;TitleProperty(name="title")</code> on {@code 
DefaultCitation} implementation class and
  * <code>&#64;TitleProperty(name="date")</code> on {@code DefaultCitationDate} 
class,
- * Apache SIS can produce a more compact tree table view should be as below:
+ * Apache <abbr>SIS</abbr> can produce a more compact tree table view should 
be as below:
  *
  * <pre class="text">
  *   Citation……………………… My document
@@ -53,15 +53,16 @@ import java.lang.annotation.Documented;
  *       └─Date type…… Creation</pre>
  *
  * <h2>Condition</h2>
- * The property referenced by this annotation should be the main property if 
possible, but not necessarily
- * since it may be only a label. However, the property shall be a singleton 
([0…1] or [1…1] multiplicity)
- * and cannot be another metadata object.
+ * The property referenced by this annotation should be a title, a label or a 
property which is considered
+ * as the main property of the metadata object. The property shall be a 
singleton ([0…1] or [1…1] multiplicity).
+ * If the property is another metadata object, then that other object shall 
itself have this annotation.
  *
  * @author  Martin Desruisseaux (Geomatys)
  * @version 0.8
  * @since   0.8
  *
  * @see ValueExistencePolicy#TITLED
+ * @see MetadataStandard#getTitle(Object)
  */
 @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 c92448097d..ef951e6654 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
@@ -207,6 +207,13 @@ class TreeNode implements Node {
         return table.standard.isMetadata(type);
     }
 
+    /**
+     * Returns the metadata interface for the object represented by this node.
+     */
+    private Class<?> getInterface() {
+        return table.standard.getInterface(key(), null);
+    }
+
     /**
      * Returns the key to use for calls to {@link MetadataStandard} methods.
      * This key is used only for some default method implementations in the 
root node.
@@ -236,7 +243,7 @@ class TreeNode implements Node {
      * order to return the property identifier instead.
      */
     String getIdentifier() {
-        final Class<?> type = table.standard.getInterface(key(), null);
+        final Class<?> type = getInterface();
         final String id = Types.getStandardName(type);
         return (id != null) ? id : Classes.getShortName(type);
     }
@@ -258,8 +265,7 @@ class TreeNode implements Node {
      * <p>The default implementation is suitable only for the root node - 
subclasses must override.</p>
      */
     CharSequence getName() {
-        return CharSequences.camelCaseToSentence(Classes.getShortName(
-                table.standard.getInterface(key(), null))).toString();
+        return 
CharSequences.camelCaseToSentence(Classes.getShortName(getInterface())).toString();
     }
 
     /**
@@ -348,10 +354,13 @@ class TreeNode implements Node {
      * this condition should ensure that two equal nodes have the same values 
and children.
      */
     @Override
-    public boolean equals(final Object other) {
-        return (other != null) && other.getClass() == getClass()
-                && ((TreeNode) other).metadata == metadata
-                && ((TreeNode) other).baseType == baseType;
+    public boolean equals(final Object obj) {
+        if (obj != null && obj.getClass() == getClass()) {
+            final var other = (TreeNode) obj;
+            return other.metadata == metadata &&
+                   other.baseType == baseType;
+        }
+        return false;
     }
 
     /**
@@ -1028,7 +1037,7 @@ class TreeNode implements Node {
     /**
      * 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()}.
+     * as {@link TreeNodeChildren#getParentTitle(boolean)}.
      */
     private TreeNodeChildren getCompactChildren() {
         if (table.valuePolicy.isTitled()) {
@@ -1042,9 +1051,9 @@ class TreeNode implements Node {
     }
 
     /**
-     * Returns the value of this node in the given column, or {@code null} if 
none. This method verifies
-     * the {@code column} argument, then delegates to {@link #getName()}, 
{@link #getUserObject()} or
-     * other properties.
+     * Returns the value of this node in the given column, or {@code null} if 
none.
+     * This method verifies the {@code column} argument, then delegates to 
{@link #getName()},
+     * {@link #getUserObject()} or other properties.
      */
     @Override
     public final <V> V getValue(final TableColumn<V> column) {
@@ -1062,7 +1071,7 @@ class TreeNode implements Node {
         } else if (column == TableColumn.TYPE) {
             @SuppressWarnings("LocalVariableHidesMemberVariable")
             final TreeNodeChildren children = getCompactChildren();
-            if (children == null || (value = children.getParentType()) == 
null) {
+            if (children == null || (value = (Class<?>) 
children.getParentTitle(true)) == null) {
                 value = baseType;
             }
         } else if (column == TableColumn.VALUE) {
@@ -1072,7 +1081,7 @@ class TreeNode implements Node {
                 @SuppressWarnings("LocalVariableHidesMemberVariable")
                 final TreeNodeChildren children = getCompactChildren();
                 if (children != null) {
-                    value = children.getParentTitle();
+                    value = children.getParentTitle(false);
                 }
             }
         } else if (column == MetadataColumn.OBLIGATION) {
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 04538753ac..677735f593 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
@@ -146,23 +146,31 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
         /*
          * 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 make trees more 
compact and easier to read.
-         * That property shall be a singleton for a simple value (not another 
metadata object).
+         * That property shall be a singleton for a simple value.
          */
-        if (parent.table.valuePolicy.isTitled()) {
+        final TreeTableView table = parent.table;
+        if (table.valuePolicy.isTitled()) {
             TitleProperty an = 
accessor.implementation.getAnnotation(TitleProperty.class);
             if (an == null) {
-                Class<?> implementation = 
parent.table.standard.getImplementation(accessor.type);
-                if (implementation != null) {
+                Class<?> implementation = 
table.standard.getImplementation(accessor.type);
+                if (implementation != null && accessor.implementation != 
implementation) {
                     an = implementation.getAnnotation(TitleProperty.class);
                 }
             }
             if (an != null) {
                 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;
+                if (type != null && type == accessor.type(index, 
TypeValuePolicy.PROPERTY_TYPE)) {
+                    /*
+                     * In compact mode, do not use metadata object as title 
(with recursive search of title)
+                     * because we don't want to hide the whole metadata object 
from the tree.
+                     */
+                    final boolean isCompact = (table.valuePolicy == 
ValueExistencePolicy.COMPACT);
+                    if (!(isCompact && parent.isMetadata(type))) {
+                        hiddenProperty = isCompact ? index : -1;
+                        titleProperty  = index;
+                        return;
+                    }
                 }
             }
         }
@@ -178,19 +186,34 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
     }
 
     /**
-     * If a simple value should be associated to the parent node, returns the 
type of that value.
-     * Otherwise returns {@code null}.
-     */
-    final Class<?> getParentType() {
-        return (titleProperty >= 0) ? accessor.type(titleProperty, 
TypeValuePolicy.ELEMENT_TYPE) : null;
-    }
-
-    /**
-     * If a simple value should be associated to the parent node, returns that 
value.
-     * Otherwise returns {@code null}.
+     * If a simple value should be associated to the parent node, returns that 
value or the type of that value.
+     * Otherwise returns {@code null}. The title is identified by {@link 
TitleProperty} annotation on the class.
+     * If {@code wantTypeOnly} is {@code true}, then the returned object can 
be safety cast to {@link Class}.
+     *
+     * @param  wantTypeOnly  whether to return the property type (as a {@link 
Class}) instead of the value.
+     * @return the value or type of the title property of the given metadata, 
or {@code null} if none.
      */
-    final Object getParentTitle() {
-        return (titleProperty >= 0) ? valueAt(titleProperty) : null;
+    final Object getParentTitle(final boolean wantTypeOnly) {
+        if (isNodeTitle()) {
+            final boolean isCompact = (hiddenProperty >= 0);
+            if (isCompact && !wantTypeOnly) {
+                return valueAt(titleProperty);          // Skip the recursive 
search in metadata objects.
+            }
+            final Class<?> type = accessor.type(titleProperty, 
TypeValuePolicy.ELEMENT_TYPE);
+            if (isCompact) {
+                return type;                            // Skip the recursive 
search in metadata objects.
+            }
+            final Object value = valueAt(titleProperty);
+            if (value != null || wantTypeOnly) {
+                return parent.table.standard.getTitle(
+                        (value != null) ? value : type, // Value or type to 
check for `TitleProperty` annotation.
+                        type,                           // For disambiguation 
if `value` implements many interfaces.
+                        value == null,                  // Whether the first 
argument is a class rather than instance.
+                        wantTypeOnly,                   // Whether the caller 
wants only a `Class`.
+                        true);                          // Tells that the 
first argument is already a title.
+            }
+        }
+        return null;
     }
 
     /**
@@ -198,11 +221,12 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
      * The returned Boolean tells whether the value has been written.
      */
     final boolean setParentTitle(final Object value) {
-        if (titleProperty < 0) {
-            return false;
+        if (hiddenProperty >= 0) {
+            // Accept only the hidden property because it requires no 
recursive search of title.
+            accessor.set(hiddenProperty, metadata, value, 
PropertyAccessor.RETURN_NULL);
+            return true;
         }
-        accessor.set(titleProperty, metadata, value, 
PropertyAccessor.RETURN_NULL);
-        return true;
+        return false;
     }
 
     /**
@@ -273,7 +297,7 @@ final class TreeNodeChildren extends 
AbstractCollection<TreeTable.Node> {
      * @param  index     the index in the accessor (<em>not</em> the index in 
this collection).
      * @param  subIndex  if the property at {@code index} is a collection, the 
index in that
      *         collection (<em>not</em> the index in <em>this</em> 
collection). Otherwise -1.
-     * @return the node to be returned by public API.
+     * @return the node to be returned by public <abbr>API</abbr>.
      */
     final TreeNode.Element childAt(final int index, final int subIndex) {
         TreeNode.Element node = children[index];
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 c283cef3c3..8ec800c108 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
@@ -141,6 +141,7 @@ public enum ValueExistencePolicy {
      * See {@link #COMPACT} Javadoc for an example.</p>
      *
      * @see TitleProperty
+     * @see MetadataStandard#getTitle(Object)
      *
      * @since 1.7
      */
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataSource.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataSource.java
index a9c7527db7..183769612e 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataSource.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataSource.java
@@ -367,8 +367,10 @@ public class MetadataSource implements AutoCloseable {
      * @param  schema      the database schema were metadata tables are 
stored, or {@code null} if none.
      * @param  properties  additional options, or {@code null} if none. See 
class javadoc for a description.
      */
-    public MetadataSource(final MetadataStandard standard, final DataSource 
dataSource,
-            final String schema, final Map<String,?> properties)
+    public MetadataSource(final MetadataStandard standard,
+                          final DataSource       dataSource,
+                          final String           schema,
+                          final Map<String, ?>   properties)
     {
         ArgumentChecks.ensureNonNull("standard",   standard);
         ArgumentChecks.ensureNonNull("dataSource", dataSource);
@@ -935,10 +937,10 @@ public class MetadataSource implements AutoCloseable {
                 final Dispatcher toSearch = new Dispatcher(identifier, this);
                 try {
                     value = subType.getConstructor().newInstance();
-                    final LookupInfo info            = getLookupInfo(subType);
-                    final Map<String,Object> map     = asValueMap(value);
-                    final Map<String,String> methods = 
standard.asNameMap(subType, NAME_POLICY, KeyNamePolicy.METHOD_NAME);
-                    for (final Map.Entry<String,Object> entry : 
map.entrySet()) {
+                    final LookupInfo info             = getLookupInfo(subType);
+                    final Map<String, Object> map     = asValueMap(value);
+                    final Map<String, String> methods = 
standard.asNameMap(subType, NAME_POLICY, KeyNamePolicy.METHOD_NAME);
+                    for (final Map.Entry<String, Object> entry : 
map.entrySet()) {
                         method = 
subType.getMethod(methods.get(entry.getKey()));
                         info.setMetadataType(subType);
                         final Object p = readColumn(info, method, toSearch);
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataWriter.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataWriter.java
index 1eb1fd9488..b6dcc0d6d5 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataWriter.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/sql/MetadataWriter.java
@@ -146,8 +146,11 @@ public class MetadataWriter extends MetadataSource {
      * @param  schema      the database schema were metadata tables are 
stored, or {@code null} if none.
      * @param  properties  additional options, or {@code null} if none. See 
class javadoc for a description.
      */
-    public MetadataWriter(final MetadataStandard standard, final DataSource 
dataSource, final String schema,
-            final Map<String,?> properties)
+    @SuppressWarnings("LocalVariableHidesMemberVariable")
+    public MetadataWriter(final MetadataStandard standard,
+                          final DataSource       dataSource,
+                          final String           schema,
+                          final Map<String, ?>   properties)
     {
         super(standard, dataSource, schema, properties);
         Integer maximumIdentifierLength           = 
Containers.property(properties, "maximumIdentifierLength", Integer.class);
@@ -235,8 +238,11 @@ public class MetadataWriter extends MetadataSource {
      * @throws ClassCastException if the metadata object does not implement a 
metadata interface
      *         of the expected package.
      */
-    private String add(final Statement stmt, final Object metadata, final 
Map<Object,String> done,
-            final String parent) throws ClassCastException, SQLException, 
FactoryException
+    private String add(final Statement stmt,
+                       final Object metadata,
+                       final Map<Object, String> done,
+                       final String parent)
+            throws ClassCastException, SQLException, FactoryException
     {
         final SQLBuilder helper = helper();
         /*
@@ -244,8 +250,8 @@ public class MetadataWriter extends MetadataSource {
          * concurrent changes in the metadata object. This protection is 
needed because we need to
          * perform multiple passes on the same metadata.
          */
-        final Map<String,Object> asValueMap = asValueMap(metadata);
-        final Map<String,Object> asSingletons = new LinkedHashMap<>();
+        final Map<String, Object> asValueMap = asValueMap(metadata);
+        final Map<String, Object> asSingletons = new LinkedHashMap<>();
         for (final Map.Entry<String,Object> entry : asValueMap.entrySet()) {
             asSingletons.put(entry.getKey(), 
extractFromCollection(entry.getValue()));
         }
@@ -288,8 +294,8 @@ public class MetadataWriter extends MetadataSource {
          * this process but will not create the constraints now because the 
foreigner tables may not exist yet.
          * They will be created later by recursive calls to this method a 
little bit below.
          */
-        Map<String,Class<?>> colTypes = null, colTables = null;
-        final Map<String,FKey> foreigners = new LinkedHashMap<>();
+        Map<String, Class<?>> colTypes = null, colTables = null;
+        final var foreigners = new LinkedHashMap<String, FKey>();
         for (final String column : asSingletons.keySet()) {
             if (!columns.contains(column)) {
                 if (colTypes == null) {
@@ -401,8 +407,8 @@ public class MetadataWriter extends MetadataSource {
          * Once a dependency has been added to the database, the corresponding 
value in
          * the `asMap` HashMap is replaced by the identifier of the dependency 
we just added.
          */
-        Map<String,FKey> referencedTables = null;
-        for (final Map.Entry<String,Object> entry : asSingletons.entrySet()) {
+        Map<String, FKey> referencedTables = null;
+        for (final Map.Entry<String, Object> entry : asSingletons.entrySet()) {
             Object value = entry.getValue();
             final Class<?> type = value.getClass();
             if (ControlledVocabulary.class.isAssignableFrom(type)) {
@@ -689,7 +695,7 @@ public class MetadataWriter extends MetadataSource {
      * @return the proposed identifier, or {@code null} if this method does 
not have any suggestion.
      * @throws SQLException if an access to the database was desired but 
failed.
      */
-    protected String suggestIdentifier(final Object metadata, final 
Map<String,Object> asValueMap) throws SQLException {
+    protected String suggestIdentifier(final Object metadata, final 
Map<String, Object> asValueMap) throws SQLException {
         String identifier = null;
         final Collection<? extends Identifier> identifiers;
         if (metadata instanceof Identifier) {
@@ -717,7 +723,7 @@ public class MetadataWriter extends MetadataSource {
             if (tp != null) {
                 final Object value = 
asValueMap.get(Strings.trimOrNull(tp.name()));
                 if (value != null) {
-                    identifier = Strings.trimOrNull(value.toString());
+                    identifier = 
Strings.trimOrNull(standard.getTitle(value).orElse(value).toString());
                 }
             }
         }
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/ModifiableMetadataTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/ModifiableMetadataTest.java
index ce7abe57ac..389f94111b 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/ModifiableMetadataTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/ModifiableMetadataTest.java
@@ -38,6 +38,7 @@ import static org.apache.sis.test.Assertions.assertSetEquals;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class ModifiableMetadataTest extends TestCase {
     /**
      * An arbitrary metadata on which to perform the tests.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/PropertyConsistencyCheck.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/PropertyConsistencyCheck.java
index a7c2044c91..bf51fbac2f 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/PropertyConsistencyCheck.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/PropertyConsistencyCheck.java
@@ -77,7 +77,7 @@ public abstract class PropertyConsistencyCheck extends 
AnnotationConsistencyChec
     }
 
     /**
-     * Returns the SIS implementation for the given GeoAPI interface.
+     * Returns the <abbr>SIS</abbr> implementation for the given GeoAPI 
interface.
      *
      * @return {@inheritDoc}
      */
@@ -136,7 +136,7 @@ public abstract class PropertyConsistencyCheck extends 
AnnotationConsistencyChec
             if (type == CodeList.class) {
                 return null;
             }
-            final ControlledVocabulary[] codes = (ControlledVocabulary[]) 
type.getMethod("values", (Class[]) null).invoke(null, (Object[]) null);
+            final var codes = (ControlledVocabulary[]) 
type.getMethod("values", (Class[]) null).invoke(null, (Object[]) null);
             return codes[random.nextInt(codes.length)];
         } catch (ReflectiveOperationException e) {
             fail(e.toString());
@@ -328,7 +328,7 @@ public abstract class PropertyConsistencyCheck extends 
AnnotationConsistencyChec
                 if (an != null) {
                     final String name = an.name();
                     final String message = impl.getSimpleName() + '.' + name;
-                    final PropertyAccessor accessor = new 
PropertyAccessor(type, impl, impl);
+                    final var accessor = new PropertyAccessor(type, impl, 
impl);
 
                     // Property shall exist.
                     final int index = accessor.indexOf(name, false);
@@ -357,7 +357,7 @@ public abstract class PropertyConsistencyCheck extends 
AnnotationConsistencyChec
         for (final Class<?> type : types) {
             final Class<?> impl = standard.getImplementation(type);
             if (impl != null) {
-                Map<String,String> names = null;
+                Map<String, String> names = null;
                 for (final Method method : impl.getDeclaredMethods()) {
                     final Dependencies dep = 
method.getAnnotation(Dependencies.class);
                     if (dep != null) {


Reply via email to