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