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 b325a0c5f8 Allow `CRS.fromCode(String)` to return the CRS definitions found in the GML document being parsed. This feature make possible for embedded or linked data to reference CRS defined in the document. b325a0c5f8 is described below commit b325a0c5f8efecb1063a82623844cb4ecde6738d Author: Martin Desruisseaux <martin.desruisse...@geomatys.com> AuthorDate: Fri Dec 29 18:54:50 2023 +0100 Allow `CRS.fromCode(String)` to return the CRS definitions found in the GML document being parsed. This feature make possible for embedded or linked data to reference CRS defined in the document. --- .../main/org/apache/sis/xml/bind/Context.java | 81 ++++++----- .../org/apache/sis/xml/bind/ScopedIdentifier.java | 145 ++++++++++++++++++++ .../test/org/apache/sis/xml/test/TestCase.java | 26 ++-- .../sis/referencing/AbstractIdentifiedObject.java | 9 +- .../main/org/apache/sis/referencing/CRS.java | 46 +++++-- .../org/apache/sis/xml/bind/referencing/Code.java | 28 +++- .../sis/parameter/ParameterMarshallingTest.java | 81 +++++++---- .../operation/SingleOperationMarshallingTest.java | 148 ++++++++++++--------- .../sis/referencing/operation/Transformation.xml | 4 +- .../apache/sis/util/internal/DefinitionURI.java | 22 ++- .../sis/util/internal/DefinitionURITest.java | 10 ++ 11 files changed, 443 insertions(+), 157 deletions(-) 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 85e11932f2..2ab7f6aa79 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 @@ -176,23 +176,24 @@ public final class Context extends MarshalContext { /** * The objects associated to XML identifiers in the current document. + * Map keys are {@code gml:id} attribute values, and map values are the identified objects. * At marhalling time, this map is used for avoiding duplicated identifiers in the same XML document. * At unmarshalling time, this is used for getting a previously unmarshalled object from its identifier. * * @see #getObjectForID(Context, String) */ - private final Map<String,Object> identifiers; + private final Map<String,Object> xmlidToObject; /** * The identifiers used for marshalled objects in the current document. - * This is the converse of the {@link #identifiers} map, used in order to identify which {@code gml:id} to use + * This is the converse of the {@link #xmlidToObject} map, used in order to identify which {@code gml:id} to use * for a given object. The {@code gml:id} values to use are not necessarily the same than the values associated * to {@link IdentifierSpace#ID} if some identifiers were already used for other objects in the same XML document. */ - private final Map<Object,String> identifiedObjects; + private final Map<Object,String> objectToXmlid; /** - * The {@link #identifiers} map for each document being unmarshalled. + * The {@link #xmlidToObject} map for each document being unmarshalled. * This cache is populated if the document contains {@code xlink:href} to other documents. * This cache contains only the documents seen by following the references since the root document. * This is not a system-wide cache. @@ -201,10 +202,20 @@ public final class Context extends MarshalContext { * from a file or an URL, {@code systemId} should be the value of {@link java.net.URI#toASCIIString()} for * consistency with {@link javax.xml.transform.stream.StreamSource}. However, URI instances are preferred * in this map because the {@link URI#equals(Object)} method applies some rules regarding case-sensitivity - * that {@link String#equals(Object)} cannot know. Values of the map are the {@link #identifiers} maps of + * that {@link String#equals(Object)} cannot know. Values of the map are the {@link #xmlidToObject} maps of * the corresponding document. By convention, the object associated to the null key is the whole document.</p> */ - private final Map<Object, Map<String,Object>> identifiersPerDocuments; + private final Map<Object, Map<String,Object>> documentToXmlids; + + /** + * All identified objects associated to a global identifier (not {@code gml:id}). + * This map differs from {@link #xmlidToObject} in that it is not local to the current document, + * but instead contains all identified objects in all documents parsed since the root document. + * The keys of this map cannot be {@code gml:id} attribute values, because the latter are local. + * Instead, keys are typically {@link org.opengis.referencing.IdentifiedObject#getIdentifiers()}. + * The map may contain many entries for the same object if that object has many identifiers. + */ + final Map<ScopedIdentifier<?>, Object> identifiedObjects; /** * The object to inform about warnings, or {@code null} if none. @@ -270,19 +281,20 @@ public final class Context extends MarshalContext { if (versionMetadata != null && versionMetadata.compareTo(LegacyNamespaces.VERSION_2014) < 0) { bitMasks |= LEGACY_METADATA; } - this.pool = pool; - this.locale = locale; - this.timezone = timezone; - this.schemas = schemas; // No clone, because this class is internal. - this.versionGML = versionGML; - this.linkHandler = linkHandler; - this.resolver = resolver; - this.converter = converter; - this.logFilter = logFilter; - identifiers = new HashMap<>(); - identifiersPerDocuments = new HashMap<>(); - identifiedObjects = new IdentityHashMap<>(); - previous = CURRENT.get(); + this.pool = pool; + this.locale = locale; + this.timezone = timezone; + this.schemas = schemas; // No clone, because this class is internal. + this.versionGML = versionGML; + this.linkHandler = linkHandler; + this.resolver = resolver; + this.converter = converter; + this.logFilter = logFilter; + xmlidToObject = new HashMap<>(); + objectToXmlid = new IdentityHashMap<>(); + documentToXmlids = new HashMap<>(); + identifiedObjects = new HashMap<>(); + previous = CURRENT.get(); if ((bitMasks & MARSHALLING) != 0) { /* * Set global semaphore last after our best effort to ensure that construction @@ -310,6 +322,8 @@ public final class Context extends MarshalContext { private Context(final Context parent, final Locale locale, final ExternalLinkHandler linkHandler, final boolean inline) { + assert parent == CURRENT.get(); + this.previous = parent; this.locale = locale; this.linkHandler = linkHandler; this.pool = parent.pool; @@ -321,15 +335,14 @@ public final class Context extends MarshalContext { this.logFilter = parent.logFilter; this.bitMasks = parent.bitMasks & ~CLEAR_SEMAPHORE; if (inline) { - identifiers = parent.identifiers; - identifiedObjects = parent.identifiedObjects; + xmlidToObject = parent.xmlidToObject; + objectToXmlid = parent.objectToXmlid; } else { - identifiers = new HashMap<>(); - identifiedObjects = new IdentityHashMap<>(); + xmlidToObject = new HashMap<>(); + objectToXmlid = new IdentityHashMap<>(); } - identifiersPerDocuments = parent.identifiersPerDocuments; - previous = parent; - assert parent == CURRENT.get(); + documentToXmlids = parent.documentToXmlids; + identifiedObjects = parent.identifiedObjects; } /** @@ -433,7 +446,7 @@ public final class Context extends MarshalContext { public final Context createChild(final Object systemId, final ExternalLinkHandler linkHandler) { // Use Map.put(…) instead of putIfAbsent(…) because maybe the previous map is unmodifiable. final Context context = new Context(this, locale, linkHandler, false); - identifiersPerDocuments.put(systemId, context.identifiers); + documentToXmlids.put(systemId, context.xmlidToObject); CURRENT.set(context); return context; } @@ -614,7 +627,7 @@ public final class Context extends MarshalContext { * @return the identifier used in the current XML document for the given object, or {@code null} if none. */ public static String getObjectID(final Context context, final Object object) { - return (context != null) ? context.identifiedObjects.get(object) : null; + return (context != null) ? context.objectToXmlid.get(object) : null; } /** @@ -627,7 +640,7 @@ public final class Context extends MarshalContext { * @return the object associated to the given identifier, or {@code null} if none. */ public static Object getObjectForID(final Context context, final String id) { - return (context != null) ? context.identifiers.get(id) : null; + return (context != null) ? context.xmlidToObject.get(id) : null; } /** @@ -643,11 +656,11 @@ public final class Context extends MarshalContext { */ public static boolean setObjectForID(final Context context, final Object object, final String id) { if (context != null) { - final Object existing = context.identifiers.putIfAbsent(id, object); + final Object existing = context.xmlidToObject.putIfAbsent(id, object); if (existing != null) { return existing == object; } - if (context.identifiedObjects.put(object, id) != null) { + if (context.objectToXmlid.put(object, id) != null) { throw new CorruptedObjectException(id); // Should never happen since all put calls are in this method. } } @@ -670,7 +683,7 @@ public final class Context extends MarshalContext { * @return the object associated to the given identifier, or {@code null} if none. */ public final Object getExternalObjectForID(final Object systemId, final String id) { - final Map<String,Object> cache = identifiersPerDocuments.get(systemId); + final Map<String,Object> cache = documentToXmlids.get(systemId); return (cache != null) ? cache.get(id) : null; } @@ -683,9 +696,9 @@ public final class Context extends MarshalContext { * @param document the document to cache. */ public final void cacheDocument(final Object systemId, final Object document) { - final Map<String, Object> cache = identifiersPerDocuments.get(systemId); + final Map<String, Object> cache = documentToXmlids.get(systemId); if (cache == null || cache.isEmpty()) { - identifiersPerDocuments.put(systemId, Collections.singletonMap(null, document)); + documentToXmlids.put(systemId, Collections.singletonMap(null, document)); } else { cache.put(null, document); } diff --git a/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/ScopedIdentifier.java b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/ScopedIdentifier.java new file mode 100644 index 0000000000..8b42b60360 --- /dev/null +++ b/endorsed/src/org.apache.sis.metadata/main/org/apache/sis/xml/bind/ScopedIdentifier.java @@ -0,0 +1,145 @@ +/* + * 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.bind; + +import java.util.logging.Level; +import org.apache.sis.util.Classes; +import org.apache.sis.util.resources.Errors; + + +/** + * An identifier in the scope of a type, for use as keys in an hash map. + * This object is for handing ISO 19111 identifiers, because they may have + * the same value for different types. The type is usually a GeoAPI interface. + * + * @param <T> base type of the identified object. + * @author Martin Desruisseaux (Geomatys) + */ +public final class ScopedIdentifier<T> { + /** + * Sentinel value for meaning that an identifier is used many times. + */ + private static final Object DUPLICATED = Void.TYPE; + + /** + * The identifier scope, usually a GeoAPI interface. + */ + private final Class<? extends T> scope; + + /** + * The identifier of the object to associate to this key. + */ + private final String identifier; + + /** + * Creates a new key. + * + * @param scope the identifier scope, usually a GeoAPI interface. + * @param identifier the identifier of the object to associate to this key. + */ + public ScopedIdentifier(final Class<? extends T> scope, final String identifier) { + this.scope = scope; + this.identifier = identifier; + } + + /** + * Returns an identifier with the same scope than this identifier by a different character string. + * + * @param alt the new identifier. + * @return the new identifier, or {@code this} if no change. + */ + public ScopedIdentifier<T> rename(final String alt) { + return alt.equals(identifier) ? this : new ScopedIdentifier<>(scope, alt); + } + + /** + * Stores an identified object for this identifier. + * The identifier is typically {@link org.opengis.referencing.IdentifiedObject#getIdentifiers()}. + * If the given identifier is already associated to another identified object, a warning is logged. + * This method can be invoked many times if an object has many identifiers. + * + * @param base limit to follow when storing the object for parent interfaces. + * @param object the identified object to store. Shall be an instance of {@code T}. + * @param caller the class to declare as the source if a warning is logged, or {@code null} for no warning. + * @param method the name of the method to declare as the source if a warning is logged, or {@code null}. + */ + public void store(final Class<T> base, final T object, final Class<?> caller, final String method) { + final Context context = Context.current(); + if (context != null) { + boolean warn = (caller != null); + ScopedIdentifier<?> key = this; + final Class<?>[] parents = Classes.getAllInterfaces(scope); + for (int index = -1 ;;) { + final Object previous = context.identifiedObjects.putIfAbsent(key, object); + if (previous != null && previous != object && previous != DUPLICATED) { + context.identifiedObjects.put(key, DUPLICATED); + if (warn) { + warn = false; // Report warning only once for this identifier. + Context.warningOccured(context, (key == this) ? Level.WARNING : Level.FINE, + caller, method, null, Errors.class, Errors.Keys.DuplicatedIdentifier_1, identifier); + } + } + Class<?> parent; + do if (++index >= parents.length) return; + while (!base.isAssignableFrom(parent = parents[index])); + key = new ScopedIdentifier<>(parent, identifier); + } + } + } + + /** + * Returns the object associated to this scoped identifier. + * + * @param context the unmarshalling context. Shall not be null. + * @return the object associated to this scoped identifier, or {@code null} if none. + */ + public T get(final Context context) { + final Object value = context.identifiedObjects.get(this); + return (value != DUPLICATED) ? scope.cast(value) : null; + } + + /** + * {@return an hash code value for this key}. + */ + @Override + public int hashCode() { + return scope.hashCode() + identifier.hashCode(); + } + + /** + * Tests this key with the given object for equality. + * + * @param obj the object to compare with this key. + * @return whether the two objects are equal. + */ + @Override + public boolean equals(Object obj) { + if (obj instanceof ScopedIdentifier) { + final var other = (ScopedIdentifier<?>) obj; + return scope.equals(other.scope) && identifier.equals(other.identifier); + } + return false; + } + + /** + * {@return a string representation for debugging purposes}. + */ + @Override + public String toString() { + return scope.getSimpleName() + ':' + identifier; + } +} 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 227147518c..2ab6497a00 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 @@ -37,13 +37,11 @@ import org.apache.sis.xml.XML; import org.apache.sis.xml.bind.Context; import org.apache.sis.xml.util.LegacyNamespaces; import org.apache.sis.xml.bind.cat.CodeListUID; -import org.apache.sis.util.ArgumentChecks; import org.apache.sis.util.Version; // Test dependencies import org.junit.After; -import static org.junit.Assert.*; -import static org.opengis.test.Assert.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.*; import static org.apache.sis.metadata.Assertions.assertXmlEquals; @@ -175,7 +173,7 @@ public abstract class TestCase extends org.apache.sis.test.TestCase { */ @After public final void clearContext() { - assertSame("Unexpected context. Is this method invoked from the right thread?", context, Context.current()); + assertSame(context, Context.current(), "Unexpected context. Is this method invoked from the right thread?"); if (context != null) { context.finish(); context = null; @@ -210,7 +208,7 @@ public abstract class TestCase extends org.apache.sis.test.TestCase { protected final void assertMarshalEqualsFile(final InputStream expected, final Object object, final String... ignoredAttributes) throws JAXBException { - assertNotNull("Test resource is not found or not accessible.", expected); + assertNotNull(expected, "Test resource is not found or not accessible."); try (expected) { assertXmlEquals(expected, marshal(object), ignoredAttributes); } catch (IOException e) { @@ -233,7 +231,7 @@ public abstract class TestCase extends org.apache.sis.test.TestCase { protected final void assertMarshalEqualsFile(final InputStream expected, final Object object, final Version metadataVersion, final String... ignoredAttributes) throws JAXBException { - assertNotNull("Test resource is not found or not accessible.", expected); + assertNotNull(expected, "Test resource is not found or not accessible."); try (expected) { assertXmlEquals(expected, marshal(object, metadataVersion), ignoredAttributes); } catch (IOException e) { @@ -261,7 +259,7 @@ public abstract class TestCase extends org.apache.sis.test.TestCase { protected final void assertMarshalEqualsFile(final InputStream expected, final Object object, final Version metadataVersion, final double tolerance, final String[] ignoredNodes, final String[] ignoredAttributes) throws JAXBException { - assertNotNull("Test resource is not found or not accessible.", expected); + assertNotNull(expected, "Test resource is not found or not accessible."); try (expected) { assertXmlEquals(expected, marshal(object, metadataVersion), tolerance, ignoredNodes, ignoredAttributes); } catch (IOException e) { @@ -316,8 +314,8 @@ public abstract class TestCase extends org.apache.sis.test.TestCase { * @see #unmarshal(Unmarshaller, String) */ protected final String marshal(final Marshaller marshaller, final Object object) throws JAXBException { - ArgumentChecks.ensureNonNull("marshaller", marshaller); - ArgumentChecks.ensureNonNull("object", object); + assertNotNull(marshaller, "marshaller"); + assertNotNull(object, "object"); if (buffer == null) { buffer = new StringWriter(); } @@ -340,7 +338,7 @@ public abstract class TestCase extends org.apache.sis.test.TestCase { * @see #assertMarshalEqualsFile(String, Object, String...) */ protected final <T> T unmarshalFile(final Class<T> type, final InputStream input) throws JAXBException { - assertNotNull("Test resource is not found or not accessible.", input); + assertNotNull(input, "Test resource is not found or not accessible."); try (input) { final MarshallerPool pool = getMarshallerPool(); final Unmarshaller unmarshaller = pool.acquireUnmarshaller(); @@ -368,7 +366,7 @@ public abstract class TestCase extends org.apache.sis.test.TestCase { final Unmarshaller unmarshaller = pool.acquireUnmarshaller(); final Object object = unmarshal(unmarshaller, xml); pool.recycle(unmarshaller); - assertInstanceOf("unmarshal", type, object); + assertInstanceOf(type, object, "unmarshal"); return type.cast(object); } @@ -383,8 +381,8 @@ public abstract class TestCase extends org.apache.sis.test.TestCase { * @see #marshal(Marshaller, Object) */ protected final Object unmarshal(final Unmarshaller unmarshaller, final String xml) throws JAXBException { - ArgumentChecks.ensureNonNull("unmarshaller", unmarshaller); - ArgumentChecks.ensureNonNull("xml", xml); + assertNotNull(unmarshaller, "unmarshaller"); + assertNotNull(xml, "xml"); return unmarshaller.unmarshal(new StringReader(xml)); } @@ -396,7 +394,7 @@ public abstract class TestCase extends org.apache.sis.test.TestCase { * @return the date as a {@link Date}. */ protected static Date xmlDate(final String date) { - ArgumentChecks.ensureNonNull("date", date); + assertNotNull(date, "date"); try { synchronized (dateFormat) { return dateFormat.parse(date); diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AbstractIdentifiedObject.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AbstractIdentifiedObject.java index b1a29eeff2..8d4b7d9cc4 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AbstractIdentifiedObject.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/AbstractIdentifiedObject.java @@ -46,6 +46,7 @@ import org.opengis.referencing.AuthorityFactory; import org.opengis.referencing.IdentifiedObject; import org.opengis.referencing.ReferenceSystem; import org.apache.sis.xml.Namespaces; +import org.apache.sis.xml.bind.ScopedIdentifier; import org.apache.sis.xml.bind.UseLegacyMetadata; import org.apache.sis.xml.bind.referencing.Code; import org.apache.sis.xml.bind.metadata.EX_Extent; @@ -125,7 +126,7 @@ import org.opengis.referencing.ObjectDomain; * objects and passed between threads without synchronization. * * @author Martin Desruisseaux (IRD, Geomatys) - * @version 1.4 + * @version 1.5 * @since 0.4 */ @XmlType(name = "IdentifiedObjectType", propOrder = { @@ -1057,6 +1058,7 @@ public class AbstractIdentifiedObject extends FormattableObject implements Ident /** * Invoked by JAXB at unmarshalling time for setting the identifier. + * The identifier is temporarily stored in a map local to the unmarshalling process. */ private void setIdentifier(final Code identifier) { if (identifiers != null) { @@ -1065,6 +1067,11 @@ public class AbstractIdentifiedObject extends FormattableObject implements Ident final Identifier id = identifier.getIdentifier(); if (id != null) { identifiers = Collections.singleton(id); + ScopedIdentifier<IdentifiedObject> key = new ScopedIdentifier<>(getInterface(), identifier.toString()); + key.store(IdentifiedObject.class, this, AbstractIdentifiedObject.class, "setIdentifier"); + if (key != (key = key.rename(identifier.code))) { + key.store(IdentifiedObject.class, this, null, null); // Shorter form without codespace. + } } } } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java index cc2600eaa3..074f173e93 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/referencing/CRS.java @@ -55,19 +55,16 @@ import org.opengis.metadata.extent.GeographicBoundingBox; import org.apache.sis.measure.Units; import org.apache.sis.geometry.Envelopes; import org.apache.sis.geometry.GeneralEnvelope; +import org.apache.sis.system.Modules; +import org.apache.sis.system.Loggers; +import org.apache.sis.xml.bind.Context; +import org.apache.sis.xml.bind.ScopedIdentifier; import org.apache.sis.referencing.util.AxisDirections; import org.apache.sis.referencing.util.EllipsoidalHeightCombiner; import org.apache.sis.referencing.util.PositionalAccuracyConstant; import org.apache.sis.referencing.util.ReferencingUtilities; import org.apache.sis.referencing.util.DefinitionVerifier; import org.apache.sis.referencing.internal.Resources; -import org.apache.sis.system.Modules; -import org.apache.sis.system.Loggers; -import org.apache.sis.util.OptionalCandidate; -import org.apache.sis.util.ArgumentChecks; -import org.apache.sis.util.Utilities; -import org.apache.sis.util.Static; -import org.apache.sis.util.internal.Numerics; import org.apache.sis.referencing.cs.AxisFilter; import org.apache.sis.referencing.cs.CoordinateSystems; import org.apache.sis.referencing.cs.DefaultVerticalCS; @@ -84,6 +81,11 @@ import org.apache.sis.referencing.factory.GeodeticObjectFactory; import org.apache.sis.referencing.factory.UnavailableFactoryException; import org.apache.sis.metadata.iso.extent.DefaultGeographicBoundingBox; import org.apache.sis.metadata.iso.extent.Extents; +import org.apache.sis.util.ArgumentChecks; +import org.apache.sis.util.OptionalCandidate; +import org.apache.sis.util.Static; +import org.apache.sis.util.Utilities; +import org.apache.sis.util.internal.Numerics; import org.apache.sis.util.resources.Errors; import org.apache.sis.util.logging.Logging; @@ -200,6 +202,15 @@ public final class CRS extends Static { * If the EPSG geodetic dataset has been used, the {@linkplain NamedIdentifier#getAuthority() authority} title * will be something like <q>EPSG geodetic dataset</q>, otherwise it will be <q>Subset of EPSG</q>. * + * <h4>Extended set of codes</h4> + * If this method is invoked during the parsing of a GML document, + * then the set of known codes temporarily includes the {@code <gml:identifier>} values + * of all {@link CoordinateReferenceSystem} definitions which have been parsed before this method call. + * Those codes are local to the document being parsed (many documents can be parsed concurrently without conflict), + * and are discarded after the parsing is completed (e.g., on {@link org.apache.sis.xml.XML#unmarshal(String)} return). + * This feature allows embedded or linked data to references a CRS definition + * in the same file or a file included by an {@code xlink:href} attribute. + * * <h4>URI forms</h4> * This method accepts also the URN and URL syntaxes. * For example, the following codes are considered equivalent to {@code "EPSG:4326"}: @@ -251,9 +262,24 @@ public final class CRS extends Static { { ArgumentChecks.ensureNonNull("code", code); try { - return AuthorityFactories.ALL.createCoordinateReferenceSystem(code); - } catch (UnavailableFactoryException e) { - return AuthorityFactories.fallback(e).createCoordinateReferenceSystem(code); + /* + * Gives precedence to the database for consistency reasons. + * The GML definitions are checked only in last resort. + */ + try { + return AuthorityFactories.ALL.createCoordinateReferenceSystem(code); + } catch (UnavailableFactoryException e) { + return AuthorityFactories.fallback(e).createCoordinateReferenceSystem(code); + } + } catch (NoSuchAuthorityCodeException e) { + final Context context = Context.current(); + if (context != null) { + var crs = new ScopedIdentifier<>(CoordinateReferenceSystem.class, code).get(context); + if (crs != null) { + return crs; + } + } + throw e; } } diff --git a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/xml/bind/referencing/Code.java b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/xml/bind/referencing/Code.java index 9681ae9b94..8dd7e0461d 100644 --- a/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/xml/bind/referencing/Code.java +++ b/endorsed/src/org.apache.sis.referencing/main/org/apache/sis/xml/bind/referencing/Code.java @@ -25,9 +25,8 @@ import org.apache.sis.util.internal.Constants; import org.apache.sis.util.internal.DefinitionURI; import org.apache.sis.metadata.internal.NameMeaning; import org.apache.sis.metadata.internal.Identifiers; -import org.apache.sis.referencing.NamedIdentifier; import org.apache.sis.metadata.iso.citation.Citations; -import static org.apache.sis.metadata.iso.citation.Citations.toCodeSpace; +import org.apache.sis.referencing.NamedIdentifier; /** @@ -236,7 +235,7 @@ public final class Code { } } } else { - code.codeSpace = toCodeSpace(authority); + code.codeSpace = Citations.toCodeSpace(authority); } code.code = urn; return code; @@ -247,4 +246,27 @@ public final class Code { } return null; } + + /** + * Returns a string representation of the code in the form {@code codeSpace:code}. + * If the {@link #codeSpace} or the {@link #code} is absent, that part is omitted. + * If the code seems already qualified, e.g., a code in URN or HTTP name space, + * then the code space is also omitted. The intent for the latter condition is to + * avoid spurious components as in {@code "IOGP:urn:ogc:def:parameter:EPSG::8801"}. + * + * <p>This string representation is used as keys in a map of identified objects.</p> + * + * @return {@code codeSpace:code}. + * + * @see org.apache.sis.xml.bind.ScopedIdentifier + */ + @Override + public String toString() { + if (code == null) return codeSpace; + if (codeSpace == null || DefinitionURI.isAbsolute(code)) { + // Above condition may be refined in any future version. + return code; + } + return codeSpace + DefinitionURI.SEPARATOR + code; + } } diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/parameter/ParameterMarshallingTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/parameter/ParameterMarshallingTest.java index 9c9420606a..81227cc0fe 100644 --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/parameter/ParameterMarshallingTest.java +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/parameter/ParameterMarshallingTest.java @@ -32,16 +32,19 @@ import org.opengis.parameter.GeneralParameterDescriptor; import org.apache.sis.measure.Units; import org.apache.sis.measure.Range; import org.apache.sis.measure.MeasurementRange; +import org.apache.sis.system.Loggers; import org.apache.sis.xml.Namespaces; import org.apache.sis.xml.XML; // Test dependencies +import org.junit.Rule; import org.junit.Test; -import static org.junit.Assert.*; +import org.junit.After; +import static org.junit.jupiter.api.Assertions.*; import org.opengis.test.Validators; -import static org.opengis.test.Assert.assertInstanceOf; import org.apache.sis.test.DependsOn; import org.apache.sis.test.DependsOnMethod; +import org.apache.sis.test.LoggingWatcher; import org.apache.sis.xml.test.TestCase; import static org.apache.sis.metadata.Assertions.assertXmlEquals; import static org.apache.sis.referencing.Assertions.assertAliasTipEquals; @@ -58,6 +61,21 @@ import static org.apache.sis.referencing.Assertions.assertEpsgNameAndIdentifierE DefaultParameterValueGroupTest.class }) public final class ParameterMarshallingTest extends TestCase { + /** + * A JUnit {@link Rule} for listening to log events. This field is public because JUnit requires us to + * do so, but should be considered as an implementation details (it should have been a private field). + */ + @Rule + public final LoggingWatcher loggings = new LoggingWatcher(Loggers.XML); + + /** + * Verifies that no unexpected warning has been emitted in any test defined in this class. + */ + @After + public void assertNoUnexpectedLog() { + loggings.assertNoUnexpectedLog(); + } + /** * Creates a new test case. */ @@ -114,23 +132,25 @@ public final class ParameterMarshallingTest extends TestCase { assertXmlEquals(expected, xml, "xmlns:*"); final DefaultParameterValue<?> r = (DefaultParameterValue<?>) XML.unmarshal(xml); if (!Objects.deepEquals(parameter.getValue(), r.getValue())) { - // If we enter in this block, then the line below should always fail. - // But we use this assertion for getting a better error message. - assertEquals("value", parameter.getValue(), r.getValue()); + /* + * If we enter in this block, then the line below should always fail. + * But we use this assertion for getting a better error message. + */ + assertEquals(parameter.getValue(), r.getValue(), "value"); } - assertEquals("unit", parameter.getUnit(), r.getUnit()); + assertEquals(parameter.getUnit(), r.getUnit(), "unit"); /* * Verify the descriptor, especially the 'valueClass' property. That property is not part of GML, * so Apache SIS has to rely on some tricks for finding this information (see CC_OperationParameter). */ final ParameterDescriptor<?> reference = parameter.getDescriptor(); final ParameterDescriptor<?> descriptor = r.getDescriptor(); - assertNotNull("descriptor", descriptor); - assertEquals ("descriptor.name", reference.getName(), descriptor.getName()); - assertEquals ("descriptor.unit", reference.getUnit(), descriptor.getUnit()); - assertEquals ("descriptor.valueClass", reference.getValueClass(), descriptor.getValueClass()); - assertEquals ("descriptor.minimumOccurs", reference.getMinimumOccurs(), descriptor.getMinimumOccurs()); - assertEquals ("descriptor.maximumOccurs", reference.getMaximumOccurs(), descriptor.getMaximumOccurs()); + assertNotNull( descriptor, "descriptor"); + assertEquals (reference.getName(), descriptor.getName(), "descriptor.name"); + assertEquals (reference.getUnit(), descriptor.getUnit(), "descriptor.unit"); + assertEquals (reference.getValueClass(), descriptor.getValueClass(), "descriptor.valueClass"); + assertEquals (reference.getMinimumOccurs(), descriptor.getMinimumOccurs(), "descriptor.minimumOccurs"); + assertEquals (reference.getMaximumOccurs(), descriptor.getMaximumOccurs(), "descriptor.maximumOccurs"); Validators.validate(r); } @@ -144,24 +164,26 @@ public final class ParameterMarshallingTest extends TestCase { final DefaultParameterDescriptor<Double> descriptor = new DefaultParameterDescriptor<>( Map.of(DefaultParameterDescriptor.NAME_KEY, "A descriptor"), 0, 1, Double.class, null, null, null); + final String xml = XML.marshal(descriptor); assertXmlEquals( "<gml:OperationParameter xmlns:gml=\"" + Namespaces.GML + "\">\n" + " <gml:name>A descriptor</gml:name>\n" + " <gml:minimumOccurs>0</gml:minimumOccurs>\n" + "</gml:OperationParameter>", xml, "xmlns:*"); - final DefaultParameterDescriptor<?> r = (DefaultParameterDescriptor<?>) XML.unmarshal(xml); - assertEquals("name", "A descriptor", r.getName().getCode()); - assertEquals("minimumOccurs", 0, r.getMinimumOccurs()); - assertEquals("maximumOccurs", 1, r.getMaximumOccurs()); + + final var r = (DefaultParameterDescriptor<?>) XML.unmarshal(xml); + assertEquals("A descriptor", r.getName().getCode(), "name"); + assertEquals(0, r.getMinimumOccurs(), "minimumOccurs"); + assertEquals(1, r.getMaximumOccurs(), "maximumOccurs"); /* * A DefaultParameterDescriptor with null 'valueClass' is illegal, but there is no way we can guess * this information if the <gml:OperationParameter> element was not a child of <gml:ParameterValue>. * The current implementation leaves 'valueClass' to null despite being illegal. This behavior may * change in any future Apache SIS version. */ - assertNull("valueDomain", r.getValueDomain()); - assertNull("valueClass", r.getValueClass()); // May change in any future SIS release. + assertNull(r.getValueDomain(), "valueDomain"); + assertNull(r.getValueClass(), "valueClass"); // May change in any future SIS release. } /** @@ -346,7 +368,7 @@ public final class ParameterMarshallingTest extends TestCase { verifyDescriptor(8805, "Scale factor at natural origin", "scale_factor", true, it.next()); verifyDescriptor(8806, "False easting", "false_easting", false, it.next()); verifyDescriptor(8807, "False northing", "false_northing", false, it.next()); - assertFalse("Unexpected parameter.", it.hasNext()); + assertFalse(it.hasNext()); } /** @@ -363,8 +385,8 @@ public final class ParameterMarshallingTest extends TestCase { { assertEpsgNameAndIdentifierEqual(name, code, descriptor); assertAliasTipEquals(alias, descriptor); - assertEquals("maximumOccurs", 1, descriptor.getMaximumOccurs()); - assertEquals("minimumOccurs", required ? 1 : 0, descriptor.getMinimumOccurs()); + assertEquals(1, descriptor.getMaximumOccurs(), "maximumOccurs"); + assertEquals(required ? 1 : 0, descriptor.getMinimumOccurs(), "minimumOccurs"); } /** @@ -382,15 +404,15 @@ public final class ParameterMarshallingTest extends TestCase { final double value, final Unit<?> unit, final GeneralParameterDescriptor descriptor, final GeneralParameterValue parameter) { - assertInstanceOf(name, ParameterValue.class, parameter); + assertInstanceOf(ParameterValue.class, parameter, name); final ParameterValue<?> p = (ParameterValue<?>) parameter; final ParameterDescriptor<?> d = p.getDescriptor(); verifyDescriptor(code, name, alias, true, d); - assertSame ("descriptor", descriptor, d); - assertEquals("value", value, p.doubleValue(), STRICT); - assertEquals("unit", unit, p.getUnit()); - assertEquals("valueClass", Double.class, d.getValueClass()); - assertEquals("unit", unit, d.getUnit()); + assertSame (descriptor, d, "descriptor"); + assertEquals(value, p.doubleValue(), "value"); + assertEquals(unit, p.getUnit(), "unit"); + assertEquals(Double.class, d.getValueClass(), "valueClass"); + assertEquals(unit, d.getUnit(), "unit"); } /** @@ -429,6 +451,9 @@ public final class ParameterMarshallingTest extends TestCase { @DependsOnMethod("testValueGroupUnmarshalling") public void testDuplicatedParametersUnmarshalling() throws JAXBException { testValueGroupUnmarshalling(TestFile.DUPLICATED); + loggings.assertNextLogContains("EPSG::8801"); + loggings.assertNextLogContains("EPSG::8802"); + loggings.assertNextLogContains("EPSG::8805"); } /** @@ -442,6 +467,6 @@ public final class ParameterMarshallingTest extends TestCase { verifyParameter(8801, "Latitude of natural origin", "latitude_of_origin", 40, Units.DEGREE, itd.next(), it.next()); verifyParameter(8802, "Longitude of natural origin", "central_meridian", -60, Units.DEGREE, itd.next(), it.next()); verifyParameter(8805, "Scale factor at natural origin", "scale_factor", 1, Units.UNITY, itd.next(), it.next()); - assertFalse("Unexpected parameter.", it.hasNext()); + assertFalse(it.hasNext()); } } diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/SingleOperationMarshallingTest.java b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/SingleOperationMarshallingTest.java index b84fece8c9..bc07a02972 100644 --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/SingleOperationMarshallingTest.java +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/SingleOperationMarshallingTest.java @@ -16,7 +16,6 @@ */ package org.apache.sis.referencing.operation; -import java.util.Map; import java.util.HashMap; import java.util.Iterator; import java.io.InputStream; @@ -32,23 +31,26 @@ import org.opengis.referencing.crs.CoordinateReferenceSystem; import org.opengis.referencing.crs.GeodeticCRS; import org.opengis.referencing.operation.MathTransform; import org.opengis.referencing.operation.OperationMethod; -import org.apache.sis.measure.Units; -import org.apache.sis.parameter.ParameterBuilder; import org.apache.sis.referencing.operation.provider.Mercator1SP; -import org.apache.sis.xml.Namespaces; -import org.apache.sis.xml.XML; import org.apache.sis.referencing.operation.transform.LinearTransform; import org.apache.sis.referencing.operation.matrix.Matrix3; +import org.apache.sis.parameter.ParameterBuilder; +import org.apache.sis.measure.Units; +import org.apache.sis.system.Loggers; +import org.apache.sis.xml.Namespaces; +import org.apache.sis.xml.XML; import static org.apache.sis.metadata.iso.citation.Citations.EPSG; // Test dependencies +import org.junit.Rule; import org.junit.Test; -import static org.junit.Assert.*; +import org.junit.After; +import static org.junit.jupiter.api.Assertions.*; import org.opengis.test.Validators; -import static org.opengis.test.Assert.assertInstanceOf; import org.apache.sis.xml.bind.referencing.CC_OperationParameterGroupTest; import org.apache.sis.test.DependsOn; import org.apache.sis.test.DependsOnMethod; +import org.apache.sis.test.LoggingWatcher; import org.apache.sis.xml.test.TestCase; import static org.apache.sis.test.TestUtilities.getSingleton; import static org.apache.sis.metadata.Assertions.assertXmlEquals; @@ -69,6 +71,21 @@ import static org.opengis.test.Assert.assertMatrixEquals; org.apache.sis.parameter.ParameterMarshallingTest.class }) public final class SingleOperationMarshallingTest extends TestCase { + /** + * A JUnit {@link Rule} for listening to log events. This field is public because JUnit requires us to + * do so, but should be considered as an implementation details (it should have been a private field). + */ + @Rule + public final LoggingWatcher loggings = new LoggingWatcher(Loggers.XML); + + /** + * Verifies that no unexpected warning has been emitted in any test defined in this class. + */ + @After + public void assertNoUnexpectedLog() { + loggings.assertNoUnexpectedLog(); + } + /** * Creates a new test case. */ @@ -91,7 +108,7 @@ public final class SingleOperationMarshallingTest extends TestCase { * Creates the test operation method. */ private static DefaultOperationMethod createMercatorMethod() { - final ParameterBuilder builder = new ParameterBuilder(); + final var builder = new ParameterBuilder(); builder.setCodeSpace(EPSG, "EPSG").setRequired(true); ParameterDescriptor<?>[] parameters = { builder.addIdentifier("8801").addName("Latitude of natural origin" ).create(0, Units.DEGREE), @@ -100,7 +117,7 @@ public final class SingleOperationMarshallingTest extends TestCase { }; builder.addName(null, "Mercator (1SP)"); final ParameterDescriptorGroup descriptor = builder.createGroup(parameters); - final Map<String,Object> properties = new HashMap<>(4); + final var properties = new HashMap<String,Object>(4); properties.put(DefaultOperationMethod.NAME_KEY, descriptor.getName()); properties.put(DefaultOperationMethod.FORMULA_KEY, new DefaultFormula("See EPSG guide.")); return new DefaultOperationMethod(properties, descriptor); @@ -131,7 +148,7 @@ public final class SingleOperationMarshallingTest extends TestCase { " </gml:parameter>\n" + "</gml:OperationMethod>", xml, "xmlns:*"); - final OperationMethod method = (OperationMethod) XML.unmarshal(xml); + final var method = (OperationMethod) XML.unmarshal(xml); verifyMethod(method); Validators.validate(method); } @@ -141,13 +158,13 @@ public final class SingleOperationMarshallingTest extends TestCase { */ private static void verifyMethod(final OperationMethod method) { assertIdentifierEquals("name", null, null, null, "Mercator (1SP)", method.getName()); - assertEquals("formula", "See EPSG guide.", method.getFormula().getFormula().toString()); + assertEquals("See EPSG guide.", method.getFormula().getFormula().toString(), "formula"); final ParameterDescriptorGroup parameters = method.getParameters(); - assertEquals("parameters.name", "Mercator (1SP)", parameters.getName().getCode()); + assertEquals("Mercator (1SP)", parameters.getName().getCode(), "parameters.name"); final Iterator<GeneralParameterDescriptor> it = parameters.descriptors().iterator(); CC_OperationParameterGroupTest.verifyMethodParameter(Mercator1SP.LATITUDE_OF_ORIGIN, (ParameterDescriptor<?>) it.next()); CC_OperationParameterGroupTest.verifyMethodParameter(Mercator1SP.LONGITUDE_OF_ORIGIN, (ParameterDescriptor<?>) it.next()); - assertFalse("Unexpected parameter.", it.hasNext()); + assertFalse(it.hasNext()); } /** @@ -159,36 +176,41 @@ public final class SingleOperationMarshallingTest extends TestCase { @DependsOnMethod("testOperationMethod") public void testConversionUnmarshalling() throws JAXBException { final DefaultConversion c = unmarshalFile(DefaultConversion.class, openTestFile(false)); - assertEquals("name", "World Mercator", c.getName().getCode()); - assertEquals("identifier", "3395", getSingleton(c.getIdentifiers()).getCode()); - assertEquals("scope", "Very small scale mapping.", String.valueOf(c.getScope())); - assertNull ("operationVersion", c.getOperationVersion()); + assertEquals("World Mercator", c.getName().getCode(), "name"); + assertEquals("3395", getSingleton(c.getIdentifiers()).getCode(), "identifier"); + assertEquals("Very small scale mapping.", String.valueOf(c.getScope()), "scope"); + assertNull (c.getOperationVersion(), "operationVersion"); - final GeographicBoundingBox e = (GeographicBoundingBox) getSingleton(c.getDomainOfValidity().getGeographicElements()); - assertEquals("eastBoundLongitude", +180, e.getEastBoundLongitude(), STRICT); - assertEquals("westBoundLongitude", -180, e.getWestBoundLongitude(), STRICT); - assertEquals("northBoundLatitude", 84, e.getNorthBoundLatitude(), STRICT); - assertEquals("southBoundLatitude", -80, e.getSouthBoundLatitude(), STRICT); + final var e = (GeographicBoundingBox) getSingleton(c.getDomainOfValidity().getGeographicElements()); + assertEquals(+180, e.getEastBoundLongitude(), "eastBoundLongitude"); + assertEquals(-180, e.getWestBoundLongitude(), "westBoundLongitude"); + assertEquals( 84, e.getNorthBoundLatitude(), "northBoundLatitude"); + assertEquals( -80, e.getSouthBoundLatitude(), "southBoundLatitude"); // This is a defining conversion, so we do not expect CRS. - assertNull("sourceCRS", c.getSourceCRS()); - assertNull("targetCRS", c.getTargetCRS()); - assertNull("interpolationCRS", c.getInterpolationCRS()); - assertNull("mathTransform", c.getMathTransform()); + assertNull(c.getSourceCRS(), "sourceCRS"); + assertNull(c.getTargetCRS(), "targetCRS"); + assertNull(c.getInterpolationCRS(), "interpolationCRS"); + assertNull(c.getMathTransform(), "mathTransform"); // The most difficult part. final OperationMethod method = c.getMethod(); - assertNotNull("method", method); + assertNotNull(method, "method"); verifyMethod(method); final ParameterValueGroup parameters = c.getParameterValues(); - assertNotNull("parameters", parameters); + assertNotNull(parameters, "parameters"); final Iterator<GeneralParameterValue> it = parameters.values().iterator(); verifyParameter(method, parameters, -0.0, (ParameterValue<?>) it.next()); verifyParameter(method, parameters, -90.0, (ParameterValue<?>) it.next()); - assertFalse("Unexpected parameter.", it.hasNext()); - + assertFalse(it.hasNext()); + /* + * Validate object, then discard warnings caused by duplicated identifiers. + * Those duplications are intentional, see comment in `Conversion.xml`. + */ Validators.validate(c); + loggings.assertNextLogContains("EPSG::8801"); + loggings.assertNextLogContains("EPSG::8802"); } /** @@ -205,9 +227,9 @@ public final class SingleOperationMarshallingTest extends TestCase { { final ParameterDescriptor<?> descriptor = parameter.getDescriptor(); final String name = descriptor.getName().getCode(); - assertSame("parameterValues.descriptor", descriptor, group.getDescriptor().descriptor(name)); - assertSame("method.descriptor", descriptor, method.getParameters().descriptor(name)); - assertEquals("value", expectedValue, parameter.doubleValue(), STRICT); + assertSame(descriptor, group.getDescriptor().descriptor(name), "parameterValues.descriptor"); + assertSame(descriptor, method.getParameters().descriptor(name), "method.descriptor"); + assertEquals(expectedValue, parameter.doubleValue(), "value"); } /** @@ -219,50 +241,54 @@ public final class SingleOperationMarshallingTest extends TestCase { @DependsOnMethod("testConversionUnmarshalling") public void testTransformationUnmarshalling() throws JAXBException { final DefaultTransformation c = unmarshalFile(DefaultTransformation.class, openTestFile(true)); - assertEquals("name", "NTF (Paris) to NTF (1)", c.getName().getCode()); - assertEquals("identifier", "1763", getSingleton(c.getIdentifiers()).getCode()); - assertEquals("scope", "Change of prime meridian.", String.valueOf(c.getScope())); - assertEquals("operationVersion", "IGN-Fra", c.getOperationVersion()); + assertEquals("NTF (Paris) to NTF (1)", c.getName().getCode(), "name"); + assertEquals("1763", getSingleton(c.getIdentifiers()).getCode(), "identifier"); + assertEquals("Change of prime meridian.", String.valueOf(c.getScope()), "scope"); + assertEquals("IGN-Fra", c.getOperationVersion(), "operationVersion"); final OperationMethod method = c.getMethod(); - assertNotNull("method", method); - assertEquals ("method.name", "Longitude rotation", method.getName().getCode()); - assertEquals ("method.identifier", "9601", getSingleton(method.getIdentifiers()).getCode()); - assertEquals ("method.formula", "Target_longitude = Source_longitude + longitude_offset.", method.getFormula().getFormula().toString()); + assertNotNull(method, "method"); + assertEquals("Longitude rotation", method.getName().getCode(), "method.name"); + assertEquals("9601", getSingleton(method.getIdentifiers()).getCode(), "method.identifier"); + assertEquals("Target_longitude = Source_longitude + longitude_offset.", method.getFormula().getFormula().toString(), "method.formula"); - final ParameterDescriptor<?> descriptor = (ParameterDescriptor<?>) getSingleton(method.getParameters().descriptors()); - assertEquals("descriptor.name", "Longitude offset", descriptor.getName().getCode()); - assertEquals("descriptor.identifier", "8602", getSingleton(descriptor.getIdentifiers()).getCode()); - assertEquals("descriptor.valueClass", Double.class, descriptor.getValueClass()); + final var descriptor = (ParameterDescriptor<?>) getSingleton(method.getParameters().descriptors()); + assertEquals("Longitude offset", descriptor.getName().getCode(), "descriptor.name"); + assertEquals("8602", getSingleton(descriptor.getIdentifiers()).getCode(), "descriptor.identifier"); + assertEquals(Double.class, descriptor.getValueClass(), "descriptor.valueClass"); final ParameterValueGroup parameters = c.getParameterValues(); - assertNotNull("parameters", parameters); - assertSame("parameters.descriptors", method.getParameters(), parameters.getDescriptor()); + assertNotNull(parameters, "parameters"); + assertSame(method.getParameters(), parameters.getDescriptor(), "parameters.descriptors"); - final ParameterValue<?> parameter = (ParameterValue<?>) getSingleton(parameters.values()); - assertSame ("parameters.descriptor", descriptor, parameter.getDescriptor()); - assertEquals("parameters.unit", Units.GRAD, parameter.getUnit()); - assertEquals("parameters.value", 2.5969213, parameter.getValue()); + final var parameter = (ParameterValue<?>) getSingleton(parameters.values()); + assertSame (descriptor, parameter.getDescriptor(), "parameters.descriptor"); + assertEquals(Units.GRAD, parameter.getUnit(), "parameters.unit"); + assertEquals(2.5969213, parameter.getValue(), "parameters.value"); final CoordinateReferenceSystem sourceCRS = c.getSourceCRS(); - assertInstanceOf("sourceCRS", GeodeticCRS.class, sourceCRS); - assertEquals ("sourceCRS.name", "NTF (Paris)", sourceCRS.getName().getCode()); - assertEquals ("sourceCRS.scope", "Geodetic survey.", sourceCRS.getScope().toString()); - assertEquals ("sourceCRS.identifier", "4807", getSingleton(sourceCRS.getIdentifiers()).getCode()); + assertInstanceOf(GeodeticCRS.class, sourceCRS, "sourceCRS"); + assertEquals("NTF (Paris)", sourceCRS.getName().getCode(), "sourceCRS.name"); + assertEquals("Geodetic survey.", sourceCRS.getScope().toString(), "sourceCRS.scope"); + assertEquals("4807", getSingleton(sourceCRS.getIdentifiers()).getCode(), "sourceCRS.identifier"); final CoordinateReferenceSystem targetCRS = c.getTargetCRS(); - assertInstanceOf("targetCRS", GeodeticCRS.class, targetCRS); - assertEquals ("targetCRS.name", "NTF", targetCRS.getName().getCode()); - assertEquals ("targetCRS.scope", "Geodetic survey.", targetCRS.getScope().toString()); - assertEquals ("targetCRS.identifier", "4275", getSingleton(targetCRS.getIdentifiers()).getCode()); + assertInstanceOf(GeodeticCRS.class, targetCRS, "targetCRS"); + assertEquals("NTF", targetCRS.getName().getCode(), "targetCRS.name"); + assertEquals("Geodetic survey.", targetCRS.getScope().toString(), "targetCRS.scope"); + assertEquals("4275", getSingleton(targetCRS.getIdentifiers()).getCode(), "targetCRS.identifier"); final MathTransform tr = c.getMathTransform(); - assertInstanceOf("mathTransform", LinearTransform.class, tr); + assertInstanceOf(LinearTransform.class, tr, "mathTransform"); assertMatrixEquals("mathTransform.matrix", new Matrix3(1, 0, 0, 0, 1, 2.33722917, 0, 0, 1), ((LinearTransform) tr).getMatrix(), STRICT); - + /* + * Validate object, then discard warnings caused by duplicated identifiers. + * Those duplications are intentional, see comment in `Transformation.xml`. + */ Validators.validate(c); + loggings.assertNextLogContains("EPSG::8602"); } } diff --git a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/Transformation.xml b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/Transformation.xml index 780cf53be1..d5acfc6525 100644 --- a/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/Transformation.xml +++ b/endorsed/src/org.apache.sis.referencing/test/org/apache/sis/referencing/operation/Transformation.xml @@ -69,8 +69,8 @@ <gml:scope>Topographic mapping.</gml:scope> <gml:primeMeridian> <gml:PrimeMeridian gml:id="ParisMeridian"> - <gml:identifier codeSpace="IOGP">urn:ogc:def:meridian:EPSG::8901</gml:identifier> - <gml:name>Greenwich</gml:name> + <gml:identifier codeSpace="IOGP">urn:ogc:def:meridian:EPSG::8903</gml:identifier> + <gml:name>Paris</gml:name> <gml:greenwichLongitude uom="urn:ogc:def:uom:EPSG::9105">2.5969213</gml:greenwichLongitude> </gml:PrimeMeridian> </gml:primeMeridian> diff --git a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/DefinitionURI.java b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/DefinitionURI.java index 8ec72c4001..e57f627865 100644 --- a/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/DefinitionURI.java +++ b/endorsed/src/org.apache.sis.util/main/org/apache/sis/util/internal/DefinitionURI.java @@ -455,10 +455,10 @@ public final class DefinitionURI { * Returns {@code true} if a sub-region of {@code urn} matches the given {@code part}, * ignoring case, leading and trailing whitespaces. * - * @param part the expected part ({@code "urn"}, {@code "ogc"}, {@code "def"}, <i>etc.</i>) - * @param urn the URN for which to test a subregion. - * @param lower index of the first character in {@code urn} to compare, after skipping whitespaces. - * @param upper index after the last character in {@code urn} to compare, ignoring whitespaces. + * @param part the expected part ({@code "urn"}, {@code "ogc"}, {@code "def"}, <i>etc.</i>) + * @param urn the URN for which to test a subregion. + * @param lower index of the first character in {@code urn} to compare, after skipping whitespaces. + * @param upper index after the last character in {@code urn} to compare, ignoring whitespaces. * @return {@code true} if the given sub-region of {@code urn} match the given part. */ public static boolean regionMatches(final String part, final String urn, int lower, int upper) { @@ -485,6 +485,20 @@ public final class DefinitionURI { return -1; } + /** + * Returns {@code true} if the given URI is recognized as an URN or URL. + * The details of this check may change in any future Apache SIS version. + * + * @param uri the URI to check. + * @return whether the given URI seems to be an URN or URL. + */ + public static boolean isAbsolute(final String uri) { + final int s = uri.indexOf(SEPARATOR); + if (s <= 0) return false; + final String c = CharSequences.trimWhitespaces(uri, 0, s).toString(); + return c.equalsIgnoreCase("urn") || c.equalsIgnoreCase(Constants.HTTP) || c.equalsIgnoreCase(Constants.HTTPS); + } + /** * Returns the code part of the given URI, provided that it matches the given object type and authority. * This method is useful when: diff --git a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/internal/DefinitionURITest.java b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/internal/DefinitionURITest.java index f361f385d8..668d3957d6 100644 --- a/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/internal/DefinitionURITest.java +++ b/endorsed/src/org.apache.sis.util/test/org/apache/sis/util/internal/DefinitionURITest.java @@ -192,6 +192,16 @@ public final class DefinitionURITest extends TestCase { + "2=http://www.opengis.net/def/crs/EPSG/9.1/5701", parsed.toString()); } + /** + * Tests {@link DefinitionURI#isAbsolute(String)}. + */ + @Test + public void testIsAbsolute() { + assertTrue (DefinitionURI.isAbsolute("http://www.opengis.net/def/crs/EPSG/0/4326")); + assertTrue (DefinitionURI.isAbsolute("urn:ogc:def:crs:EPSG:8.2:4326")); + assertFalse(DefinitionURI.isAbsolute("EPSG:4326")); + } + /** * Convenience method invoking {@link DefinitionURI#codeOf(String, String[], CharSequence)} * with a single authority.