sis-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From desruisse...@apache.org
Subject [sis] branch geoapi-4.0 updated: Automatically add identifier when the WKT provided by `WKTDictionary.fetchDefinition(…)` does not contain an ID[…] or AUTHORITY[…] element. This is needed when the WKT is provided by the "spatial_ref_sys" table if a PostGIS spatial database among others.
Date Sun, 22 Nov 2020 12:57:36 GMT
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 89f8591  Automatically add identifier when the WKT provided by `WKTDictionary.fetchDefinition(…)`
does not contain an ID[…] or AUTHORITY[…] element. This is needed when the WKT is provided
by the "spatial_ref_sys" table if a PostGIS spatial database among others.
89f8591 is described below

commit 89f85913a1e6a36573b4acb2a19d37d8b906856b
Author: Martin Desruisseaux <martin.desruisseaux@geomatys.com>
AuthorDate: Sun Nov 22 13:48:28 2020 +0100

    Automatically add identifier when the WKT provided by `WKTDictionary.fetchDefinition(…)`
does not contain an ID[…] or AUTHORITY[…] element.
    This is needed when the WKT is provided by the "spatial_ref_sys" table if a PostGIS spatial
database among others.
---
 .../main/java/org/apache/sis/io/wkt/Element.java   |  8 +++
 .../apache/sis/io/wkt/GeodeticObjectParser.java    | 25 +++++++-
 .../java/org/apache/sis/io/wkt/WKTDictionary.java  | 41 ++++++++------
 .../main/java/org/apache/sis/io/wkt/WKTFormat.java | 48 +++++++++++++++-
 .../referencing/factory/GeodeticObjectFactory.java |  6 +-
 .../org/apache/sis/io/wkt/WKTDictionaryTest.java   | 66 ++++++++++++++++++++++
 6 files changed, 172 insertions(+), 22 deletions(-)

diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Element.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Element.java
index 68c309c..4a6349f 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Element.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/Element.java
@@ -110,6 +110,13 @@ final class Element {
     final boolean isFragment;
 
     /**
+     * Whether this element is the root of a tree of WKT elements.
+     * This field is not used by this {@link Element} class.
+     * It is provided for {@link WKTDictionary} convenience.
+     */
+    boolean isRoot;
+
+    /**
      * An ordered sequence of {@link String}s, {@link Number}s and other {@link Element}s.
      * Access to this collection should be done using the iterator, not by random access.
      * Parsing will remove elements (in any order) from this list as they are consumed.
@@ -160,6 +167,7 @@ final class Element {
         isFragment    = false;
         children      = new LinkedList<>();     // Needs to be a modifiable collection.
         children.add(singleton);
+        singleton.isRoot = true;
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
index 706f0f4..03117d4 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/GeodeticObjectParser.java
@@ -98,7 +98,7 @@ import static java.util.Collections.singletonMap;
  * @author  Rémi Eve (IRD)
  * @author  Martin Desruisseaux (IRD, Geomatys)
  * @author  Johann Sorel (Geomatys)
- * @version 1.0
+ * @version 1.1
  * @since   0.6
  * @module
  */
@@ -228,6 +228,22 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo
     }
 
     /**
+     * Completes or edits properties of the root {@link IdentifiedObject}. This method is
invoked
+     * before a {@code Factory.createFoo(Map, …)} method is invoked for creating the root
object.
+     * The {@code properties} map is filled with all information that this parser found in
the WKT elements.
+     * Subclasses can override this method for adding additional information if desired.
+     *
+     * <p>The most typical use case is to add a default {@link Identifier} when the
WKT does not contain
+     * an explicit {@code ID[…]} or {@code AUTHORITY[…]} element.</p>
+     *
+     * @param  properties  the properties to be given in a call to a {@code createFoo(Map,
…)} method.
+     *
+     * @see org.apache.sis.referencing.factory.GeodeticObjectFactory#complete(Map)
+     */
+    void completeRoot(Map<String,Object> properties) {
+    }
+
+    /**
      * Parses a <cite>Well-Know Text</cite> from specified position as a geodetic
object.
      * Caller should invoke {@link #getAndClearWarnings(Object)} in a {@code finally} block
      * after this method.
@@ -374,7 +390,7 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo
 
     /**
      * Parses an <strong>optional</strong> metadata elements and close.
-     * This include elements like {@code "SCOPE"}, {@code "ID"} (WKT 2) or {@code "AUTHORITY"}
(WKT 1).
+     * This includes elements like {@code "SCOPE"}, {@code "ID"} (WKT 2) or {@code "AUTHORITY"}
(WKT 1).
      * This WKT 1 element has the following pattern:
      *
      * {@preformat wkt
@@ -546,6 +562,9 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo
             }
         }
         parent.close(ignoredElements);
+        if (parent.isRoot) {
+            completeRoot(properties);
+        }
         return properties;
     }
 
@@ -1822,7 +1841,7 @@ class GeodeticObjectParser extends MathTransformParser implements Comparator<Coo
             final PrimeMeridian meridian = parsePrimeMeridian(OPTIONAL, element, isWKT1,
longitudeUnit);
             final GeodeticDatum datum = parseDatum(MANDATORY, element, meridian);
             final Map<String,?> properties = parseMetadataAndClose(element, name, datum);
-            if (cs instanceof EllipsoidalCS) {  // By far the most frequent case.
+            if (cs instanceof EllipsoidalCS) {                                  // By far
the most frequent case.
                 return crsFactory.createGeographicCRS(properties, datum, (EllipsoidalCS)
cs);
             }
             if (cs instanceof CartesianCS) {                                    // The second
most frequent case.
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java
index d7e6d7b..b0f79bd 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTDictionary.java
@@ -47,6 +47,7 @@ import org.apache.sis.internal.util.CollectionsExt;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.internal.util.Strings;
 import org.apache.sis.internal.jdk9.JDK9;
+import org.apache.sis.metadata.iso.DefaultIdentifier;
 import org.apache.sis.metadata.iso.citation.Citations;
 import org.apache.sis.util.CharSequences;
 import org.apache.sis.util.ArgumentChecks;
@@ -71,7 +72,7 @@ import org.apache.sis.util.iso.SimpleInternationalString;
  * <ul>
  *   <li>Invoke {@link #load(BufferedReader)} for reading definitions from file(s).</li>
  *   <li>Invoke {@link #addDefinitions(Stream)} for providing definitions from an arbitrary
source.</li>
- *   <li>Override {@link #fetchDefinition(String, String, String)} in a subclass for
fetching WKT definitions
+ *   <li>Override {@link #fetchDefinition(DefaultIdentifier)} in a subclass for fetching
WKT definitions
  *       on-the-fly (for example from the {@code "spatial_ref_sys"} table of a spatial database.</li>
  * </ul>
  *
@@ -740,23 +741,25 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
 
     /**
      * Parses immediately the given WKT and caches the result under the given identifier.
This method is invoked
-     * only if subclass overrides {@link #fetchDefinition(String, String, String)} for producing
WKT on-the-fly.
+     * only if subclass overrides {@link #fetchDefinition(DefaultIdentifier)} for producing
WKT on-the-fly.
      *
-     * @param  codespace  the authority (or other kind of code space) providing CRS definitions.
-     * @param  version    version of the CRS definition, or {@code null} if unspecified.
-     * @param  code       code allocated by the authority for the CRS definition.
-     * @param  wkt         the Well-Known Text to parse immediately.
+     * @param  codespace          the authority (or other kind of code space) providing CRS
definitions.
+     * @param  version            version of the CRS definition, or {@code null} if unspecified.
+     * @param  code               code allocated by the authority for the CRS definition.
+     * @param  wkt                the Well-Known Text to parse immediately.
+     * @param  defaultIdentifier  identifier to assign to the object if the WKT does not
provide one.
      * @return the parsed object.
      * @throws FactoryException if parsing failed.
      */
     private IdentifiedObject parseAndAdd(final String codespace, final String version,
-            final String code, final String wkt) throws FactoryException
+            final String code, final String wkt, final Identifier defaultIdentifier) throws
FactoryException
     {
         ArgumentChecks.ensureNonEmpty("code", code);
         ArgumentChecks.ensureNonEmpty("wkt",  wkt);
         lock.writeLock().lock();
         try {
             try {
+                parser.setDefaultIdentifier(defaultIdentifier);
                 final Object object = parser.parseObject(wkt);
                 if (!(object instanceof IdentifiedObject)) {
                     throw new FactoryDataException(parser.errors().getString(
@@ -770,6 +773,7 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
             } catch (ParseException | IllegalArgumentException e) {
                 throw new FactoryDataException(e.getLocalizedMessage());
             } finally {
+                parser.setDefaultIdentifier(null);
                 parser.clear();
                 updateAuthority();
             }
@@ -779,24 +783,28 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
     }
 
     /**
-     * Fetches the Well-Known Text for a user-specified code not found in this {@code WKTDictionary}.
+     * Fetches the Well-Known Text for a user-specified identifier not found in this {@code
WKTDictionary}.
      * Subclasses can override this method if WKT strings are not {@linkplain #load(BufferedReader)
loaded}
      * or {@linkplain #addDefinitions(Stream) specified} in advance, but instead fetched
when first needed.
      * An example of such scenario is WKTs provided by the {@code "spatial_ref_sys"} table
of a spatial database.
-     * If no WKT is found for the given code, then this method returns {@code null}.
+     * If no WKT is found for the given identifier, then this method returns {@code null}.
+     *
+     * <p>On input, {@code identifier} contains only the pieces of information provided
by user. For example if user
+     * invoked {@code createGeographicCRS("Foo")}, then the identifier {@linkplain DefaultIdentifier#getCode()
code}
+     * will be {@code "Foo"} but the {@linkplain DefaultIdentifier#getCodeSpace() codespace}
and
+     * {@linkplain DefaultIdentifier#getVersion() version} will be undefined ({@code null}).
+     * On output, {@code identifier} should be completed with missing code space and version
(if available).</p>
      *
      * <h4>Overriding</h4>
      * The default implementation returns {@code null}. If a subclass overrides this method,
then it should
      * also override {@link #getAuthorityCodes(Class)} because {@code WKTDictionary} does
not know the codes
      * that this method can recognize.
      *
-     * @param  codespace  the authority specified by user, or {@code null} if none.
-     * @param  version    the version specified by user, or {@code null} if none.
-     * @param  code       the code specified by user.
-     * @return Well-Known Text (WKT) for the given code, or {@code null} if none.
+     * @param  identifier  the code specified by user, possible with code space and version.
+     * @return Well-Known Text (WKT) for the given identifier, or {@code null} if none.
      * @throws FactoryException if an error occurred while fetching the WKT.
      */
-    protected String fetchDefinition(String codespace, String version, String code) throws
FactoryException {
+    protected String fetchDefinition(DefaultIdentifier identifier) throws FactoryException
{
         return null;
     }
 
@@ -1064,9 +1072,10 @@ public class WKTDictionary extends GeodeticAuthorityFactory {
          * "spatial_ref_syst" table of a database.
          */
         if (value == null) {
-            final String wkt = fetchDefinition(codespace, localCode, version);
+            final DefaultIdentifier identifier = new DefaultIdentifier(codespace, localCode,
version);
+            final String wkt = fetchDefinition(identifier);
             if (wkt != null) {
-                return parseAndAdd(codespace, localCode, version, wkt);
+                return parseAndAdd(codespace, version, localCode, wkt, identifier);
             }
             throw new NoSuchAuthorityCodeException(parser.errors().getString(
                     Errors.Keys.NoSuchValue_1, code), codespace, localCode, code);
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java
index 7d87093..9425f8a 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/io/wkt/WKTFormat.java
@@ -28,6 +28,7 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.logging.Level;
 import java.util.logging.LogRecord;
+import java.util.function.Function;
 import java.io.IOException;
 import java.text.Format;
 import java.text.NumberFormat;
@@ -37,6 +38,7 @@ import java.text.ParseException;
 import javax.measure.Unit;
 import org.opengis.util.Factory;
 import org.opengis.util.InternationalString;
+import org.opengis.metadata.Identifier;
 import org.opengis.metadata.citation.Citation;
 import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.cs.CSFactory;
@@ -54,6 +56,7 @@ import org.apache.sis.internal.system.Loggers;
 import org.apache.sis.internal.util.Constants;
 import org.apache.sis.internal.util.StandardDateFormat;
 import org.apache.sis.internal.referencing.ReferencingFactoryContainer;
+import org.apache.sis.referencing.ImmutableIdentifier;
 
 
 /**
@@ -206,6 +209,18 @@ public class WKTFormat extends CompoundFormat<Object> {
     private int listSizeLimit;
 
     /**
+     * Identifier to assign to parsed {@link IdentifiedObject} if the WKT does not contain
an
+     * explicit {@code ID[…]} or {@code AUTHORITY[…]} element. The main use case is for
implementing
+     * a {@link org.opengis.referencing.crs.CRSAuthorityFactory} backed by definitions in
WKT format.
+     *
+     * <p>This field is transient because this is not yet a public API. The {@code
transient}
+     * keyword may be removed in a future version if we commit to this API.</p>
+     *
+     * @see #setDefaultIdentifier(Identifier)
+     */
+    private transient Identifier defaultIdentifier;
+
+    /**
      * WKT fragments that can be inserted in longer WKT strings, or {@code null} if none.
Keys are short identifiers
      * and values are WKT subtrees to substitute to the identifiers when they are found in
a WKT to parse.
      * The same map instance may be shared by different {@linkplain #clone() clones} as long
as they are not modified.
@@ -643,6 +658,24 @@ public class WKTFormat extends CompoundFormat<Object> {
     }
 
     /**
+     * Sets the identifier to assign to parsed {@link IdentifiedObject} if the WKT does not
contain an
+     * explicit {@code ID[…]} or {@code AUTHORITY[…]} element. The main use case is for
implementing
+     * a {@link org.opengis.referencing.crs.CRSAuthorityFactory} backed by definitions in
WKT format.
+     *
+     * <p>Note that this identifier apply to all objects to be created, which is generally
not desirable.
+     * Callers should invoke {@code setDefaultIdentifier(null)} in a {@code finally} block.</p>
+     *
+     * <p>This is not a publicly committed API. If we want to make this functionality
public in a future
+     * version, we should investigate if we should make it applicable to a wider range of
properties and
+     * how to handle the fact that the a given identifier should be used for only one object.</p>
+     *
+     * @param  identifier  the default identifier, or {@code null} if none.
+     */
+    final void setDefaultIdentifier(final Identifier identifier) {
+        defaultIdentifier = identifier;
+    }
+
+    /**
      * Verifies if the given type is a valid key for the {@link #factories} map.
      */
     private void ensureValidFactoryType(final Class<?> type) throws IllegalArgumentException
{
@@ -926,8 +959,10 @@ public class WKTFormat extends CompoundFormat<Object> {
     /**
      * The parser created by {@link #parser(boolean)}, identical to {@link GeodeticObjectParser}
except
      * for the source of logging messages which is the enclosing {@code WKTParser} instead
than a factory.
+     * Also provides a mechanism for adding default identifier to root {@link IdentifiedObject}.
      */
-    private static final class Parser extends GeodeticObjectParser {
+    private final class Parser extends GeodeticObjectParser implements Function<Object,Object>
{
+        /** Creates a new parser. */
         Parser(final Symbols symbols, final Map<String,StoredTree> fragments,
                 final NumberFormat numberFormat, final DateFormat dateFormat, final UnitFormat
unitFormat,
                 final Convention convention, final Transliterator transliterator, final Locale
errorLocale,
@@ -936,8 +971,19 @@ public class WKTFormat extends CompoundFormat<Object> {
             super(symbols, fragments, numberFormat, dateFormat, unitFormat, convention, transliterator,
errorLocale, factories);
         }
 
+        /** Returns the source class and method to declare in log records. */
         @Override String getPublicFacade() {return WKTFormat.class.getName();}
         @Override String getFacadeMethod() {return "parse";}
+
+        /** Invoked when an identifier need to be supplied to root {@link IdentifiedObject}.
*/
+        @Override public Object apply(Object key) {return new ImmutableIdentifier(defaultIdentifier);}
+
+        /** Invoked when a root {@link IdentifiedObject} is about to be created. */
+        @Override void completeRoot(final Map<String,Object> properties) {
+            if (defaultIdentifier != null) {
+                properties.computeIfAbsent(IdentifiedObject.IDENTIFIERS_KEY, this);
+            }
+        }
     }
 
     /**
diff --git a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
index 3245f66..d5b4efa 100644
--- a/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
+++ b/core/sis-referencing/src/main/java/org/apache/sis/referencing/factory/GeodeticObjectFactory.java
@@ -269,9 +269,11 @@ public class GeodeticObjectFactory extends AbstractFactory implements
CRSFactory
      * (i.e. a null value "erase" the default property value).
      * Entries with null value after the union will be omitted.
      *
-     * <p>This method is invoked by all {@code createFoo(Map<String,?>, …)}
methods.</p>
+     * <p>This method is invoked by all {@code createFoo(Map<String,?>, …)}
methods. Subclasses can
+     * override this method if they want to add, remove or edit property values with more
flexibility
+     * than {@linkplain #GeodeticObjectFactory(Map) constant values specified at construction
time}.</p>
      *
-     * @param  properties  the user-supplied properties.
+     * @param  properties  the properties supplied in a call to a {@code createFoo(Map, …)}
method.
      * @return the union of the given properties with the default properties.
      */
     protected Map<String,?> complete(final Map<String,?> properties) {
diff --git a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTDictionaryTest.java
b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTDictionaryTest.java
index cc88abf..809e62e 100644
--- a/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTDictionaryTest.java
+++ b/core/sis-referencing/src/test/java/org/apache/sis/io/wkt/WKTDictionaryTest.java
@@ -27,6 +27,7 @@ import java.util.stream.Stream;
 import java.io.BufferedReader;
 import java.io.InputStreamReader;
 import java.io.IOException;
+import org.opengis.metadata.Identifier;
 import org.opengis.util.FactoryException;
 import org.opengis.referencing.crs.SingleCRS;
 import org.opengis.referencing.crs.ProjectedCRS;
@@ -35,6 +36,8 @@ import org.opengis.referencing.crs.GeodeticCRS;
 import org.opengis.referencing.cs.AxisDirection;
 import org.opengis.referencing.IdentifiedObject;
 import org.opengis.referencing.NoSuchAuthorityCodeException;
+import org.apache.sis.metadata.iso.DefaultIdentifier;
+import org.apache.sis.test.TestUtilities;
 import org.apache.sis.test.DependsOn;
 import org.apache.sis.test.TestCase;
 import org.junit.Test;
@@ -332,4 +335,67 @@ public final strictfp class WKTDictionaryTest extends TestCase {
         assertTrue(message, message.contains("GeodCRS"));
         assertTrue(message, message.contains("‘]’"));
     }
+
+    /**
+     * Tests {@link WKTDictionary#fetchDefinition(DefaultIdentifier)}.
+     *
+     * @throws FactoryException if an error occurred while parsing a WKT.
+     */
+    @Test
+    public void testFetchDefinition() throws FactoryException {
+        final WKTDictionary factory = new WKTDictionary(null) {
+            @Override protected String fetchDefinition(final DefaultIdentifier identifier)
{
+                identifier.setCodeSpace("aNS");
+                switch (identifier.getCode()) {
+                    case "2C": return "GeodCRS[\"Anguilla 1957\",\n" +
+                                      " Datum[\"Anguilla 1957\",\n" +
+                                      "  Ellipsoid[\"Clarke 1880\", 6378249.145, 293.465]],\n"
+
+                                      " CS[ellipsoidal, 2],\n" +
+                                      "  Axis[\"Latitude\", north],\n" +
+                                      "  Axis[\"Longitude\", east],\n" +
+                                      "  Unit[\"Degree\", 0.0174532925199433],\n" +
+                                      " Id[\"TEST\", 21]]";     // Intentionally mismatched
code.
+
+                    case "2N": return "GeodCRS[\"Anguilla 1957\",\n" +
+                                      " Datum[\"Anguilla 1957\",\n" +
+                                      "  Ellipsoid[\"Clarke 1880\", 6378249.145, 293.465]],\n"
+
+                                      " CS[ellipsoidal, 2],\n" +
+                                      "  Axis[\"Latitude\", north],\n" +
+                                      "  Axis[\"Longitude\", east],\n" +
+                                      "  Unit[\"Degree\", 0.0174532925199433]]";
+
+                    default: return null;
+                }
+            }
+        };
+        /*
+         * Test a CRS with an identifier specified in the WKT. We intentionally declare an
ID[…] element
+         * with a different code than the one recognized by the `switch` statement ("2C"
versus "21")
+         * for checking precedence.
+         */
+        GeographicCRS crs = factory.createGeographicCRS("2C");
+        Identifier id = TestUtilities.getSingleton(crs.getIdentifiers());
+        assertEquals("TEST", id.getCodeSpace());
+        assertEquals("21",   id.getCode());
+        assertSame(crs, factory.createGeographicCRS("2C"));                         // Test
caching.
+        /*
+         * Test a CRS without identifier in the WKT. An identifier should be automatically
generated
+         * by `WKTFormat.Parser.complete(…)`.
+         */
+        crs = factory.createGeographicCRS("2N");
+        id = TestUtilities.getSingleton(crs.getIdentifiers());
+        assertEquals("aNS", id.getCodeSpace());
+        assertEquals("2N",  id.getCode());
+        assertSame(crs, factory.createGeographicCRS("2N"));                         // Test
caching.
+        /*
+         * Test non-existent code.
+         */
+        try {
+            factory.createGeographicCRS("21");
+            fail("Parsing should have failed.");
+        } catch (FactoryException e) {
+            final String message = e.getMessage();
+            assertTrue(message, message.contains("21"));
+        }
+    }
 }


Mime
View raw message