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

commit 50c0a3ec2947d278f37f1deed485d50ed3170b29
Author: Martin Desruisseaux <martin.desruisse...@geomatys.com>
AuthorDate: Tue Dec 19 20:21:27 2023 +0100

    Partial support of xlink:href referencing a fragment of an external 
document.
---
 .../main/org/apache/sis/xml/Pooled.java            |  31 ++++
 .../main/org/apache/sis/xml/ReferenceResolver.java |  68 +++++++--
 .../main/org/apache/sis/xml/XML.java               |   2 +-
 .../main/org/apache/sis/xml/bind/Context.java      | 159 +++++++++++++++------
 .../apache/sis/xml/util/ExternalLinkHandler.java   |  55 ++++---
 .../main/org/apache/sis/xml/util/URISource.java    |  48 +++++--
 .../sis/metadata/xml/2016/UsingExternalXLink.xml   |  16 +++
 7 files changed, 286 insertions(+), 93 deletions(-)

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 a66af506b2..593d71f0bd 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
@@ -83,6 +83,7 @@ abstract class Pooled {
      * Bit masks for various boolean attributes. This include whatever the 
language codes
      * or the country codes should be substituted by a simpler character 
string elements.
      * Those bits are determined by the {@link XML#STRING_SUBSTITUTES} 
property.
+     * The meaning of the bits are defined by constants in {@link Context} 
class.
      */
     private int bitMasks;
 
@@ -121,6 +122,16 @@ abstract class Pooled {
      */
     private Version versionMetadata;
 
+    /**
+     * If the document to (un)marshal is included inside a larger document,
+     * the {@code systemId} of the included document. Otherwise {@code null}.
+     * This is used for caching the map of fragments (identified by {@code 
gml:id} attributes)
+     * included inside a document referenced by an {@code xlink:href} 
attribute.
+     *
+     * @see Context#getObjectForID(Context, Object, String)
+     */
+    private Object includedDocumentSystemId;
+
     /**
      * The reference resolver to use during unmarshalling.
      * Can be set by the {@link XML#RESOLVER} property.
@@ -200,6 +211,7 @@ abstract class Pooled {
             reset(entry.getKey(), entry.getValue());
         }
         initialProperties.clear();
+        includedDocumentSystemId = null;
         bitMasks         = template.bitMasks;
         locale           = template.locale;
         timezone         = template.timezone;
@@ -519,6 +531,16 @@ abstract class Pooled {
         return 0;
     }
 
+    /**
+     * Notifies this object that it will be used for marshalling or 
unmarshalling a document
+     * included in a larger document. It happens when following {@code 
xlink:href}.
+     *
+     * @param  systemId  key to use for caching the parsing result in the 
marshal {@link Context}.
+     */
+    final void forIncludedDocument(final Object systemId) {
+        includedDocumentSystemId = systemId;
+    }
+
     /**
      * 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.
@@ -537,6 +559,15 @@ abstract class Pooled {
      * @param  linkHandler  the document-dependent resolver or relativizer of 
URIs, or {@code null}.
      */
     final Context begin(final ExternalLinkHandler linkHandler) {
+        if (includedDocumentSystemId != null) {
+            if (linkHandler != null) {
+                linkHandler.includedDocumentSystemId = 
includedDocumentSystemId;
+            }
+            final Context current = Context.current();
+            if (current != null) {
+                return current.createChild(linkHandler);
+            }
+        }
         return new Context(bitMasks | specificBitMasks(), pool, locale, 
timezone,
                            schemas, versionGML, versionMetadata,
                            linkHandler, resolver, converter, logFilter);
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 706b9d4d57..c6cab8471e 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
@@ -19,7 +19,6 @@ 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;
@@ -33,6 +32,7 @@ import org.apache.sis.util.internal.Strings;
 import org.apache.sis.xml.bind.Context;
 import org.apache.sis.xml.bind.gcx.Anchor;
 import org.apache.sis.xml.util.ExternalLinkHandler;
+import org.apache.sis.xml.util.URISource;
 
 
 /**
@@ -141,22 +141,30 @@ public class ReferenceResolver {
         if (href == null || href.isOpaque()) {
             return null;
         }
-        final Object label, object;
+        final Object 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)) {
+            /*
+             * URI defined only by an anchor in the same document. There is 
nothing to load,
+             * we just check for previously unmarshalled objects in the same 
marshal context.
+             * Current implementation supports only backward references.
+             *
+             * For forward references, see 
https://issues.apache.org/jira/browse/SIS-420
+             */
+            final String fragment = Strings.trimOrNull(href.getFragment());
+            if (fragment == null) {
                 return null;
             }
-            object = Context.getObjectForID(c, id);
-            label  = id;                // Used if the object is invalid.
+            object = Context.getObjectForID(c, fragment);
         } else try {
+            /*
+             * URI to an external document. We let `ExternalLinkHandler` 
decide how to replace relative URI
+             * by absolute URI. It may depend on whether user has specified a 
`javax.xml.stream.XMLResolver`
+             */
             final Source source = Context.linkHandler(c).openReader(href);
-            object = (source != null) ? resolveExternal(c, source) : null;
-            label  = href;              // Used if the object is invalid.
+            object = (source != null) ? resolveExternal(context, source) : 
null;
         } catch (Exception e) {
-            Context.warningOccured(c, Level.WARNING, ReferenceResolver.class, 
"resolve",
-                                   e, Errors.class, Errors.Keys.CanNotRead_1, 
href);
+            ExternalLinkHandler.warningOccured(href, e);
             return null;
         }
         if (type.isInstance(object)) {
@@ -166,10 +174,10 @@ public class ReferenceResolver {
             final Object[] args;
             if (object == null) {
                 key = Errors.Keys.NotABackwardReference_1;
-                args = new Object[] {label.toString()};
+                args = new Object[] {href.toString()};
             } else {
                 key = Errors.Keys.UnexpectedTypeForReference_3;
-                args = new Object[] {label.toString(), type, 
object.getClass()};
+                args = new Object[] {href.toString(), type, object.getClass()};
             }
             Context.warningOccured(c, ReferenceResolver.class, "resolve", 
Errors.class, key, args);
         }
@@ -194,16 +202,48 @@ public class ReferenceResolver {
      * @since 1.5
      */
     protected Object resolveExternal(final MarshalContext context, final 
Source source) throws IOException, JAXBException {
+        final Object systemId;
+        final String fragment;
+        final URI uri;
+        if (source instanceof URISource) {
+            final var s = (URISource) source;
+            uri = s.getReadableURI();
+            systemId = s.document;
+            fragment = s.fragment;
+        } else {
+            systemId = source.getSystemId();
+            fragment = null;
+            uri      = null;
+        }
+        /*
+         * At this point, we got the system identifier (usually as a resolved 
URI, but not necessarily)
+         * and the URI fragment to use as a GML identifier. Check if the 
document is in the cache.
+         */
+        final Context c = Context.current();
+        Object object = Context.getObjectForID(c, systemId, fragment);
+        if (object != null) {
+            return object;
+        }
+        /*
+         * Object not found in the cache. Parse it.
+         */
         final MarshallerPool pool = context.getPool();
         final Unmarshaller m = pool.acquireUnmarshaller();
-        final URI uri = ExternalLinkHandler.ifOnlyURI(source);
-        final Object object;
+        if (m instanceof Pooled) {
+            ((Pooled) m).forIncludedDocument(systemId);
+        }
         if (uri != null) {
             object = m.unmarshal(uri.toURL());
         } else {
             object = m.unmarshal(source);
         }
         pool.recycle(m);
+        /*
+         * Object should be in the cache now.
+         */
+        if (fragment != null) {
+            object = Context.getObjectForID(c, systemId, fragment);
+        }
         return object;
     }
 
diff --git 
a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XML.java 
b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XML.java
index 60e6de5d4e..304d9723f9 100644
--- a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XML.java
+++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/XML.java
@@ -84,7 +84,7 @@ import static 
org.apache.sis.util.ArgumentChecks.ensureNonNull;
  * @author  Cédric Briançon (Geomatys)
  * @author  Martin Desruisseaux (Geomatys)
  * @author  Cullen Rombach (Image Matters)
- * @version 1.4
+ * @version 1.5
  * @since   0.3
  */
 public final class XML extends Static {
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 b4342d931f..85d6ec4e21 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
@@ -16,6 +16,7 @@
  */
 package org.apache.sis.xml.bind;
 
+import java.net.URI;
 import java.util.Map;
 import java.util.HashMap;
 import java.util.IdentityHashMap;
@@ -28,6 +29,7 @@ import java.util.logging.Filter;
 import org.apache.sis.util.Version;
 import org.apache.sis.util.Exceptions;
 import org.apache.sis.util.CorruptedObjectException;
+import org.apache.sis.util.internal.Strings;
 import org.apache.sis.util.resources.Errors;
 import org.apache.sis.util.resources.Messages;
 import org.apache.sis.util.resources.IndexedResourceBundle;
@@ -171,21 +173,37 @@ public final class Context extends MarshalContext {
     private final ValueConverter converter;
 
     /**
-     * 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.
+     * The objects associated to XML identifiers in the current document.
+     * At marhalling time, this map is used for avoiding duplicated 
identifiers in the same XML document.
+     * At unmarshalling time, this is used for getting a previously 
unmarshalled object from its identifier.
      *
      * @see #getObjectForID(Context, String)
      */
     private final Map<String,Object> identifiers;
 
     /**
-     * The identifiers used for marshalled objects. This is the converse of 
{@link #identifiers}, used in order to
-     * identify which {@code gml:id} to use for the given object. The {@code 
gml:id} to use are not necessarily the
-     * same than the one associated to {@link IdentifierSpace#ID} if the 
identifier was already used for another
-     * object in the same XML document.
+     * The identifiers used for marshalled objects in the current document.
+     * This is the converse of the {@link #identifiers} map, used in order to 
identify which {@code gml:id} to use
+     * for a given object. The {@code gml:id} values to use are not 
necessarily the same than the values associated
+     * to {@link IdentifierSpace#ID} if some identifiers were already used for 
other objects in the same XML document.
      */
     private final Map<Object,String> identifiedObjects;
 
+    /**
+     * The {@link #identifiers} map for each document being unmarshalled.
+     * This cache is populated if the document contains {@code xlink:href} to 
other documents.
+     * This cache contains only the documents seen by following the references 
since the root document.
+     * This is not a system-wide cache.
+     *
+     * <p>Keys are {@code systemId} as instances of {@link URI} or {@link 
String}. For documents that are read
+     * from a file or an URL, {@code systemId} should be the value of {@link 
java.net.URI#toASCIIString()} for
+     * consistency with {@link javax.xml.transform.stream.StreamSource}.  
However, URI instances are preferred
+     * in this map because the {@link URI#equals(Object)} method applies some 
rules regarding case-sensitivity
+     * that {@link String#equals(Object)} cannot know. Values of the map are 
the {@link #identifiers} maps of
+     * the corresponding document. By convention, the object associated to the 
null key is the whole document.</p>
+     */
+    private final Map<Object, Map<String,Object>> identifiersPerDocuments;
+
     /**
      * The object to inform about warnings, or {@code null} if none.
      */
@@ -250,18 +268,19 @@ public final class Context extends MarshalContext {
         if (versionMetadata != null && 
versionMetadata.compareTo(LegacyNamespaces.VERSION_2014) < 0) {
             bitMasks |= LEGACY_METADATA;
         }
-        this.pool              = pool;
-        this.locale            = locale;
-        this.timezone          = timezone;
-        this.schemas           = schemas;               // No clone, because 
this class is internal.
-        this.versionGML        = versionGML;
-        this.linkHandler       = linkHandler;
-        this.resolver          = resolver;
-        this.converter         = converter;
-        this.logFilter         = logFilter;
-        this.identifiers       = new HashMap<>();
-        this.identifiedObjects = new IdentityHashMap<>();
-        this.previous          = CURRENT.get();
+        this.pool               = pool;
+        this.locale             = locale;
+        this.timezone           = timezone;
+        this.schemas            = schemas;              // No clone, because 
this class is internal.
+        this.versionGML         = versionGML;
+        this.linkHandler        = linkHandler;
+        this.resolver           = resolver;
+        this.converter          = converter;
+        this.logFilter          = logFilter;
+        identifiers             = new HashMap<>();
+        identifiersPerDocuments = new HashMap<>();
+        identifiedObjects       = new IdentityHashMap<>();
+        previous                = CURRENT.get();
         if ((bitMasks & MARSHALLING) != 0) {
             /*
              * Set global semaphore last after our best effort to ensure that 
construction
@@ -298,7 +317,7 @@ public final class Context extends MarshalContext {
         this.resolver    = parent.resolver;
         this.converter   = parent.converter;
         this.logFilter   = parent.logFilter;
-        this.bitMasks    = parent.bitMasks;
+        this.bitMasks    = parent.bitMasks & ~CLEAR_SEMAPHORE;
         if (inline) {
             identifiers       = parent.identifiers;
             identifiedObjects = parent.identifiedObjects;
@@ -306,7 +325,9 @@ public final class Context extends MarshalContext {
             identifiers       = new HashMap<>();
             identifiedObjects = new IdentityHashMap<>();
         }
-        previous = CURRENT.get();
+        identifiersPerDocuments = parent.identifiersPerDocuments;
+        previous = parent;
+        assert parent == CURRENT.get();
     }
 
     /**
@@ -383,6 +404,21 @@ public final class Context extends MarshalContext {
         return CURRENT.get();
     }
 
+    /**
+     * Creates the context for following a {@code xlink:href} to a separated 
document.
+     * Because the separated document may be in a different directory, the new 
context
+     * will use a different {@link ExternalLinkHandler}. The returned context 
shall be
+     * used in the same way as a context created from the public constructor.
+     *
+     * @param  linkHandler  the document-dependent resolver of relative URIs, 
or {@code null}.
+     * @return the context as a child of current context, never {@code null}.
+     */
+    public final Context createChild(final ExternalLinkHandler linkHandler) {
+        final Context context = new Context(this, locale, linkHandler, false);
+        CURRENT.set(context);
+        return context;
+    }
+
     /**
      * Sets the locale to the given value. The old locales are remembered and 
will
      * be restored by the next call to {@link #pull()}. This method can be 
invoked
@@ -412,23 +448,10 @@ public final class Context extends MarshalContext {
     }
 
     /**
-     * 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));
-        }
-    }
-
-    /**
-     * Restores the locale which was used prior the call to {@link 
#push(Locale)}.
-     * It is not necessary to invoke this method in a {@code finally} block.
+     * Restores the locale which was used prior the call to {@code push(…)}.
+     * It is not necessary to invoke this method in a {@code finally} block,
+     * provided that {@link #finish()} is invoked in a finally block by some
+     * enclosing method calls.
      */
     public static void pull() {
         Context c = current();
@@ -589,10 +612,10 @@ public final class Context extends MarshalContext {
     }
 
     /**
-     * Returns {@code true} if the given identifier is available, or {@code 
false} if it is used by another object.
-     * If this method returns {@code true}, then the given identifier is 
associated to the given object for future
-     * invocation of {@code Context} methods. If this method returns {@code 
false}, then the caller is responsible
-     * for computing another identifier candidate.
+     * Associates the given object to the given identifier if that identifier 
is available.
+     * Returns {@code true} if the given identifier was available, or {@code 
false} if used by another object.
+     * In the latter case, this method does nothing (the existing object is 
not replaced) and the caller should
+     * try again with another identifier candidate.
      *
      * @param  context  the current context, or {@code null} if none.
      * @param  object   the object for which to assign the {@code gml:id}.
@@ -612,6 +635,32 @@ public final class Context extends MarshalContext {
         return true;
     }
 
+    /**
+     * Returns the object for the given {@code gml:id} in the specified 
document, or {@code null} if none.
+     * GML identifiers can be referenced by the fragment part of URI in 
XLinks. This association is valid
+     * only for documents referenced in {@code xlink:href} attributes since 
the root document of current
+     * unmarshalling process. This is not a system-wide cache.
+     *
+     * <p>For documents that are read from a file or an URL, the {@code 
systemId} argument should be the value of
+     * {@link java.net.URI#toASCIIString()} for consistency with {@link 
javax.xml.transform.stream.StreamSource}.
+     * However, the original URI instances should be used instead when they 
are available.
+     * By convention, a null {@code id} returns the whole document.</p>
+     *
+     * @param  context   the current context, or {@code null} if none.
+     * @param  systemId  document identifier (without the fragment part) as an 
{@link URI} or a {@link String}.
+     * @param  id        the fragment part of the URI identifying the object 
to get.
+     * @return the object associated to the given identifier, or {@code null} 
if none.
+     */
+    public static Object getObjectForID(final Context context, final Object 
systemId, final String id) {
+        if (context != null) {
+            final Map<String,Object> identifiers = 
context.identifiersPerDocuments.get(systemId);
+            if (identifiers != null) {
+                return identifiers.get(id);
+            }
+        }
+        return null;
+    }
+
     /**
      * 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}.
@@ -761,16 +810,36 @@ public final class Context extends MarshalContext {
     }
 
     /**
-     * Invoked in a {@code finally} block when a unmarshalling process is 
finished.
+     * Invoked in a {@code finally} block when a (un)marshalling process is 
finished.
+     * This method should be invoked on the instance created by the 
constructor or
+     * on the instance returned by the {@link #push(ExternalLinkHandler)} 
method,
+     * <em>not</em> on the instance returned by {@link #current()} because the 
latter
+     * may cause a memory leak if some calls to the {@link #pull()} method are 
missing.
      */
     public final void finish() {
         if ((bitMasks & CLEAR_SEMAPHORE) != 0) {
             Semaphores.clear(Semaphores.NULL_COLLECTION);
         }
-        if (previous != null) {
-            CURRENT.set(previous);
-        } else {
+        if (previous == null) {
             CURRENT.remove();
+        } else {
+            CURRENT.set(previous);
+            if (linkHandler != null) {
+                final Object systemId = linkHandler.includedDocumentSystemId;
+                if (systemId != null) {
+                    previous.identifiersPerDocuments.putIfAbsent(systemId, 
identifiers);
+                }
+            }
         }
     }
+
+    /**
+     * {@return a string representation of this context for debugging 
purposes}.
+     */
+    @Override
+    public String toString() {
+        return Strings.toString(getClass(), "operation", (bitMasks & 
MARSHALLING) != 0 ? "marshal" : "unmarshal",
+                "locale", locale, "timezone", timezone,
+                null, (bitMasks & LENIENT_UNMARSHAL) != 0 ? "lenient"  : null);
+    }
 }
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
index 65901d938d..89f8db0884 100644
--- 
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
@@ -16,11 +16,12 @@
  */
 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.URL;
+import java.net.URI;
 import java.net.URISyntaxException;
+import java.util.logging.Level;
 import javax.xml.stream.Location;
 import javax.xml.stream.XMLResolver;
 import javax.xml.stream.XMLInputFactory;
@@ -39,7 +40,8 @@ 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.
+ * This class is used for resolving {@code xlink:href} values referencing a 
fragment
+ * outside the document being parsed.
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
@@ -62,6 +64,18 @@ public class ExternalLinkHandler {
      */
     private Object base;
 
+    /**
+     * Key to use for caching the result of (un)marshalling the document 
resolved by this link handler.
+     * This is usually the same value than the {@link String}, {@link File} or 
{@link URL} specified at
+     * construction time, but as an {@link URI} or {@link String} instance.
+     *
+     * <p>This field is not used by {@code ExternalLinkHandler}. It is defined 
only as a way
+     * to transfer information between {@code PooledUnmarshaller} and {@link 
Context}.</p>
+     *
+     * @see Context#getObjectForID(Context, Object, String)
+     */
+    public Object includedDocumentSystemId;
+
     /**
      * 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}.
@@ -99,7 +113,7 @@ public class ExternalLinkHandler {
      */
     public ExternalLinkHandler(final Source sibling) {
         if (sibling instanceof URISource) {
-            base = ((URISource) sibling).source;
+            base = ((URISource) sibling).document;
         } else {
             base = sibling.getSystemId();
         }
@@ -143,7 +157,7 @@ valid:  if (b != null) {
                         break valid;
                     }
                 } catch (URISyntaxException e) {
-                    Context.warningOccured(Context.current(), 
ReferenceResolver.class, "resolve", e, true);
+                    warningOccured(b, e);
                     break valid;
                 }
                 base = baseURI;
@@ -153,6 +167,21 @@ valid:  if (b != null) {
         return path.isAbsolute() ? path : null;
     }
 
+    /**
+     * Reports a warning about a URI that cannot be parsed.
+     * This method declares {@link ReferenceResolver} as the public source of 
the warning.
+     * The latter assumption is valid if {@code ReferenceResolver.resolve(…)} 
is the only
+     * code invoking, directly or indirectly, this {@code warning(…)} method.
+     *
+     * @param  href   the URI that cannot be parsed.
+     * @param  cause  the exception that occurred while trying to process the 
document.
+     */
+    public static void warningOccured(final Object href, final Exception 
cause) {
+        Context.warningOccured(Context.current(), ReferenceResolver.class, 
"resolve", cause, true);
+        Context.warningOccured(Context.current(), Level.WARNING, 
ReferenceResolver.class, "resolve",
+                               cause, Errors.class, Errors.Keys.CanNotRead_1, 
href);
+    }
+
     /**
      * Returns the source of the XML document at the given path.
      *
@@ -243,22 +272,6 @@ valid:  if (b != null) {
         };
     }
 
-    /**
-     * 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.
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
index f9fc89a2d0..7ea961e943 100644
--- 
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
@@ -17,6 +17,7 @@
 package org.apache.sis.xml.util;
 
 import java.net.URI;
+import java.net.URISyntaxException;
 import java.io.InputStream;
 import javax.xml.transform.stream.StreamSource;
 import org.apache.sis.util.internal.Strings;
@@ -29,30 +30,43 @@ import org.apache.sis.util.internal.Strings;
  *
  * @author  Martin Desruisseaux (Geomatys)
  */
-final class URISource extends StreamSource {
+public final class URISource extends StreamSource {
     /**
-     * URL of the XML document.
+     * Normalized URI of the XML document, without the fragment part if the 
document will be read from this URL.
+     * The URI is normalized for making possible to use it as a key in a cache 
of previously loaded documents.
      */
-    final URI source;
+    public final URI document;
 
     /**
-     * Creates a source from a URL.
+     * The fragment part of the original URI, or {@code null} if none of if 
the fragment is in the document URI.
+     * The latter case happen when the XML will need to be read from the input 
stream rather than from the URI,
+     * in which case we do not know if the input stream is not already for the 
fragment.
+     */
+    public final String fragment;
+
+    /**
+     * Creates a source from an URI.
      *
-     * @param source  URL of the XML document.
+     * @param source  URI to the XML document.
+     * @throws URISyntaxException if the URI is not valid.
      */
-    URISource(final URI source) {
-        this.source = source;
+    URISource(URI source) throws URISyntaxException {
+        source = source.normalize();
+        final URI c = new URI(source.getScheme(), 
source.getSchemeSpecificPart(), null);
+        document = source.equals(c) ? source : c;       // Share the existing 
instance if applicable.
+        fragment = source.getFragment();
     }
 
     /**
-     * Creates a new source.
+     * Creates a new source from an input stream.
      *
      * @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;
+        document = source.normalize();
+        fragment = null;
     }
 
     /**
@@ -62,7 +76,7 @@ final class URISource extends StreamSource {
      * @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) {
+    static StreamSource create(final InputStream input, final URI source) {
         if (source != null) {
             return new URISource(input, source);
         } else {
@@ -70,6 +84,16 @@ final class URISource extends StreamSource {
         }
     }
 
+    /**
+     * If this source if defined only by URI (no input stream), returns that 
URI.
+     * Otherwise returns {@code null}.
+     *
+     * @return the URI, or {@code null} if not applicable for reading the 
document.
+     */
+    public URI getReadableURI() {
+        return getInputStream() == null ? document : null;
+    }
+
     /**
      * Gets the system identifier derived from the URI.
      * The system identifier is the URL encoded in ASCII, computed when first 
needed.
@@ -78,7 +102,7 @@ final class URISource extends StreamSource {
     public String getSystemId() {
         String systemId = super.getSystemId();
         if (systemId == null) {
-            systemId = source.toASCIIString();
+            systemId = document.toASCIIString();
             setSystemId(systemId);
         }
         return systemId;
@@ -89,6 +113,6 @@ final class URISource extends StreamSource {
      */
     @Override
     public String toString() {
-        return Strings.toString(getClass(), "source", source, "inputStream", 
getInputStream());
+        return Strings.toString(getClass(), "document", document, "fragment", 
fragment, "inputStream", getInputStream());
     }
 }
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
index 77f9e1b5f3..91d745fc72 100644
--- 
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
@@ -31,5 +31,21 @@
   <mri:abstract>
     <gco:CharacterString>Test the use of XLink to an external 
document.</gco:CharacterString>
   </mri:abstract>
+  <mri:pointOfContact>
+    <cit:CI_Responsibility>
+      <cit:role>
+        <cit:CI_RoleCode 
codeList="http://standards.iso.org/iso/19115/resources/Codelist/cat/codelists.xml#CI_RoleCode";
+                         
codeListValue="pointOfContact">pointOfContact</cit:CI_RoleCode>
+      </cit:role>
+      <cit:party>
+        <cit:CI_Individual>
+          <cit:name>
+            <gco:CharacterString>Little John</gco:CharacterString>
+          </cit:name>
+          <cit:contactInfo xlink:href="Citation.xml#ip-protocol"/>
+        </cit:CI_Individual>
+      </cit:party>
+    </cit:CI_Responsibility>
+  </mri:pointOfContact>
 
 </mri:MD_DataIdentification>

Reply via email to