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 f1d35685a5 Resolve external xlinks when parsing a GML document. This 
is a first step, not yet resolving fragment and not yet caching.
f1d35685a5 is described below

commit f1d35685a564f9e333caf11b96353a440b8b5817
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Mon Dec 18 14:55:36 2023 +0100

    Resolve external xlinks when parsing a GML document.
    This is a first step, not yet resolving fragment and not yet caching.
    
    https://issues.apache.org/jira/browse/SIS-387
    https://issues.apache.org/jira/browse/SIS-591
---
 .../main/org/apache/sis/xml/MarshalContext.java    |  15 +-
 .../main/org/apache/sis/xml/MarshallerPool.java    |  47 +++-
 .../main/org/apache/sis/xml/Pooled.java            |  41 ++-
 .../main/org/apache/sis/xml/PooledMarshaller.java  |  53 ++--
 .../main/org/apache/sis/xml/PooledTemplate.java    |   6 +-
 .../org/apache/sis/xml/PooledUnmarshaller.java     |  91 ++++---
 .../main/org/apache/sis/xml/ReferenceResolver.java | 114 +++++++--
 .../main/org/apache/sis/xml/XLink.java             |  13 +-
 .../main/org/apache/sis/xml/bind/Context.java      | 181 ++++++++++---
 .../apache/sis/xml/util/ExternalLinkHandler.java   | 279 +++++++++++++++++++++
 .../main/org/apache/sis/xml/util/URISource.java    |  94 +++++++
 .../metadata/iso/citation/DefaultCitationTest.java |  13 +-
 .../metadata/iso/citation/DefaultContactTest.java  |   2 +-
 .../sis/metadata/xml/2016/UsingExternalXLink.xml   |  35 +++
 .../org/apache/sis/xml/ReferenceResolverMock.java  |   2 +-
 .../org/apache/sis/xml/ReferenceResolverTest.java  |  54 ++++
 .../apache/sis/xml/bind/gco/StringAdapterTest.java |   2 +-
 .../apache/sis/xml/bind/gml/TimePeriodTest.java    |   2 +-
 .../test/org/apache/sis/xml/test/TestCase.java     |  13 +-
 .../org/apache/sis/xml/util/XmlUtilitiesTest.java  |   7 +-
 .../referencing/AbstractIdentifiedObjectTest.java  |   2 +-
 .../sis/util/resources/IndexedResourceBundle.java  |   2 +-
 22 files changed, 904 insertions(+), 164 deletions(-)

diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/MarshalContext.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/MarshalContext.java
index 4f44abdbe6..57b4e22f16 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/MarshalContext.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/MarshalContext.java
@@ -26,7 +26,7 @@ import org.apache.sis.util.Version;
  * Context of a marshalling or unmarshalling process.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.0
+ * @version 1.5
  * @since   0.3
  */
 public abstract class MarshalContext {
@@ -37,7 +37,18 @@ public abstract class MarshalContext {
     }
 
     /**
-     * Returns the locale to use for (un)marshalling, or {@code null} if no 
locale were explicitly specified.
+     * Returns the marshaller pool that produced the marshaller or 
unmarshaller in use.
+     * This pool may be used for creating new (un)marshaller when a document 
contains
+     * {@code xlink:href} to another document.
+     *
+     * @return the marshaller pool that produced the marshaller or 
unmarshaller in use.
+     *
+     * @since 1.5
+     */
+    public abstract MarshallerPool getPool();
+
+    /**
+     * Returns the locale to use for (un)marshalling, or {@code null} if no 
locale was explicitly specified.
      * The locale returned by this method can be used for choosing a language 
in an {@link InternationalString}.
      *
      * <p>This locale may vary in different fragments of the same XML document.
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/MarshallerPool.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/MarshallerPool.java
index 13ebc24588..3914f3ed34 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/MarshallerPool.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/MarshallerPool.java
@@ -25,6 +25,7 @@ import jakarta.xml.bind.JAXBContext;
 import jakarta.xml.bind.JAXBException;
 import jakarta.xml.bind.Marshaller;
 import jakarta.xml.bind.Unmarshaller;
+import org.apache.sis.util.Classes;
 import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.logging.Logging;
@@ -60,7 +61,7 @@ import org.apache.sis.util.internal.Constants;
  * from multiple threads.
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 1.4
+ * @version 1.5
  *
  * @see XML
  * @see <a 
href="http://jaxb.java.net/guide/Performance_and_thread_safety.html";>JAXB 
Performance and thread-safety</a>
@@ -176,17 +177,13 @@ public class MarshallerPool {
      * @param  properties  the properties to be given to the (un)marshaller, 
or {@code null} if none.
      * @throws JAXBException if the marshaller pool cannot be created.
      */
-    @SuppressWarnings({"unchecked", "rawtypes"})          // Generic array 
creation
+    @SuppressWarnings("this-escape")
     public MarshallerPool(final JAXBContext context, final Map<String,?> 
properties) throws JAXBException {
         ArgumentChecks.ensureNonNull("context", context);
-        this.context = context;
-        replacements = ServiceLoader.load(AdapterReplacement.class, 
Reflect.getContextClassLoader());
-        implementation = Implementation.detect(context);
-        /*
-         * Prepares a copy of the property map (if any), then removes the
-         * properties which are handled especially by this constructor.
-         */
-        template           = new PooledTemplate(properties, implementation);
+        this.context       = context;
+        replacements       = ServiceLoader.load(AdapterReplacement.class, 
Reflect.getContextClassLoader());
+        implementation     = Implementation.detect(context);
+        template           = new PooledTemplate(this, properties, 
implementation);
         marshallers        = new ConcurrentLinkedDeque<>();
         unmarshallers      = new ConcurrentLinkedDeque<>();
         isRemovalScheduled = new AtomicBoolean();
@@ -446,4 +443,34 @@ public class MarshallerPool {
         }
         return unmarshaller;
     }
+
+    /**
+     * {@return a string representation of this pool for debugging purposes}.
+     * The string representation is unspecified and may change in any future
+     * Apache SIS version.
+     *
+     * @since 1.5
+     */
+    @Override
+    public String toString() {
+        final var buffer = new 
StringBuilder(Classes.getShortClassName(this)).append('[');
+        final Context c = Context.current();
+        boolean s = (c != null && c.getPool() == this);
+        if (s) buffer.append("in use");
+        s = appendSize(buffer,   marshallers,   "marshallers", s);
+            appendSize(buffer, unmarshallers, "unmarshallers", s);
+        return buffer.append(']').toString();
+    }
+
+    /**
+     * Appends the size of the marshaller or unmarshaller pool to the given 
buffer.
+     * This is an helper method for {@link #toString()} only.
+     */
+    private static boolean appendSize(final StringBuilder buffer, final 
Deque<?> pool, final String label, boolean s) {
+        int n = pool.size();
+        if (n == 0) return s;
+        if (s) buffer.append(", ");
+        buffer.append(n).append(' ').append(label);
+        return true;
+    }
 }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/Pooled.java 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/Pooled.java
index f3e25a9499..a66af506b2 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/Pooled.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/Pooled.java
@@ -40,6 +40,7 @@ import org.apache.sis.util.internal.Strings;
 import org.apache.sis.xml.bind.Context;
 import org.apache.sis.xml.bind.TypeRegistration;
 import org.apache.sis.xml.util.LegacyNamespaces;
+import org.apache.sis.xml.util.ExternalLinkHandler;
 
 
 /**
@@ -58,6 +59,11 @@ abstract class Pooled {
      */
     private static final String[] SCHEMA_KEYS = {"cat", "gmd", "gmi", "gml"};
 
+    /**
+     * The pool that produced this marshaller or unmarshaller.
+     */
+    private final MarshallerPool pool;
+
     /**
      * The initial state of the (un)marshaller. Will be filled only as needed,
      * often with null values (which must be supported by the map 
implementation).
@@ -71,7 +77,7 @@ abstract class Pooled {
      *
      * This map is never {@code null}.
      */
-    final Map<Object,Object> initialProperties;
+    final Map<Object,Object> initialProperties = new LinkedHashMap<>();
 
     /**
      * Bit masks for various boolean attributes. This include whatever the 
language codes
@@ -149,9 +155,11 @@ abstract class Pooled {
 
     /**
      * Creates a {@link PooledTemplate}.
+     *
+     * @param pool  the pool that produced this template.
      */
-    Pooled() {
-        initialProperties = new LinkedHashMap<>();
+    Pooled(final MarshallerPool pool) {
+        this.pool = pool;
     }
 
     /**
@@ -161,7 +169,7 @@ abstract class Pooled {
      * @param template the {@link PooledTemplate} from which to get the 
initial values.
      */
     Pooled(final Pooled template) {
-        initialProperties = new LinkedHashMap<>();
+        pool = template.pool;
     }
 
     /**
@@ -512,11 +520,11 @@ abstract class Pooled {
     }
 
     /**
-     * Must be invoked by subclasses before a {@code try} block performing a 
(un)marshalling
-     * operation. Must be followed by a call to {@code finish()} in a {@code 
finally} block.
+     * Must be invoked by subclasses before a {@code try} block performing a 
(un)marshalling operation.
+     * Must be followed by a call to {@code finish()} in a {@code finally} 
block.
      *
      * {@snippet lang="java" :
-     *     Context context = begin();
+     *     Context context = begin(linkHandler);
      *     try {
      *         ...
      *     } finally {
@@ -525,9 +533,22 @@ abstract class Pooled {
      *     }
      *
      * @see Context#finish()
+     *
+     * @param  linkHandler  the document-dependent resolver or relativizer of 
URIs, or {@code null}.
+     */
+    final Context begin(final ExternalLinkHandler linkHandler) {
+        return new Context(bitMasks | specificBitMasks(), pool, locale, 
timezone,
+                           schemas, versionGML, versionMetadata,
+                           linkHandler, resolver, converter, logFilter);
+    }
+
+    /**
+     * {@return a string representation of this (un)marshaller for debugging 
purposes}.
      */
-    final Context begin() {
-        return new Context(bitMasks | specificBitMasks(), locale, timezone,
-                schemas, versionGML, versionMetadata, resolver, converter, 
logFilter);
+    @Override
+    public String toString() {
+        return Strings.toString(getClass(), "baseURI", 
ExternalLinkHandler.getCurrentURI(),
+                "locale", locale, "timezone", timezone,
+                "versionGML", versionGML, "versionMetadata", versionMetadata);
     }
 }
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledMarshaller.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledMarshaller.java
index 1058a9961c..f52cc440b6 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledMarshaller.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledMarshaller.java
@@ -38,6 +38,7 @@ import org.xml.sax.ContentHandler;
 import org.w3c.dom.Node;
 import org.apache.sis.xml.bind.Context;
 import org.apache.sis.xml.bind.UseLegacyMetadata;
+import org.apache.sis.xml.util.ExternalLinkHandler;
 
 
 /**
@@ -152,15 +153,16 @@ final class PooledMarshaller extends Pooled implements 
Marshaller {
      * This method is invoked when the user asked to marshal to a different 
GML or metadata version than the
      * one supported natively by SIS, i.e. when {@link #getTransformVersion()} 
returns a non-null value.
      *
-     * @param object   the object to marshal.
-     * @param output   the writer created by SIS (<b>not</b> the writer given 
by the user).
-     * @param version  identifies the namespace substitutions to perform.
+     * @param object       the object to marshal.
+     * @param output       the writer created by SIS (<b>not</b> the writer 
given by the user).
+     * @param version      identifies the namespace substitutions to perform.
+     * @param linkHandler  the document-dependent creator of relative URIs, or 
{@code null}.
      */
-    private void marshal(Object object, XMLEventWriter output, final 
TransformVersion version)
+    private void marshal(Object object, XMLEventWriter output, final 
TransformVersion version, final ExternalLinkHandler linkHandler)
             throws XMLStreamException, JAXBException
     {
         output = new TransformingWriter(output, version);
-        final Context context = begin();
+        final Context context = begin(linkHandler);
         try {
             marshaller.marshal(object, output);
         } finally {
@@ -175,14 +177,15 @@ final class PooledMarshaller extends Pooled implements 
Marshaller {
     @Override
     public void marshal(Object object, final Result output) throws 
JAXBException {
         object = toImplementation(object);                          // Must be 
call before getTransformVersion()
+        final var linkHandler = new ExternalLinkHandler(output);
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            marshal(object, OutputFactory.createXMLEventWriter(output), 
version);
+            marshal(object, OutputFactory.createXMLEventWriter(output), 
version, linkHandler);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
             // Marshalling to the default GML version.
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 marshaller.marshal(object, output);
             } finally {
@@ -197,14 +200,15 @@ final class PooledMarshaller extends Pooled implements 
Marshaller {
     @Override
     public void marshal(Object object, final OutputStream output) throws 
JAXBException {
         object = toImplementation(object);                          // Must be 
call before getTransformVersion()
+        final var linkHandler = ExternalLinkHandler.forStream(output);
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            marshal(object, OutputFactory.createXMLEventWriter(output, 
getEncoding()), version);
+            marshal(object, OutputFactory.createXMLEventWriter(output, 
getEncoding()), version, linkHandler);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
             // Marshalling to the default GML version.
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 marshaller.marshal(object, output);
             } finally {
@@ -219,16 +223,17 @@ final class PooledMarshaller extends Pooled implements 
Marshaller {
     @Override
     public void marshal(Object object, final File output) throws JAXBException 
{
         object = toImplementation(object);                          // Must be 
call before getTransformVersion()
+        final var linkHandler = new ExternalLinkHandler(output);
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
             try (OutputStream s = new BufferedOutputStream(new 
FileOutputStream(output))) {
-                marshal(object, OutputFactory.createXMLEventWriter(s, 
getEncoding()), version);
+                marshal(object, OutputFactory.createXMLEventWriter(s, 
getEncoding()), version, linkHandler);
             }
         } catch (IOException | XMLStreamException e) {
             throw new JAXBException(e);
         } else {
             // Marshalling to the default GML version.
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 marshaller.marshal(object, output);
             } finally {
@@ -243,14 +248,15 @@ final class PooledMarshaller extends Pooled implements 
Marshaller {
     @Override
     public void marshal(Object object, final Writer output) throws 
JAXBException {
         object = toImplementation(object);                          // Must be 
call before getTransformVersion()
+        final var linkHandler = ExternalLinkHandler.forStream(output);
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            marshal(object, OutputFactory.createXMLEventWriter(output), 
version);
+            marshal(object, OutputFactory.createXMLEventWriter(output), 
version, linkHandler);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
             // Marshalling to the default GML version.
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 marshaller.marshal(object, output);
             } finally {
@@ -265,14 +271,15 @@ final class PooledMarshaller extends Pooled implements 
Marshaller {
     @Override
     public void marshal(Object object, final ContentHandler output) throws 
JAXBException {
         object = toImplementation(object);                          // Must be 
call before getTransformVersion()
+        final ExternalLinkHandler linkHandler = null;               // We 
don't know how to get the base URI.
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            marshal(object, OutputFactory.createXMLEventWriter(output), 
version);
+            marshal(object, OutputFactory.createXMLEventWriter(output), 
version, linkHandler);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
             // Marshalling to the default GML version.
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 marshaller.marshal(object, output);
             } finally {
@@ -287,14 +294,15 @@ final class PooledMarshaller extends Pooled implements 
Marshaller {
     @Override
     public void marshal(Object object, final Node output) throws JAXBException 
{
         object = toImplementation(object);                          // Must be 
call before getTransformVersion()
+        final var linkHandler = new ExternalLinkHandler(output.getBaseURI());
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            marshal(object, OutputFactory.createXMLEventWriter(output), 
version);
+            marshal(object, OutputFactory.createXMLEventWriter(output), 
version, linkHandler);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
             // Marshalling to the default GML version.
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 marshaller.marshal(object, output);
             } finally {
@@ -309,14 +317,15 @@ final class PooledMarshaller extends Pooled implements 
Marshaller {
     @Override
     public void marshal(Object object, final XMLStreamWriter output) throws 
JAXBException {
         object = toImplementation(object);                          // Must be 
call before getTransformVersion()
+        final ExternalLinkHandler linkHandler = null;               // We 
don't know how to get the base URI.
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            marshal(object, OutputFactory.createXMLEventWriter(output), 
version);
+            marshal(object, OutputFactory.createXMLEventWriter(output), 
version, linkHandler);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
             // Marshalling to the default GML version.
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 marshaller.marshal(object, output);
             } finally {
@@ -331,11 +340,12 @@ final class PooledMarshaller extends Pooled implements 
Marshaller {
     @Override
     public void marshal(Object object, XMLEventWriter output) throws 
JAXBException {
         object = toImplementation(object);                          // Must be 
call before getTransformVersion()
+        final ExternalLinkHandler linkHandler = null;               // We 
don't know how to get the base URI.
         final TransformVersion version = getTransformVersion();
         if (version != null) {
             output = new TransformingWriter(output, version);
         }
-        final Context context = begin();
+        final Context context = begin(linkHandler);
         try {
             marshaller.marshal(object, output);
         } finally {
@@ -349,12 +359,13 @@ final class PooledMarshaller extends Pooled implements 
Marshaller {
     @Override
     public Node getNode(Object object) throws JAXBException {
         object = toImplementation(object);                          // Must be 
call before getTransformVersion()
+        final ExternalLinkHandler linkHandler = null;               // We 
don't know how to get the base URI.
         final TransformVersion version = getTransformVersion();
         if (version != null) {
             // This exception is thrown by 
jakarta.xml.bind.helpers.AbstractMarshallerImpl anyway.
             throw new UnsupportedOperationException();
         } else {
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 return marshaller.getNode(object);
             } finally {
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledTemplate.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledTemplate.java
index 7405515561..c0f53b59f8 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledTemplate.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledTemplate.java
@@ -36,10 +36,14 @@ final class PooledTemplate extends Pooled {
     /**
      * Creates a new template.
      *
+     * @param pool            the pool that produced this template.
      * @param properties      the properties to be given to JAXB 
(un)marshallers, or {@code null} if none.
      * @param implementation  the JAXB implementation used.
      */
-    PooledTemplate(final Map<String,?> properties, final Implementation 
implementation) throws PropertyException {
+    PooledTemplate(final MarshallerPool pool, final Map<String,?> properties, 
final Implementation implementation)
+            throws PropertyException
+    {
+        super(pool);
         if (properties != null) {
             for (final Map.Entry<String,?> entry : properties.entrySet()) {
                 final String key = entry.getKey();
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledUnmarshaller.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledUnmarshaller.java
index 34a839f3cc..0f5177e642 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledUnmarshaller.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/PooledUnmarshaller.java
@@ -39,6 +39,7 @@ import jakarta.xml.bind.attachment.AttachmentUnmarshaller;
 import org.w3c.dom.Node;
 import org.xml.sax.InputSource;
 import org.apache.sis.xml.bind.Context;
+import org.apache.sis.xml.util.ExternalLinkHandler;
 
 
 /**
@@ -107,15 +108,16 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      * This method is invoked when we may marshal a different GML or metadata 
version than the one
      * supported natively by SIS, i.e. when {@link #getTransformVersion()} 
returns a non-null value.
      *
-     * @param  input    the reader created by SIS (<b>not</b> the reader given 
by the user).
-     * @param  version  identify the namespace substitutions to perform.
+     * @param  input        the reader created by SIS (<b>not</b> the reader 
given by the user).
+     * @param  version      identify the namespace substitutions to perform.
+     * @param  linkHandler  the document-dependent resolver of relative URIs, 
or {@code null}.
      * @return the unmarshalled object.
      */
-    private Object unmarshal(XMLEventReader input, final TransformVersion 
version)
+    private Object unmarshal(XMLEventReader input, final TransformVersion 
version, final ExternalLinkHandler linkHandler)
             throws XMLStreamException, JAXBException
     {
         input = new TransformingReader(input, version);
-        final Context context = begin();
+        final Context context = begin(linkHandler);
         final Object object;
         try {
             object = unmarshaller.unmarshal(input);
@@ -127,14 +129,16 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
     }
 
     /**
-     * Same as {@link #unmarshal(XMLEventReader, TransformVersion)}, but 
delegating to the unmarshaller
-     * methods returning a JAXB element instead of the one returning the 
object.
+     * Same as {@link #unmarshal(XMLEventReader, TransformVersion, 
ExternalLinkHandler)},
+     * but delegating to the unmarshaller methods returning a JAXB element 
instead
+     * of the one returning the object.
      */
-    private <T> JAXBElement<T> unmarshal(XMLEventReader input, final 
TransformVersion version, final Class<T> declaredType)
+    private <T> JAXBElement<T> unmarshal(XMLEventReader input, final 
TransformVersion version,
+            final ExternalLinkHandler linkHandler, final Class<T> declaredType)
             throws XMLStreamException, JAXBException
     {
         input = new TransformingReader(input, version);
-        final Context context = begin();
+        final Context context = begin(linkHandler);
         final JAXBElement<T> object;
         try {
             object = unmarshaller.unmarshal(input, declaredType);
@@ -150,13 +154,14 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public Object unmarshal(final InputStream input) throws JAXBException {
+        final var linkHandler = ExternalLinkHandler.forStream(input);
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            return unmarshal(InputFactory.createXMLEventReader(input), 
version);
+            return unmarshal(InputFactory.createXMLEventReader(input), 
version, linkHandler);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 return unmarshaller.unmarshal(input);
             } finally {
@@ -170,15 +175,16 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public Object unmarshal(final URL input) throws JAXBException {
+        final var linkHandler = new ExternalLinkHandler(input);
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
             try (InputStream s = input.openStream()) {
-                return unmarshal(InputFactory.createXMLEventReader(s), 
version);
+                return unmarshal(InputFactory.createXMLEventReader(s), 
version, linkHandler);
             }
         } catch (IOException | XMLStreamException e) {
             throw new JAXBException(e);
         } else {
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 return unmarshaller.unmarshal(input);
             } finally {
@@ -192,15 +198,16 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public Object unmarshal(final File input) throws JAXBException {
+        final var linkHandler = new ExternalLinkHandler(input);
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
             try (InputStream s = new BufferedInputStream(new 
FileInputStream(input))) {
-                return unmarshal(InputFactory.createXMLEventReader(s), 
version);
+                return unmarshal(InputFactory.createXMLEventReader(s), 
version, linkHandler);
             }
         } catch (IOException | XMLStreamException e) {
             throw new JAXBException(e);
         } else {
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 return unmarshaller.unmarshal(input);
             } finally {
@@ -214,13 +221,14 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public Object unmarshal(final Reader input) throws JAXBException {
+        final var linkHandler = ExternalLinkHandler.forStream(input);
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            return unmarshal(InputFactory.createXMLEventReader(input), 
version);
+            return unmarshal(InputFactory.createXMLEventReader(input), 
version, linkHandler);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 return unmarshaller.unmarshal(input);
             } finally {
@@ -234,13 +242,14 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public Object unmarshal(final InputSource input) throws JAXBException {
+        final var linkHandler = new ExternalLinkHandler(input.getSystemId());
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            return unmarshal(InputFactory.createXMLEventReader(input), 
version);
+            return unmarshal(InputFactory.createXMLEventReader(input), 
version, linkHandler);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 return unmarshaller.unmarshal(input);
             } finally {
@@ -254,13 +263,14 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public Object unmarshal(final Node input) throws JAXBException {
+        final var linkHandler = new ExternalLinkHandler(input.getBaseURI());
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            return unmarshal(InputFactory.createXMLEventReader(input), 
version);
+            return unmarshal(InputFactory.createXMLEventReader(input), 
version, linkHandler);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 return unmarshaller.unmarshal(input);
             } finally {
@@ -274,13 +284,14 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public <T> JAXBElement<T> unmarshal(final Node input, final Class<T> 
declaredType) throws JAXBException {
+        final var linkHandler = new ExternalLinkHandler(input.getBaseURI());
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            return unmarshal(InputFactory.createXMLEventReader(input), 
version, declaredType);
+            return unmarshal(InputFactory.createXMLEventReader(input), 
version, linkHandler, declaredType);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 return unmarshaller.unmarshal(input, declaredType);
             } finally {
@@ -294,13 +305,14 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public Object unmarshal(final Source input) throws JAXBException {
+        final var linkHandler = new ExternalLinkHandler(input);
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            return unmarshal(InputFactory.createXMLEventReader(input), 
version);
+            return unmarshal(InputFactory.createXMLEventReader(input), 
version, linkHandler);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 return unmarshaller.unmarshal(input);
             } finally {
@@ -314,13 +326,14 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public <T> JAXBElement<T> unmarshal(final Source input, final Class<T> 
declaredType) throws JAXBException {
+        final var linkHandler = new ExternalLinkHandler(input);
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            return unmarshal(InputFactory.createXMLEventReader(input), 
version, declaredType);
+            return unmarshal(InputFactory.createXMLEventReader(input), 
version, linkHandler, declaredType);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 return unmarshaller.unmarshal(input, declaredType);
             } finally {
@@ -334,13 +347,14 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public Object unmarshal(final XMLStreamReader input) throws JAXBException {
+        final var linkHandler = ExternalLinkHandler.create(input);
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            return unmarshal(InputFactory.createXMLEventReader(input), 
version);
+            return unmarshal(InputFactory.createXMLEventReader(input), 
version, linkHandler);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 return unmarshaller.unmarshal(input);
             } finally {
@@ -354,13 +368,14 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public <T> JAXBElement<T> unmarshal(final XMLStreamReader input, final 
Class<T> declaredType) throws JAXBException {
+        final var linkHandler = ExternalLinkHandler.create(input);
         final TransformVersion version = getTransformVersion();
         if (version != null) try {
-            return unmarshal(InputFactory.createXMLEventReader(input), 
version, declaredType);
+            return unmarshal(InputFactory.createXMLEventReader(input), 
version, linkHandler, declaredType);
         } catch (XMLStreamException e) {
             throw new JAXBException(e);
         } else {
-            final Context context = begin();
+            final Context context = begin(linkHandler);
             try {
                 return unmarshaller.unmarshal(input, declaredType);
             } finally {
@@ -374,11 +389,17 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public Object unmarshal(XMLEventReader input) throws JAXBException {
+        final ExternalLinkHandler linkHandler;
+        try {
+            linkHandler = ExternalLinkHandler.create(input);
+        } catch (XMLStreamException e) {
+            throw new JAXBException(e);
+        }
         final TransformVersion version = getTransformVersion();
         if (version != null) {
             input = new TransformingReader(input, version);
         }
-        final Context context = begin();
+        final Context context = begin(linkHandler);
         try {
             return unmarshaller.unmarshal(input);
         } finally {
@@ -391,11 +412,17 @@ final class PooledUnmarshaller extends Pooled implements 
Unmarshaller {
      */
     @Override
     public <T> JAXBElement<T> unmarshal(XMLEventReader input, final Class<T> 
declaredType) throws JAXBException {
+        final ExternalLinkHandler linkHandler;
+        try {
+            linkHandler = ExternalLinkHandler.create(input);
+        } catch (XMLStreamException e) {
+            throw new JAXBException(e);
+        }
         final TransformVersion version = getTransformVersion();
         if (version != null) {
             input = new TransformingReader(input, version);
         }
-        final Context context = begin();
+        final Context context = begin(linkHandler);
         try {
             return unmarshaller.unmarshal(input, declaredType);
         } finally {
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/ReferenceResolver.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/ReferenceResolver.java
index c21d39d300..706b9d4d57 100644
--- 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/ReferenceResolver.java
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/ReferenceResolver.java
@@ -17,15 +17,22 @@
 package org.apache.sis.xml;
 
 import java.net.URI;
+import java.io.IOException;
 import java.util.UUID;
+import java.util.logging.Level;
 import java.lang.reflect.Proxy;
+import javax.xml.transform.Source;
+import jakarta.xml.bind.Unmarshaller;
+import jakarta.xml.bind.JAXBException;
 import org.opengis.metadata.Identifier;
+import org.apache.sis.util.ArgumentChecks;
 import org.apache.sis.util.Emptiable;
 import org.apache.sis.util.LenientComparable;
 import org.apache.sis.util.resources.Errors;
+import org.apache.sis.util.internal.Strings;
 import org.apache.sis.xml.bind.Context;
 import org.apache.sis.xml.bind.gcx.Anchor;
-import static org.apache.sis.util.ArgumentChecks.*;
+import org.apache.sis.xml.util.ExternalLinkHandler;
 
 
 /**
@@ -39,7 +46,7 @@ import static org.apache.sis.util.ArgumentChecks.*;
  * to a unmarshaller.</p>
  *
  * @author  Martin Desruisseaux (Geomatys)
- * @version 0.7
+ * @version 1.5
  * @since   0.3
  */
 public class ReferenceResolver {
@@ -99,22 +106,28 @@ public class ReferenceResolver {
      * @return an object of the given type for the given {@code uuid} 
attribute, or {@code null} if none.
      */
     public <T> T resolve(final MarshalContext context, final Class<T> type, 
final UUID uuid) {
-        ensureNonNull("type", type);
-        ensureNonNull("uuid", uuid);
+        ArgumentChecks.ensureNonNull("type", type);
+        ArgumentChecks.ensureNonNull("uuid", uuid);
         return null;
     }
 
     /**
      * Returns an object of the given type for the given {@code xlink} 
attribute, or {@code null} if none.
-     * The default implementation performs the following lookups:
+     * The default implementation fetches the {@link XLink#getHRef() 
xlink:href} attribute, then:
      *
      * <ul>
-     *   <li>If the {@link XLink#getHRef() xlink:href} attribute is a 
{@linkplain URI#getFragment() URI fragment}
-     *       of the form {@code "#foo"} and if an object of class {@code type} 
with the {@code gml:id="foo"} attribute
-     *       has previously been seen in the same XML document, then that 
object is returned.</li>
-     *   <li>Otherwise returns {@code null}.</li>
+     *   <li>If {@code xlink:href} is null or {@linkplain URI#isOpaque() 
opaque}, returns {@code null}.</li>
+     *   <li>Otherwise, if {@code xlink:href} {@linkplain URI#isAbsolute() is 
absolute} or has a non-empty
+     *       {@linkplain URI#getPath() path}, delegate to {@link 
#resolveExternal(MarshalContext, Source)}.</li>
+     *   <li>Otherwise, if {@code xlink:href} is a {@linkplain 
URI#getFragment() fragment} of the form {@code "#foo"}
+     *       and if an object of class {@code type} with the {@code 
gml:id="foo"} attribute has previously been seen
+     *       in the same XML document, then return that object.</li>
+     *   <li>Otherwise, returns {@code null}.</li>
      * </ul>
      *
+     * If an object is found but is not of the class declared in {@code type},
+     * then this method emits a warning and returns {@code null}.
+     *
      * @param  <T>      the compile-time type of the {@code type} argument.
      * @param  context  context (GML version, locale, <i>etc.</i>) of the 
(un)marshalling process.
      * @param  type     the type of object to be unmarshalled, often as a 
GeoAPI interface.
@@ -122,31 +135,78 @@ public class ReferenceResolver {
      * @return an object of the given type for the given {@code xlink} 
attribute, or {@code null} if none.
      */
     public <T> T resolve(final MarshalContext context, final Class<T> type, 
final XLink link) {
-        ensureNonNull("type",  type);
-        ensureNonNull("xlink", link);
+        ArgumentChecks.ensureNonNull("type",  type);
+        ArgumentChecks.ensureNonNull("xlink", link);
         final URI href = link.getHRef();
-        if (href != null && href.toString().startsWith("#")) {
-            final String id = href.getFragment();
-            final Context c = (context instanceof Context) ? (Context) context 
: Context.current();
-            final Object object = Context.getObjectForID(c, id);
-            if (type.isInstance(object)) {
-                return type.cast(object);
+        if (href == null || href.isOpaque()) {
+            return null;
+        }
+        final Object label, object;
+        final Context c = (context instanceof Context) ? (Context) context : 
Context.current();
+        if (!href.isAbsolute() && Strings.isNullOrEmpty(href.getPath())) {
+            final String id = href.getFragment();       // Taken as the 
`gml:id` value to look for.
+            if (Strings.isNullOrEmpty(id)) {
+                return null;
+            }
+            object = Context.getObjectForID(c, id);
+            label  = id;                // Used if the object is invalid.
+        } else try {
+            final Source source = Context.linkHandler(c).openReader(href);
+            object = (source != null) ? resolveExternal(c, source) : null;
+            label  = href;              // Used if the object is invalid.
+        } catch (Exception e) {
+            Context.warningOccured(c, Level.WARNING, ReferenceResolver.class, 
"resolve",
+                                   e, Errors.class, Errors.Keys.CanNotRead_1, 
href);
+            return null;
+        }
+        if (type.isInstance(object)) {
+            return type.cast(object);
+        } else {
+            final short key;
+            final Object[] args;
+            if (object == null) {
+                key = Errors.Keys.NotABackwardReference_1;
+                args = new Object[] {label.toString()};
             } else {
-                final short key;
-                final Object[] args;
-                if (object == null) {
-                    key = Errors.Keys.NotABackwardReference_1;
-                    args = new Object[] {id};
-                } else {
-                    key = Errors.Keys.UnexpectedTypeForReference_3;
-                    args = new Object[] {id, type, object.getClass()};
-                }
-                Context.warningOccured(c, ReferenceResolver.class, "resolve", 
Errors.class, key, args);
+                key = Errors.Keys.UnexpectedTypeForReference_3;
+                args = new Object[] {label.toString(), type, 
object.getClass()};
             }
+            Context.warningOccured(c, ReferenceResolver.class, "resolve", 
Errors.class, key, args);
         }
         return null;
     }
 
+    /**
+     * Returns an object defined in an external document, or {@code null} if 
none.
+     * This method is invoked automatically by {@link #resolve(MarshalContext, 
Class, XLink)}
+     * when the {@code xlink:href} attribute is absolute or contains the path 
to a document.
+     * The default implementation loads the file from the given source if it 
is not in the cache,
+     * then returns the object identified by the fragment part of the URI.
+     *
+     * <p>The URL of the document to load, if known, should be given by {@link 
Source#getSystemId()}.</p>
+     *
+     * @param  context  context (GML version, locale, <i>etc.</i>) of the 
(un)marshalling process.
+     * @param  source   source of the document specified by the {@code 
xlink:href} attribute value.
+     * @return an object for the given source, or {@code null} if none.
+     * @throws IOException if an error occurred while opening the document.
+     * @throws JAXBException if an error occurred while parsing the document.
+     *
+     * @since 1.5
+     */
+    protected Object resolveExternal(final MarshalContext context, final 
Source source) throws IOException, JAXBException {
+        final MarshallerPool pool = context.getPool();
+        final Unmarshaller m = pool.acquireUnmarshaller();
+        final URI uri = ExternalLinkHandler.ifOnlyURI(source);
+        final Object object;
+        if (uri != null) {
+            object = m.unmarshal(uri.toURL());
+        } else {
+            object = m.unmarshal(source);
+        }
+        pool.recycle(m);
+        return object;
+    }
+
     /**
      * Returns {@code true} if the marshaller can use a {@code 
xlink:href="#id"} reference to the given object
      * instead of writing the full XML element. This method is invoked by the 
marshaller when:
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XLink.java 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XLink.java
index ad9e16575d..15b81353a5 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XLink.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XLink.java
@@ -373,9 +373,9 @@ public class XLink implements Serializable {
     }
 
     /**
-     * Sets the type of link. Any value different than {@link 
org.apache.sis.xml.XLink.Type#AUTO
-     * Type.AUTO} (including {@code null}) will overwrite the value inferred 
automatically by
-     * {@link #getType()}. A {@code AUTO} value will enable automatic type 
detection.
+     * Sets the type of link. Any value different than {@link 
org.apache.sis.xml.XLink.Type#AUTO Type.AUTO}
+     * (including {@code null}) will overwrite the value inferred 
automatically by {@link #getType()}.
+     * An {@code AUTO} value will enable automatic type detection.
      *
      * @param  type  the new type of link, or {@code null} if none.
      */
@@ -600,8 +600,7 @@ public class XLink implements Serializable {
     }
 
     /**
-     * Communicates the desired timing of traversal from the starting resource 
to the ending
-     * resource.
+     * Communicates the desired timing of traversal from the starting resource 
to the ending resource.
      *
      * @author  Martin Desruisseaux (Geomatys)
      * @version 1.4
@@ -634,8 +633,8 @@ public class XLink implements Serializable {
     }
 
     /**
-     * Returns the desired timing of traversal from the starting resource to 
the ending
-     * resource. It's value should be treated as follows:
+     * Returns the desired timing of traversal from the starting resource to 
the ending resource.
+     * It's value should be treated as follows:
      *
      * <ul>
      *   <li><b>onLoad:</b>    traverse to the ending resource immediately on 
loading the starting resource</li>
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 9f714f40f1..b4342d931f 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
@@ -17,12 +17,10 @@
 package org.apache.sis.xml.bind;
 
 import java.util.Map;
-import java.util.Deque;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
 import java.util.Locale;
 import java.util.TimeZone;
-import java.util.LinkedList;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 import java.util.logging.LogRecord;
@@ -35,10 +33,12 @@ import org.apache.sis.util.resources.Messages;
 import org.apache.sis.util.resources.IndexedResourceBundle;
 import org.apache.sis.xml.IdentifierSpace;
 import org.apache.sis.xml.MarshalContext;
+import org.apache.sis.xml.MarshallerPool;
 import org.apache.sis.xml.ValueConverter;
 import org.apache.sis.xml.ReferenceResolver;
 import org.apache.sis.xml.bind.gco.PropertyType;
 import org.apache.sis.xml.util.LegacyNamespaces;
+import org.apache.sis.xml.util.ExternalLinkHandler;
 import org.apache.sis.system.Semaphores;
 import org.apache.sis.system.Loggers;
 
@@ -118,15 +118,20 @@ public final class Context extends MarshalContext {
      */
     public static final Logger LOGGER = Logger.getLogger(Loggers.XML);
 
+    /**
+     * The pool that produced the marshaller or unmarshaller.
+     */
+    private final MarshallerPool pool;
+
     /**
      * Various boolean attributes determines by the above static constants.
      */
     final int bitMasks;
 
     /**
-     * The locale to use for marshalling, or an empty queue if no locale were 
explicitly specified.
+     * The locale to use for marshalling, or {@code null} if no locale was 
explicitly specified.
      */
-    private final Deque<Locale> locales;
+    private final Locale locale;
 
     /**
      * The timezone, or {@code null} if unspecified.
@@ -135,8 +140,8 @@ public final class Context extends MarshalContext {
     private final TimeZone timezone;
 
     /**
-     * The base URL of ISO 19115-3 (or other standards) schemas. The valid 
values
-     * are documented in the {@link org.apache.sis.xml.XML#SCHEMAS} property.
+     * The base URL of ISO 19115-3 (or other standards) schemas.
+     * The valid values are documented in the {@link 
org.apache.sis.xml.XML#SCHEMAS} property.
      */
     private final Map<String,String> schemas;
 
@@ -146,8 +151,17 @@ public final class Context extends MarshalContext {
      */
     private final Version versionGML;
 
+    /**
+     * The {@code XLink} reference resolver for converting relative URL to 
absolute URL.
+     * Contrarily to {@link #resolver}, this instance depends on the document 
being read.
+     * If {@code null}, then {@link ExternalLinkHandler#DEFAULT} is assumed.
+     */
+    private final ExternalLinkHandler linkHandler;
+
     /**
      * The reference resolver currently in use, or {@code null} for {@link 
ReferenceResolver#DEFAULT}.
+     * This class does not necessarily parses XML document. It may returns 
objects from a database.
+     * The same instance may be used for many documents to parse.
      */
     private final ReferenceResolver resolver;
 
@@ -159,6 +173,8 @@ public final class Context extends MarshalContext {
     /**
      * The objects associated to XML identifiers. At marhalling time, this is 
used for avoiding duplicated identifiers
      * in the same XML document. At unmarshalling time, this is used for 
getting a previous object from its identifier.
+     *
+     * @see #getObjectForID(Context, String)
      */
     private final Map<String,Object> identifiers;
 
@@ -207,42 +223,45 @@ public final class Context extends MarshalContext {
      *     }
      *
      * @param  bitMasks         a combination of {@link #MARSHALLING}, {@code 
SUBSTITUTE_*} or other bit masks.
+     * @param  pool             the pool that produced the marshaller or 
unmarshaller.
      * @param  locale           the locale, or {@code null} if unspecified.
      * @param  timezone         the timezone, or {@code null} if unspecified.
      * @param  schemas          the schemas root URL, or {@code null} if none.
      * @param  versionGML       the GML version, or {@code null}.
      * @param  versionMetadata  the metadata version, or {@code null}.
+     * @param  linkHandler      the document-dependent resolver of relative 
URIs, or {@code null}.
      * @param  resolver         the resolver in use.
      * @param  converter        the converter in use.
      * @param  logFilter        the object to inform about warnings.
      */
     @SuppressWarnings("ThisEscapedInObjectConstruction")
-    public Context(int                      bitMasks,
-                   final Locale             locale,
-                   final TimeZone           timezone,
-                   final Map<String,String> schemas,
-                   final Version            versionGML,
-                   final Version            versionMetadata,
-                   final ReferenceResolver  resolver,
-                   final ValueConverter     converter,
-                   final Filter             logFilter)
+    public Context(int                       bitMasks,
+                   final MarshallerPool      pool,
+                   final Locale              locale,
+                   final TimeZone            timezone,
+                   final Map<String,String>  schemas,
+                   final Version             versionGML,
+                   final Version             versionMetadata,
+                   final ExternalLinkHandler linkHandler,
+                   final ReferenceResolver   resolver,
+                   final ValueConverter      converter,
+                   final Filter              logFilter)
     {
         if (versionMetadata != null && 
versionMetadata.compareTo(LegacyNamespaces.VERSION_2014) < 0) {
             bitMasks |= LEGACY_METADATA;
         }
-        this.locales           = new LinkedList<>();
+        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;
         this.identifiers       = new HashMap<>();
         this.identifiedObjects = new IdentityHashMap<>();
-        if (locale != null) {
-            locales.add(locale);
-        }
-        previous = CURRENT.get();
+        this.previous          = CURRENT.get();
         if ((bitMasks & MARSHALLING) != 0) {
             /*
              * Set global semaphore last after our best effort to ensure that 
construction
@@ -258,13 +277,56 @@ public final class Context extends MarshalContext {
     }
 
     /**
-     * Returns the locale to use for marshalling, or {@code null} if no locale 
were explicitly specified.
+     * Creates a new context with different properties than the current 
context.
+     * The old context shall be restored by a call to {@link #pull()} in a 
{@code finally} block.
+     *
+     * @param parent       the context from which to inherit.
+     * @param locale       the locale in the new context.
+     * @param linkHandler  the document-dependent resolver of relative URIs, 
or {@code null}.
+     * @param inline       {@code true} if the context is for reading the same 
document, or
+     *                     {@code false} for reading a separated document.
+     */
+    private Context(final Context parent, final Locale locale, final 
ExternalLinkHandler linkHandler,
+                    final boolean inline)
+    {
+        this.locale      = locale;
+        this.linkHandler = linkHandler;
+        this.pool        = parent.pool;
+        this.timezone    = parent.timezone;
+        this.schemas     = parent.schemas;
+        this.versionGML  = parent.versionGML;
+        this.resolver    = parent.resolver;
+        this.converter   = parent.converter;
+        this.logFilter   = parent.logFilter;
+        this.bitMasks    = parent.bitMasks;
+        if (inline) {
+            identifiers       = parent.identifiers;
+            identifiedObjects = parent.identifiedObjects;
+        } else {
+            identifiers       = new HashMap<>();
+            identifiedObjects = new IdentityHashMap<>();
+        }
+        previous = CURRENT.get();
+    }
+
+    /**
+     * Returns the marshaller pool that produced the marshaller or 
unmarshaller in use.
+     *
+     * @return the pool in the context of current (un)marshalling process.
+     */
+    @Override
+    public final MarshallerPool getPool() {
+        return pool;
+    }
+
+    /**
+     * Returns the locale to use for marshalling, or {@code null} if no locale 
was explicitly specified.
      *
      * @return the locale in the context of current (un)marshalling process.
      */
     @Override
     public final Locale getLocale() {
-        return locales.peekLast();
+        return locale;
     }
 
     /**
@@ -299,15 +361,17 @@ public final class Context extends MarshalContext {
 
 
 
-    
////////////////////////////////////////////////////////////////////////////////////////
-    ////////                                                                   
     ////////
-    ////////    END OF PUBLIC (non-internal) API.                              
     ////////
-    ////////                                                                   
     ////////
-    ////////    Following are internal API. They are provided as static 
methods     ////////
-    ////////    with a Context argument rather than normal member methods      
     ////////
-    ////////    in order to accept null context.                               
     ////////
-    ////////                                                                   
     ////////
-    
////////////////////////////////////////////////////////////////////////////////////////
+    /*
+     ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
+     ┃                                                                        ┃
+     ┃    END OF PUBLIC (non-internal) API.                                   ┃
+     ┃                                                                        ┃
+     ┃    Following are internal API. They are provided as static methods     ┃
+     ┃    with a Context argument rather than normal member methods           ┃
+     ┃    in order to accept null context.                                    ┃
+     ┃                                                                        ┃
+     ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
+     */
 
     /**
      * Returns the context of the XML (un)marshalling currently progressing in 
the current thread,
@@ -341,9 +405,24 @@ public final class Context extends MarshalContext {
         final Context current = current();
         if (current != null) {
             if (locale == null) {
-                locale = current.getLocale();
+                locale = current.locale;
             }
-            current.locales.addLast(locale);
+            CURRENT.set(new Context(current, locale, current.linkHandler, 
true));
+        }
+    }
+
+    /**
+     * Sets the context for reading a separated document.
+     * Because the separated document may be in a different directory,
+     * it uses a different {@link ExternalLinkHandler}.
+     * Caller shall invoke {@link #pull()} in a {@code finally} block.
+     *
+     * @param linkHandler  the document-dependent resolver of relative URIs, 
or {@code null}.
+     */
+    public static void push(final ExternalLinkHandler linkHandler) {
+        final Context current = current();
+        if (current != null) {
+            CURRENT.set(new Context(current, current.locale, linkHandler, 
false));
         }
     }
 
@@ -352,9 +431,14 @@ public final class Context extends MarshalContext {
      * It is not necessary to invoke this method in a {@code finally} block.
      */
     public static void pull() {
-        final Context current = current();
-        if (current != null) {
-            current.locales.removeLast();
+        Context c = current();
+        if (c != null) {
+            c = c.previous;
+            if (c != null) {
+                CURRENT.set(c);
+            } else {
+                CURRENT.remove();       // For more robustness, but should not 
happen.
+            }
         }
     }
 
@@ -493,6 +577,7 @@ public final class Context extends MarshalContext {
 
     /**
      * Returns the object for the given {@code gml:id}, or {@code null} if 
none.
+     * GML identifiers can be referenced by the fragment part of URI in XLinks.
      * This association is valid only for the current XML document.
      *
      * @param  context  the current context, or {@code null} if none.
@@ -527,9 +612,29 @@ public final class Context extends MarshalContext {
         return true;
     }
 
+    /**
+     * Returns the {@code XLink} reference resolver for the current 
marshalling or unmarshalling process.
+     * If no link handler has been explicitly set, then this method returns 
{@link ExternalLinkHandler#DEFAULT}.
+     *
+     * <div class="note"><b>API note:</b>
+     * This method is static for the convenience of performing the check for 
null context.</div>
+     *
+     * @param  context  the current context, or {@code null} if none.
+     * @return the current link handler (never null).
+     */
+    public static ExternalLinkHandler linkHandler(final Context context) {
+        if (context != null) {
+            final ExternalLinkHandler linkHandler = context.linkHandler;
+            if (linkHandler != null) {
+                return linkHandler;
+            }
+        }
+        return ExternalLinkHandler.DEFAULT;
+    }
+
     /**
      * Returns the reference resolver in use for the current marshalling or 
unmarshalling process.
-     * If no resolver were explicitly set, then this method returns {@link 
ReferenceResolver#DEFAULT}.
+     * If no resolver has been explicitly set, then this method returns {@link 
ReferenceResolver#DEFAULT}.
      *
      * <div class="note"><b>API note:</b>
      * This method is static for the convenience of performing the check for 
null context.</div>
@@ -587,7 +692,7 @@ public final class Context extends MarshalContext {
             final Level level, final Class<?> classe, final String method, 
final Throwable exception,
             final Class<? extends IndexedResourceBundle> resources, final 
short key, final Object... arguments)
     {
-        final Locale locale = (context != null) ? context.getLocale() : null;
+        final Locale locale = (context != null) ? context.locale : null;
         final LogRecord record;
         if (resources != null) {
             final IndexedResourceBundle bundle;
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExternalLinkHandler.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExternalLinkHandler.java
new file mode 100644
index 0000000000..65901d938d
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/ExternalLinkHandler.java
@@ -0,0 +1,279 @@
+/*
+ * 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.util;
+
+import java.net.URL;
+import java.net.URI;
+import java.io.File;
+import java.io.InputStream;
+import java.net.URISyntaxException;
+import javax.xml.stream.Location;
+import javax.xml.stream.XMLResolver;
+import javax.xml.stream.XMLInputFactory;
+import javax.xml.stream.XMLEventReader;
+import javax.xml.stream.XMLStreamReader;
+import javax.xml.stream.XMLStreamException;
+import javax.xml.transform.Result;
+import javax.xml.transform.Source;
+import javax.xml.transform.stax.StAXSource;
+import org.apache.sis.util.Debug;
+import org.apache.sis.util.internal.Strings;
+import org.apache.sis.util.resources.Errors;
+import org.apache.sis.xml.ReferenceResolver;
+import org.apache.sis.xml.bind.Context;
+
+
+/**
+ * Resolves relative or absolute {@code xlink:href} attribute as an absolute 
URI.
+ * This class is used for links outside the document being parsed.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public class ExternalLinkHandler {
+    /**
+     * The default resolver used when URIs cannot be resolved.
+     * This resolver lets absolute URIs pass-through, and returns {@code null} 
for all others.
+     */
+    public static final ExternalLinkHandler DEFAULT = new 
ExternalLinkHandler((String) null);
+
+    /**
+     * The base URI as a {@link String}, {@link File}, {@link URL} or {@link 
URI}, or {@code null}.
+     * If the value is not already an URI, then it will be converted to {@link 
URI} when first needed.
+     * If the conversion fails, then this value is set to {@code null} for 
avoiding to try again.
+     *
+     * <p>Note that the URI is a path to the sibling document rather than a 
path to the parent directory.
+     * This is okay, {@link URI#resolve(URI)} appears to behave as intended 
for deriving relative paths.</p>
+     *
+     * @see #resolve(URI)
+     */
+    private Object base;
+
+    /**
+     * Creates a new resolver for documents relative to the document in the 
specified URL.
+     * The given URL can be what StAX, SAX and DOM call {@code systemId}.
+     * According StAX documentation, {@code systemId} value is used to resolve 
relative URIs.
+     * {@link javax.xml.transform.stream.StreamSource} sets it to {@link 
URI#toASCIIString()}.
+     *
+     * @param  sibling  URL to the sibling document, or {@code null} if none.
+     */
+    public ExternalLinkHandler(final String sibling) {
+        base = sibling;
+    }
+
+    /**
+     * Creates a new resolver for documents relative to the document in the 
specified file.
+     *
+     * @param  sibling  path to the sibling document, or {@code null} if none.
+     */
+    public ExternalLinkHandler(final File sibling) {
+        base = sibling;
+    }
+
+    /**
+     * Creates a new resolver for documents relative to the document at the 
specified URL.
+     *
+     * @param  sibling  URL to the sibling document, or {@code null} if none.
+     */
+    public ExternalLinkHandler(final URL sibling) {
+        base = sibling;
+    }
+
+    /**
+     * Creates a new resolver for documents relative to the document read from 
the specified source.
+     *
+     * @param  sibling  source to the sibling document, or {@code null} if 
none.
+     */
+    public ExternalLinkHandler(final Source sibling) {
+        if (sibling instanceof URISource) {
+            base = ((URISource) sibling).source;
+        } else {
+            base = sibling.getSystemId();
+        }
+    }
+
+    /**
+     * Creates a new resolver for documents relative to the document written 
to the specified result.
+     *
+     * @param  sibling  result of the sibling document, or {@code null} if 
none.
+     */
+    public ExternalLinkHandler(final Result sibling) {
+        base = sibling.getSystemId();
+    }
+
+    /**
+     * Resolves the given path as an URI. This method behaves as specified in 
{@link URI#resolve(URI)},
+     * with the URI given at construction-time as the base URI. If the given 
path is relative and there
+     * is no base URI, then the path cannot be resolved and this method 
returns {@code null}.
+     *
+     * @param  path  path to resolve.
+     * @return resolved path, or {@code null} it it cannot be resolved.
+     *
+     * @see URI#resolve(URI)
+     */
+    final URI resolve(final URI path) {
+        final Object b = base;
+valid:  if (b != null) {
+            final URI baseURI;
+            if (b instanceof URI) {         // `instanceof` check of final 
classes are efficient.
+                baseURI = (URI) b;
+            } else {
+                base = null;                // Clear first in case of failure, 
for avoiding to try again later.
+                try {
+                    if (b instanceof String) {
+                        baseURI = new URI((String) b);
+                    } else if (b instanceof URL) {
+                        baseURI = ((URL) b).toURI();
+                    } else if (b instanceof File) {
+                        baseURI = ((File) b).toURI();
+                    } else {
+                        break valid;
+                    }
+                } catch (URISyntaxException e) {
+                    Context.warningOccured(Context.current(), 
ReferenceResolver.class, "resolve", e, true);
+                    break valid;
+                }
+                base = baseURI;
+            }
+            return baseURI.resolve(path);
+        }
+        return path.isAbsolute() ? path : null;
+    }
+
+    /**
+     * Returns the source of the XML document at the given path.
+     *
+     * @param  path  relative or absolute path to the XML document to read.
+     * @return source of the XML document, or {@code null} if the path cannot 
be resolved.
+     * @throws Exception if an error occurred while creating the source.
+     */
+    public Source openReader(URI path) throws Exception {
+        path = resolve(path);
+        return (path != null) ? new URISource(path) : null;
+    }
+
+    /*
+     * A future version may add `openWriter(URI)` for splitting a large 
document into many smaller documents.
+     */
+
+    /**
+     * Creates a link resolver for a XML document reads from the given input 
stream or character reader.
+     * Also invoked for document written to the given output stream or 
character writer.
+     *
+     * @param  input  the {@link java.io.InputStream} or {@link 
java.io.Reader}, or {@code null} if none.
+     * @return the resolver for the given input stream or character reader, or 
{@code null} if none.
+     */
+    public static ExternalLinkHandler forStream(final Object input) {
+        // TODO: define an interface for allowing us to fetch this information.
+        return null;
+    }
+
+    /**
+     * Creates a link resolver for a XML document reads from the given stream.
+     *
+     * @param  input  the XML stream reader.
+     * @return the resolver for the given input, or {@code null} if none.
+     */
+    public static ExternalLinkHandler create(final XMLStreamReader input) {
+        return forStAX(input.getProperty(XMLInputFactory.RESOLVER), 
input.getLocation());
+    }
+
+    /**
+     * Creates a link resolver for a XML document reads from the given events.
+     *
+     * @param  input  the XML event reader.
+     * @return the resolver for the given input, or {@code null} if none.
+     * @throws XMLStreamException if an error occurred while inspecting the 
reader.
+     */
+    public static ExternalLinkHandler create(final XMLEventReader input) 
throws XMLStreamException {
+        return forStAX(input.getProperty(XMLInputFactory.RESOLVER), 
input.peek().getLocation());
+    }
+
+    /**
+     * Creates a link resolver for a XML document reads a StAX stream or event 
reader.
+     *
+     * @param  property  value of the {@value XMLInputFactory#RESOLVER} 
property. May be null.
+     * @param  location  current location of the reader, or {@code null} if 
unknown.
+     * @return the resolver for the stream or even reader, or {@code null} if 
none.
+     */
+    private static ExternalLinkHandler forStAX(final Object property, final 
Location location) {
+        final String base;
+        if (location == null || (base = location.getSystemId()) == null) {
+            return null;
+        }
+        if (!(property instanceof XMLResolver)) {
+            return new ExternalLinkHandler(base);
+        }
+        final XMLResolver resolver = (XMLResolver) property;
+        return new ExternalLinkHandler(base) {
+            @Override public Source openReader(final URI path) throws 
XMLStreamException {
+                /*
+                 * According StAX specification, the return type can be either 
InputStream,
+                 * XMLStreamReader or XMLEventReader. We additionally accept 
Source as well.
+                 * Types are tested from highest-level to lowest-level.
+                 *
+                 * TODO: need to provide a non-null namespace (the last 
argument).
+                 */
+                final Object source = resolver.resolveEntity(null, 
path.toString(), base, null);
+                if (source == null || source instanceof Source) {
+                    return (Source) source;
+                } else if (source instanceof XMLEventReader) {
+                    return new StAXSource((XMLEventReader) source);
+                } else if (source instanceof XMLStreamReader) {
+                    return new StAXSource((XMLStreamReader) source);
+                } else if (source instanceof InputStream) {
+                    return URISource.create((InputStream) source, 
resolve(path));
+                } else {
+                    throw new 
XMLStreamException(Errors.format(Errors.Keys.UnsupportedType_1, 
source.getClass()));
+                }
+            }
+        };
+    }
+
+    /**
+     * If the given source if defined only by URI (no input stream), returns 
that source.
+     *
+     * @param  source  the source.
+     * @return the URI of the source, or {@code null} if not applicable for 
reading the document.
+     */
+    public static URI ifOnlyURI(final Source source) {
+        if (source instanceof URISource) {
+            final var input = (URISource) source;
+            if (input.getInputStream() == null && input.getReader() == null) {
+                return input.source;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * {@return the base URI of the link handler in current (un)marshalling 
context}.
+     * This is a helper method for diagnostic purposes only.
+     */
+    @Debug
+    public static Object getCurrentURI() {
+        final var handler = Context.linkHandler(Context.current());
+        return (handler != null) ? handler.base : null;
+    }
+
+    /**
+     * {@return a string representation of this link handler for debugging 
purposes}.
+     */
+    @Override
+    public String toString() {
+        return Strings.toString(getClass(), "base", base);
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/URISource.java
 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/URISource.java
new file mode 100644
index 0000000000..f9fc89a2d0
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/util/URISource.java
@@ -0,0 +1,94 @@
+/*
+ * 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.util;
+
+import java.net.URI;
+import java.io.InputStream;
+import javax.xml.transform.stream.StreamSource;
+import org.apache.sis.util.internal.Strings;
+
+
+/**
+ * A source of XML document which is read from an URL.
+ * This class should be handled as a standard {@link StreamSource} by Java API,
+ * but allows Apache SIS to keep a reference to the original {@link URI} 
object.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+final class URISource extends StreamSource {
+    /**
+     * URL of the XML document.
+     */
+    final URI source;
+
+    /**
+     * Creates a source from a URL.
+     *
+     * @param source  URL of the XML document.
+     */
+    URISource(final URI source) {
+        this.source = source;
+    }
+
+    /**
+     * Creates a new source.
+     *
+     * @param input   stream of the XML document.
+     * @param source  URL of the XML document.
+     */
+    private URISource(final InputStream input, final URI source) {
+        super(input);
+        this.source = source;
+    }
+
+    /**
+     * Creates a new source.
+     *
+     * @param  input   stream of the XML document.
+     * @param  source  URL of the XML document, or {@code null} if none.
+     * @return the given input stream as a source.
+     */
+    public static StreamSource create(final InputStream input, final URI 
source) {
+        if (source != null) {
+            return new URISource(input, source);
+        } else {
+            return new StreamSource(input);
+        }
+    }
+
+    /**
+     * Gets the system identifier derived from the URI.
+     * The system identifier is the URL encoded in ASCII, computed when first 
needed.
+     */
+    @Override
+    public String getSystemId() {
+        String systemId = super.getSystemId();
+        if (systemId == null) {
+            systemId = source.toASCIIString();
+            setSystemId(systemId);
+        }
+        return systemId;
+    }
+
+    /**
+     * {@return a string representation of this source for debugging purposes}.
+     */
+    @Override
+    public String toString() {
+        return Strings.toString(getClass(), "source", source, "inputStream", 
getInputStream());
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/citation/DefaultCitationTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/citation/DefaultCitationTest.java
index 187213b965..a943ac8c67 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/citation/DefaultCitationTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/citation/DefaultCitationTest.java
@@ -25,6 +25,7 @@ import java.util.Locale;
 import java.io.InputStream;
 import jakarta.xml.bind.JAXBException;
 import org.opengis.metadata.Identifier;
+import org.opengis.metadata.citation.Citation;
 import org.opengis.metadata.citation.CitationDate;
 import org.opengis.metadata.citation.Contact;
 import org.opengis.metadata.citation.DateType;
@@ -294,7 +295,15 @@ public final class DefaultCitationTest extends 
TestUsingFile {
      * @param  format  whether to use the 2007 or 2016 version of ISO 19115.
      */
     private void testUnmarshalling(final Format format) throws JAXBException {
-        final DefaultCitation c = unmarshalFile(DefaultCitation.class, 
openTestFile(format));
+        verifyUnmarshalledCitation(unmarshalFile(DefaultCitation.class, 
openTestFile(format)));
+    }
+
+    /**
+     * Verifies the citation unmarshalled from the XML file.
+     *
+     * @param c  the citation.
+     */
+    public static void verifyUnmarshalledCitation(final Citation c) {
         assertTitleEquals("title", "Fight against poverty", c);
 
         final CitationDate date = getSingleton(c.getDates());
@@ -302,7 +311,7 @@ public final class DefaultCitationTest extends 
TestUsingFile {
         assertEquals("dateType", DateType.ADOPTED, date.getDateType());
         assertEquals("presentationForm", PresentationForm.PHYSICAL_OBJECT, 
getSingleton(c.getPresentationForms()));
 
-        final Iterator<Responsibility> it = 
c.getCitedResponsibleParties().iterator();
+        final Iterator<? extends Responsibility> it = 
c.getCitedResponsibleParties().iterator();
         final Contact contact = assertResponsibilityEquals(Role.ORIGINATOR, 
"Maid Marian", it.next());
         assertEquals("Contact instruction", "Send carrier pigeon.", 
String.valueOf(contact.getContactInstructions()));
 
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/citation/DefaultContactTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/citation/DefaultContactTest.java
index 67b1ae9e44..5887e06a4d 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/citation/DefaultContactTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/iso/citation/DefaultContactTest.java
@@ -77,7 +77,7 @@ public final class DefaultContactTest extends TestCase 
implements Filter {
      * Initializes the test for catching warning messages.
      */
     private void init() {
-        context = new Context(0, null, null, null, null, null, null, null, 
this);
+        context = new Context(0, null, null, null, null, null, null, null, 
null, null, this);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/xml/2016/UsingExternalXLink.xml
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/xml/2016/UsingExternalXLink.xml
new file mode 100644
index 0000000000..77f9e1b5f3
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/metadata/xml/2016/UsingExternalXLink.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<!--
+  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.
+-->
+
+<mri:MD_DataIdentification
+    xmlns:mri   = "http://standards.iso.org/iso/19115/-3/mri/1.0";
+    xmlns:cit   = "http://standards.iso.org/iso/19115/-3/cit/1.0";
+    xmlns:gco   = "http://standards.iso.org/iso/19115/-3/gco/1.0";
+    xmlns:xsi   = "http://www.w3.org/2001/XMLSchema-instance";
+    xmlns:xlink = "http://www.w3.org/1999/xlink";
+    xsi:schemaLocation = "http://standards.iso.org/iso/19115/-3/mri/1.0
+                          
https://schemas.isotc211.org/19115/-3/mri/1.0/mri.xsd";>
+
+  <mri:citation xlink:href="Citation.xml"/>
+  <mri:abstract>
+    <gco:CharacterString>Test the use of XLink to an external 
document.</gco:CharacterString>
+  </mri:abstract>
+
+</mri:MD_DataIdentification>
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ReferenceResolverMock.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ReferenceResolverMock.java
index 966a724086..90bd05afa8 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ReferenceResolverMock.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ReferenceResolverMock.java
@@ -54,7 +54,7 @@ public final class ReferenceResolverMock extends 
ReferenceResolver {
      * @return the (un)marshalling context.
      */
     public static Context begin(final boolean marshalling) {
-        return new Context(marshalling ? Context.MARSHALLING : 0, null, null, 
null, null,
+        return new Context(marshalling ? Context.MARSHALLING : 0, null, null, 
null, null, null, null,
                 null, new ReferenceResolverMock(), null, null);
     }
 
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ReferenceResolverTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ReferenceResolverTest.java
new file mode 100644
index 0000000000..caca2472e4
--- /dev/null
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/ReferenceResolverTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import jakarta.xml.bind.JAXBException;
+import org.opengis.metadata.identification.DataIdentification;
+
+// Test dependencies
+import org.junit.Test;
+import static org.junit.jupiter.api.Assertions.*;
+import org.apache.sis.metadata.xml.TestUsingFile;
+import org.apache.sis.metadata.iso.citation.DefaultCitationTest;
+
+
+/**
+ * Tests {@link ReferenceResolver}.
+ *
+ * @author  Martin Desruisseaux (Geomatys)
+ */
+public final class ReferenceResolverTest extends TestUsingFile {
+    /**
+     * Creates a new test case.
+     */
+    public ReferenceResolverTest() {
+    }
+
+    /**
+     * Tests loading a document with a {@code xlink:href} to an external 
document.
+     *
+     * @throws IOException if an error occurred while opening the test file.
+     * @throws JAXBException if an error occurred while parsing the test file.
+     */
+    @Test
+    public void testUsingExternalXLink() throws IOException, JAXBException {
+        final var data = (DataIdentification) 
XML.unmarshal(Format.XML2016.getURL("UsingExternalXLink.xml"));
+        assertEquals("Test the use of XLink to an external document.", 
data.getAbstract().toString());
+        DefaultCitationTest.verifyUnmarshalledCitation(data.getCitation());
+    }
+}
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/gco/StringAdapterTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/gco/StringAdapterTest.java
index 017ab1a82b..54ff16eccc 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/gco/StringAdapterTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/gco/StringAdapterTest.java
@@ -61,7 +61,7 @@ public final class StringAdapterTest extends TestCase {
         i18n.add(Locale.ENGLISH,  "A word");
         i18n.add(Locale.FRENCH,   "Un mot");
         i18n.add(Locale.JAPANESE, "言葉");
-        final Context context = new Context(0, Locale.ENGLISH, null, null, 
null, null, null, null, null);
+        final Context context = new Context(0, null, Locale.ENGLISH, null, 
null, null, null, null, null, null, null);
         try {
             Context.push(Locale.JAPANESE);  assertEquals("言葉",    
StringAdapter.toString(i18n));
             Context.push(Locale.FRENCH);    assertEquals("Un mot", 
StringAdapter.toString(i18n));
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/gml/TimePeriodTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/gml/TimePeriodTest.java
index e03e195945..ed7e9ed023 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/gml/TimePeriodTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/bind/gml/TimePeriodTest.java
@@ -66,7 +66,7 @@ public final class TimePeriodTest extends TestCase {
      * Set the marshalling context to a fixed locale and timezone before to 
create the
      * JAXB wrappers for temporal objects.
      */
-    private void createContext() {
+    private void createContext() throws JAXBException {
         createContext(true, Locale.FRANCE, "CET");
     }
 
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 6ebc620fe8..583e05cf35 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
@@ -149,15 +149,16 @@ public abstract class TestCase extends 
org.apache.sis.test.TestCase {
     /**
      * Initializes the {@link #context} to the given locale and timezone.
      *
-     * @param marshal   {@code true} for setting the {@link 
Context#MARSHALLING} flag.
-     * @param locale    the locale, or {@code null} for the default.
-     * @param timezone  the timezone, or {@code null} for the default.
+     * @param  marshal   {@code true} for setting the {@link 
Context#MARSHALLING} flag.
+     * @param  locale    the locale, or {@code null} for the default.
+     * @param  timezone  the timezone, or {@code null} for the default.
+     * @throws JAXBException if an error occurred while initializing the 
context.
      *
      * @see #clearContext()
      */
-    protected final void createContext(final boolean marshal, final Locale 
locale, final String timezone) {
-        context = new Context(marshal ? Context.MARSHALLING : 0, locale,
-                (timezone != null) ? TimeZone.getTimeZone(timezone) : null, 
null, null, null, null, null, null);
+    protected final void createContext(final boolean marshal, final Locale 
locale, final String timezone) throws JAXBException {
+        context = new Context(marshal ? Context.MARSHALLING : 0, 
getMarshallerPool(), locale,
+                (timezone != null) ? TimeZone.getTimeZone(timezone) : null, 
null, null, null, null, null, null, null);
     }
 
     /**
diff --git 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/util/XmlUtilitiesTest.java
 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/util/XmlUtilitiesTest.java
index de63bad4ef..8319638469 100644
--- 
a/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/util/XmlUtilitiesTest.java
+++ 
b/endorsed/src/org.apache.sis.metadata/test/org/apache/sis/xml/util/XmlUtilitiesTest.java
@@ -29,6 +29,7 @@ import java.util.Locale;
 import javax.xml.datatype.XMLGregorianCalendar;
 import javax.xml.datatype.DatatypeConfigurationException;
 import static javax.xml.datatype.DatatypeConstants.FIELD_UNDEFINED;
+import jakarta.xml.bind.JAXBException;
 import org.apache.sis.xml.bind.Context;
 
 // Test dependencies
@@ -55,9 +56,10 @@ public final class XmlUtilitiesTest extends TestCase {
      * The reverse operation is also tested.
      *
      * @throws DatatypeConfigurationException if the XML factory cannot be 
created.
+     * @throws JAXBException if an error occurred while creating the JAXB 
context used for XML tests in SIS.
      */
     @Test
-    public void testDateToXML() throws DatatypeConfigurationException {
+    public void testDateToXML() throws DatatypeConfigurationException, 
JAXBException {
         createContext(false, Locale.FRANCE, "CET");
         final Date date = new Date(1230786000000L);
         final XMLGregorianCalendar calendar = XmlUtilities.toXML(context, 
date);
@@ -73,9 +75,10 @@ public final class XmlUtilitiesTest extends TestCase {
      * This test arbitrarily uses the JST timezone.
      *
      * @throws DatatypeConfigurationException if the XML factory cannot be 
created.
+     * @throws JAXBException if an error occurred while creating the JAXB 
context used for XML tests in SIS.
      */
     @Test
-    public void testTemporalToXML() throws DatatypeConfigurationException {
+    public void testTemporalToXML() throws DatatypeConfigurationException, 
JAXBException {
         createContext(false, Locale.JAPAN, "JST");
         XMLGregorianCalendar calendar;
         Temporal t;
diff --git 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/AbstractIdentifiedObjectTest.java
 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/AbstractIdentifiedObjectTest.java
index 7f914c7815..2ad0764a1a 100644
--- 
a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/AbstractIdentifiedObjectTest.java
+++ 
b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/AbstractIdentifiedObjectTest.java
@@ -216,7 +216,7 @@ public final class AbstractIdentifiedObjectTest extends 
TestCase {
         final AbstractIdentifiedObject o2 = new 
AbstractIdentifiedObject(properties);
         final AbstractIdentifiedObject o3 = new 
AbstractIdentifiedObject(properties);
         final AbstractIdentifiedObject o4 = new 
AbstractIdentifiedObject(properties);
-        final Context context = new Context(0, null, null, null, null, null, 
null, null, null);
+        final Context context = new Context(0, null, null, null, null, null, 
null, null, null, null, null);
         try {
             final String c1, c2, c3, c4;
             assertEquals("o1", "epsg-7019", c1 = o1.getID());
diff --git 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/IndexedResourceBundle.java
 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/IndexedResourceBundle.java
index 1d83f4f092..cbffab180b 100644
--- 
a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/IndexedResourceBundle.java
+++ 
b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/resources/IndexedResourceBundle.java
@@ -408,7 +408,7 @@ public abstract class IndexedResourceBundle extends 
ResourceBundle implements Lo
                 }
                 replacement = CharSequences.shortSentence(text, 
MAX_STRING_LENGTH);
             } else if (element instanceof URI) {
-                replacement = ((URI) element).getPath();        // For 
decoding encoded characters.
+                replacement = ((URI) element).getSchemeSpecificPart();      // 
For decoding encoded characters.
             } else if (element instanceof Class<?>) {
                 replacement = Classes.getShortName(getPublicType((Class<?>) 
element));
             } else if (element instanceof ControlledVocabulary) {


Reply via email to