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>