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

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


The following commit(s) were added to refs/heads/geoapi-4.0 by this push:
     new b325a0c5f8 Allow `CRS.fromCode(String)` to return the CRS definitions 
found in the GML document being parsed. This feature make possible for embedded 
or linked data to reference CRS defined in the document.
b325a0c5f8 is described below

commit b325a0c5f8efecb1063a82623844cb4ecde6738d
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Fri Dec 29 18:54:50 2023 +0100

    Allow `CRS.fromCode(String)` to return the CRS definitions found in the GML 
document being parsed.
    This feature make possible for embedded or linked data to reference CRS 
defined in the document.
---
 .../main/org/apache/sis/xml/bind/Context.java      |  81 ++++++-----
 .../org/apache/sis/xml/bind/ScopedIdentifier.java  | 145 ++++++++++++++++++++
 .../test/org/apache/sis/xml/test/TestCase.java     |  26 ++--
 .../sis/referencing/AbstractIdentifiedObject.java  |   9 +-
 .../main/org/apache/sis/referencing/CRS.java       |  46 +++++--
 .../org/apache/sis/xml/bind/referencing/Code.java  |  28 +++-
 .../sis/parameter/ParameterMarshallingTest.java    |  81 +++++++----
 .../operation/SingleOperationMarshallingTest.java  | 148 ++++++++++++---------
 .../sis/referencing/operation/Transformation.xml   |   4 +-
 .../apache/sis/util/internal/DefinitionURI.java    |  22 ++-
 .../sis/util/internal/DefinitionURITest.java       |  10 ++
 11 files changed, 443 insertions(+), 157 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/Context.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/Context.java
index 85e11932f2..2ab7f6aa79 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/Context.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/Context.java
@@ -176,23 +176,24 @@ public final class Context extends MarshalContext {
 
     /**
      * The objects associated to XML identifiers in the current document.
+     * Map keys are {@code gml:id} attribute values, and map values are the 
identified objects.
      * At marhalling time, this map is used for avoiding duplicated 
identifiers in the same XML document.
      * At unmarshalling time, this is used for getting a previously 
unmarshalled object from its identifier.
      *
      * @see #getObjectForID(Context, String)
      */
-    private final Map<String,Object> identifiers;
+    private final Map<String,Object> xmlidToObject;
 
     /**
      * The identifiers used for marshalled objects in the current document.
-     * This is the converse of the {@link #identifiers} map, used in order to 
identify which {@code gml:id} to use
+     * This is the converse of the {@link #xmlidToObject} map, used in order 
to identify which {@code gml:id} to use
      * for a given object. The {@code gml:id} values to use are not 
necessarily the same than the values associated
      * to {@link IdentifierSpace#ID} if some identifiers were already used for 
other objects in the same XML document.
      */
-    private final Map<Object,String> identifiedObjects;
+    private final Map<Object,String> objectToXmlid;
 
     /**
-     * The {@link #identifiers} map for each document being unmarshalled.
+     * The {@link #xmlidToObject} map for each document being unmarshalled.
      * This cache is populated if the document contains {@code xlink:href} to 
other documents.
      * This cache contains only the documents seen by following the references 
since the root document.
      * This is not a system-wide cache.
@@ -201,10 +202,20 @@ public final class Context extends MarshalContext {
      * from a file or an URL, {@code systemId} should be the value of {@link 
java.net.URI#toASCIIString()} for
      * consistency with {@link javax.xml.transform.stream.StreamSource}.  
However, URI instances are preferred
      * in this map because the {@link URI#equals(Object)} method applies some 
rules regarding case-sensitivity
-     * that {@link String#equals(Object)} cannot know. Values of the map are 
the {@link #identifiers} maps of
+     * that {@link String#equals(Object)} cannot know. Values of the map are 
the {@link #xmlidToObject} maps of
      * the corresponding document. By convention, the object associated to the 
null key is the whole document.</p>
      */
-    private final Map<Object, Map<String,Object>> identifiersPerDocuments;
+    private final Map<Object, Map<String,Object>> documentToXmlids;
+
+    /**
+     * All identified objects associated to a global identifier (not {@code 
gml:id}).
+     * This map differs from {@link #xmlidToObject} in that it is not local to 
the current document,
+     * but instead contains all identified objects in all documents parsed 
since the root document.
+     * The keys of this map cannot be {@code gml:id} attribute values, because 
the latter are local.
+     * Instead, keys are typically {@link 
org.opengis.referencing.IdentifiedObject#getIdentifiers()}.
+     * The map may contain many entries for the same object if that object has 
many identifiers.
+     */
+    final Map<ScopedIdentifier<?>, Object> identifiedObjects;
 
     /**
      * The object to inform about warnings, or {@code null} if none.
@@ -270,19 +281,20 @@ public final class Context extends MarshalContext {
         if (versionMetadata != null && 
versionMetadata.compareTo(LegacyNamespaces.VERSION_2014) < 0) {
             bitMasks |= LEGACY_METADATA;
         }
-        this.pool               = pool;
-        this.locale             = locale;
-        this.timezone           = timezone;
-        this.schemas            = schemas;              // No clone, because 
this class is internal.
-        this.versionGML         = versionGML;
-        this.linkHandler        = linkHandler;
-        this.resolver           = resolver;
-        this.converter          = converter;
-        this.logFilter          = logFilter;
-        identifiers             = new HashMap<>();
-        identifiersPerDocuments = new HashMap<>();
-        identifiedObjects       = new IdentityHashMap<>();
-        previous                = CURRENT.get();
+        this.pool         = pool;
+        this.locale       = locale;
+        this.timezone     = timezone;
+        this.schemas      = schemas;            // No clone, because this 
class is internal.
+        this.versionGML   = versionGML;
+        this.linkHandler  = linkHandler;
+        this.resolver     = resolver;
+        this.converter    = converter;
+        this.logFilter    = logFilter;
+        xmlidToObject     = new HashMap<>();
+        objectToXmlid     = new IdentityHashMap<>();
+        documentToXmlids  = new HashMap<>();
+        identifiedObjects = new HashMap<>();
+        previous          = CURRENT.get();
         if ((bitMasks & MARSHALLING) != 0) {
             /*
              * Set global semaphore last after our best effort to ensure that 
construction
@@ -310,6 +322,8 @@ public final class Context extends MarshalContext {
     private Context(final Context parent, final Locale locale, final 
ExternalLinkHandler linkHandler,
                     final boolean inline)
     {
+        assert parent == CURRENT.get();
+        this.previous    = parent;
         this.locale      = locale;
         this.linkHandler = linkHandler;
         this.pool        = parent.pool;
@@ -321,15 +335,14 @@ public final class Context extends MarshalContext {
         this.logFilter   = parent.logFilter;
         this.bitMasks    = parent.bitMasks & ~CLEAR_SEMAPHORE;
         if (inline) {
-            identifiers       = parent.identifiers;
-            identifiedObjects = parent.identifiedObjects;
+            xmlidToObject = parent.xmlidToObject;
+            objectToXmlid = parent.objectToXmlid;
         } else {
-            identifiers       = new HashMap<>();
-            identifiedObjects = new IdentityHashMap<>();
+            xmlidToObject = new HashMap<>();
+            objectToXmlid = new IdentityHashMap<>();
         }
-        identifiersPerDocuments = parent.identifiersPerDocuments;
-        previous = parent;
-        assert parent == CURRENT.get();
+        documentToXmlids  = parent.documentToXmlids;
+        identifiedObjects = parent.identifiedObjects;
     }
 
     /**
@@ -433,7 +446,7 @@ public final class Context extends MarshalContext {
     public final Context createChild(final Object systemId, final 
ExternalLinkHandler linkHandler) {
         // Use Map.put(…) instead of putIfAbsent(…) because maybe the previous 
map is unmodifiable.
         final Context context = new Context(this, locale, linkHandler, false);
-        identifiersPerDocuments.put(systemId, context.identifiers);
+        documentToXmlids.put(systemId, context.xmlidToObject);
         CURRENT.set(context);
         return context;
     }
@@ -614,7 +627,7 @@ public final class Context extends MarshalContext {
      * @return the identifier used in the current XML document for the given 
object, or {@code null} if none.
      */
     public static String getObjectID(final Context context, final Object 
object) {
-        return (context != null) ? context.identifiedObjects.get(object) : 
null;
+        return (context != null) ? context.objectToXmlid.get(object) : null;
     }
 
     /**
@@ -627,7 +640,7 @@ public final class Context extends MarshalContext {
      * @return the object associated to the given identifier, or {@code null} 
if none.
      */
     public static Object getObjectForID(final Context context, final String 
id) {
-        return (context != null) ? context.identifiers.get(id) : null;
+        return (context != null) ? context.xmlidToObject.get(id) : null;
     }
 
     /**
@@ -643,11 +656,11 @@ public final class Context extends MarshalContext {
      */
     public static boolean setObjectForID(final Context context, final Object 
object, final String id) {
         if (context != null) {
-            final Object existing = context.identifiers.putIfAbsent(id, 
object);
+            final Object existing = context.xmlidToObject.putIfAbsent(id, 
object);
             if (existing != null) {
                 return existing == object;
             }
-            if (context.identifiedObjects.put(object, id) != null) {
+            if (context.objectToXmlid.put(object, id) != null) {
                 throw new CorruptedObjectException(id);    // Should never 
happen since all put calls are in this method.
             }
         }
@@ -670,7 +683,7 @@ public final class Context extends MarshalContext {
      * @return the object associated to the given identifier, or {@code null} 
if none.
      */
     public final Object getExternalObjectForID(final Object systemId, final 
String id) {
-        final Map<String,Object> cache = identifiersPerDocuments.get(systemId);
+        final Map<String,Object> cache = documentToXmlids.get(systemId);
         return (cache != null) ? cache.get(id) : null;
     }
 
@@ -683,9 +696,9 @@ public final class Context extends MarshalContext {
      * @param  document  the document to cache.
      */
     public final void cacheDocument(final Object systemId, final Object 
document) {
-        final Map<String, Object> cache = 
identifiersPerDocuments.get(systemId);
+        final Map<String, Object> cache = documentToXmlids.get(systemId);
         if (cache == null || cache.isEmpty()) {
-            identifiersPerDocuments.put(systemId, 
Collections.singletonMap(null, document));
+            documentToXmlids.put(systemId, Collections.singletonMap(null, 
document));
         } else {
             cache.put(null, document);
         }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/ScopedIdentifier.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/ScopedIdentifier.java
new file mode 100644
index 0000000000..8b42b60360
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/ScopedIdentifier.java
@@ -0,0 +1,145 @@
+/*
+ * 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.xml.bind;
+
+import java.util.logging.Level;
+import org.apache.sis.util.Classes;
+import org.apache.sis.util.resources.Errors;
+
+
+/**
+ * An identifier in the scope of a type, for use as keys in an hash map.
+ * This object is for handing ISO 19111 identifiers, because they may have
+ * the same value for different types. The type is usually a GeoAPI interface.
+ *
+ * @param  <T>  base type of the identified object.
+ * @author Martin Desruisseaux (Geomatys)
+ */
+public final class ScopedIdentifier<T> {
+    /**
+     * Sentinel value for meaning that an identifier is used many times.
+     */
+    private static final Object DUPLICATED = Void.TYPE;
+
+    /**
+     * The identifier scope, usually a GeoAPI interface.
+     */
+    private final Class<? extends T> scope;
+
+    /**
+     * The identifier of the object to associate to this key.
+     */
+    private final String identifier;
+
+    /**
+     * Creates a new key.
+     *
+     * @param scope       the identifier scope, usually a GeoAPI interface.
+     * @param identifier  the identifier of the object to associate to this 
key.
+     */
+    public ScopedIdentifier(final Class<? extends T> scope, final String 
identifier) {
+        this.scope      = scope;
+        this.identifier = identifier;
+    }
+
+    /**
+     * Returns an identifier with the same scope than this identifier by a 
different character string.
+     *
+     * @param  alt  the new identifier.
+     * @return the new identifier, or {@code this} if no change.
+     */
+    public ScopedIdentifier<T> rename(final String alt) {
+        return alt.equals(identifier) ? this : new ScopedIdentifier<>(scope, 
alt);
+    }
+
+    /**
+     * Stores an identified object for this identifier.
+     * The identifier is typically {@link 
org.opengis.referencing.IdentifiedObject#getIdentifiers()}.
+     * If the given identifier is already associated to another identified 
object, a warning is logged.
+     * This method can be invoked many times if an object has many identifiers.
+     *
+     * @param  base    limit to follow when storing the object for parent 
interfaces.
+     * @param  object  the identified object to store. Shall be an instance of 
{@code T}.
+     * @param  caller  the class to declare as the source if a warning is 
logged, or {@code null} for no warning.
+     * @param  method  the name of the method to declare as the source if a 
warning is logged, or {@code null}.
+     */
+    public void store(final Class<T> base, final T object, final Class<?> 
caller, final String method) {
+        final Context context = Context.current();
+        if (context != null) {
+            boolean warn = (caller != null);
+            ScopedIdentifier<?> key = this;
+            final Class<?>[] parents = Classes.getAllInterfaces(scope);
+            for (int index = -1 ;;) {
+                final Object previous = 
context.identifiedObjects.putIfAbsent(key, object);
+                if (previous != null && previous != object && previous != 
DUPLICATED) {
+                    context.identifiedObjects.put(key, DUPLICATED);
+                    if (warn) {
+                        warn = false;           // Report warning only once 
for this identifier.
+                        Context.warningOccured(context, (key == this) ? 
Level.WARNING : Level.FINE,
+                                caller, method, null, Errors.class, 
Errors.Keys.DuplicatedIdentifier_1, identifier);
+                    }
+                }
+                Class<?> parent;
+                do if (++index >= parents.length) return;
+                while (!base.isAssignableFrom(parent = parents[index]));
+                key = new ScopedIdentifier<>(parent, identifier);
+            }
+        }
+    }
+
+    /**
+     * Returns the object associated to this scoped identifier.
+     *
+     * @param  context  the unmarshalling context. Shall not be null.
+     * @return the object associated to this scoped identifier, or {@code 
null} if none.
+     */
+    public T get(final Context context) {
+        final Object value = context.identifiedObjects.get(this);
+        return (value != DUPLICATED) ? scope.cast(value) : null;
+    }
+
+    /**
+     * {@return an hash code value for this key}.
+     */
+    @Override
+    public int hashCode() {
+        return scope.hashCode() + identifier.hashCode();
+    }
+
+    /**
+     * Tests this key with the given object for equality.
+     *
+     * @param  obj  the object to compare with this key.
+     * @return whether the two objects are equal.
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof ScopedIdentifier) {
+            final var other = (ScopedIdentifier<?>) obj;
+            return scope.equals(other.scope) && 
identifier.equals(other.identifier);
+        }
+        return false;
+    }
+
+    /**
+     * {@return a string representation for debugging purposes}.
+     */
+    @Override
+    public String toString() {
+        return scope.getSimpleName() + ':' + identifier;
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/test/TestCase.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/test/TestCase.java
index 227147518c..2ab6497a00 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/test/TestCase.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/test/TestCase.java
@@ -37,13 +37,11 @@ import org.apache.sis.xml.XML;
 import org.apache.sis.xml.bind.Context;
 import org.apache.sis.xml.util.LegacyNamespaces;
 import org.apache.sis.xml.bind.cat.CodeListUID;
-import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Version;
 
 // Test dependencies
 import org.junit.After;
-import static org.junit.Assert.*;
-import static org.opengis.test.Assert.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.*;
 import static org.apache.sis.metadata.Assertions.assertXmlEquals;
 
 
@@ -175,7 +173,7 @@ public abstract class TestCase extends 
org.apache.sis.test.TestCase {
      */
     @After
     public final void clearContext() {
-        assertSame("Unexpected context. Is this method invoked from the right 
thread?", context, Context.current());
+        assertSame(context, Context.current(), "Unexpected context. Is this 
method invoked from the right thread?");
         if (context != null) {
             context.finish();
             context = null;
@@ -210,7 +208,7 @@ public abstract class TestCase extends 
org.apache.sis.test.TestCase {
     protected final void assertMarshalEqualsFile(final InputStream expected, 
final Object object,
             final String... ignoredAttributes) throws JAXBException
     {
-        assertNotNull("Test resource is not found or not accessible.", 
expected);
+        assertNotNull(expected, "Test resource is not found or not 
accessible.");
         try (expected) {
             assertXmlEquals(expected, marshal(object), ignoredAttributes);
         } catch (IOException e) {
@@ -233,7 +231,7 @@ public abstract class TestCase extends 
org.apache.sis.test.TestCase {
     protected final void assertMarshalEqualsFile(final InputStream expected, 
final Object object,
             final Version metadataVersion, final String... ignoredAttributes) 
throws JAXBException
     {
-        assertNotNull("Test resource is not found or not accessible.", 
expected);
+        assertNotNull(expected, "Test resource is not found or not 
accessible.");
         try (expected) {
             assertXmlEquals(expected, marshal(object, metadataVersion), 
ignoredAttributes);
         } catch (IOException e) {
@@ -261,7 +259,7 @@ public abstract class TestCase extends 
org.apache.sis.test.TestCase {
     protected final void assertMarshalEqualsFile(final InputStream expected, 
final Object object, final Version metadataVersion,
             final double tolerance, final String[] ignoredNodes, final 
String[] ignoredAttributes) throws JAXBException
     {
-        assertNotNull("Test resource is not found or not accessible.", 
expected);
+        assertNotNull(expected, "Test resource is not found or not 
accessible.");
         try (expected) {
             assertXmlEquals(expected, marshal(object, metadataVersion), 
tolerance, ignoredNodes, ignoredAttributes);
         } catch (IOException e) {
@@ -316,8 +314,8 @@ public abstract class TestCase extends 
org.apache.sis.test.TestCase {
      * @see #unmarshal(Unmarshaller, String)
      */
     protected final String marshal(final Marshaller marshaller, final Object 
object) throws JAXBException {
-        ArgumentChecks.ensureNonNull("marshaller", marshaller);
-        ArgumentChecks.ensureNonNull("object", object);
+        assertNotNull(marshaller, "marshaller");
+        assertNotNull(object, "object");
         if (buffer == null) {
             buffer = new StringWriter();
         }
@@ -340,7 +338,7 @@ public abstract class TestCase extends 
org.apache.sis.test.TestCase {
      * @see #assertMarshalEqualsFile(String, Object, String...)
      */
     protected final <T> T unmarshalFile(final Class<T> type, final InputStream 
input) throws JAXBException {
-        assertNotNull("Test resource is not found or not accessible.", input);
+        assertNotNull(input, "Test resource is not found or not accessible.");
         try (input) {
             final MarshallerPool pool = getMarshallerPool();
             final Unmarshaller unmarshaller = pool.acquireUnmarshaller();
@@ -368,7 +366,7 @@ public abstract class TestCase extends 
org.apache.sis.test.TestCase {
         final Unmarshaller unmarshaller = pool.acquireUnmarshaller();
         final Object object = unmarshal(unmarshaller, xml);
         pool.recycle(unmarshaller);
-        assertInstanceOf("unmarshal", type, object);
+        assertInstanceOf(type, object, "unmarshal");
         return type.cast(object);
     }
 
@@ -383,8 +381,8 @@ public abstract class TestCase extends 
org.apache.sis.test.TestCase {
      * @see #marshal(Marshaller, Object)
      */
     protected final Object unmarshal(final Unmarshaller unmarshaller, final 
String xml) throws JAXBException {
-        ArgumentChecks.ensureNonNull("unmarshaller", unmarshaller);
-        ArgumentChecks.ensureNonNull("xml", xml);
+        assertNotNull(unmarshaller, "unmarshaller");
+        assertNotNull(xml, "xml");
         return unmarshaller.unmarshal(new StringReader(xml));
     }
 
@@ -396,7 +394,7 @@ public abstract class TestCase extends 
org.apache.sis.test.TestCase {
      * @return the date as a {@link Date}.
      */
     protected static Date xmlDate(final String date) {
-        ArgumentChecks.ensureNonNull("date", date);
+        assertNotNull(date, "date");
         try {
             synchronized (dateFormat) {
                 return dateFormat.parse(date);
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AbstractIdentifiedObject.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AbstractIdentifiedObject.java
index b1a29eeff2..8d4b7d9cc4 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AbstractIdentifiedObject.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AbstractIdentifiedObject.java
@@ -46,6 +46,7 @@ import org.opengis.referencing.AuthorityFactory;
 import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.ReferenceSystem;
 import org.apache.sis.xml.Namespaces;
+import org.apache.sis.xml.bind.ScopedIdentifier;
 import org.apache.sis.xml.bind.UseLegacyMetadata;
 import org.apache.sis.xml.bind.referencing.Code;
 import org.apache.sis.xml.bind.metadata.EX_Extent;
@@ -125,7 +126,7 @@ import org.opengis.referencing.ObjectDomain;
  * objects and passed between threads without synchronization.
  *
  * @author  Martin Desruisseaux (IRD, Geomatys)
- * @version 1.4
+ * @version 1.5
  * @since   0.4
  */
 @XmlType(name = "IdentifiedObjectType", propOrder = {
@@ -1057,6 +1058,7 @@ public class AbstractIdentifiedObject extends 
FormattableObject implements Ident
 
     /**
      * Invoked by JAXB at unmarshalling time for setting the identifier.
+     * The identifier is temporarily stored in a map local to the 
unmarshalling process.
      */
     private void setIdentifier(final Code identifier) {
         if (identifiers != null) {
@@ -1065,6 +1067,11 @@ public class AbstractIdentifiedObject extends 
FormattableObject implements Ident
             final Identifier id = identifier.getIdentifier();
             if (id != null) {
                 identifiers = Collections.singleton(id);
+                ScopedIdentifier<IdentifiedObject> key = new 
ScopedIdentifier<>(getInterface(), identifier.toString());
+                key.store(IdentifiedObject.class, this, 
AbstractIdentifiedObject.class, "setIdentifier");
+                if (key != (key = key.rename(identifier.code))) {
+                    key.store(IdentifiedObject.class, this, null, null);       
 // Shorter form without codespace.
+                }
             }
         }
     }
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
index cc2600eaa3..074f173e93 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java
@@ -55,19 +55,16 @@ import org.opengis.metadata.extent.GeographicBoundingBox;
 import org.apache.sis.measure.Units;
 import org.apache.sis.geometry.Envelopes;
 import org.apache.sis.geometry.GeneralEnvelope;
+import org.apache.sis.system.Modules;
+import org.apache.sis.system.Loggers;
+import org.apache.sis.xml.bind.Context;
+import org.apache.sis.xml.bind.ScopedIdentifier;
 import org.apache.sis.referencing.util.AxisDirections;
 import org.apache.sis.referencing.util.EllipsoidalHeightCombiner;
 import org.apache.sis.referencing.util.PositionalAccuracyConstant;
 import org.apache.sis.referencing.util.ReferencingUtilities;
 import org.apache.sis.referencing.util.DefinitionVerifier;
 import org.apache.sis.referencing.internal.Resources;
-import org.apache.sis.system.Modules;
-import org.apache.sis.system.Loggers;
-import org.apache.sis.util.OptionalCandidate;
-import org.apache.sis.util.ArgumentChecks;
-import org.apache.sis.util.Utilities;
-import org.apache.sis.util.Static;
-import org.apache.sis.util.internal.Numerics;
 import org.apache.sis.referencing.cs.AxisFilter;
 import org.apache.sis.referencing.cs.CoordinateSystems;
 import org.apache.sis.referencing.cs.DefaultVerticalCS;
@@ -84,6 +81,11 @@ import 
org.apache.sis.referencing.factory.GeodeticObjectFactory;
 import org.apache.sis.referencing.factory.UnavailableFactoryException;
 import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox;
 import org.apache.sis.metadata.iso.extent.Extents;
+import org.apache.sis.util.ArgumentChecks;
+import org.apache.sis.util.OptionalCandidate;
+import org.apache.sis.util.Static;
+import org.apache.sis.util.Utilities;
+import org.apache.sis.util.internal.Numerics;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.logging.Logging;
 
@@ -200,6 +202,15 @@ public final class CRS extends Static {
      * If the EPSG geodetic dataset has been used, the {@linkplain 
NamedIdentifier#getAuthority() authority} title
      * will be something like <q>EPSG geodetic dataset</q>, otherwise it will 
be <q>Subset of EPSG</q>.
      *
+     * <h4>Extended set of codes</h4>
+     * If this method is invoked during the parsing of a GML document,
+     * then the set of known codes temporarily includes the {@code 
<gml:identifier>} values
+     * of all {@link CoordinateReferenceSystem} definitions which have been 
parsed before this method call.
+     * Those codes are local to the document being parsed (many documents can 
be parsed concurrently without conflict),
+     * and are discarded after the parsing is completed (e.g., on {@link 
org.apache.sis.xml.XML#unmarshal(String)} return).
+     * This feature allows embedded or linked data to references a CRS 
definition
+     * in the same file or a file included by an {@code xlink:href} attribute.
+     *
      * <h4>URI forms</h4>
      * This method accepts also the URN and URL syntaxes.
      * For example, the following codes are considered equivalent to {@code 
"EPSG:4326"}:
@@ -251,9 +262,24 @@ public final class CRS extends Static {
     {
         ArgumentChecks.ensureNonNull("code", code);
         try {
-            return 
AuthorityFactories.ALL.createCoordinateReferenceSystem(code);
-        } catch (UnavailableFactoryException e) {
-            return 
AuthorityFactories.fallback(e).createCoordinateReferenceSystem(code);
+            /*
+             * Gives precedence to the database for consistency reasons.
+             * The GML definitions are checked only in last resort.
+             */
+            try {
+                return 
AuthorityFactories.ALL.createCoordinateReferenceSystem(code);
+            } catch (UnavailableFactoryException e) {
+                return 
AuthorityFactories.fallback(e).createCoordinateReferenceSystem(code);
+            }
+        } catch (NoSuchAuthorityCodeException e) {
+            final Context context = Context.current();
+            if (context != null) {
+                var crs = new 
ScopedIdentifier<>(CoordinateReferenceSystem.class, code).get(context);
+                if (crs != null) {
+                    return crs;
+                }
+            }
+            throw e;
         }
     }
 
diff --git 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/xml/bind/referencing/Code.java
 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/xml/bind/referencing/Code.java
index 9681ae9b94..8dd7e0461d 100644
--- 
a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/xml/bind/referencing/Code.java
+++ 
b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/xml/bind/referencing/Code.java
@@ -25,9 +25,8 @@ import org.apache.sis.util.internal.Constants;
 import org.apache.sis.util.internal.DefinitionURI;
 import org.apache.sis.metadata.internal.NameMeaning;
 import org.apache.sis.metadata.internal.Identifiers;
-import org.apache.sis.referencing.NamedIdentifier;
 import org.apache.sis.metadata.iso.citation.Citations;
-import static org.apache.sis.metadata.iso.citation.Citations.toCodeSpace;
+import org.apache.sis.referencing.NamedIdentifier;
 
 
 /**
@@ -236,7 +235,7 @@ public final class Code {
                                 }
                             }
                         } else {
-                            code.codeSpace = toCodeSpace(authority);
+                            code.codeSpace = Citations.toCodeSpace(authority);
                         }
                         code.code = urn;
                         return code;
@@ -247,4 +246,27 @@ public final class Code {
         }
         return null;
     }
+
+    /**
+     * Returns a string representation of the code in the form {@code 
codeSpace:code}.
+     * If the {@link #codeSpace} or the {@link #code} is absent, that part is 
omitted.
+     * If the code seems already qualified, e.g., a code in URN or HTTP name 
space,
+     * then the code space is also omitted. The intent for the latter 
condition is to
+     * avoid spurious components as in {@code 
"IOGP:urn:ogc:def:parameter:EPSG::8801"}.
+     *
+     * <p>This string representation is used as keys in a map of identified 
objects.</p>
+     *
+     * @return {@code codeSpace:code}.
+     *
+     * @see org.apache.sis.xml.bind.ScopedIdentifier
+     */
+    @Override
+    public String toString() {
+        if (code == null) return codeSpace;
+        if (codeSpace == null || DefinitionURI.isAbsolute(code)) {
+            // Above condition may be refined in any future version.
+            return code;
+        }
+        return codeSpace + DefinitionURI.SEPARATOR + code;
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/parameter/ParameterMarshallingTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/parameter/ParameterMarshallingTest.java
index 9c9420606a..81227cc0fe 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/parameter/ParameterMarshallingTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/parameter/ParameterMarshallingTest.java
@@ -32,16 +32,19 @@ import org.opengis.parameter.GeneralParameterDescriptor;
 import org.apache.sis.measure.Units;
 import org.apache.sis.measure.Range;
 import org.apache.sis.measure.MeasurementRange;
+import org.apache.sis.system.Loggers;
 import org.apache.sis.xml.Namespaces;
 import org.apache.sis.xml.XML;
 
 // Test dependencies
+import org.junit.Rule;
 import org.junit.Test;
-import static org.junit.Assert.*;
+import org.junit.After;
+import static org.junit.jupiter.api.Assertions.*;
 import org.opengis.test.Validators;
-import static org.opengis.test.Assert.assertInstanceOf;
 import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.DependsOnMethod;
+import org.apache.sis.test.LoggingWatcher;
 import org.apache.sis.xml.test.TestCase;
 import static org.apache.sis.metadata.Assertions.assertXmlEquals;
 import static org.apache.sis.referencing.Assertions.assertAliasTipEquals;
@@ -58,6 +61,21 @@ import static 
org.apache.sis.referencing.Assertions.assertEpsgNameAndIdentifierE
     DefaultParameterValueGroupTest.class
 })
 public final class ParameterMarshallingTest extends TestCase {
+    /**
+     * A JUnit {@link Rule} for listening to log events. This field is public 
because JUnit requires us to
+     * do so, but should be considered as an implementation details (it should 
have been a private field).
+     */
+    @Rule
+    public final LoggingWatcher loggings = new LoggingWatcher(Loggers.XML);
+
+    /**
+     * Verifies that no unexpected warning has been emitted in any test 
defined in this class.
+     */
+    @After
+    public void assertNoUnexpectedLog() {
+        loggings.assertNoUnexpectedLog();
+    }
+
     /**
      * Creates a new test case.
      */
@@ -114,23 +132,25 @@ public final class ParameterMarshallingTest extends 
TestCase {
         assertXmlEquals(expected, xml, "xmlns:*");
         final DefaultParameterValue<?> r = (DefaultParameterValue<?>) 
XML.unmarshal(xml);
         if (!Objects.deepEquals(parameter.getValue(), r.getValue())) {
-            // If we enter in this block, then the line below should always 
fail.
-            // But we use this assertion for getting a better error message.
-            assertEquals("value", parameter.getValue(), r.getValue());
+            /*
+             * If we enter in this block, then the line below should always 
fail.
+             * But we use this assertion for getting a better error message.
+             */
+            assertEquals(parameter.getValue(), r.getValue(), "value");
         }
-        assertEquals("unit", parameter.getUnit(), r.getUnit());
+        assertEquals(parameter.getUnit(), r.getUnit(), "unit");
         /*
          * Verify the descriptor, especially the 'valueClass' property. That 
property is not part of GML,
          * so Apache SIS has to rely on some tricks for finding this 
information (see CC_OperationParameter).
          */
         final ParameterDescriptor<?> reference = parameter.getDescriptor();
         final ParameterDescriptor<?> descriptor = r.getDescriptor();
-        assertNotNull("descriptor",                                            
 descriptor);
-        assertEquals ("descriptor.name",          reference.getName(),         
 descriptor.getName());
-        assertEquals ("descriptor.unit",          reference.getUnit(),         
 descriptor.getUnit());
-        assertEquals ("descriptor.valueClass",    reference.getValueClass(),   
 descriptor.getValueClass());
-        assertEquals ("descriptor.minimumOccurs", 
reference.getMinimumOccurs(), descriptor.getMinimumOccurs());
-        assertEquals ("descriptor.maximumOccurs", 
reference.getMaximumOccurs(), descriptor.getMaximumOccurs());
+        assertNotNull(                              descriptor,                
    "descriptor");
+        assertEquals (reference.getName(),          descriptor.getName(),      
    "descriptor.name");
+        assertEquals (reference.getUnit(),          descriptor.getUnit(),      
    "descriptor.unit");
+        assertEquals (reference.getValueClass(),    
descriptor.getValueClass(),    "descriptor.valueClass");
+        assertEquals (reference.getMinimumOccurs(), 
descriptor.getMinimumOccurs(), "descriptor.minimumOccurs");
+        assertEquals (reference.getMaximumOccurs(), 
descriptor.getMaximumOccurs(), "descriptor.maximumOccurs");
         Validators.validate(r);
     }
 
@@ -144,24 +164,26 @@ public final class ParameterMarshallingTest extends 
TestCase {
         final DefaultParameterDescriptor<Double> descriptor = new 
DefaultParameterDescriptor<>(
                 Map.of(DefaultParameterDescriptor.NAME_KEY, "A descriptor"),
                 0, 1, Double.class, null, null, null);
+
         final String xml = XML.marshal(descriptor);
         assertXmlEquals(
                 "<gml:OperationParameter xmlns:gml=\"" + Namespaces.GML + 
"\">\n"
               + "  <gml:name>A descriptor</gml:name>\n"
               + "  <gml:minimumOccurs>0</gml:minimumOccurs>\n"
               + "</gml:OperationParameter>", xml, "xmlns:*");
-        final DefaultParameterDescriptor<?> r = 
(DefaultParameterDescriptor<?>) XML.unmarshal(xml);
-        assertEquals("name", "A descriptor", r.getName().getCode());
-        assertEquals("minimumOccurs", 0, r.getMinimumOccurs());
-        assertEquals("maximumOccurs", 1, r.getMaximumOccurs());
+
+        final var r = (DefaultParameterDescriptor<?>) XML.unmarshal(xml);
+        assertEquals("A descriptor", r.getName().getCode(), "name");
+        assertEquals(0, r.getMinimumOccurs(), "minimumOccurs");
+        assertEquals(1, r.getMaximumOccurs(), "maximumOccurs");
         /*
          * A DefaultParameterDescriptor with null 'valueClass' is illegal, but 
there is no way we can guess
          * this information if the <gml:OperationParameter> element was not a 
child of <gml:ParameterValue>.
          * The current implementation leaves 'valueClass' to null despite 
being illegal. This behavior may
          * change in any future Apache SIS version.
          */
-        assertNull("valueDomain", r.getValueDomain());
-        assertNull("valueClass",  r.getValueClass());               // May 
change in any future SIS release.
+        assertNull(r.getValueDomain(), "valueDomain");
+        assertNull(r.getValueClass(),  "valueClass");               // May 
change in any future SIS release.
     }
 
     /**
@@ -346,7 +368,7 @@ public final class ParameterMarshallingTest extends 
TestCase {
         verifyDescriptor(8805, "Scale factor at natural origin", 
"scale_factor",       true,  it.next());
         verifyDescriptor(8806, "False easting",                  
"false_easting",      false, it.next());
         verifyDescriptor(8807, "False northing",                 
"false_northing",     false, it.next());
-        assertFalse("Unexpected parameter.", it.hasNext());
+        assertFalse(it.hasNext());
     }
 
     /**
@@ -363,8 +385,8 @@ public final class ParameterMarshallingTest extends 
TestCase {
     {
         assertEpsgNameAndIdentifierEqual(name, code, descriptor);
         assertAliasTipEquals(alias, descriptor);
-        assertEquals("maximumOccurs", 1, descriptor.getMaximumOccurs());
-        assertEquals("minimumOccurs", required ? 1 : 0, 
descriptor.getMinimumOccurs());
+        assertEquals(1, descriptor.getMaximumOccurs(), "maximumOccurs");
+        assertEquals(required ? 1 : 0, descriptor.getMinimumOccurs(), 
"minimumOccurs");
     }
 
     /**
@@ -382,15 +404,15 @@ public final class ParameterMarshallingTest extends 
TestCase {
             final double value, final Unit<?> unit, final 
GeneralParameterDescriptor descriptor,
             final GeneralParameterValue parameter)
     {
-        assertInstanceOf(name, ParameterValue.class, parameter);
+        assertInstanceOf(ParameterValue.class, parameter, name);
         final ParameterValue<?> p = (ParameterValue<?>) parameter;
         final ParameterDescriptor<?> d = p.getDescriptor();
         verifyDescriptor(code, name, alias, true, d);
-        assertSame  ("descriptor", descriptor,   d);
-        assertEquals("value",      value,        p.doubleValue(), STRICT);
-        assertEquals("unit",       unit,         p.getUnit());
-        assertEquals("valueClass", Double.class, d.getValueClass());
-        assertEquals("unit",       unit,         d.getUnit());
+        assertSame  (descriptor,   d,                 "descriptor");
+        assertEquals(value,        p.doubleValue(),   "value");
+        assertEquals(unit,         p.getUnit(),       "unit");
+        assertEquals(Double.class, d.getValueClass(), "valueClass");
+        assertEquals(unit,         d.getUnit(),       "unit");
     }
 
     /**
@@ -429,6 +451,9 @@ public final class ParameterMarshallingTest extends 
TestCase {
     @DependsOnMethod("testValueGroupUnmarshalling")
     public void testDuplicatedParametersUnmarshalling() throws JAXBException {
         testValueGroupUnmarshalling(TestFile.DUPLICATED);
+        loggings.assertNextLogContains("EPSG::8801");
+        loggings.assertNextLogContains("EPSG::8802");
+        loggings.assertNextLogContains("EPSG::8805");
     }
 
     /**
@@ -442,6 +467,6 @@ public final class ParameterMarshallingTest extends 
TestCase {
         verifyParameter(8801, "Latitude of natural origin",     
"latitude_of_origin", 40, Units.DEGREE, itd.next(), it.next());
         verifyParameter(8802, "Longitude of natural origin",    
"central_meridian",  -60, Units.DEGREE, itd.next(), it.next());
         verifyParameter(8805, "Scale factor at natural origin", 
"scale_factor",        1, Units.UNITY,    itd.next(), it.next());
-        assertFalse("Unexpected parameter.", it.hasNext());
+        assertFalse(it.hasNext());
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/SingleOperationMarshallingTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/SingleOperationMarshallingTest.java
index b84fece8c9..bc07a02972 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/SingleOperationMarshallingTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/SingleOperationMarshallingTest.java
@@ -16,7 +16,6 @@
  */
 package org.apache.sis.referencing.operation;
 
-import java.util.Map;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.io.InputStream;
@@ -32,23 +31,26 @@ import 
org.opengis.referencing.crs.CoordinateReferenceSystem;
 import org.opengis.referencing.crs.GeodeticCRS;
 import org.opengis.referencing.operation.MathTransform;
 import org.opengis.referencing.operation.OperationMethod;
-import org.apache.sis.measure.Units;
-import org.apache.sis.parameter.ParameterBuilder;
 import org.apache.sis.referencing.operation.provider.Mercator1SP;
-import org.apache.sis.xml.Namespaces;
-import org.apache.sis.xml.XML;
 import org.apache.sis.referencing.operation.transform.LinearTransform;
 import org.apache.sis.referencing.operation.matrix.Matrix3;
+import org.apache.sis.parameter.ParameterBuilder;
+import org.apache.sis.measure.Units;
+import org.apache.sis.system.Loggers;
+import org.apache.sis.xml.Namespaces;
+import org.apache.sis.xml.XML;
 import static org.apache.sis.metadata.iso.citation.Citations.EPSG;
 
 // Test dependencies
+import org.junit.Rule;
 import org.junit.Test;
-import static org.junit.Assert.*;
+import org.junit.After;
+import static org.junit.jupiter.api.Assertions.*;
 import org.opengis.test.Validators;
-import static org.opengis.test.Assert.assertInstanceOf;
 import org.apache.sis.xml.bind.referencing.CC_OperationParameterGroupTest;
 import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.DependsOnMethod;
+import org.apache.sis.test.LoggingWatcher;
 import org.apache.sis.xml.test.TestCase;
 import static org.apache.sis.test.TestUtilities.getSingleton;
 import static org.apache.sis.metadata.Assertions.assertXmlEquals;
@@ -69,6 +71,21 @@ import static org.opengis.test.Assert.assertMatrixEquals;
     org.apache.sis.parameter.ParameterMarshallingTest.class
 })
 public final class SingleOperationMarshallingTest extends TestCase {
+    /**
+     * A JUnit {@link Rule} for listening to log events. This field is public 
because JUnit requires us to
+     * do so, but should be considered as an implementation details (it should 
have been a private field).
+     */
+    @Rule
+    public final LoggingWatcher loggings = new LoggingWatcher(Loggers.XML);
+
+    /**
+     * Verifies that no unexpected warning has been emitted in any test 
defined in this class.
+     */
+    @After
+    public void assertNoUnexpectedLog() {
+        loggings.assertNoUnexpectedLog();
+    }
+
     /**
      * Creates a new test case.
      */
@@ -91,7 +108,7 @@ public final class SingleOperationMarshallingTest extends 
TestCase {
      * Creates the test operation method.
      */
     private static DefaultOperationMethod createMercatorMethod() {
-        final ParameterBuilder builder = new ParameterBuilder();
+        final var builder = new ParameterBuilder();
         builder.setCodeSpace(EPSG, "EPSG").setRequired(true);
         ParameterDescriptor<?>[] parameters = {
             builder.addIdentifier("8801").addName("Latitude of natural origin" 
).create(0, Units.DEGREE),
@@ -100,7 +117,7 @@ public final class SingleOperationMarshallingTest extends 
TestCase {
         };
         builder.addName(null, "Mercator (1SP)");
         final ParameterDescriptorGroup descriptor = 
builder.createGroup(parameters);
-        final Map<String,Object> properties = new HashMap<>(4);
+        final var properties = new HashMap<String,Object>(4);
         properties.put(DefaultOperationMethod.NAME_KEY, descriptor.getName());
         properties.put(DefaultOperationMethod.FORMULA_KEY, new 
DefaultFormula("See EPSG guide."));
         return new DefaultOperationMethod(properties, descriptor);
@@ -131,7 +148,7 @@ public final class SingleOperationMarshallingTest extends 
TestCase {
                         "  </gml:parameter>\n" +
                         "</gml:OperationMethod>", xml, "xmlns:*");
 
-        final OperationMethod method = (OperationMethod) XML.unmarshal(xml);
+        final var method = (OperationMethod) XML.unmarshal(xml);
         verifyMethod(method);
         Validators.validate(method);
     }
@@ -141,13 +158,13 @@ public final class SingleOperationMarshallingTest extends 
TestCase {
      */
     private static void verifyMethod(final OperationMethod method) {
         assertIdentifierEquals("name", null, null, null, "Mercator (1SP)", 
method.getName());
-        assertEquals("formula", "See EPSG guide.", 
method.getFormula().getFormula().toString());
+        assertEquals("See EPSG guide.", 
method.getFormula().getFormula().toString(), "formula");
         final ParameterDescriptorGroup parameters = method.getParameters();
-        assertEquals("parameters.name", "Mercator (1SP)", 
parameters.getName().getCode());
+        assertEquals("Mercator (1SP)", parameters.getName().getCode(), 
"parameters.name");
         final Iterator<GeneralParameterDescriptor> it = 
parameters.descriptors().iterator();
         
CC_OperationParameterGroupTest.verifyMethodParameter(Mercator1SP.LATITUDE_OF_ORIGIN,
  (ParameterDescriptor<?>) it.next());
         
CC_OperationParameterGroupTest.verifyMethodParameter(Mercator1SP.LONGITUDE_OF_ORIGIN,
 (ParameterDescriptor<?>) it.next());
-        assertFalse("Unexpected parameter.", it.hasNext());
+        assertFalse(it.hasNext());
     }
 
     /**
@@ -159,36 +176,41 @@ public final class SingleOperationMarshallingTest extends 
TestCase {
     @DependsOnMethod("testOperationMethod")
     public void testConversionUnmarshalling() throws JAXBException {
         final DefaultConversion c = unmarshalFile(DefaultConversion.class, 
openTestFile(false));
-        assertEquals("name", "World Mercator", c.getName().getCode());
-        assertEquals("identifier", "3395", 
getSingleton(c.getIdentifiers()).getCode());
-        assertEquals("scope", "Very small scale mapping.", 
String.valueOf(c.getScope()));
-        assertNull  ("operationVersion", c.getOperationVersion());
+        assertEquals("World Mercator", c.getName().getCode(), "name");
+        assertEquals("3395", getSingleton(c.getIdentifiers()).getCode(), 
"identifier");
+        assertEquals("Very small scale mapping.", 
String.valueOf(c.getScope()), "scope");
+        assertNull  (c.getOperationVersion(), "operationVersion");
 
-        final GeographicBoundingBox e = (GeographicBoundingBox) 
getSingleton(c.getDomainOfValidity().getGeographicElements());
-        assertEquals("eastBoundLongitude", +180, e.getEastBoundLongitude(), 
STRICT);
-        assertEquals("westBoundLongitude", -180, e.getWestBoundLongitude(), 
STRICT);
-        assertEquals("northBoundLatitude",   84, e.getNorthBoundLatitude(), 
STRICT);
-        assertEquals("southBoundLatitude",  -80, e.getSouthBoundLatitude(), 
STRICT);
+        final var e = (GeographicBoundingBox) 
getSingleton(c.getDomainOfValidity().getGeographicElements());
+        assertEquals(+180, e.getEastBoundLongitude(), "eastBoundLongitude");
+        assertEquals(-180, e.getWestBoundLongitude(), "westBoundLongitude");
+        assertEquals(  84, e.getNorthBoundLatitude(), "northBoundLatitude");
+        assertEquals( -80, e.getSouthBoundLatitude(), "southBoundLatitude");
 
         // This is a defining conversion, so we do not expect CRS.
-        assertNull("sourceCRS",        c.getSourceCRS());
-        assertNull("targetCRS",        c.getTargetCRS());
-        assertNull("interpolationCRS", c.getInterpolationCRS());
-        assertNull("mathTransform",    c.getMathTransform());
+        assertNull(c.getSourceCRS(),        "sourceCRS");
+        assertNull(c.getTargetCRS(),        "targetCRS");
+        assertNull(c.getInterpolationCRS(), "interpolationCRS");
+        assertNull(c.getMathTransform(),    "mathTransform");
 
         // The most difficult part.
         final OperationMethod method = c.getMethod();
-        assertNotNull("method", method);
+        assertNotNull(method, "method");
         verifyMethod(method);
 
         final ParameterValueGroup parameters = c.getParameterValues();
-        assertNotNull("parameters", parameters);
+        assertNotNull(parameters, "parameters");
         final Iterator<GeneralParameterValue> it = 
parameters.values().iterator();
         verifyParameter(method, parameters,  -0.0, (ParameterValue<?>) 
it.next());
         verifyParameter(method, parameters, -90.0, (ParameterValue<?>) 
it.next());
-        assertFalse("Unexpected parameter.", it.hasNext());
-
+        assertFalse(it.hasNext());
+        /*
+         * Validate object, then discard warnings caused by duplicated 
identifiers.
+         * Those duplications are intentional, see comment in `Conversion.xml`.
+         */
         Validators.validate(c);
+        loggings.assertNextLogContains("EPSG::8801");
+        loggings.assertNextLogContains("EPSG::8802");
     }
 
     /**
@@ -205,9 +227,9 @@ public final class SingleOperationMarshallingTest extends 
TestCase {
     {
         final ParameterDescriptor<?> descriptor = parameter.getDescriptor();
         final String name = descriptor.getName().getCode();
-        assertSame("parameterValues.descriptor", descriptor,  
group.getDescriptor().descriptor(name));
-        assertSame("method.descriptor",          descriptor, 
method.getParameters().descriptor(name));
-        assertEquals("value", expectedValue, parameter.doubleValue(), STRICT);
+        assertSame(descriptor,  group.getDescriptor().descriptor(name), 
"parameterValues.descriptor");
+        assertSame(descriptor, method.getParameters().descriptor(name), 
"method.descriptor");
+        assertEquals(expectedValue, parameter.doubleValue(), "value");
     }
 
     /**
@@ -219,50 +241,54 @@ public final class SingleOperationMarshallingTest extends 
TestCase {
     @DependsOnMethod("testConversionUnmarshalling")
     public void testTransformationUnmarshalling() throws JAXBException {
         final DefaultTransformation c = 
unmarshalFile(DefaultTransformation.class, openTestFile(true));
-        assertEquals("name",             "NTF (Paris) to NTF (1)",    
c.getName().getCode());
-        assertEquals("identifier",       "1763",                      
getSingleton(c.getIdentifiers()).getCode());
-        assertEquals("scope",            "Change of prime meridian.", 
String.valueOf(c.getScope()));
-        assertEquals("operationVersion", "IGN-Fra",                   
c.getOperationVersion());
+        assertEquals("NTF (Paris) to NTF (1)", c.getName().getCode(), "name");
+        assertEquals("1763", getSingleton(c.getIdentifiers()).getCode(), 
"identifier");
+        assertEquals("Change of prime meridian.", 
String.valueOf(c.getScope()), "scope");
+        assertEquals("IGN-Fra", c.getOperationVersion(), "operationVersion");
 
         final OperationMethod method = c.getMethod();
-        assertNotNull("method", method);
-        assertEquals ("method.name", "Longitude rotation", 
method.getName().getCode());
-        assertEquals ("method.identifier", "9601", 
getSingleton(method.getIdentifiers()).getCode());
-        assertEquals ("method.formula", "Target_longitude = Source_longitude + 
longitude_offset.", method.getFormula().getFormula().toString());
+        assertNotNull(method, "method");
+        assertEquals("Longitude rotation", method.getName().getCode(), 
"method.name");
+        assertEquals("9601", getSingleton(method.getIdentifiers()).getCode(), 
"method.identifier");
+        assertEquals("Target_longitude = Source_longitude + 
longitude_offset.", method.getFormula().getFormula().toString(), 
"method.formula");
 
-        final ParameterDescriptor<?> descriptor = (ParameterDescriptor<?>) 
getSingleton(method.getParameters().descriptors());
-        assertEquals("descriptor.name",       "Longitude offset", 
descriptor.getName().getCode());
-        assertEquals("descriptor.identifier", "8602", 
getSingleton(descriptor.getIdentifiers()).getCode());
-        assertEquals("descriptor.valueClass", Double.class, 
descriptor.getValueClass());
+        final var descriptor = (ParameterDescriptor<?>) 
getSingleton(method.getParameters().descriptors());
+        assertEquals("Longitude offset", descriptor.getName().getCode(), 
"descriptor.name");
+        assertEquals("8602", 
getSingleton(descriptor.getIdentifiers()).getCode(), "descriptor.identifier");
+        assertEquals(Double.class, descriptor.getValueClass(), 
"descriptor.valueClass");
 
         final ParameterValueGroup parameters = c.getParameterValues();
-        assertNotNull("parameters", parameters);
-        assertSame("parameters.descriptors", method.getParameters(), 
parameters.getDescriptor());
+        assertNotNull(parameters, "parameters");
+        assertSame(method.getParameters(), parameters.getDescriptor(), 
"parameters.descriptors");
 
-        final ParameterValue<?> parameter = (ParameterValue<?>) 
getSingleton(parameters.values());
-        assertSame  ("parameters.descriptor", descriptor, 
parameter.getDescriptor());
-        assertEquals("parameters.unit",       Units.GRAD, parameter.getUnit());
-        assertEquals("parameters.value",      2.5969213,  
parameter.getValue());
+        final var parameter = (ParameterValue<?>) 
getSingleton(parameters.values());
+        assertSame  (descriptor, parameter.getDescriptor(), 
"parameters.descriptor");
+        assertEquals(Units.GRAD, parameter.getUnit(),       "parameters.unit");
+        assertEquals(2.5969213,  parameter.getValue(),      
"parameters.value");
 
         final CoordinateReferenceSystem sourceCRS = c.getSourceCRS();
-        assertInstanceOf("sourceCRS",            GeodeticCRS.class,  
sourceCRS);
-        assertEquals    ("sourceCRS.name",       "NTF (Paris)",      
sourceCRS.getName().getCode());
-        assertEquals    ("sourceCRS.scope",      "Geodetic survey.", 
sourceCRS.getScope().toString());
-        assertEquals    ("sourceCRS.identifier", "4807",             
getSingleton(sourceCRS.getIdentifiers()).getCode());
+        assertInstanceOf(GeodeticCRS.class, sourceCRS, "sourceCRS");
+        assertEquals("NTF (Paris)", sourceCRS.getName().getCode(), 
"sourceCRS.name");
+        assertEquals("Geodetic survey.", sourceCRS.getScope().toString(), 
"sourceCRS.scope");
+        assertEquals("4807", 
getSingleton(sourceCRS.getIdentifiers()).getCode(), "sourceCRS.identifier");
 
         final CoordinateReferenceSystem targetCRS = c.getTargetCRS();
-        assertInstanceOf("targetCRS",            GeodeticCRS.class,  
targetCRS);
-        assertEquals    ("targetCRS.name",       "NTF",              
targetCRS.getName().getCode());
-        assertEquals    ("targetCRS.scope",      "Geodetic survey.", 
targetCRS.getScope().toString());
-        assertEquals    ("targetCRS.identifier", "4275",             
getSingleton(targetCRS.getIdentifiers()).getCode());
+        assertInstanceOf(GeodeticCRS.class,  targetCRS, "targetCRS");
+        assertEquals("NTF", targetCRS.getName().getCode(), "targetCRS.name");
+        assertEquals("Geodetic survey.", targetCRS.getScope().toString(), 
"targetCRS.scope");
+        assertEquals("4275", 
getSingleton(targetCRS.getIdentifiers()).getCode(), "targetCRS.identifier");
 
         final MathTransform tr = c.getMathTransform();
-        assertInstanceOf("mathTransform", LinearTransform.class, tr);
+        assertInstanceOf(LinearTransform.class, tr, "mathTransform");
         assertMatrixEquals("mathTransform.matrix",
                 new Matrix3(1, 0, 0,
                             0, 1, 2.33722917,
                             0, 0, 1), ((LinearTransform) tr).getMatrix(), 
STRICT);
-
+        /*
+         * Validate object, then discard warnings caused by duplicated 
identifiers.
+         * Those duplications are intentional, see comment in 
`Transformation.xml`.
+         */
         Validators.validate(c);
+        loggings.assertNextLogContains("EPSG::8602");
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/Transformation.xml
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/Transformation.xml
index 780cf53be1..d5acfc6525 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/Transformation.xml
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/Transformation.xml
@@ -69,8 +69,8 @@
           <gml:scope>Topographic mapping.</gml:scope>
           <gml:primeMeridian>
             <gml:PrimeMeridian gml:id="ParisMeridian">
-              <gml:identifier 
codeSpace="IOGP">urn:ogc:def:meridian:EPSG::8901</gml:identifier>
-              <gml:name>Greenwich</gml:name>
+              <gml:identifier 
codeSpace="IOGP">urn:ogc:def:meridian:EPSG::8903</gml:identifier>
+              <gml:name>Paris</gml:name>
               <gml:greenwichLongitude 
uom="urn:ogc:def:uom:EPSG::9105">2.5969213</gml:greenwichLongitude>
             </gml:PrimeMeridian>
           </gml:primeMeridian>
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/DefinitionURI.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/DefinitionURI.java
index 8ec72c4001..e57f627865 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/DefinitionURI.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/DefinitionURI.java
@@ -455,10 +455,10 @@ public final class DefinitionURI {
      * Returns {@code true} if a sub-region of {@code urn} matches the given 
{@code part},
      * ignoring case, leading and trailing whitespaces.
      *
-     * @param  part       the expected part ({@code "urn"}, {@code "ogc"}, 
{@code "def"}, <i>etc.</i>)
-     * @param  urn        the URN for which to test a subregion.
-     * @param  lower      index of the first character in {@code urn} to 
compare, after skipping whitespaces.
-     * @param  upper      index after the last character in {@code urn} to 
compare, ignoring whitespaces.
+     * @param  part   the expected part ({@code "urn"}, {@code "ogc"}, {@code 
"def"}, <i>etc.</i>)
+     * @param  urn    the URN for which to test a subregion.
+     * @param  lower  index of the first character in {@code urn} to compare, 
after skipping whitespaces.
+     * @param  upper  index after the last character in {@code urn} to 
compare, ignoring whitespaces.
      * @return {@code true} if the given sub-region of {@code urn} match the 
given part.
      */
     public static boolean regionMatches(final String part, final String urn, 
int lower, int upper) {
@@ -485,6 +485,20 @@ public final class DefinitionURI {
         return -1;
     }
 
+    /**
+     * Returns {@code true} if the given URI is recognized as an URN or URL.
+     * The details of this check may change in any future Apache SIS version.
+     *
+     * @param  uri  the URI to check.
+     * @return whether the given URI seems to be an URN or URL.
+     */
+    public static boolean isAbsolute(final String uri) {
+        final int s = uri.indexOf(SEPARATOR);
+        if (s <= 0) return false;
+        final String c = CharSequences.trimWhitespaces(uri, 0, s).toString();
+        return c.equalsIgnoreCase("urn") || c.equalsIgnoreCase(Constants.HTTP) 
|| c.equalsIgnoreCase(Constants.HTTPS);
+    }
+
     /**
      * Returns the code part of the given URI, provided that it matches the 
given object type and authority.
      * This method is useful when:
diff --git 
a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/internal/DefinitionURITest.java
 
b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/internal/DefinitionURITest.java
index f361f385d8..668d3957d6 100644
--- 
a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/internal/DefinitionURITest.java
+++ 
b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/internal/DefinitionURITest.java
@@ -192,6 +192,16 @@ public final class DefinitionURITest extends TestCase {
                  + "2=http://www.opengis.net/def/crs/EPSG/9.1/5701";, 
parsed.toString());
     }
 
+    /**
+     * Tests {@link DefinitionURI#isAbsolute(String)}.
+     */
+    @Test
+    public void testIsAbsolute() {
+        assertTrue 
(DefinitionURI.isAbsolute("http://www.opengis.net/def/crs/EPSG/0/4326";));
+        assertTrue (DefinitionURI.isAbsolute("urn:ogc:def:crs:EPSG:8.2:4326"));
+        assertFalse(DefinitionURI.isAbsolute("EPSG:4326"));
+    }
+
     /**
      * Convenience method invoking {@link DefinitionURI#codeOf(String, 
String[], CharSequence)}
      * with a single authority.

Reply via email to