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 <[email protected]>
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) {