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 5a3b53f3e1 Fix of a `ClassCastException` during the merge of two 
metadata. If the source metadata is a class that implements more than one 
metadata interface (for example, the GPX metadata), use the type of the target 
class or use the type of the property for resolving the ambiguity.
5a3b53f3e1 is described below

commit 5a3b53f3e1ee7980c6433f5fdb4f40157ff6cf05
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Jul 19 19:50:13 2026 +0200

    Fix of a `ClassCastException` during the merge of two metadata.
    If the source metadata is a class that implements more than one metadata 
interface
    (for example, the GPX metadata), use the type of the target class or use 
the type
    of the property for resolving the ambiguity.
---
 .../org/apache/sis/metadata/AbstractMetadata.java  |  5 +-
 .../org/apache/sis/metadata/MetadataStandard.java  | 74 ++++++++++++++++---
 .../apache/sis/metadata/ModifiableMetadata.java    | 46 +++++++++++-
 .../org/apache/sis/metadata/PropertyAccessor.java  |  3 +-
 .../sis/metadata/internal/shared/Merger.java       | 82 +++++++++++++++++----
 .../apache/sis/metadata/MetadataStandardTest.java  | 12 +--
 .../sis/metadata/internal/shared/MergerTest.java   | 24 +++---
 .../org/apache/sis/storage/gpx/MetadataTest.java   |  8 +-
 .../org/apache/sis/storage/gpx/ReaderTest.java     | 85 +++++++++++++++++-----
 .../sis/storage/metadata/MetadataBuilder.java      |  2 +-
 .../main/org/apache/sis/util/Classes.java          |  9 ++-
 11 files changed, 278 insertions(+), 72 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 44b6af2dce..076d3b13f1 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
@@ -357,11 +357,12 @@ public abstract class AbstractMetadata implements 
LenientComparable, Emptiable {
      * @param  mode    the strictness level of the comparison.
      * @return {@code true} if the given object is equal to this metadata.
      *
-     * @see MetadataStandard#equals(Object, Object, ComparisonMode)
+     * @see MetadataStandard#equals(Object, Object, Class, ComparisonMode)
      */
     @Override
     public boolean equals(final Object object, final ComparisonMode mode) {
-        return getStandard().equals(this, object, mode);
+        final MetadataStandard standard = getStandard();
+        return standard.equals(this, object, 
standard.getInterface(getClass()), mode);
     }
 
     /**
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 5746d44b2f..da2e62ba32 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
@@ -374,6 +374,8 @@ public class MetadataStandard implements Serializable {
      * @param  propertyType  base class of the metadata object, or {@code 
null} if unknown.
      * @param  mandatory     whether this method shall throw an exception if 
no accessor is found.
      * @return the accessor for the given object, or {@code null} if none and 
{@code mandatory} is {@code false}.
+     * @throws ClassCastException if the metadata object does not implement an 
expected metadata interface
+     *         and {@code mandatory} is {@code true}.
      */
     final PropertyAccessor getInstanceAccessor(final Object metadata, Class<?> 
propertyType, final boolean mandatory) {
         if (propertyType == null) {
@@ -637,19 +639,21 @@ public class MetadataStandard implements Serializable {
 
     /**
      * Returns the metadata interface implemented by the specified 
implementation class.
-     * If the given type is already an interface from this standard, then it 
is returned
-     * unchanged.
+     * If the given type is already an interface from this standard, then that 
type is returned directly.
+     * If the given type does not implement an interface from this standard, 
then a {@link ClassCastException} is thrown.
+     * If the given type implements more than one interface from this 
standard, then a {@link ClassCastException} is also
+     * thrown because of the ambiguity.
      *
      * <div class="note"><b>Note:</b>
-     * The word "interface" may be taken in a looser sense than the usual Java 
sense because
-     * if the given type is defined in this standard package, then it is 
returned unchanged.
-     * The standard package is usually made of interfaces and code lists only, 
but this is
-     * not verified by this method.</div>
+     * in this context, "interface" should be understood as
+     * "the type which is defining the public <abbr>API</abbr>".
+     * This is usually an interface in the Java sense, but not always.</div>
      *
      * @param  <T>   the compile-time {@code type}.
      * @param  type  the implementation class.
      * @return the interface implemented by the given implementation class.
-     * @throws ClassCastException if the specified implementation class does 
not implement an interface of this standard.
+     * @throws ClassCastException if the specified implementation class does 
not implement exactly one interface
+     *         of this standard,
      *
      * @see AbstractMetadata#getStandardType()
      */
@@ -657,6 +661,32 @@ public class MetadataStandard implements Serializable {
         return getInterface(new CacheKey(Objects.requireNonNull(type), 
Object.class), null);
     }
 
+    /**
+     * Returns the metadata interface by choosing in a subset of the 
interfaces implemented by the given class.
+     * This method does the same work as {@link #getInterface(Class)} except 
that it ignores all interfaces that
+     * are not assignable to {@code baseType}.
+     * This filtering can avoid some ambiguities when the same class 
implements many interfaces.
+     *
+     * @param  <T>       the compile-time {@code type}.
+     * @param  type      the implementation class.
+     * @param  baseType  base type of the metadata of interest, or {@code 
null} if unspecified.
+     * @return the interface implemented by the given implementation class.
+     * @throws ClassCastException if the specified implementation class is not 
assignable to {@code baseType},
+     *         or if the class does not implement exactly one interface which 
is a subtype of {@code baseType}.
+     *
+     * @since 1.7
+     */
+    public <T> Class<? super T> getInterface(final Class<T> type, Class<?> 
baseType) {
+        if (baseType == null) {
+            baseType = Object.class;
+        }
+        final var key = new CacheKey(Objects.requireNonNull(type), baseType);
+        if (key.isValid()) {
+            return getInterface(key, null);
+        }
+        throw new ClassCastException(key.invalid());
+    }
+
     /**
      * Implementation of {@link #getInterface(Class)} with the possibility to 
specify the property type.
      * We do not provide the additional functionality of this method in public 
API on the assumption that
@@ -1143,9 +1173,35 @@ public class MetadataStandard implements Serializable {
      * @return {@code true} if the given metadata objects are equals or if the 
two arguments are {@code null}.
      * @throws ClassCastException if {@code metadata1} does not implement an 
expected metadata interface.
      *
-     * @see AbstractMetadata#equals(Object, ComparisonMode)
+     * @deprecated Replaced by {@link #equals(Object, Object, Class, 
ComparisonMode)} because this method
+     * is ambiguous when one of the given metadata instances implements more 
than one metadata interface.
      */
+    @Deprecated(since="1.7", forRemoval=true)
     public boolean equals(final Object metadata1, final Object metadata2, 
final ComparisonMode mode) {
+        return equals(metadata1, metadata2, null, mode);
+    }
+
+    /**
+     * Compares the two specified metadata instances for equality.
+     * Unless the {@code mode} argument is {@link ComparisonMode#STRICT},
+     * the two metadata objects do not need to be instances of the same class.
+     * However, {@code metadata1} shall implement an interface defined by this 
{@code MetadataStandard},
+     * otherwise a {@link ClassCastException} may be thrown. If {@code 
metadata1} implements more than
+     * one interface managed by this {@code MetadataStandard}, the ambiguity 
can be resolved by specifying
+     * the interface of interest (or a non-ambiguous super-type of it) in the 
{@code baseType} argument.
+     *
+     * @param  metadata1  the first metadata object to compare, or {@code 
null}.
+     * @param  metadata2  the second metadata object to compare, or {@code 
null}.
+     * @param  baseType   base type of the metadata of interest, or {@code 
null} if unspecified.
+     * @param  mode       the strictness level of the comparison.
+     * @return {@code true} if the given metadata objects are equals or if the 
two arguments are {@code null}.
+     * @throws ClassCastException if {@code metadata1} does not implement an 
expected metadata interface.
+     *
+     * @see AbstractMetadata#equals(Object, ComparisonMode)
+     *
+     * @since 1.7
+     */
+    public boolean equals(final Object metadata1, final Object metadata2, 
final Class<?> baseType, final ComparisonMode mode) {
         if (metadata1 == metadata2) {
             return true;
         }
@@ -1157,7 +1213,7 @@ public class MetadataStandard implements Serializable {
         if (type1 != type2 && mode == ComparisonMode.STRICT) {
             return false;
         }
-        final PropertyAccessor accessor = getInstanceAccessor(metadata1, null, 
true);
+        final PropertyAccessor accessor = getInstanceAccessor(metadata1, 
baseType, true);
         if (type1 != type2) {
             final var key = new CacheKey(type2, accessor.type);
             // Not strictly necessary, but can avoid the relatively costly 
creation of new `PropertyAccessor`.
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 f91f1d7001..aff8483b40 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
@@ -30,8 +30,10 @@ import jakarta.xml.bind.annotation.XmlTransient;
 import org.opengis.util.CodeList;
 import org.opengis.metadata.Metadata;               // For javadoc
 import org.apache.sis.util.Classes;
+import org.apache.sis.util.ComparisonMode;
 import org.apache.sis.util.collection.Containers;
 import org.apache.sis.metadata.internal.Resources;
+import org.apache.sis.metadata.internal.shared.Merger;
 import org.apache.sis.system.Semaphores;
 import org.apache.sis.pending.jdk.JDK19;
 import static org.apache.sis.util.collection.Containers.isNullOrEmpty;
@@ -80,7 +82,7 @@ import static 
org.apache.sis.metadata.internal.shared.ImplementationHelper.value
  *     }
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.6
+ * @version 1.7
  * @since   0.3
  */
 @XmlTransient
@@ -145,6 +147,48 @@ public abstract class ModifiableMetadata extends 
AbstractMetadata {
         super(source);
     }
 
+    /**
+     * Merges the data from the given source into this metadata.
+     * The result of the merge is stored in this metadata.
+     * The given {@code source} metadata is not modified.
+     *
+     * <p>For each non-null and {@linkplain ValueExistencePolicy#NON_EMPTY 
non-empty} property
+     * value from the <var>source</var> metadata, the merge operation is 
defined as below:</p>
+     *
+     * <ul>
+     *   <li>If this metadata does not have a non-null and non-empty value for 
the same property, then the
+     *     reference to the value from the source metadata is stored 
<em>as-is</em> in this metadata.</li>
+     *   <li>Otherwise, if the target property value is a collection, then:
+     *     <ul>
+     *       <li>All source elements that are {@link 
ComparisonMode#BY_CONTRACT equal by contract}
+     *         to an existing target element are ignored.</li>
+     *       <li>For each element of the source collection, a corresponding 
element of the target collection is searched.
+     *         A pair of source and target elements is established if the pair 
meets all of the following conditions:
+     *         <ul>
+     *           <li>The {@linkplain MetadataStandard#getInterface(Class) 
standard type} of the source element
+     *               is assignable to the type of the target element.</li>
+     *           <li>There is no conflict, <i>i.e.</i> no property value that 
are not collection and not equal.</li>
+     *         </ul>
+     *         If such pair is found, then the merge operation if performed 
recursively
+     *         for that pair of source and target elements.</li>
+     *       <li>All other source elements will be added as new elements in 
the target collection.</li>
+     *     </ul>
+     *   </li>
+     *   <li>Otherwise, the merge operation is performed recursively on 
property values.</li>
+     * </ul>
+     *
+     * @param  source  the source metadata to merge into this metadata. Will 
never be modified.
+     * @throws ClassCastException if {@code source} is not an instance 
compatible with this metadata.
+     * @throws InvalidMetadataException if this metadata cannot hold all 
{@code source} properties,
+     *         for example because the source class is a more specialized type 
than this metadata class.
+     * @throws IllegalArgumentException if this method detects a 
cross-reference between source and this metadata.
+     *
+     * @since 1.7
+     */
+    public void merge(final Object source) {
+        new Merger(null).copy(source, this);
+    }
+
     /**
      * Whether the metadata is still editable or has been made final.
      * New {@link ModifiableMetadata} instances are initially {@link #EDITABLE}
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 09d9eba38f..eee084fabe 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
@@ -65,7 +65,8 @@ import static 
org.apache.sis.metadata.ValueExistencePolicy.isNullOrEmpty;
  * <ul>
  *   <li>The standard properties defined by the GeoAPI (or other standard) 
interfaces.
  *       Those properties are the only ones accessible by most methods in this 
class, except
- *       {@link #equals(Object, Object, ComparisonMode)} and {@link 
#walkWritable(MetadataVisitor, Object, Object)}.</li>
+ *       {@link #equals(Object, Object, ComparisonMode)} and
+ *       {@link #walkWritable(MetadataVisitor, Object, Object)}.</li>
  *
  *   <li>Extra properties defined by the {@link IdentifiedObject} interface. 
Those properties
  *       invisible in the ISO 19115-1 model, but appears in ISO 19115-3 XML 
marshalling. So we
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/shared/Merger.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/shared/Merger.java
index f2dac5a0e2..7cf88b7193 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/shared/Merger.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/shared/Merger.java
@@ -32,6 +32,9 @@ import org.apache.sis.metadata.KeyNamePolicy;
 import org.apache.sis.metadata.ValueExistencePolicy;
 import org.apache.sis.util.CorruptedObjectException;
 import org.apache.sis.util.Classes;
+import org.apache.sis.util.Utilities;
+import org.apache.sis.util.ComparisonMode;
+import org.apache.sis.util.collection.CheckedContainer;
 import org.apache.sis.util.internal.shared.Unsafe;
 import org.apache.sis.util.resources.Errors;
 
@@ -44,14 +47,16 @@ import org.apache.sis.util.resources.Errors;
  * <ul>
  *   <li>If the target metadata does not have a non-null and non-empty value 
for the same property, then the
  *     reference to the value from the source metadata is stored 
<em>as-is</em> in the target metadata.</li>
- *   <li>Otherwise if the target value is a collection, then:
+ *   <li>Otherwise, if the target property value is a collection, then:
  *     <ul>
+ *       <li>All source elements that are {@link ComparisonMode#BY_CONTRACT 
equal by contract}
+ *           to an existing target element are ignored.</li>
  *       <li>For each element of the source collection, a corresponding 
element of the target collection is searched.
  *         A pair of source and target elements is established if the pair 
meets all of the following conditions:
  *         <ul>
  *           <li>The {@linkplain MetadataStandard#getInterface(Class) standard 
type} of the source element
  *               is assignable to the type of the target element.</li>
- *           <li>There is no conflict, i.e. no property value that are not 
collection and not equal.
+ *           <li>There is no conflict, <i>i.e.</i> no property value that are 
not collection and not equal.
  *               This condition can be modified by overriding {@link 
#resolve(Object, ModifiableMetadata)}.</li>
  *         </ul>
  *         If such pair is found, then the merge operation if performed 
recursively
@@ -59,12 +64,14 @@ import org.apache.sis.util.resources.Errors;
  *       <li>All other source elements will be added as new elements in the 
target collection.</li>
  *     </ul>
  *   </li>
- *   <li>Otherwise the {@link #copy(Object, ModifiableMetadata) copy(…)} 
method is invoked.</li>
+ *   <li>Otherwise, the {@link #copy(Object, ModifiableMetadata) copy(…)} 
method is invoked.</li>
  * </ul>
  *
  * @author  Johann Sorel (Geomatys)
  * @author  Benjamin Garcia (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
+ *
+ * @see ModifiableMetadata#merge(Object)
  */
 public class Merger {
     /**
@@ -118,8 +125,11 @@ public class Merger {
      */
     public final void copy(final Object source, final ModifiableMetadata 
target) {
         if (!copy(source, target, false)) {
-            throw new 
InvalidMetadataException(errors().getString(Errors.Keys.IllegalArgumentClass_3, 
"target",
-                    target.getStandard().getInterface(source.getClass()), 
Classes.getClass(target)));
+            throw new InvalidMetadataException(errors().getString(
+                    Errors.Keys.IllegalArgumentClass_3,
+                    "source",
+                    target.getStandardType(),
+                    Classes.getClass(source)));
         }
     }
 
@@ -139,13 +149,25 @@ public class Merger {
          * taken will depend on the caller: it may either skips the value or 
throws an exception.
          */
         final MetadataStandard standard = target.getStandard();
-        if (!standard.getInterface(source.getClass()).isInstance(target)) {
+        Class<?> baseType = Classes.getRawClass(target.getStandardType());
+deeper: if (!baseType.isInstance(source)) {
+            // The target class is too speclalized. Search for a more general 
type.
+            for (final Class<?> parent : Classes.getAllInterfaces(baseType)) {
+                if (standard.isMetadata(parent) && parent.isInstance(source)) {
+                    baseType = parent;
+                    break deeper;
+                }
+            }
+            return false;
+        }
+        if (!standard.getInterface(source.getClass(), 
baseType).isInstance(target)) {
             return false;
         }
         /*
          * Only after we verified that the merge operation is theoretically 
allowed, remember that
          * we are going to merge those two metadata and verify that we are not 
in an infinite loop.
          * We will also verify that the target metadata does not contain a 
source, or vice-versa.
+         * Reminder: `done` values are `FALSE` for sources and `TRUE` for 
targets.
          */
         {   // For keeping `sourceDone` and `targetDone` more local.
             final Boolean sourceDone = done.put(source, Boolean.FALSE);
@@ -171,7 +193,7 @@ public class Merger {
         if (source instanceof AbstractMetadata) {
             sourceMap = ((AbstractMetadata) source).asMap();          // Gives 
to subclasses a chance to override.
         } else {
-            sourceMap = standard.asValueMap(source, null, 
KeyNamePolicy.JAVABEANS_PROPERTY, ValueExistencePolicy.NON_EMPTY);
+            sourceMap = standard.asValueMap(source, baseType, 
KeyNamePolicy.JAVABEANS_PROPERTY, ValueExistencePolicy.NON_EMPTY);
         }
         /*
          * Iterate on source values in order to find the objects that need to 
be copied or merged.
@@ -209,17 +231,49 @@ public class Merger {
                     /*
                      * If the target value is a collection, then the source 
value should be a collection too
                      * (otherwise the two objects would not be implementing 
the same standard, in which case
-                     * a ClassCastException is conform to this method 
contract). The loop tries to merge the
-                     * source elements to target elements that are specialized 
enough.
+                     * a ClassCastException is conform to this method 
contract). The list of source elements
+                     * will exclude elements that are already present in the 
target.
                      */
                     final var targetList = (Collection<?>) targetValue;
                     final var sourceList = new LinkedList<>((Collection<?>) 
sourceValue);
+                    for (Iterator<?> it = sourceList.iterator(); 
it.hasNext();) {
+                        final Object value = it.next();
+                        for (final Object element : targetList) {
+                            if (value != element) {
+                                /*
+                                 * The comparisons must be done with 
`BY_CONTRACT` mode because the source `value`
+                                 * may implement many interfaces, while we are 
interrested only in the interface
+                                 * implemented by the target `element`.
+                                 */
+                                final boolean equals;
+                                final var criteria = 
ComparisonMode.BY_CONTRACT;
+                                if (element instanceof AbstractMetadata) {
+                                    equals = ((AbstractMetadata) 
element).equals(value, criteria);
+                                } else if (targetList instanceof 
CheckedContainer<?>) {
+                                    Class<?> propertyType = 
((CheckedContainer<?>) targetList).getElementType();
+                                    equals = standard.equals(element, value, 
propertyType, criteria);
+                                } else {
+                                    equals = Utilities.deepEquals(element, 
value, criteria);
+                                }
+                                if (!equals) continue;
+                            }
+                            it.remove();
+                            break;
+                        }
+                    }
+                    if (sourceList.isEmpty()) {
+                        continue;
+                    }
+                    /*
+                     * Try to merge the source elements into target elements 
that are specialized enough.
+                     */
                     for (final Object element : targetList) {
                         if (element instanceof ModifiableMetadata) {
+                            final var targetElement = (ModifiableMetadata) 
element;
                             final Iterator<?> it = sourceList.iterator();
 distribute:                 while (it.hasNext()) {
                                 final Object value = it.next();
-                                switch (resolve(value, (ModifiableMetadata) 
element)) {
+                                switch (resolve(value, targetElement)) {
                                     default: throw new 
UnsupportedOperationException();
                                     case SEPARATE: break;               // do 
nothing.
                                     case MERGE: {
@@ -228,7 +282,7 @@ distribute:                 while (it.hasNext()) {
                                          * by recursive checks in all 
children. The intent is to have a "all or nothing"
                                          * behavior, before the copy(…, false) 
call below starts to modify the values.
                                          */
-                                        if (!copy(value, (ModifiableMetadata) 
element, false)) break;
+                                        if (!copy(value, targetElement, 
false)) break;
                                         // Fall through
                                     }
                                     case IGNORE: {
@@ -248,7 +302,7 @@ distribute:                 while (it.hasNext()) {
                     for (final Object element : sourceList) {
                         final Object old = targetMap.put(propertyName, 
element);
                         if (old instanceof Collection<?>) {
-                            final Collection<?> oldList = (Collection<?>) old;
+                            final var oldList = (Collection<?>) old;
                             if (oldList.size() <= targetList.size()) {
                                 /*
                                  * Above was only a cheap check based on 
collection size only.
@@ -399,8 +453,8 @@ distribute:                 while (it.hasNext()) {
     /**
      * Invoked when a source metadata element is about to be written in an 
existing target element.
      * The default implementation returns {@link Resolution#MERGE} if writing 
in the given target
-     * would only fill holes, without overwriting any existing value. 
Otherwise this method returns
-     * {@code Resolution#SEPARATE}.
+     * would only fill holes (i.e. without overwriting any existing value),
+     * or returns {@code Resolution#SEPARATE} otherwise.
      *
      * @param  source  the source metadata to copy.
      * @param  target  where the source metadata would be copied if this 
method returns {@link Resolution#MERGE}.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/MetadataStandardTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/MetadataStandardTest.java
index b1df00a083..94d2da09df 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/MetadataStandardTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/MetadataStandardTest.java
@@ -186,7 +186,7 @@ public final class MetadataStandardTest extends TestCase {
     }
 
     /**
-     * Tests the {@link MetadataStandard#equals(Object, Object, 
ComparisonMode)} method.
+     * Tests the {@link MetadataStandard#equals(Object, Object, Class, 
ComparisonMode)} method.
      */
     @Test
     public void testEquals() {
@@ -194,17 +194,17 @@ public final class MetadataStandardTest extends TestCase {
 
         // Self equality test
         DefaultCitation instance = HardCodedCitations.EPSG;
-        assertFalse(standard.equals(instance, HardCodedCitations.SIS,  
ComparisonMode.STRICT));
-        assertTrue (standard.equals(instance, HardCodedCitations.EPSG, 
ComparisonMode.STRICT));
+        assertFalse(standard.equals(instance, HardCodedCitations.SIS,  null, 
ComparisonMode.STRICT));
+        assertTrue (standard.equals(instance, HardCodedCitations.EPSG, null, 
ComparisonMode.STRICT));
 
         // Test comparison with a copy
         instance = new DefaultCitation(HardCodedCitations.EPSG);
-        assertFalse(standard.equals(instance, HardCodedCitations.SIS,  
ComparisonMode.STRICT));
-        assertTrue (standard.equals(instance, HardCodedCitations.EPSG, 
ComparisonMode.STRICT));
+        assertFalse(standard.equals(instance, HardCodedCitations.SIS,  null, 
ComparisonMode.STRICT));
+        assertTrue (standard.equals(instance, HardCodedCitations.EPSG, null, 
ComparisonMode.STRICT));
 
         // test comparison with a modified copy
         instance.setTitle(new SimpleInternationalString("A dummy title"));
-        assertFalse(standard.equals(instance, HardCodedCitations.EPSG, 
ComparisonMode.STRICT));
+        assertFalse(standard.equals(instance, HardCodedCitations.EPSG, null, 
ComparisonMode.STRICT));
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/internal/shared/MergerTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/internal/shared/MergerTest.java
index e33959f466..9c79d7677b 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/internal/shared/MergerTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/internal/shared/MergerTest.java
@@ -60,10 +60,10 @@ public final class MergerTest extends TestCase {
      * Creates a metadata sample with 3 content information of different kind.
      */
     private static DefaultMetadata createSample1() {
-        final DefaultFeatureCatalogueDescription features = new 
DefaultFeatureCatalogueDescription();
-        final DefaultCoverageDescription         coverage = new 
DefaultCoverageDescription();
-        final DefaultImageDescription            image    = new 
DefaultImageDescription();
-        final DefaultMetadata                    metadata = new 
DefaultMetadata();
+        final var features = new DefaultFeatureCatalogueDescription();
+        final var coverage = new DefaultCoverageDescription();
+        final var image    = new DefaultImageDescription();
+        final var metadata = new DefaultMetadata();
         features.setFeatureCatalogueCitations(Set.of(new 
DefaultCitation("Shapefile")));
         features.setIncludedWithDataset(Boolean.TRUE);
         metadata.getContentInfo().add(features);
@@ -84,9 +84,9 @@ public final class MergerTest extends TestCase {
      * than the one created by {@link #createSample1()}.
      */
     private static DefaultMetadata createSample2() {
-        final DefaultFeatureCatalogueDescription features = new 
DefaultFeatureCatalogueDescription();
-        final DefaultImageDescription            image    = new 
DefaultImageDescription();
-        final DefaultMetadata                    metadata = new 
DefaultMetadata();
+        final var features = new DefaultFeatureCatalogueDescription();
+        final var image    = new DefaultImageDescription();
+        final var metadata = new DefaultMetadata();
         image.setProcessingLevelCode(new DefaultIdentifier("Level 2"));
         metadata.getContentInfo().add(image);
 
@@ -106,7 +106,7 @@ public final class MergerTest extends TestCase {
     public void testDeepMerge() {
         final DefaultMetadata source = createSample1();
         final DefaultMetadata target = createSample2();
-        final Merger merger = new Merger(null);
+        final var merger = new Merger(null);
         merger.copy(source, target);
 
         assertSetEquals(List.of(Locale.JAPANESE, Locale.FRENCH),
@@ -114,10 +114,10 @@ public final class MergerTest extends TestCase {
         assertSetEquals(List.of(StandardCharsets.UTF_16, 
StandardCharsets.UTF_8),
                         target.getLocalesAndCharsets().values());
 
-        final Iterator<ContentInformation> it       = 
target.getContentInfo().iterator();
-        final ImageDescription             image    = (ImageDescription)       
     it.next();
-        final FeatureCatalogueDescription  features = 
(FeatureCatalogueDescription) it.next();
-        final CoverageDescription          coverage = (CoverageDescription)    
     it.next();
+        final Iterator<ContentInformation> it = 
target.getContentInfo().iterator();
+        final var  image    = assertInstanceOf(ImageDescription.class,         
   it.next());
+        final var  features = 
assertInstanceOf(FeatureCatalogueDescription.class, it.next());
+        final var  coverage = assertInstanceOf(CoverageDescription.class,      
   it.next());
         assertFalse(it.hasNext());
 
         assertEquals(ImagingCondition.CLOUD, image   .getImagingCondition());
diff --git 
a/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/MetadataTest.java
 
b/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/MetadataTest.java
index bc10977143..7db1eba6ec 100644
--- 
a/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/MetadataTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/MetadataTest.java
@@ -74,23 +74,23 @@ public final class MetadataTest extends TestCase {
      * Creates a metadata instance with the same data as the one in the {@code 
"1.1/metadata.xml"} test file.
      */
     static Metadata create() throws URISyntaxException {
-        final Person person = new Person();
+        final var person = new Person();
         person.name  = "Jean-Pierre";
         person.email = "[email protected]";
         person.link  = new Link(new URI("http://someone-site.org";));
 
-        final Copyright copyright = new Copyright();
+        final var copyright = new Copyright();
         copyright.author  = "Apache";
         copyright.year    = 2004;
         copyright.license = new 
URI("http://www.apache.org/licenses/LICENSE-2.0";);
 
-        final Bounds bounds = new Bounds();
+        final var bounds = new Bounds();
         bounds.westBoundLongitude = -20;
         bounds.eastBoundLongitude =  30;
         bounds.southBoundLatitude =  10;
         bounds.northBoundLatitude =  40;
 
-        final Metadata metadata = new Metadata();
+        final var metadata = new Metadata();
         metadata.name        = "Sample";
         metadata.description = "GPX test file";
         metadata.author      = person;
diff --git 
a/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/ReaderTest.java
 
b/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/ReaderTest.java
index 4b5a69fc3b..56fcd3e904 100644
--- 
a/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/ReaderTest.java
+++ 
b/endorsed/src/org.apache.sis.storage.xml/test/org/apache/sis/storage/gpx/ReaderTest.java
@@ -24,15 +24,19 @@ import java.time.Instant;
 import com.esri.core.geometry.Point;
 import com.esri.core.geometry.Polyline;
 import org.opengis.geometry.Envelope;
+import org.opengis.metadata.maintenance.ScopeCode;
+import org.opengis.metadata.identification.Identification;
 import org.apache.sis.setup.GeometryLibrary;
 import org.apache.sis.storage.OptionKey;
 import org.apache.sis.storage.StorageConnector;
 import org.apache.sis.storage.DataStoreException;
 import org.apache.sis.storage.gps.Fix;
+import org.apache.sis.metadata.iso.DefaultMetadata;
 
 // Test dependencies
 import org.junit.jupiter.api.Test;
 import static org.junit.jupiter.api.Assertions.*;
+import static org.apache.sis.test.Assertions.assertSingleton;
 import static org.apache.sis.test.Assertions.assertSingletonFeature;
 import org.apache.sis.test.TestCase;
 
@@ -90,13 +94,20 @@ public final class ReaderTest extends TestCase {
     }
 
     /**
-     * Verifies that the given {@code actual} envelope has the expected values.
+     * Verifies that an envelope is two-dimensional and has the expected 
values.
      * A strict equality is requested.
+     *
+     * @param xmin    the expected minimum value of the first coordinate.
+     * @param xmax    the expected maximum value of the first coordinate.
+     * @param ymin    the expected minimum value of the second coordinate.
+     * @param ymax    the expected maximum value of the second coordinate.
+     * @param actual  the feature from which to get the envelope to compare 
with the expected values.
      */
     private static void assertEnvelopeEquals(final double xmin, final double 
xmax,
                                              final double ymin, final double 
ymax,
-                                             final Envelope actual)
+                                             final Feature f)
     {
+        final Envelope actual = assertInstanceOf(Envelope.class, 
f.getPropertyValue("sis:envelope"));
         assertEquals(2, actual.getDimension(), "dimension");
         assertEquals(actual.getMinimum(0), xmin, "xmin");
         assertEquals(actual.getMaximum(0), xmax, "xmax");
@@ -114,6 +125,13 @@ public final class ReaderTest extends TestCase {
         assertEquals(expected, (actual != null) ? actual.toString() : null);
     }
 
+    /**
+     * Gets the metadata from the given store.
+     */
+    private static Metadata getMetadata(final Store reader) throws 
DataStoreException {
+        return assertInstanceOf(Metadata.class, reader.getMetadata());
+    }
+
     /**
      * Tests parsing of GPX version 1.0.0 metadata.
      *
@@ -122,7 +140,7 @@ public final class ReaderTest extends TestCase {
     @Test
     public void testMetadata100() throws DataStoreException {
         try (Store reader = create(TestData.V1_0, TestData.METADATA)) {
-            final Metadata md = (Metadata) reader.getMetadata();
+            final Metadata md = getMetadata(reader);
             verifyMetadata(md, 1);
             assertNull(md.author.link);
             assertNull(md.copyright);
@@ -138,7 +156,7 @@ public final class ReaderTest extends TestCase {
     @Test
     public void testMetadata110() throws DataStoreException {
         try (Store reader = create(TestData.V1_1, TestData.METADATA)) {
-            final Metadata md = (Metadata) reader.getMetadata();
+            final Metadata md = getMetadata(reader);
             verifyMetadata(md, 3);
             assertStringEquals("http://someone-site.org";, md.author.link);
             assertEquals("Apache", md.copyright.author);
@@ -199,6 +217,36 @@ public final class ReaderTest extends TestCase {
         assertFalse(it.hasNext());
     }
 
+    /**
+     * Tests the merging of two {@link Metadata} object.
+     * The difficulty is that, for implementation convenience, the same class 
implements many metadata interfaces.
+     * The merge process should detect which interface to use based on the 
type of the {@code merged} object as a
+     * starting point, then based on the type of each property to merge.
+     *
+     * @throws DataStoreException if reader failed to be created or failed at 
reading.
+     */
+    @Test
+    public void testMetadataMerge() throws DataStoreException {
+        final Metadata first, second;
+        final DefaultMetadata merged;
+        try (Store reader = create(TestData.V1_1, TestData.METADATA)) {
+            first = getMetadata(reader);
+            try (Store other = create(TestData.V1_1, TestData.ROUTE)) {
+                second = getMetadata(other);
+                merged = new DefaultMetadata(first);
+                merged.merge(second);
+            }
+        }
+        // Both metadata declare this scope, only one instance should be 
retained.
+        assertEquals(ScopeCode.DATASET, 
assertSingleton(merged.getMetadataScopes()).getResourceScope());
+
+        // Should find the identification info of each dataset.
+        final Iterator<Identification> it = 
merged.getIdentificationInfo().iterator();
+        assertSame(assertSingleton(first .getIdentificationInfo()), it.next());
+        assertSame(assertSingleton(second.getIdentificationInfo()), it.next());
+        assertFalse(it.hasNext());
+    }
+
     /**
      * Tests parsing of GPX version 1.0.0 way point.
      *
@@ -207,7 +255,7 @@ public final class ReaderTest extends TestCase {
     @Test
     public void testWayPoint100() throws DataStoreException {
         try (Store reader = create(TestData.V1_0, TestData.WAYPOINT)) {
-            verifyAlmostEmptyMetadata((Metadata) reader.getMetadata());
+            verifyAlmostEmptyMetadata(getMetadata(reader));
             assertEquals(StoreProvider.V1_0, reader.getVersion());
             try (Stream<Feature> features = reader.features(false)) {
                 final Iterator<Feature> it = features.iterator();
@@ -227,7 +275,7 @@ public final class ReaderTest extends TestCase {
     @Test
     public void testWayPoint110() throws DataStoreException {
         try (Store reader = create(TestData.V1_1, TestData.WAYPOINT)) {
-            verifyAlmostEmptyMetadata((Metadata) reader.getMetadata());
+            verifyAlmostEmptyMetadata(getMetadata(reader));
             assertEquals(StoreProvider.V1_1, reader.getVersion());
             try (Stream<Feature> features = reader.features(false)) {
                 final Iterator<Feature> it = features.iterator();
@@ -247,7 +295,7 @@ public final class ReaderTest extends TestCase {
     @Test
     public void testRoute100() throws DataStoreException {
         try (Store reader = create(TestData.V1_0, TestData.ROUTE)) {
-            verifyAlmostEmptyMetadata((Metadata) reader.getMetadata());
+            verifyAlmostEmptyMetadata(getMetadata(reader));
             assertEquals(StoreProvider.V1_0, reader.getVersion());
             try (Stream<Feature> features = reader.features(false)) {
                 final Iterator<Feature> it = features.iterator();
@@ -266,7 +314,7 @@ public final class ReaderTest extends TestCase {
     @Test
     public void testRoute110() throws DataStoreException {
         try (Store reader = create(TestData.V1_1, TestData.ROUTE)) {
-            verifyAlmostEmptyMetadata((Metadata) reader.getMetadata());
+            verifyAlmostEmptyMetadata(getMetadata(reader));
             assertEquals(StoreProvider.V1_1, reader.getVersion());
             verifyRoute110(reader);
         }
@@ -322,7 +370,7 @@ public final class ReaderTest extends TestCase {
         assertEquals(new Point(15, 10), p.getPoint(0));
         assertEquals(new Point(25, 20), p.getPoint(1));
         assertEquals(new Point(35, 30), p.getPoint(2));
-        assertEnvelopeEquals(15, 35, 10, 30, (Envelope) 
f.getPropertyValue("sis:envelope"));
+        assertEnvelopeEquals(15, 35, 10, 30, f);
     }
 
     /**
@@ -352,7 +400,7 @@ public final class ReaderTest extends TestCase {
     @Test
     public void testTrack100() throws DataStoreException {
         try (Store reader = create(TestData.V1_0, TestData.TRACK)) {
-            verifyAlmostEmptyMetadata((Metadata) reader.getMetadata());
+            verifyAlmostEmptyMetadata(getMetadata(reader));
             assertEquals(StoreProvider.V1_0, reader.getVersion());
             try (Stream<Feature> features = reader.features(false)) {
                 final Iterator<Feature> it = features.iterator();
@@ -371,7 +419,7 @@ public final class ReaderTest extends TestCase {
     @Test
     public void testTrack110() throws DataStoreException {
         try (Store reader = create(TestData.V1_1, TestData.TRACK)) {
-            verifyAlmostEmptyMetadata((Metadata) reader.getMetadata());
+            verifyAlmostEmptyMetadata(getMetadata(reader));
             assertEquals(StoreProvider.V1_1, reader.getVersion());
             try (Stream<Feature> features = reader.features(false)) {
                 final Iterator<Feature> it = features.iterator();
@@ -424,7 +472,7 @@ public final class ReaderTest extends TestCase {
         assertEquals(new Point(15, 10), p.getPoint(0));
         assertEquals(new Point(25, 20), p.getPoint(1));
         assertEquals(new Point(35, 30), p.getPoint(2));
-        assertEnvelopeEquals(15, 35, 10, 30, (Envelope) 
f.getPropertyValue("sis:envelope"));
+        assertEnvelopeEquals(15, 35, 10, 30, f);
     }
 
     /**
@@ -464,7 +512,7 @@ public final class ReaderTest extends TestCase {
                     assertStringEquals("http://first-address2.org";, 
links.get(1));
                     assertStringEquals("http://first-address3.org";, 
links.get(2));
                 }
-                assertEnvelopeEquals(15, 15, 10, 10, (Envelope) 
f.getPropertyValue("sis:envelope"));
+                assertEnvelopeEquals(15, 15, 10, 10, f);
                 break;
             }
             case 1: {
@@ -488,7 +536,7 @@ public final class ReaderTest extends TestCase {
                 assertNull(f.getPropertyValue("ageofdgpsdata"));
                 assertNull(f.getPropertyValue("dgpsid"));
                 assertTrue(assertInstanceOf(List.class, 
f.getPropertyValue("link")).isEmpty());
-                assertEnvelopeEquals(25, 25, 20, 20, (Envelope) 
f.getPropertyValue("sis:envelope"));
+                assertEnvelopeEquals(25, 25, 20, 20, f);
                 break;
             }
             case 2: {
@@ -517,7 +565,7 @@ public final class ReaderTest extends TestCase {
                 if (v11) {
                     assertStringEquals("http://third-address2.org";, 
links.get(1));
                 }
-                assertEnvelopeEquals(35, 35, 30, 30, (Envelope) 
f.getPropertyValue("sis:envelope"));
+                assertEnvelopeEquals(35, 35, 30, 30, f);
                 break;
             }
             default: {
@@ -534,7 +582,7 @@ public final class ReaderTest extends TestCase {
      * @throws DataStoreException if reader failed to be created or failed at 
reading.
      */
     @Test
-    public void testRouteSkipMetadata() throws DataStoreException {
+    public void testRouteIgnoringMetadata() throws DataStoreException {
         try (Store reader = create(TestData.V1_1, TestData.ROUTE)) {
             verifyRoute110(reader);
         }
@@ -545,7 +593,7 @@ public final class ReaderTest extends TestCase {
      * Using the URL makes easier for the data store to read the same data 
more than once.
      */
     private Store createFromURL() throws DataStoreException {
-        final StorageConnector connector = new 
StorageConnector(TestData.V1_1.getURL(TestData.ROUTE));
+        final var connector = new 
StorageConnector(TestData.V1_1.getURL(TestData.ROUTE));
         connector.setOption(OptionKey.GEOMETRY_LIBRARY, GeometryLibrary.ESRI);
         connector.setOption(OptionKey.URL_ENCODING, "UTF-8");
         return new Store(provider, connector);
@@ -568,7 +616,7 @@ public final class ReaderTest extends TestCase {
              * The new 'features()' call should reuse the reader created by 
'getMetadata()' - this can
              * be verified by stepping in the code with a debugger.
              */
-            md = (Metadata) reader.getMetadata();
+            md = getMetadata(reader);
             verifyRoute110(reader);
             /*
              * One more check.
@@ -586,6 +634,7 @@ public final class ReaderTest extends TestCase {
      * @throws DataStoreException if reader failed to be created or failed at 
reading.
      */
     @Test
+    @SuppressWarnings("ConvertToTryWithResources")
     public void testConcurrentReads() throws DataStoreException {
         try (Store reader = createFromURL()) {
             final Stream<Feature>   f1 = reader.features(false);
diff --git 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/metadata/MetadataBuilder.java
 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/metadata/MetadataBuilder.java
index 117500d026..878a6a8e9f 100644
--- 
a/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/metadata/MetadataBuilder.java
+++ 
b/endorsed/src/org.apache.sis.storage/main/org/apache/sis/storage/metadata/MetadataBuilder.java
@@ -3411,7 +3411,7 @@ public class MetadataBuilder {
         } else {
             return false;
         }
-        final Merger merger = new Merger(locale);
+        final var merger = new Merger(locale);
         merger.copy(source, target);
         useParentElements();
         return true;
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
index 13c3175627..6ae22a43c5 100644
--- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
+++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/Classes.java
@@ -450,13 +450,14 @@ public final class Classes {
     /**
      * Returns every interfaces implemented, directly or indirectly, by the 
given class or interface.
      * This is similar to {@link Class#getInterfaces()} except that this 
method searches recursively
-     * in the super-interfaces. For example, if the given type is {@link 
java.util.ArrayList}, then
-     * the returned set will contain {@link java.util.List} (which is 
implemented directly)
+     * in the super-interfaces, omitting duplicated values.
+     * For example, if the given type is {@link java.util.ArrayList},
+     * then the returned set will contain {@link java.util.List} (which is 
implemented directly)
      * together with its parent interfaces {@link Collection} and {@link 
Iterable}.
      *
      * <h4>Elements ordering</h4>
      * All interfaces implemented directly by the given type are first and in 
the order they are declared
-     * in the {@code implements} or {@code extends} clause. Parent interfaces 
are next.
+     * in the {@code implements} or {@code extends} clause. Interfaces 
implemented by parent interfaces are next.
      *
      * @param  <T>   the compile-time type of the {@code Class} argument.
      * @param  type  the class or interface for which to get all implemented 
interfaces, or {@code null}.
@@ -622,7 +623,7 @@ next:       for (final Class<?> candidate : candidates) {
      * @param  c1  the first class, or {@code null}.
      * @param  c2  the second class, or {@code null}.
      * @return the interfaces common to both classes, or an empty set if none.
-     *         Callers can freely modify the returned set.
+     *         CThe returned set supports remove operation.
      */
     public static Set<Class<?>> findCommonInterfaces(final Class<?> c1, final 
Class<?> c2) {
         final Class<?> baseType = Object.class;     // Not configurable for 
now, but could be an argument.

Reply via email to