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 b9ba379a4f Clarify which exceptions are thrown when using reflection 
on metadata object. Make `Merger` and `equals(…)` more robust against 
`ClassCastException`.
b9ba379a4f is described below

commit b9ba379a4f871d90219a0bf0a7ccee91f7438555
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Thu Jul 23 19:57:59 2026 +0200

    Clarify which exceptions are thrown when using reflection on metadata 
object.
    Make `Merger` and `equals(…)` more robust against `ClassCastException`.
---
 .../org/apache/sis/feature/AbstractFeature.java    |   5 -
 .../org/apache/sis/metadata/AbstractMetadata.java  |   3 +-
 .../main/org/apache/sis/metadata/CacheKey.java     |   7 +
 .../org/apache/sis/metadata/MetadataCopier.java    |   2 +-
 .../org/apache/sis/metadata/MetadataStandard.java  | 369 +++++++++++++--------
 .../org/apache/sis/metadata/MetadataVisitor.java   |  15 +-
 .../main/org/apache/sis/metadata/Pruner.java       |   4 +-
 .../main/org/apache/sis/metadata/StateChanger.java |   2 +-
 .../main/org/apache/sis/metadata/TreeNode.java     |  10 +-
 .../apache/sis/metadata/UnresolvedTypePolicy.java  |  49 +++
 .../apache/sis/metadata/internal/Resources.java    |   5 +
 .../sis/metadata/internal/Resources.properties     |   1 +
 .../sis/metadata/internal/Resources_fr.properties  |   1 +
 .../sis/metadata/internal/shared/Merger.java       |  14 +-
 .../iso/extent/DefaultGeographicBoundingBox.java   |   2 +-
 .../main/org/apache/sis/metadata/package-info.java |   2 +-
 .../org/apache/sis/util/iso/DefaultRecordType.java |   2 +-
 .../test/org/apache/sis/metadata/HashCodeTest.java |   3 +-
 .../apache/sis/metadata/InformationMapTest.java    |   1 +
 .../apache/sis/metadata/MetadataStandardTest.java  |  26 +-
 .../test/org/apache/sis/metadata/NameMapTest.java  |   1 +
 .../test/org/apache/sis/metadata/PrunerTest.java   |   1 +
 .../org/apache/sis/metadata/SpecialCasesTest.java  |   1 +
 .../apache/sis/metadata/TreeNodeChildrenTest.java  |   2 +-
 .../test/org/apache/sis/metadata/TypeMapTest.java  |   1 +
 .../org/apache/sis/parameter/MatrixParameters.java |   2 +-
 .../transform/AbstractLinearTransform.java         |   7 +-
 .../apache/sis/util/internal/shared/Numerics.java  |   2 +-
 .../main/org/apache/sis/util/resources/Errors.java |   5 +
 .../apache/sis/util/resources/Errors.properties    |   1 +
 .../apache/sis/util/resources/Errors_fr.properties |   1 +
 .../apache/sis/util/collection/RangeSetTest.java   |   4 +-
 32 files changed, 362 insertions(+), 189 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
index ce4087ebc5..17408ae4a8 100644
--- 
a/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
+++ 
b/endorsed/src/org.apache.sis.feature/main/org/apache/sis/feature/AbstractFeature.java
@@ -99,11 +99,6 @@ public abstract class AbstractFeature implements Feature, 
Serializable {
      */
     private static final long serialVersionUID = -5637918246427380190L;
 
-    /**
-     * Sentinel value for missing property.
-     */
-    private static final Object MISSING = new Object();
-
     /**
      * Information about the feature (name, characteristics, <i>etc.</i>).
      */
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 076d3b13f1..3537fa3e32 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
@@ -362,7 +362,8 @@ public abstract class AbstractMetadata implements 
LenientComparable, Emptiable {
     @Override
     public boolean equals(final Object object, final ComparisonMode mode) {
         final MetadataStandard standard = getStandard();
-        return standard.equals(this, object, 
standard.getInterface(getClass()), mode);
+        final Boolean equals = standard.equals(this, object, 
standard.getInterface(getClass()), mode);
+        return (equals != null) && equals;
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/CacheKey.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/CacheKey.java
index 9137c767aa..ed34aeddd7 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/CacheKey.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/CacheKey.java
@@ -112,4 +112,11 @@ final class CacheKey {
     final String invalid() {
         return Errors.format(Errors.Keys.IllegalArgumentClass_3, "type", 
propertyType, type);
     }
+
+    /**
+     * Creates an error message for an ambiguous property type.
+     */
+    final String ambiguous() {
+        return Errors.format(Errors.Keys.AmbiguousType_2, propertyType, type);
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataCopier.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataCopier.java
index f0740c35c0..af57f1edbd 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataCopier.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataCopier.java
@@ -187,7 +187,7 @@ public class MetadataCopier extends MetadataVisitor<Object> 
{
         if (metadata != null) {
             final MetadataStandard std = getStandard(metadata);
             if (std != null) {
-                final Object result = walk(std, type, metadata, false);
+                final Object result = walk(std, type, metadata, 
UnresolvedTypePolicy.NULL);
                 if (result != null) {
                     return result;
                 }
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 da2e62ba32..26091cfac2 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
@@ -22,8 +22,8 @@ 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.NoSuchElementException;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.ConcurrentHashMap;
 import java.io.IOException;
@@ -44,6 +44,7 @@ import org.apache.sis.system.Configuration;
 import org.apache.sis.system.Modules;
 import org.apache.sis.system.Semaphores;
 import org.apache.sis.system.SystemListener;
+import org.apache.sis.metadata.internal.Resources;
 import org.apache.sis.metadata.simple.SimpleCitation;
 import org.apache.sis.xml.NilReason;
 import static org.apache.sis.util.ArgumentChecks.ensureNonNull;
@@ -296,8 +297,8 @@ public class MetadataStandard implements Serializable {
                 return candidate;
             }
         }
-        for (final Class<?> interf : Classes.getAllInterfaces(type)) {
-            classname = interf.getName();
+        for (final Class<?> parent : Classes.getAllInterfaces(type)) {
+            classname = parent.getName();
             for (final MetadataStandard candidate : INSTANCES) {
                 if (candidate.isSupported(classname)) {
                     return candidate;
@@ -345,18 +346,21 @@ public class MetadataStandard implements Serializable {
      * Returns the accessor for the specified interface or implementation 
class.
      * The type may be an interface (typically a GeoAPI interface) or an 
implementation class.
      *
-     * @param  key        the interface or implementation class.
-     * @param  mandatory  whether this method shall throw an exception if no 
accessor is found.
-     * @return the accessor for the given type, or {@code null} if none and 
{@code mandatory} is {@code false}.
+     * @param  type    the interface or implementation class. Caller shall 
verify that it is non-null.
+     * @param  policy  whether this method shall throw an exception if no 
non-ambiguous accessor is found.
+     * @return the accessor for the given type, or {@code null} if none or 
ambiguous and {@code policy} is {@code NULL}.
+     * @throws ClassCastException if {@code getImplementation(type)} is not 
assignable to {@code type}.
+     * @throws NoSuchElementException if the given type not implement a 
metadata interface and {@code policy} is {@code THROW}.
+     * @throws IllegalArgumentException if the type implements more than one 
metadata interface and {@code policy} is {@code THROW}.
      */
-    final PropertyAccessor getTypeAccessor(Class<?> type, final boolean 
mandatory) {
+    final PropertyAccessor getTypeAccessor(Class<?> type, final 
UnresolvedTypePolicy policy) {
         Class<?> propertyType = Object.class;
         final Class<?> implementation = getImplementation(type);
         if (implementation != null) {
             propertyType = type;
             type = implementation;
         }
-        return getAccessor(new CacheKey(type, propertyType), null, mandatory);
+        return getAccessor(new CacheKey(type, propertyType), policy, null);
     }
 
     /**
@@ -372,12 +376,13 @@ public class MetadataStandard implements Serializable {
      *
      * @param  metadata      the metadata object.
      * @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}.
+     * @param  policy        whether this method shall throw an exception if 
no non-ambiguous accessor is found.
+     * @return the accessor for the given object, or {@code null} if none or 
ambiguous and {@code policy} is {@code NULL}.
+     * @throws ClassCastException if {@code metadata} is not an instance of 
{@code propertyType}.
+     * @throws NoSuchElementException if the object does not implement a 
metadata interface and {@code policy} is {@code THROW}.
+     * @throws IllegalArgumentException if the object implements more than one 
metadata interface and {@code policy} is {@code THROW}.
      */
-    final PropertyAccessor getInstanceAccessor(final Object metadata, Class<?> 
propertyType, final boolean mandatory) {
+    final PropertyAccessor getInstanceAccessor(final Object metadata, Class<?> 
propertyType, final UnresolvedTypePolicy policy) {
         if (propertyType == null) {
             if (metadata instanceof LenientComparable) {
                 propertyType = Classes.getRawClass(((LenientComparable) 
metadata).getStandardType());
@@ -386,45 +391,44 @@ public class MetadataStandard implements Serializable {
                 propertyType = Object.class;
             }
         }
-        return getAccessor(new CacheKey(metadata.getClass(), propertyType), 
null, mandatory);
+        return getAccessor(new CacheKey(metadata.getClass(), propertyType), 
policy, null);
     }
 
     /**
      * Returns the accessor for the specified implementation class, or {@code 
null} if none.
-     * The given class shall not be the standard interface, unless the 
metadata is read-only.
-     * More specifically, the given {@code type} shall be one of the following:
+     * {@link CacheKey#type} shall not be the standard interface, unless the 
metadata is read-only.
+     * More specifically, {@code CacheKey.type} shall be one of the followings:
      *
      * <ul>
-     *   <li>The value of {@code metadata.getClass()};</li>
-     *   <li>The value of {@link #getImplementation(Class)} after check for 
non-null value.</li>
+     *   <li>the value of {@code metadata.getClass()}, or</li>
+     *   <li>the value of {@link #getImplementation(Class)} after check for 
non-null value.</li>
      * </ul>
      *
-     * @param  key        the implementation class together with the type 
declared by the property.
-     * @param  visited    metadata standards already visited, or {@code null} 
if none.
-     * @param  mandatory  whether this method shall throw an exception or 
return {@code null}
-     *                    if no accessor is found for the given implementation 
class.
-     * @return the accessor for the given implementation, or {@code null} if 
the given class does not
-     *         implement a metadata interface of the expected package and 
{@code mandatory} is {@code false}.
-     * @throws ClassCastException if the specified class does not implement an 
expected metadata interface
-     *         and {@code mandatory} is {@code true}.
+     * @param  key      the implementation class together with the type 
declared by the property.
+     * @param  policy   whether this method shall throw an exception if no 
non-ambiguous accessor is found.
+     * @param  visited  metadata standards already visited, or {@code null} if 
not yet created.
+     * @return the accessor for the given key, or {@code null} if none or 
ambiguous and {@code policy} is {@code NULL}.
+     * @throws ClassCastException if {@code key.type} is not assignable to 
{@code key.propertyType}.
+     * @throws NoSuchElementException if the object does not implement a 
metadata interface and {@code policy} is {@code THROW}.
+     * @throws IllegalArgumentException if the object implements more than one 
metadata interface and {@code policy} is {@code THROW}.
      */
-    private PropertyAccessor getAccessor(final CacheKey key, 
Set<MetadataStandard> visited, final boolean mandatory) {
+    private PropertyAccessor getAccessor(final CacheKey key, final 
UnresolvedTypePolicy policy, Set<MetadataStandard> visited) {
         /*
          * Check for accessors created by previous calls to this method.
          * Values are added to this cache but never cleared.
          */
-        final Object value = accessors.get(key);
-        if (value instanceof PropertyAccessor) {
-            return (PropertyAccessor) value;
+        final Object cache = accessors.get(key);
+        if (cache instanceof PropertyAccessor) {
+            return (PropertyAccessor) cache;
         }
         /*
          * Check if we started some computation that we can finish. A partial 
computation exists
          * when we already found the Class<?> for the interface, but didn't 
created the accessor.
          */
         final Class<?> type;
-        if (value instanceof Class<?>) {
-            type = (Class<?>) value;                        // Stored result 
of previous call to findInterface(…).
-            assert type == findInterface(key) : key;
+        if (cache instanceof Class<?>) {
+            type = (Class<?>) cache;    // Stored result of previous call to 
findInterface(…).
+            assert type == findInterface(key, UnresolvedTypePolicy.NULL) : key;
         } else if (key.isValid()) {
             /*
              * Nothing was computed, we need to start from scratch. The first 
step is to find
@@ -432,22 +436,22 @@ public class MetadataStandard implements Serializable {
              * we will delegate to the dependencies and store the result for 
avoiding redoing
              * this search next time.
              */
-            type = findInterface(key);
+            type = findInterface(key, policy);
             if (type == null) {
                 visited = createIfNull(visited);
                 for (final MetadataStandard dependency : dependencies) {
                     if (visited.add(dependency)) {
-                        final PropertyAccessor accessor = 
dependency.getAccessor(key, visited, false);
+                        final PropertyAccessor accessor = 
dependency.getAccessor(key, policy, visited);
                         if (accessor != null) {
-                            accessors.put(key, accessor);               // Ok 
to overwrite existing instance here.
+                            accessors.put(key, accessor);   // Ok to overwrite 
existing instance here.
                             return accessor;
                         }
                     }
                 }
-                if (mandatory) {
-                    throw new ClassCastException(key.unrecognized());
+                if (policy != UnresolvedTypePolicy.THROW) {
+                    return null;
                 }
-                return null;
+                throw new NoSuchElementException(key.unrecognized());
             }
         } else {
             throw new ClassCastException(key.invalid());
@@ -473,12 +477,13 @@ public class MetadataStandard implements Serializable {
 
     /**
      * Returns {@code true} if the given type is assignable to a type from 
this standard or one of its dependencies.
-     * If this method returns {@code true}, then invoking {@link 
#getInterface(Class)} is guaranteed to succeed
-     * without throwing an exception.
+     * If this method returns {@code true}, then a call to {@link 
#getInterface(Class)} with the same argument will
+     * not throw {@link ClassCastException}. Note however that {@code 
getInterface(type)} may throw
+     * {@link IllegalArgumentException} if the given type is ambiguous.
      *
      * @param  type  the implementation class (can be {@code null}).
      * @return {@code true} if the given class is an interface of this 
standard,
-     *         or implements an interface of this standard.
+     *         or is a subtype of at least one interface of this standard.
      */
     public boolean isMetadata(final Class<?> type) {
         return (type != null) && !type.isPrimitive() && isMetadata(new 
CacheKey(type, Object.class));
@@ -487,10 +492,13 @@ public class MetadataStandard implements Serializable {
     /**
      * Implementation of {@link #isMetadata(Class)} with the possibility to 
specify the property type.
      * We do not provide the additional functionality of this method in public 
<abbr>API</abbr> on the
-     * assumption that if the user know the base metadata type implemented by 
the value,
-     * then (s)he already know that the value is a metadata instance.
+     * assumption that if the user knows the base metadata type implemented by 
the value,
+     * then (s)he already knows that the value is a metadata instance.
+     *
+     * @param  key  the implementation class together with the type declared 
by the property.
+     * @throws ClassCastException if {@code key.type} is not assignable to 
{@code key.propertyType}.
      *
-     * @see #getInterface(CacheKey, Set)
+     * @see #getInterface(CacheKey, UnresolvedTypePolicy, Set)
      */
     private boolean isMetadata(final CacheKey key) {
         assert key.isValid() : key;
@@ -508,18 +516,14 @@ public class MetadataStandard implements Serializable {
          * Performs the `findInterface(…)` computation only in last resort. 
Current implementation
          * does not store negative results in order to avoid filling the cache 
with unrelated classes.
          */
-        final Class<?> standardType = findInterface(key);
-        if (standardType != null) {
-            accessors.putIfAbsent(key, standardType);
-            return true;
-        }
-        return false;
+        return findInterface(key, UnresolvedTypePolicy.ANY) != null;
     }
 
     /**
-     * Returns {@code true} if the given implementation class, normally 
rejected by {@link #findInterface(CacheKey)},
-     * should be accepted as a pseudo-interface. We use this undocumented 
feature when Apache SIS experiments a new
-     * API which is not yet published in GeoAPI. This happen for example when 
upgrading Apache SIS public API from
+     * Returns {@code true} if the given implementation class, normally 
rejected by
+     * {@link #findInterface(CacheKey, UnresolvedTypePolicy)}, should be 
accepted as a pseudo-interface.
+     * We use this undocumented feature when Apache <abbr>SIS</abbr> 
experiments a new <abbr>API</abbr>
+     * which is not yet published in GeoAPI. This happen for example when 
upgrading Apache SIS public API from
      * the ISO 19115:2003 standard to the ISO 19115:2014 version, but GeoAPI 
interfaces are still the old version.
      * In such case, API that would normally be present in GeoAPI interfaces 
are temporarily available only in
      * Apache SIS implementation classes.
@@ -530,8 +534,8 @@ public class MetadataStandard implements Serializable {
 
     /**
      * Returns the metadata interface implemented by the specified 
implementation.
-     * Only one metadata interface can be implemented. If the given type is 
already
-     * an interface from the standard, then it is returned directly.
+     * Only one metadata interface can be implemented, except with {@link 
UnresolvedTypePolicy#ANY}.
+     * If the given type is already an interface from the standard, then it is 
returned directly.
      *
      * <p>If the given class is the return value of a property, then the type 
of that property should be specified
      * in the {@code key.propertyType} argument. This information allows this 
method to take in account only types
@@ -539,16 +543,22 @@ public class MetadataStandard implements Serializable {
      * For example, the {@link org.apache.sis.metadata.simple} package have 
various examples of implementing more than
      * one interface for convenience.</p>
      *
+     * <p>This method does not check whether the interface is cached. However, 
if this method finds an interface,
+     * then it caches the result in {@link #accessors} if there is no value 
already associated to the given key.</p>
+     *
      * <p>This method ignores dependencies. Fallback on metadata standard 
dependencies shall be done by the caller.</p>
      *
-     * @param  key  the standard interface or the implementation class.
-     * @return the single interface, or {@code null} if none was found.
+     * @param  key     the standard interface or the implementation class.
+     * @param  policy  whether to throw an exception in case of ambiguity.
+     * @return the single interface, or {@code null} if none or ambiguous.
+     * @throws IllegalArgumentException if the specified type is ambiguous and 
{@code policy} if {@code THROW}.
      */
-    private Class<?> findInterface(final CacheKey key) {
+    private Class<?> findInterface(final CacheKey key, final 
UnresolvedTypePolicy policy) {
         assert key.isValid() : key;
         final Class<?> type = key.type;
         if (type.isInterface()) {
             if (isSupported(type.getName())) {
+                accessors.putIfAbsent(key, type);
                 return type;
             }
         } else {
@@ -582,7 +592,13 @@ public class MetadataStandard implements Serializable {
             if (it.hasNext()) {
                 final Class<?> candidate = it.next();
                 if (!it.hasNext()) {
+                    accessors.putIfAbsent(key, candidate);
                     return candidate;
+                } else if (policy == UnresolvedTypePolicy.ANY) {
+                    // Do not cache this result since it is ambiguous.
+                    return candidate;
+                } else if (policy == UnresolvedTypePolicy.THROW) {
+                    throw new IllegalArgumentException(key.ambiguous());
                 }
                 /*
                  * Found more than one interface and we don't know which one 
to pick.
@@ -599,6 +615,7 @@ public class MetadataStandard implements Serializable {
                  * So we use those implementation classes as a temporary 
substitute for the interfaces.
                  */
                 if (isPendingAPI(type)) {
+                    accessors.putIfAbsent(key, type);
                     return type;
                 }
             }
@@ -641,8 +658,8 @@ 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 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.
+     * If the given type implements more than one interface from this 
standard, then an {@link IllegalArgumentException}
+     * is thrown because of the ambiguity.
      *
      * <div class="note"><b>Note:</b>
      * in this context, "interface" should be understood as
@@ -652,13 +669,17 @@ public class MetadataStandard implements Serializable {
      * @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 exactly one interface
-     *         of this standard,
+     * @throws ClassCastException if the given type does not implement an 
interface of this standard.
+     * @throws IllegalArgumentException if the given type implements more than 
one interface of this standard.
      *
      * @see AbstractMetadata#getStandardType()
      */
     public <T> Class<? super T> getInterface(final Class<T> type) {
-        return getInterface(new CacheKey(Objects.requireNonNull(type), 
Object.class), null);
+        try {
+            return getInterface(type, null);
+        } catch (NoSuchElementException e) {
+            throw unrecognized(e, type);
+        }
     }
 
     /**
@@ -671,64 +692,67 @@ public class MetadataStandard implements Serializable {
      * @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}.
+     * @throws ClassCastException if {@code type} is not assignable to {@code 
baseType}.
+     * @throws NoSuchElementException if the given type does not implement a 
standard subtype of {@code baseType}.
+     * @throws IllegalArgumentException if the given type implements more than 
one standard subtype of {@code baseType}.
      *
      * @since 1.7
      */
+    @SuppressWarnings("unchecked")
     public <T> Class<? super T> getInterface(final Class<T> type, Class<?> 
baseType) {
+        ensureNonNull("type", type);
         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());
+        final var key = new CacheKey(type, baseType);
+        return (Class) getInterface(key, UnresolvedTypePolicy.THROW, null);
     }
 
     /**
-     * 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
-     * users who want to invoke a {@code getInterface(…)} method does not know 
what that interface is.
-     * In Apache SIS case, we invoke this method when we almost know what the 
interface is but want to
-     * check if the actual value is a subtype.
+     * Implementation of {@link #getInterface(Class)} and {@link 
#getInterface(Class, Class)}.
+     * This method may invoke itself recursively, and therefore needs a safety 
against infinite
+     * recursive calls.
      *
      * @param  key      the implementation class together with the type 
declared by the property.
-     * @param  visited  metadata standards already visited, or {@code null} if 
none.
-     * @throws ClassCastException if the implementation class does not 
implement an interface of this standard.
+     * @param  policy   whether to throw an exception in case of ambiguity.
+     * @param  visited  metadata standards already visited, or {@code null} if 
not yet created.
+     * @return the interface implemented by the given class, or {@code null} 
if unknown and the policy is {@code NULL}.
+     * @throws ClassCastException if {@code key.type} is not assignable to 
{@code key.propertyType}.
+     * @throws NoSuchElementException if the metadata class does not implement 
a metadata interface of this standard.
+     * @throws IllegalArgumentException if the metadata class implements more 
than one metadata interface of this standard.
      *
      * @see #isMetadata(CacheKey)
      */
-    @SuppressWarnings("unchecked")
-    final <T> Class<? super T> getInterface(final CacheKey key, 
Set<MetadataStandard> visited) {
-        final Class<?> interf;
+    final Class<?> getInterface(final CacheKey key, final UnresolvedTypePolicy 
policy, Set<MetadataStandard> visited) {
+        final Class<?> type;
         final Object value = accessors.get(key);
         if (value instanceof PropertyAccessor) {
-            interf = ((PropertyAccessor) value).type;
+            type = ((PropertyAccessor) value).type;
         } else if (value instanceof Class<?>) {
-            interf = (Class<?>) value;
+            type = (Class<?>) value;
         } else if (value instanceof MetadataStandard) {
-            interf = ((MetadataStandard) value).getInterface(key, 
createIfNull(visited));
+            type = ((MetadataStandard) value).getInterface(key, policy, 
createIfNull(visited));
         } else if (key.isValid()) {
-            interf = findInterface(key);
-            if (interf != null) {
-                accessors.putIfAbsent(key, interf);
-            } else {
+            type = findInterface(key, policy);
+            if (type == null) {
                 visited = createIfNull(visited);
                 for (final MetadataStandard dependency : dependencies) {
-                    if (dependency.isMetadata(key)) {
-                        accessors.putIfAbsent(key, dependency);
-                        return dependency.getInterface(key, visited);
+                    final Class<?> candidate = dependency.getInterface(key, 
policy.lenient(), visited);
+                    if (candidate != null) {
+                        accessors.putIfAbsent(key, dependency);   // Not the 
same cache as `dependency`.
+                        return candidate;
                     }
                 }
-                throw new ClassCastException(key.unrecognized());
+                if (policy != UnresolvedTypePolicy.THROW) {
+                    return null;
+                }
+                throw new NoSuchElementException(key.unrecognized());
             }
         } else {
             throw new ClassCastException(key.invalid());
         }
-        assert interf.isAssignableFrom(key.type) : key;
-        return (Class<? super T>) interf;
+        assert type.isAssignableFrom(key.type) : key;
+        return type;
     }
 
     /**
@@ -781,7 +805,7 @@ public class MetadataStandard implements Serializable {
      * 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  propertyType  the interface which should be implemented to the 
metadata, or {@code null} if unknown.
      * @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.
@@ -799,10 +823,10 @@ public class MetadataStandard implements Serializable {
             final PropertyAccessor accessor;
             if (isTypeOnly) {
                 type     = (Class<?>) metadata;
-                accessor = getTypeAccessor(type, false);
+                accessor = getTypeAccessor(type, UnresolvedTypePolicy.NULL);
             } else {
                 type     = metadata.getClass();
-                accessor = getInstanceAccessor(metadata, propertyType, false);
+                accessor = getInstanceAccessor(metadata, propertyType, 
UnresolvedTypePolicy.NULL);
             }
             if (accessor == null) break;    // Not a metadata.
             TitleProperty a = type.getAnnotation(TitleProperty.class);
@@ -855,14 +879,18 @@ public class MetadataStandard implements Serializable {
      * @param  keyPolicy    determines the string representation of map keys.
      * @param  valuePolicy  determines the string representation of map values.
      * @return the names of all properties defined by the given metadata type.
-     * @throws ClassCastException if the specified interface or implementation 
class does
-     *         not extend or implement a metadata interface of the expected 
package.
+     * @throws ClassCastException if the metadata class does not implement a 
metadata interface of this standard.
+     * @throws IllegalArgumentException if the metadata class implements more 
than one metadata interface of this standard.
      */
     public Map<String, String> asNameMap(Class<?> type, KeyNamePolicy 
keyPolicy, KeyNamePolicy valuePolicy) {
         ensureNonNull("type",        type);
         ensureNonNull("keyPolicy",   keyPolicy);
         ensureNonNull("valuePolicy", valuePolicy);
-        return new NameMap(getTypeAccessor(type, true), keyPolicy, 
valuePolicy);
+        try {
+            return new NameMap(getTypeAccessor(type, 
UnresolvedTypePolicy.THROW), keyPolicy, valuePolicy);
+        } catch (NoSuchElementException e) {
+            throw unrecognized(e, type);
+        }
     }
 
     /**
@@ -887,14 +915,18 @@ public class MetadataStandard implements Serializable {
      * @param  valuePolicy  whether the values shall be property types, the 
element types
      *         (same as property types except for collections) or the 
declaring interface or class.
      * @return the types or declaring type of all properties defined in the 
given metadata type.
-     * @throws ClassCastException if the specified interface or implementation 
class does
-     *         not extend or implement a metadata interface of the expected 
package.
+     * @throws ClassCastException if the metadata class does not implement a 
metadata interface of this standard.
+     * @throws IllegalArgumentException if the metadata class implements more 
than one metadata interface of this standard.
      */
     public Map<String, Class<?>> asTypeMap(Class<?> type, KeyNamePolicy 
keyPolicy, TypeValuePolicy valuePolicy) {
         ensureNonNull("type",        type);
         ensureNonNull("keyPolicy",   keyPolicy);
         ensureNonNull("valuePolicy", valuePolicy);
-        return new TypeMap(getTypeAccessor(type, true), keyPolicy, 
valuePolicy);
+        try {
+            return new TypeMap(getTypeAccessor(type, 
UnresolvedTypePolicy.THROW), keyPolicy, valuePolicy);
+        } catch (NoSuchElementException e) {
+            throw unrecognized(e, type);
+        }
     }
 
     /**
@@ -937,14 +969,19 @@ public class MetadataStandard implements Serializable {
      * @param  type       the metadata interface or implementation class.
      * @param  keyPolicy  determines the string representation of map keys.
      * @return information about all properties defined in the given metadata 
type.
-     * @throws ClassCastException if the given type does not implement a 
metadata interface of the expected package.
+     * @throws ClassCastException if the metadata class does not implement a 
metadata interface of this standard.
+     * @throws IllegalArgumentException if the metadata class implements more 
than one metadata interface of this standard.
      *
      * @see org.apache.sis.metadata.iso.DefaultExtendedElementInformation
      */
     public Map<String, ExtendedElementInformation> asInformationMap(Class<?> 
type, KeyNamePolicy keyPolicy) {
         ensureNonNull("type",     type);
         ensureNonNull("keyNames", keyPolicy);
-        return new InformationMap(citation, getTypeAccessor(type, true), 
keyPolicy);
+        try {
+            return new InformationMap(citation, getTypeAccessor(type, 
UnresolvedTypePolicy.THROW), keyPolicy);
+        } catch (NoSuchElementException e) {
+            throw unrecognized(e, type);
+        }
     }
 
     /**
@@ -961,13 +998,17 @@ public class MetadataStandard implements Serializable {
      * @param  type       the interface or implementation class of a metadata.
      * @param  keyPolicy  determines the string representation of map keys.
      * @return indices of all properties defined by the given metadata type.
-     * @throws ClassCastException if the specified interface or implementation 
class does
-     *         not extend or implement a metadata interface of the expected 
package.
+     * @throws ClassCastException if the metadata class does not implement a 
metadata interface of this standard.
+     * @throws IllegalArgumentException if the metadata class implements more 
than one metadata interface of this standard.
      */
     public Map<String, Integer> asIndexMap(Class<?> type, KeyNamePolicy 
keyPolicy) {
         ensureNonNull("type",      type);
         ensureNonNull("keyPolicy", keyPolicy);
-        return new IndexMap(getTypeAccessor(type, true), keyPolicy);
+        try {
+            return new IndexMap(getTypeAccessor(type, 
UnresolvedTypePolicy.THROW), keyPolicy);
+        } catch (NoSuchElementException e) {
+            throw unrecognized(e, type);
+        }
     }
 
     /**
@@ -1017,7 +1058,8 @@ public class MetadataStandard implements Serializable {
      * @param  keyPolicy    determines the string representation of map keys.
      * @param  valuePolicy  whether the entries having null value or empty 
collection shall be included in the map.
      * @return a map view over the metadata object.
-     * @throws ClassCastException if the metadata object does not implement a 
metadata interface of the expected package.
+     * @throws ClassCastException if the metadata object does not implement a 
metadata interface of this standard.
+     * @throws IllegalArgumentException if the metadata object implements more 
than one metadata interface of this standard.
      *
      * @see AbstractMetadata#asMap()
      *
@@ -1031,7 +1073,11 @@ public class MetadataStandard implements Serializable {
         ensureNonNull("metadata",    metadata);
         ensureNonNull("keyPolicy",   keyPolicy);
         ensureNonNull("valuePolicy", valuePolicy);
-        return new ValueMap(metadata, getInstanceAccessor(metadata, baseType, 
true), keyPolicy, valuePolicy);
+        try {
+            return new ValueMap(metadata, getInstanceAccessor(metadata, 
baseType, UnresolvedTypePolicy.THROW), keyPolicy, valuePolicy);
+        } catch (NoSuchElementException e) {
+            throw unrecognized(e, metadata.getClass());
+        }
     }
 
     /**
@@ -1057,7 +1103,8 @@ public class MetadataStandard implements Serializable {
      * @param  baseType     base type of the metadata of interest, or {@code 
null} if unspecified.
      * @param  keyPolicy    determines the string representation of map keys.
      * @return a map view over the metadata object.
-     * @throws ClassCastException if the metadata object does not implement a 
metadata interface of the expected package.
+     * @throws ClassCastException if the metadata object does not implement a 
metadata interface of this standard.
+     * @throws IllegalArgumentException if the metadata object implements more 
than one metadata interface of this standard.
      *
      * @see AbstractMetadata#nilReasons()
      *
@@ -1066,7 +1113,11 @@ public class MetadataStandard implements Serializable {
     public Map<String, NilReason> asNilReasonMap(Object metadata, Class<?> 
baseType, KeyNamePolicy keyPolicy) {
         ensureNonNull("metadata",  metadata);
         ensureNonNull("keyPolicy", keyPolicy);
-        return new NilReasonMap(metadata, getInstanceAccessor(metadata, 
baseType, true), keyPolicy);
+        try {
+            return new NilReasonMap(metadata, getInstanceAccessor(metadata, 
baseType, UnresolvedTypePolicy.THROW), keyPolicy);
+        } catch (NoSuchElementException e) {
+            throw unrecognized(e, metadata.getClass());
+        }
     }
 
     /**
@@ -1145,7 +1196,8 @@ public class MetadataStandard implements Serializable {
      * @param  baseType     base type of the metadata of interest, or {@code 
null} if unspecified.
      * @param  valuePolicy  whether the property having null value or empty 
collection shall be included in the tree.
      * @return a tree table representation of the specified metadata.
-     * @throws ClassCastException if the metadata object does not implement a 
metadata interface of the expected package.
+     * @throws ClassCastException if the metadata object does not implement a 
metadata interface of this standard.
+     * @throws IllegalArgumentException if the metadata object implements more 
than one metadata interface of this standard.
      *
      * @see AbstractMetadata#asTreeTable()
      *
@@ -1154,8 +1206,10 @@ public class MetadataStandard implements Serializable {
     public TreeTable asTreeTable(final Object metadata, Class<?> baseType, 
final ValueExistencePolicy valuePolicy) {
         ensureNonNull("metadata",    metadata);
         ensureNonNull("valuePolicy", valuePolicy);
-        if (baseType == null) {
+        if (baseType == null) try {
             baseType = getInterface(metadata.getClass());
+        } catch (NoSuchElementException e) {
+            throw unrecognized(e, metadata.getClass());
         }
         return new TreeTableView(this, metadata, baseType, valuePolicy);
     }
@@ -1170,59 +1224,77 @@ public class MetadataStandard implements Serializable {
      * @param  metadata1  the first metadata object to compare, or {@code 
null}.
      * @param  metadata2  the second metadata object to compare, or {@code 
null}.
      * @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.
+     * @return {@code true} if the given metadata objects are equal or if the 
two arguments are {@code null}.
      *
      * @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);
+        final Boolean equals = equals(metadata1, metadata2, null, mode);
+        if (equals == null) throw new ClassCastException();
+        return equals;
     }
 
     /**
      * 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.
+     * The two metadata arguments should be implementations of a
+     * {@linkplain #getInterface(Class, Class) metadata interface} defined by 
this {@code MetadataStandard}.
+     * However, they do not need to be of the exact same class unless {@code 
criteria} is {@link ComparisonMode#STRICT}.
+     * If the equality cannot be determined in a non-ambiguous way, for 
example because the given metadata instances
+     * do not implement exactly one interface assignable to {@code baseType}, 
then this method returns {@code null}.
+     *
+     * <h4>Resolution of type ambiguity</h4>
+     * Because some metadata standards define hundreds of interfaces,
+     * some metadata classes may implement many interfaces at once for 
convenience.
+     * For avoiding ambiguity, the interface of interest, or a non-ambiguous 
super-type of the interface of interest,
+     * should be specified in the {@code baseType} argument. This information 
can be obtained, for example, from the
+     * {@linkplain java.lang.reflect.Method#getReturnType() return type} of 
the method that provided the metadata.
+     * Only properties inherited from interfaces that are assignable to {@code 
baseType} will be compared.
+     * If {@code baseType} is {@code null}, then this method tries to find the 
base type automatically.
+     * The latter is not always equivalent to specifying {@code Object.class} 
as the base type.
      *
      * @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.
+     * @param  criteria   the strictness level of the comparison.
+     * @return whether the given metadata objects are equal, or {@code null} 
if undetermined.
+     * @throws ClassCastException if {@code metadata1} is non-null and not an 
instance of {@code baseType}.
      *
      * @see AbstractMetadata#equals(Object, ComparisonMode)
      *
      * @since 1.7
      */
-    public boolean equals(final Object metadata1, final Object metadata2, 
final Class<?> baseType, final ComparisonMode mode) {
+    public Boolean equals(final Object metadata1, final Object metadata2, 
final Class<?> baseType, final ComparisonMode criteria) {
+        ensureNonNull("criteria", criteria);
         if (metadata1 == metadata2) {
-            return true;
+            return Boolean.TRUE;
         }
         if (metadata1 == null || metadata2 == null) {
-            return false;
+            return Boolean.FALSE;
         }
         final Class<?> type1 = metadata1.getClass();
         final Class<?> type2 = metadata2.getClass();
-        if (type1 != type2 && mode == ComparisonMode.STRICT) {
-            return false;
+        if (type1 != type2 && criteria == ComparisonMode.STRICT) {
+            return Boolean.FALSE;
+        }
+        final PropertyAccessor accessor = getInstanceAccessor(metadata1, 
baseType, UnresolvedTypePolicy.NULL);
+        if (accessor == null) {
+            // Do not invoke `Object.equals(Object)` because it may cause an 
infinite loop.
+            return null;
         }
-        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`.
-            if (!accessor.type.isAssignableFrom(type2)) {
-                return false;
+            if (!key.isValid()) {
+                return Boolean.FALSE;
             }
-            final PropertyAccessor other = getAccessor(key, null, false);
-            if (other == null || accessor.type != other.type) {
-                return false;
+            final PropertyAccessor other = getAccessor(key, 
UnresolvedTypePolicy.NULL, null);
+            if (other == null) {
+                return null;
+            }
+            if (accessor.type != other.type) {
+                return Boolean.FALSE;
             }
         }
         /*
@@ -1240,7 +1312,7 @@ public class MetadataStandard implements Serializable {
              * some getters with an implementation invoking other getters.
              */
             try {
-                return Semaphores.NULL_FOR_EMPTY_COLLECTION.execute(() -> 
accessor.equals(metadata1, metadata2, mode));
+                return Semaphores.NULL_FOR_EMPTY_COLLECTION.execute(() -> 
accessor.equals(metadata1, metadata2, criteria));
             } finally {
                 inProgress.remove(pair);
             }
@@ -1250,7 +1322,7 @@ public class MetadataStandard implements Serializable {
              * comparing other properties. It is okay because someone else is 
comparing those two same objects,
              * and that later comparison will do the actual check for property 
values.
              */
-            return true;
+            return Boolean.TRUE;
         }
     }
 
@@ -1262,13 +1334,14 @@ public class MetadataStandard implements Serializable {
      *
      * @param  metadata  the metadata object to compute hash code.
      * @return a hash code value for the specified metadata, or 0 if the given 
metadata is null.
-     * @throws ClassCastException if the metadata object does not implement a 
metadata interface of the expected package.
+     * @throws ClassCastException if the metadata object does not implement a 
metadata interface of this standard.
+     * @throws IllegalArgumentException if the metadata object implements more 
than one metadata interface of this standard.
      *
      * @see AbstractMetadata#hashCode()
      */
     public int hashCode(final Object metadata) {
-        if (metadata != null) {
-            final Integer hash = HashCode.getOrCreate().walk(this, 
Object.class, metadata, true);
+        if (metadata != null) try {
+            final Integer hash = HashCode.getOrCreate().walk(this, 
Object.class, metadata, UnresolvedTypePolicy.NULL);
             if (hash != null) return hash;
             /*
              * `hash` may be null if a cycle has been found. Example: A 
depends on B which depends on A,
@@ -1277,10 +1350,22 @@ public class MetadataStandard implements Serializable {
              * of a bigger metadata object, and that enclosing object should 
have other properties for computing
              * its hash code.
              */
+        } catch (NoSuchElementException e) {
+            throw unrecognized(e, metadata.getClass());
         }
         return 0;
     }
 
+    /**
+     * Creates an exception for an object which is not an instance of this 
standard.
+     */
+    private ClassCastException unrecognized(final NoSuchElementException 
cause, final Class<?> type) {
+        final var e = new ClassCastException(Resources.format(
+                Resources.Keys.CannotHandleAsStandardType_2, 
citation.getTitle(), Classes.getShortName(type)));
+        e.initCause(cause);
+        return e;
+    }
+
     /**
      * Returns a string representation of this metadata standard.
      * This is for debugging purpose only and may change in any future version.
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataVisitor.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataVisitor.java
index 3b2b53fd44..0682ccfec5 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataVisitor.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/MetadataVisitor.java
@@ -143,17 +143,20 @@ abstract class MetadataVisitor<R> {
      * {@link AbstractMetadata#getStandardType()} to be invoked, then it is 
preferable
      * to specify {@code Object.class} for avoiding to do reflection twice.
      *
-     * @param  standard   the metadata standard implemented by the object to 
visit.
-     * @param  baseType   the standard interface implemented by the metadata 
object, or {@code null} if unknown.
-     * @param  metadata   the metadata to visit.
-     * @param  mandatory  {@code true} for throwing an exception for 
unsupported metadata type, or {@code false} for ignoring.
+     * @param  standard  the metadata standard implemented by the object to 
visit.
+     * @param  baseType  the standard interface implemented by the metadata 
object, or {@code null} if unknown.
+     * @param  metadata  the metadata to visit.
+     * @param  policy    {@code true} for throwing an exception for 
unsupported metadata type, or {@code false} for ignoring.
      * @return the value of {@link #result()} after all elements of the given 
metadata have been visited.
      *         If the given metadata instance has already been visited, then 
this is the previously computed value.
      *         If the computation is in progress (e.g. a cyclic graph), then 
this method returns {@code null}.
+     * @throws ClassCastException if {@code metadata} is not an instance of 
{@code baseType}.
+     * @throws NoSuchElementException if the object does not implement a 
metadata interface and {@code policy} is {@code THROW}.
+     * @throws IllegalArgumentException if the object implements more than one 
metadata interface and {@code policy} is {@code THROW}.
      */
-    final R walk(final MetadataStandard standard, final Class<?> baseType, 
final Object metadata, final boolean mandatory) {
+    final R walk(final MetadataStandard standard, final Class<?> baseType, 
final Object metadata, final UnresolvedTypePolicy policy) {
         if (!visited.containsKey(metadata)) {               // Reminder: the 
associated value may be null.
-            final PropertyAccessor accessor = 
standard.getInstanceAccessor(metadata, baseType, mandatory);
+            final PropertyAccessor accessor = 
standard.getInstanceAccessor(metadata, baseType, policy);
             if (accessor != null) {
                 final Filter filter = preVisit(accessor);       // NONE, 
NON_EMPTY, WRITABLE or WRITABLE_RESULT.
                 final boolean preconstructed;                   // Whether to 
write in instance provided by `result()`.
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/Pruner.java 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/Pruner.java
index 91029169c3..10cfcc2538 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/Pruner.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/Pruner.java
@@ -81,7 +81,7 @@ final class Pruner extends MetadataVisitor<Boolean> {
         final Pruner visitor = VISITORS.get();
         final boolean p = visitor.prune;
         visitor.prune = prune;
-        final Boolean r = visitor.walk(metadata.getStandard(), Object.class, 
metadata, false);
+        final Boolean r = visitor.walk(metadata.getStandard(), Object.class, 
metadata, UnresolvedTypePolicy.NULL);
         visitor.prune = p;
         return (r != null) && r;        // If there is a cycle (r == null), 
then the metadata is non-empty.
     }
@@ -140,7 +140,7 @@ final class Pruner extends MetadataVisitor<Boolean> {
                          * For implementation that are not subtype of 
AbstractMetadata but nevertheless
                          * implement some metadata interfaces, we will invoke 
recursively this method.
                          */
-                        final Boolean r = walk(standard, type, element, false);
+                        final Boolean r = walk(standard, type, element, 
UnresolvedTypePolicy.NULL);
                         if (r != null) {
                             isEmptyElement = r;
                             if (!isEmptyElement && element instanceof 
Emptiable) {
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/StateChanger.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/StateChanger.java
index 5a0a8efc38..e0490b6324 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/StateChanger.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/StateChanger.java
@@ -71,7 +71,7 @@ final class StateChanger extends MetadataVisitor<Boolean> {
         final StateChanger changer = VISITORS.get();
         final ModifiableMetadata.State previous = changer.target;
         changer.target = target;
-        changer.walk(metadata.getStandard(), Object.class, metadata, true);
+        changer.walk(metadata.getStandard(), Object.class, metadata, 
UnresolvedTypePolicy.THROW);
         changer.target = previous;
     }
 
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 ef951e6654..48c2755539 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
@@ -209,9 +209,11 @@ class TreeNode implements Node {
 
     /**
      * Returns the metadata interface for the object represented by this node.
+     *
+     * @return the interface implemented by the value, or {@code null} if 
unknown.
      */
     private Class<?> getInterface() {
-        return table.standard.getInterface(key(), null);
+        return table.standard.getInterface(key(), UnresolvedTypePolicy.NULL, 
null);
     }
 
     /**
@@ -898,9 +900,11 @@ class TreeNode implements Node {
              * If we need to create a new collection, we know that the 
property accessor
              * exists otherwise the call to `isLeaf()` above would have 
returned `true`.
              */
-            if (children == null || ((TreeNodeChildren) children).metadata != 
value) {
-                PropertyAccessor accessor = 
table.standard.getInstanceAccessor(value, baseType, true);
+            if (children == null || ((TreeNodeChildren) children).metadata != 
value) try {
+                PropertyAccessor accessor = 
table.standard.getInstanceAccessor(value, baseType, UnresolvedTypePolicy.THROW);
                 children = new TreeNodeChildren(this, value, accessor);
+            } catch (ClassCastException | NoSuchElementException | 
IllegalArgumentException e) {
+                throw new IllegalStateException(e.getMessage(), e);
             }
         }
         return children;
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/UnresolvedTypePolicy.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/UnresolvedTypePolicy.java
new file mode 100644
index 0000000000..a34499afb3
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/UnresolvedTypePolicy.java
@@ -0,0 +1,49 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sis.metadata;
+
+
+/**
+ * Controls the behavior when a metadata type cannot be resolved.
+ * {@link MetadataStandard} may throw an exception, return {@code null}
+ * or return an arbitrary instance.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+enum UnresolvedTypePolicy {
+    /**
+     * Returns {@code null} if the metadata type cannot be identified or is 
ambiguous.
+     */
+    NULL,
+
+    /**
+     * Returns an arbitrary value if the metadata type is ambiguous.
+     */
+    ANY,
+
+    /**
+     * Throw an exception if the metadata type cannot be identified or is 
ambiguous.
+     */
+    THROW;
+
+    /**
+     * Returns a variant of this policy that does not throw exception.
+     */
+    UnresolvedTypePolicy lenient() {
+        return (this != THROW) ? this : NULL;
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/Resources.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/Resources.java
index d2322ec966..b9cd253db8 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/Resources.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/Resources.java
@@ -67,6 +67,11 @@ public class Resources extends IndexedResourceBundle {
             throw new IllegalAccessException();
         }
 
+        /**
+         * Cannot handle `{1}` as a type derived from the {0} standard.
+         */
+        public static final short CannotHandleAsStandardType_2 = 9;
+
         /**
          * Connection to “{0}” database is already initialized.
          */
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/Resources.properties
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/Resources.properties
index 203146e98e..b20abf46fc 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/Resources.properties
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/Resources.properties
@@ -19,6 +19,7 @@
 # Resources in this file are for `org.apache.sis.metadata` usage only and 
should not be used by any other module.
 # For resources shared by all modules in the Apache SIS project, see 
"org.apache.sis.util.resources" package.
 #
+CannotHandleAsStandardType_2      = Cannot handle `{1}` as a type derived from 
the {0} standard.
 ConnectionAlreadyInitialized_1    = Connection to \u201c{0}\u201d database is 
already initialized.
 ElementAlreadyInitialized_1       = This metadata element is already 
initialized with value \u201c{0}\u201d.
 ExpectedInterface_2               = Illegal class `{1}`. Specify the `{0}` 
interface instead.
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/Resources_fr.properties
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/Resources_fr.properties
index aa6fc11df7..12e98cbee0 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/Resources_fr.properties
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/internal/Resources_fr.properties
@@ -24,6 +24,7 @@
 #   U+202F NARROW NO-BREAK SPACE  before  ; ! and ?
 #   U+00A0 NO-BREAK SPACE         before  :
 #
+CannotHandleAsStandardType_2      = Ne peut pas g\u00e9rer `{1}` comme un type 
d\u00e9riv\u00e9 du standard {0}.
 ConnectionAlreadyInitialized_1    = La connexion \u00e0 la base de 
donn\u00e9es \u00ab\u202f{0}\u202f\u00bb est d\u00e9j\u00e0 initialis\u00e9e.
 ElementAlreadyInitialized_1       = Cet \u00e9l\u00e9ment de 
m\u00e9ta-donn\u00e9e est d\u00e9j\u00e0 initialis\u00e9 avec la valeur 
\u00ab\u202f{0}\u202f\u00bb.
 ExpectedInterface_2               = La classe `{1}` est ill\u00e9gale. 
Sp\u00e9cifiez l\u2019interface `{0}` \u00e0 la place.
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 7cf88b7193..4d4120d541 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
@@ -34,6 +34,7 @@ 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.LenientComparable;
 import org.apache.sis.util.collection.CheckedContainer;
 import org.apache.sis.util.internal.shared.Unsafe;
 import org.apache.sis.util.resources.Errors;
@@ -212,8 +213,8 @@ deeper: if (!baseType.isInstance(source)) {
                     if (!success) {
                         /*
                          * This exception may happen if the source is a 
subclass of the target. This is the converse
-                         * of what we usually have in Java (we can assign a 
sub-type to a more generic Java variable)
-                         * but happen here because if the source is a 
sub-type, we may not be able to copy all values
+                         * of what we usually have in Java (we can assign a 
subtype to a more generic Java variable)
+                         * but happen here because if the source is a subtype, 
we may not be able to copy all values
                          * from the source to the target. We do not use 
ClassCastException type in the hope to reduce
                          * confusion.
                          */
@@ -245,17 +246,18 @@ deeper: if (!baseType.isInstance(source)) {
                                  * may implement many interfaces, while we are 
interrested only in the interface
                                  * implemented by the target `element`.
                                  */
-                                final boolean equals;
+                                final Boolean equals;
                                 final var criteria = 
ComparisonMode.BY_CONTRACT;
-                                if (element instanceof AbstractMetadata) {
-                                    equals = ((AbstractMetadata) 
element).equals(value, criteria);
+                                if (element instanceof LenientComparable) {
+                                    equals = ((LenientComparable) 
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;
+                                // In case of doubt (null), do not remove.
+                                if (equals == null || !equals) continue;
                             }
                             it.remove();
                             break;
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBox.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBox.java
index ca60a06bf5..595195993f 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBox.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/metadata/iso/extent/DefaultGeographicBoundingBox.java
@@ -776,7 +776,7 @@ public class DefaultGeographicBoundingBox extends 
AbstractGeographicExtent imple
          * is generic enough for all other cases.
          */
         if (object != null && object.getClass() == 
DefaultGeographicBoundingBox.class) {
-            final DefaultGeographicBoundingBox that = 
(DefaultGeographicBoundingBox) object;
+            final var that = (DefaultGeographicBoundingBox) object;
             return Objects.equals(getInclusion(), that.getInclusion()) &&
                    doubleToLongBits(southBoundLatitude) == 
doubleToLongBits(that.southBoundLatitude) &&
                    doubleToLongBits(northBoundLatitude) == 
doubleToLongBits(that.northBoundLatitude) &&
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 28ea1288ae..1d7cc263aa 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
@@ -49,7 +49,7 @@
  * <ul class="verbose">
  *   <li>The {@code Abstract} prefix means that the class is abstract in the 
sense of the implemented standard.
  *       It it not necessarily abstract in the sense of Java. Because 
incomplete metadata are common in practice,
- *       sometimes we wish to instantiate an "abstract" class because of the 
lack of knowledge about the exact sub-type.</li>
+ *       sometimes we wish to instantiate an "abstract" class because of the 
lack of knowledge about the exact subtype.</li>
  *   <li>The properties are determined by the getter methods declared in the 
interfaces.
  *       Getter methods declared in the implementation classes are 
ignored.</li>
  *   <li>Setter methods, if any, can be declared in the implementation classes 
without the need for declarations
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/DefaultRecordType.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/DefaultRecordType.java
index 0c94341564..f83a933802 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/DefaultRecordType.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/util/iso/DefaultRecordType.java
@@ -431,7 +431,7 @@ public class DefaultRecordType extends RecordDefinition 
implements RecordType, S
      *
      * <h4>Implementation note</h4>
      * We do not require that {@code record.getRecordType() == this} in order 
to allow record
-     * "sub-types" to define additional fields, in a way similar to Java 
sub-classing.
+     * subtypes to define additional fields, in a way similar to Java 
sub-classing.
      *
      * @param  record  the record to test for compatibility with this type, or 
{@code null}.
      * @return {@code true} if the given record is non-null and compatible 
with this record type.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/HashCodeTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/HashCodeTest.java
index 08732ebaa8..44ceb944e0 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/HashCodeTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/HashCodeTest.java
@@ -50,6 +50,7 @@ import 
org.apache.sis.metadata.iso.citation.DefaultResponsibility;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class HashCodeTest extends TestCase {
     /**
      * Creates a new test case.
@@ -61,7 +62,7 @@ public final class HashCodeTest extends TestCase {
      * Computes the hash code value of the given object.
      */
     private static Integer hash(final Object metadata) {
-        return HashCode.getOrCreate().walk(MetadataStandard.ISO_19115, null, 
metadata, true);
+        return HashCode.getOrCreate().walk(MetadataStandard.ISO_19115, null, 
metadata, UnresolvedTypePolicy.THROW);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/InformationMapTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/InformationMapTest.java
index 189a1a8d38..1e58f7e5e5 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/InformationMapTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/InformationMapTest.java
@@ -38,6 +38,7 @@ import org.apache.sis.test.TestCase;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class InformationMapTest extends TestCase {
     /**
      * Creates a new test case.
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 94d2da09df..a9159376c5 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
@@ -19,6 +19,7 @@ package org.apache.sis.metadata;
 import java.util.Set;
 import java.util.Map;
 import java.util.HashSet;
+import java.util.NoSuchElementException;
 import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.quality.Completeness;
 import org.opengis.metadata.extent.GeographicExtent;
@@ -151,7 +152,8 @@ public final class MetadataStandardTest extends TestCase {
      * Returns the interface type declared by the accessor for the given class.
      */
     private Class<?> getAccessor(final Class<?> type, final boolean mandatory) 
{
-        final PropertyAccessor accessor = standard.getTypeAccessor(type, 
mandatory);
+        final PropertyAccessor accessor = standard.getTypeAccessor(type,
+                mandatory ? UnresolvedTypePolicy.THROW : 
UnresolvedTypePolicy.NULL);
         return (accessor != null) ? accessor.type : null;
     }
 
@@ -180,9 +182,11 @@ public final class MetadataStandardTest extends TestCase {
      */
     @Test
     public void testGetWrongInterface() {
-        standard = new MetadataStandard("SIS", "org.apache.sis.dummy.");
+        standard = new MetadataStandard("Pseudo Standard", 
"org.apache.sis.dummy.");
         var e = assertThrows(ClassCastException.class, () -> 
getInterface(DefaultCitation.class));
-        assertMessageContains(e, "DefaultCitation");
+        assertMessageContains(e, "DefaultCitation", "Pseudo Standard");
+        var cause = assertInstanceOf(NoSuchElementException.class, 
e.getCause());
+        assertMessageContains(cause, "DefaultCitation");
     }
 
     /**
@@ -192,19 +196,19 @@ public final class MetadataStandardTest extends TestCase {
     public void testEquals() {
         standard = MetadataStandard.ISO_19115;
 
-        // Self equality test
+        // Self equality test.
         DefaultCitation instance = HardCodedCitations.EPSG;
-        assertFalse(standard.equals(instance, HardCodedCitations.SIS,  null, 
ComparisonMode.STRICT));
-        assertTrue (standard.equals(instance, HardCodedCitations.EPSG, null, 
ComparisonMode.STRICT));
+        assertFalse(standard.equals(instance, HardCodedCitations.SIS,  
Object.class, ComparisonMode.STRICT));
+        assertTrue (standard.equals(instance, HardCodedCitations.EPSG, 
Object.class, ComparisonMode.STRICT));
 
-        // Test comparison with a copy
+        // Test comparison with a copy.
         instance = new DefaultCitation(HardCodedCitations.EPSG);
-        assertFalse(standard.equals(instance, HardCodedCitations.SIS,  null, 
ComparisonMode.STRICT));
-        assertTrue (standard.equals(instance, HardCodedCitations.EPSG, null, 
ComparisonMode.STRICT));
+        assertFalse(standard.equals(instance, HardCodedCitations.SIS,  
Object.class, ComparisonMode.STRICT));
+        assertTrue (standard.equals(instance, HardCodedCitations.EPSG, 
Object.class, ComparisonMode.STRICT));
 
-        // test comparison with a modified copy
+        // Test comparison with a modified copy.
         instance.setTitle(new SimpleInternationalString("A dummy title"));
-        assertFalse(standard.equals(instance, HardCodedCitations.EPSG, null, 
ComparisonMode.STRICT));
+        assertFalse(standard.equals(instance, HardCodedCitations.EPSG, 
Object.class, ComparisonMode.STRICT));
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/NameMapTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/NameMapTest.java
index 46ab70956d..69c7981e69 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/NameMapTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/NameMapTest.java
@@ -36,6 +36,7 @@ import org.apache.sis.test.TestCase;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class NameMapTest extends TestCase {
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/PrunerTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/PrunerTest.java
index 3d68f5b171..f0de5727bf 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/PrunerTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/PrunerTest.java
@@ -38,6 +38,7 @@ import org.apache.sis.test.TestCase;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class PrunerTest extends TestCase {
     /**
      * The root metadata object being tested.
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/SpecialCasesTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/SpecialCasesTest.java
index ed09fdd515..827fdcf02b 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/SpecialCasesTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/SpecialCasesTest.java
@@ -35,6 +35,7 @@ import 
org.apache.sis.metadata.iso.citation.HardCodedCitations;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class SpecialCasesTest extends TestCase {
     /**
      * The accessor instance to be tested.
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 13784c2647..90678615fc 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
@@ -151,7 +151,7 @@ public final class TreeNodeChildrenTest extends TestCase {
         final MetadataStandard standard = MetadataStandard.ISO_19115;
         final TreeTableView    table    = new TreeTableView(standard, 
citation, Citation.class, valuePolicy);
         final TreeNode         node     = (TreeNode) table.getRoot();
-        final PropertyAccessor accessor = 
standard.getTypeAccessor(citation.getClass(), true);
+        final PropertyAccessor accessor = 
standard.getTypeAccessor(citation.getClass(), UnresolvedTypePolicy.THROW);
         return new TreeNodeChildren(node, citation, accessor);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TypeMapTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TypeMapTest.java
index c2caea2066..7645bccb21 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TypeMapTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/TypeMapTest.java
@@ -50,6 +50,7 @@ import org.opengis.metadata.citation.Responsibility;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class TypeMapTest extends TestCase {
     /**
      * Creates a new test case.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/MatrixParameters.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/MatrixParameters.java
index f8d7dace0e..fb5a53815a 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/MatrixParameters.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/parameter/MatrixParameters.java
@@ -700,7 +700,7 @@ public class MatrixParameters<E> implements 
CheckedContainer<E>, Serializable {
 
     /**
      * Returns the default value for the parameter descriptor at the given 
indices.
-     * The default implementation returns 1 if all indices are equals, or 0 
otherwise.
+     * The default implementation returns 1 if all indices are equal, or 0 
otherwise.
      *
      * @param  indices  the indices of the matrix element for which to get the 
default value,
      *                  in (<var>row</var>, <var>column</var>, …) order.
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractLinearTransform.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractLinearTransform.java
index 6921b185e6..bca744f72e 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractLinearTransform.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/operation/transform/AbstractLinearTransform.java
@@ -267,9 +267,10 @@ abstract class AbstractLinearTransform extends 
AbstractMathTransform implements
      * Compares the specified object with this linear transform for equality.
      * This implementation returns {@code true} if the following conditions 
are met:
      * <ul>
-     *   <li>In {@code STRICT} mode, the objects are of the same class and 
{@link #equalsSameClass(Object)}
-     *       returns {@code true}.</li>
-     *   <li>In other modes, the matrix are equals or approximately equals 
(depending on the mode).</li>
+     *   <li>In {@code STRICT} mode, the objects are of the same class
+     *       and {@link #equalsSameClass(Object)} returns {@code true}.</li>
+     *   <li>In other modes, the matrices are equal or approximately equal
+     *       (depending on the comparison criteria).</li>
      * </ul>
      *
      * @param  object  the object to compare with this transform.
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
index b015ade95b..b1b53c0cdc 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/shared/Numerics.java
@@ -343,7 +343,7 @@ public final class Numerics {
     }
 
     /**
-     * Returns {@code true} if the given floats are equals. Positive and 
negative zero are
+     * Returns {@code true} if the given floats are equal. Positive and 
negative zero are
      * considered different, while a NaN value is considered equal to all 
other NaN values.
      *
      * @param  v1  the first value to compare.
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
index ae65d1bfd1..ca443d01ff 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.java
@@ -81,6 +81,11 @@ public class Errors extends IndexedResourceBundle {
          */
         public static final short AmbiguousName_3 = 2;
 
+        /**
+         * Type ‘{1}’ is ambiguous because it implements more than one subtype 
of ‘{0}’.
+         */
+        public static final short AmbiguousType_2 = 207;
+
         /**
          * Computation in background failed.
          */
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
index f4e2bbf66d..14a647c6b6 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors.properties
@@ -26,6 +26,7 @@
 #
 AlreadyInitialized_1              = \u2018{0}\u2019 is already initialized.
 AmbiguousName_3                   = Name \u201c{2}\u201d is ambiguous because 
it can be understood as either \u201c{0}\u201d or \u201c{1}\u201d.
+AmbiguousType_2                   = Type \u2018{1}\u2019 is ambiguous because 
it implements more than one subtype of \u2018{0}\u2019.
 BackgroundComputationFailed       = Computation in background failed.
 CanIterateOnlyOnce                = This object can iterate only once.
 CanNotAddToExclusiveSet_2         = No element can be added to this set 
because properties \u2018{0}\u2019 and \u2018{1}\u2019 are mutually exclusive.
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors_fr.properties
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors_fr.properties
index 5e4ec30103..e0a2b070fc 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors_fr.properties
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/Errors_fr.properties
@@ -23,6 +23,7 @@
 #
 AlreadyInitialized_1              = \u2018{0}\u2019 est d\u00e9j\u00e0 
initialis\u00e9.
 AmbiguousName_3                   = Le nom \u00ab\u202f{2}\u202f\u00bb est 
ambigu\u00eb car il peut \u00eatre interpr\u00e9t\u00e9 comme 
\u00ab\u202f{0}\u202f\u00bb ou \u00ab\u202f{1}\u202f\u00bb.
+AmbiguousType_2                   = Le type \u2018{1}\u2019 est ambigu\u00eb 
car il impl\u00e9mente plus qu'un sous-type de \u2018{0}\u2019.
 BackgroundComputationFailed       = Le calcul en arri\u00e8re-plan a 
\u00e9chou\u00e9.
 CanIterateOnlyOnce                = Cet objet ne peut it\u00e9rer qu\u2019une 
seule fois.
 CanNotAddToExclusiveSet_2         = Aucun \u00e9l\u00e9ment ne peut \u00eatre 
ajout\u00e9 \u00e0 cet ensemble car les propri\u00e9t\u00e9s \u2018{0}\u2019 et 
\u2018{1}\u2019 sont mutuellement exclusives.
diff --git 
a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/collection/RangeSetTest.java
 
b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/collection/RangeSetTest.java
index d9a34539a5..3dec096daa 100644
--- 
a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/collection/RangeSetTest.java
+++ 
b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/collection/RangeSetTest.java
@@ -43,6 +43,7 @@ import static 
org.apache.sis.test.Assertions.assertSerializedEquals;
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Rémi Maréchal (Geomatys)
  */
+@SuppressWarnings("exports")
 public final class RangeSetTest extends TestCase {
     /**
      * Tolerance factor for comparison of floating point numbers.
@@ -58,7 +59,7 @@ public final class RangeSetTest extends TestCase {
     }
 
     /**
-     * Asserts that the two given values are equals to the expected one.
+     * Asserts that the two given values are equal to the expected range.
      * This method is used for testing {@link RangeSet#first()} and {@link 
RangeSet#last()}
      * at the same time as the values from the iterator.
      */
@@ -573,6 +574,7 @@ public final class RangeSetTest extends TestCase {
      * @throws InterruptedException if the test has been interrupted.
      */
     @Benchmark
+    @SuppressWarnings("SleepWhileInLoop")
     public void stress() throws InterruptedException {
         final Random r = TestUtilities.createRandomNumberGenerator();
         for (int p=0; p<10; p++) {

Reply via email to