This is an automated email from the ASF dual-hosted git repository.

afs pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/jena.git


The following commit(s) were added to refs/heads/main by this push:
     new f46dbf394a GH-4259: Add GeoSPARQL geometryType function
f46dbf394a is described below

commit f46dbf394a3aa48814a519aa8d0f1b0789020f63
Author: Edmond Chuc <[email protected]>
AuthorDate: Sun Sep 27 20:03:33 2026 +1000

    GH-4259: Add GeoSPARQL geometryType function
    
    Preserve GML source subtypes when resolving geometry types. Cache the
    resolved URI on GeometryWrapper and map constructed GML geometries
    directly from JTS types to avoid an XML serialization round trip.
    
    Add tests for GML subtypes, derived geometries, caching, and function
    errors.
    
    Separate transformed WKT and GML wrappers when their lexical forms and 
target SRS are identical. Add a regression test for both cache insertion orders 
and the resulting datatype and subtype URIs.
---
 .../geometry_property/GeometryTypeFF.java          |  42 ++++
 .../geosparql/implementation/GeometryWrapper.java  |  24 +++
 .../implementation/datatype/GMLDatatype.java       |  23 +++
 .../implementation/datatype/GeometryDatatype.java  |  14 ++
 .../implementation/datatype/WKTDatatype.java       |  11 +
 .../function_registration/GeometryProperty.java    |   2 +
 .../implementation/index/GeometryLiteralIndex.java |  16 +-
 .../index/GeometryTransformIndex.java              |  15 +-
 .../implementation/parsers/gml/GMLReader.java      |  15 +-
 .../geosparql/implementation/vocabulary/Geof.java  |   1 +
 .../geometry_property/GeometryTypeFFTest.java      | 165 +++++++++++++++
 .../implementation/GeometryTransformIndexTest.java |  61 ++++++
 .../GeometryTypeSerializationTest.java             | 230 +++++++++++++++++++++
 13 files changed, 606 insertions(+), 13 deletions(-)

diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryTypeFF.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryTypeFF.java
new file mode 100644
index 0000000000..000766d1ee
--- /dev/null
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryTypeFF.java
@@ -0,0 +1,42 @@
+/*
+ * 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
+ *
+ *   https://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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property;
+
+import org.apache.jena.datatypes.DatatypeFormatException;
+import org.apache.jena.datatypes.xsd.XSDDatatype;
+import org.apache.jena.geosparql.implementation.GeometryWrapper;
+import org.apache.jena.sparql.expr.ExprEvalException;
+import org.apache.jena.sparql.expr.NodeValue;
+import org.apache.jena.sparql.function.FunctionBase1;
+
+/** Implements geof:geometryType. */
+public class GeometryTypeFF extends FunctionBase1 {
+
+    @Override
+    public NodeValue exec(NodeValue value) {
+        try {
+            GeometryWrapper geometry = GeometryWrapper.extract(value);
+            return NodeValue.makeNode(geometry.getGeometryTypeURI(), 
XSDDatatype.XSDanyURI);
+        } catch (DatatypeFormatException | IllegalArgumentException ex) {
+            throw new ExprEvalException(ex.getMessage(), ex);
+        }
+    }
+}
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryWrapper.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryWrapper.java
index 8978171e54..038d471348 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryWrapper.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/GeometryWrapper.java
@@ -80,6 +80,7 @@ public class GeometryWrapper implements Serializable {
     private final String geometryDatatypeURI;
     private GeometryDatatype geometryDatatype;
     private String lexicalForm;
+    private String geometryTypeURI;
     private String utmURI = null;
     private Double latitude = null;
 
@@ -192,6 +193,7 @@ public class GeometryWrapper implements Serializable {
         this.srsInfo = geometryWrapper.srsInfo;
         this.dimensionInfo = geometryWrapper.dimensionInfo;
         this.lexicalForm = geometryWrapper.lexicalForm;
+        this.geometryTypeURI = geometryWrapper.geometryTypeURI;
     }
 
     /**
@@ -420,6 +422,20 @@ public class GeometryWrapper implements Serializable {
         return parsingGeometry.getNumGeometries();
     }
 
+    /**
+     * Returns the geometry subtype URI defined by this wrapper's datatype.
+     * The datatype determines the type on first use; the result is cached.
+     *
+     * @return The subtype URI as a string.
+     * @throws DatatypeFormatException if the datatype cannot resolve the 
geometry type.
+     */
+    public String getGeometryTypeURI() {
+        if (geometryTypeURI == null) {
+            geometryTypeURI = getGeometryDatatype().getGeometryTypeURI(this);
+        }
+        return geometryTypeURI;
+    }
+
     /**
      * Returns the minimum ordinate of the first SRS dimension (X)
      * across all geometry members.
@@ -1166,6 +1182,14 @@ public class GeometryWrapper implements Serializable {
         return dimensionInfo;
     }
 
+    /**
+     * Returns whether serialized text is already available without generating 
it.
+     * The text may have been supplied at construction or generated by {@link 
#asLiteral()}.
+     */
+    public boolean hasLexicalForm() {
+        return lexicalForm != null;
+    }
+
     /**
      *
      * @return String literal of Geometry Wrapper.
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GMLDatatype.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GMLDatatype.java
index 4863c78f2c..621e75f295 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GMLDatatype.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GMLDatatype.java
@@ -28,6 +28,7 @@ import 
org.apache.jena.geosparql.implementation.GeometryWrapper;
 import org.apache.jena.geosparql.implementation.parsers.gml.GMLReader;
 import org.apache.jena.geosparql.implementation.parsers.gml.GMLWriter;
 import org.apache.jena.geosparql.implementation.vocabulary.Geo;
+import org.apache.jena.geosparql.implementation.vocabulary.GeoSPARQL_URI;
 import org.jdom2.JDOMException;
 import org.locationtech.jts.geom.Geometry;
 
@@ -91,6 +92,28 @@ public class GMLDatatype extends GeometryDatatype {
         }
     }
 
+    @Override
+    public String getGeometryTypeURI(GeometryWrapper geometry) {
+        String type;
+        if (geometry.hasLexicalForm()) {
+            try {
+                type = GMLReader.readGeometryType(geometry.getLexicalForm());
+            } catch (JDOMException | IOException ex) {
+                throw new DatatypeFormatException("Unable to read GML geometry 
type", ex);
+            }
+        } else {
+            String jtsType = geometry.getParsingGeometry().getGeometryType();
+            type = switch (jtsType) {
+                case "Point", "LineString", "Polygon", "MultiPoint" -> jtsType;
+                case "MultiLineString" -> "MultiCurve";
+                case "MultiPolygon" -> "MultiSurface";
+                case "GeometryCollection" -> "MultiGeometry";
+                default -> throw new DatatypeFormatException("Geometry type 
not supported: " + jtsType);
+            };
+        }
+        return GeoSPARQL_URI.GML_URI + type;
+    }
+
     @Override
     public String toString() {
         return "GMLDatatype{" + URI + '}';
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GeometryDatatype.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GeometryDatatype.java
index df0ba9d900..3dfd798ead 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GeometryDatatype.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/GeometryDatatype.java
@@ -37,6 +37,20 @@ public abstract class GeometryDatatype extends BaseDatatype {
 
     public abstract GeometryWrapper read(String geometryLiteral);
 
+    /**
+     * Resolves a geometry subtype URI using this datatype's type system.
+     * Datatypes that support {@code geof:geometryType}, such as WKT and GML,
+     * override this method. The default throws so existing custom datatypes
+     * can remain unchanged and report that type resolution is unsupported.
+     *
+     * @param geometry A wrapper whose geometry datatype is this datatype.
+     * @return The subtype URI as a non-null string.
+     * @throws DatatypeFormatException if type resolution is unsupported for 
this datatype or geometry.
+     */
+    public String getGeometryTypeURI(GeometryWrapper geometry) {
+        throw new DatatypeFormatException("Geometry type resolution is not 
supported for datatype: " + getURI());
+    }
+
     /**
      * This method Parses the Geometry Literal to the JTS Geometry
      *
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/WKTDatatype.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/WKTDatatype.java
index 7be20c1421..7668d67a18 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/WKTDatatype.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/datatype/WKTDatatype.java
@@ -26,6 +26,7 @@ import 
org.apache.jena.geosparql.implementation.GeometryWrapper;
 import org.apache.jena.geosparql.implementation.parsers.wkt.WKTReader;
 import org.apache.jena.geosparql.implementation.parsers.wkt.WKTWriter;
 import org.apache.jena.geosparql.implementation.vocabulary.Geo;
+import org.apache.jena.geosparql.implementation.vocabulary.GeoSPARQL_URI;
 import org.locationtech.jts.geom.Geometry;
 
 /**
@@ -93,6 +94,16 @@ public class WKTDatatype extends GeometryDatatype {
         return new GeometryWrapper(geometry, srsURI, URI, dimensionInfo, 
geometryLiteral);
     }
 
+    @Override
+    public String getGeometryTypeURI(GeometryWrapper geometry) {
+        String type = geometry.getGeometryType();
+        return switch (type) {
+            case "Point", "LineString", "LinearRing", "Polygon", "MultiPoint", 
"MultiLineString",
+                 "MultiPolygon", "GeometryCollection" -> GeoSPARQL_URI.SF_URI 
+ type;
+            default -> throw new DatatypeFormatException("Unsupported Simple 
Features geometry type: " + type);
+        };
+    }
+
     @Override
     public String toString() {
         return "WKTDatatype{" + URI + '}';
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/GeometryProperty.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/GeometryProperty.java
index b595cbadd6..9c448ca47e 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/GeometryProperty.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/function_registration/GeometryProperty.java
@@ -29,6 +29,7 @@ import 
org.apache.jena.geosparql.geo.topological.property_functions.geometry_pro
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.CoordinateDimensionFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.DimensionFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.GeometryNFF;
+import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.GeometryTypeFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.Is3DFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.IsEmptyFF;
 import 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property.IsMeasuredFF;
@@ -75,6 +76,7 @@ public class GeometryProperty {
      */
     public static void loadFilterFunctions(FunctionRegistry registry) {
 
+        registry.put(Geof.GEOMETRY_TYPE, GeometryTypeFF.class);
         registry.put(Geof.IS_3D, Is3DFF.class);
         registry.put(Geof.IS_MEASURED, IsMeasuredFF.class);
         registry.put(Geof.NUM_GEOMETRIES, NumGeometriesFF.class);
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/index/GeometryLiteralIndex.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/index/GeometryLiteralIndex.java
index 550014d0a5..75419ed6a5 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/index/GeometryLiteralIndex.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/index/GeometryLiteralIndex.java
@@ -34,9 +34,11 @@ import 
org.apache.jena.geosparql.implementation.datatype.GeometryDatatype;
 public class GeometryLiteralIndex {
 
     private static boolean INDEX_ACTIVE = false;
-    private static Cache<String, GeometryWrapper>
+    private record GeometryLiteralKey(String datatypeURI, String lexicalForm) 
{}
+
+    private static Cache<GeometryLiteralKey, GeometryWrapper>
             PRIMARY_INDEX = CacheConfiguration.create(UNLIMITED_MAP, 
MAP_EXPIRY_INTERVAL);
-    private static Cache<String, GeometryWrapper> SECONDARY_INDEX = 
CacheConfiguration.create(UNLIMITED_MAP, MAP_EXPIRY_INTERVAL);
+    private static Cache<GeometryLiteralKey, GeometryWrapper> SECONDARY_INDEX 
= CacheConfiguration.create(UNLIMITED_MAP, MAP_EXPIRY_INTERVAL);
 
     public enum GeometryIndex {
         PRIMARY, SECONDARY
@@ -56,19 +58,19 @@ public class GeometryLiteralIndex {
         return geometryWrapper;
     }
 
-    private static GeometryWrapper retrieveMemoryIndex(String geometryLiteral, 
GeometryDatatype geometryDatatype, Cache<String, GeometryWrapper> index, 
Cache<String, GeometryWrapper> otherIndex) {
+    private static GeometryWrapper retrieveMemoryIndex(String geometryLiteral, 
GeometryDatatype geometryDatatype, Cache<GeometryLiteralKey, GeometryWrapper> 
index, Cache<GeometryLiteralKey, GeometryWrapper> otherIndex) {
 
         GeometryWrapper geometryWrapper;
 
         if (INDEX_ACTIVE) {
-
-            geometryWrapper = index.getIfPresent(geometryLiteral);
+            GeometryLiteralKey key = new 
GeometryLiteralKey(geometryDatatype.getURI(), geometryLiteral);
+            geometryWrapper = index.getIfPresent(key);
             if (geometryWrapper == null) {
-                geometryWrapper = otherIndex.getIfPresent(geometryLiteral);
+                geometryWrapper = otherIndex.getIfPresent(key);
                 if (geometryWrapper == null) {
                     geometryWrapper = geometryDatatype.read(geometryLiteral);
                 }
-                index.put(geometryLiteral, geometryWrapper);
+                index.put(key, geometryWrapper);
             }
 
             return geometryWrapper;
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/index/GeometryTransformIndex.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/index/GeometryTransformIndex.java
index 6193a90e0a..94ef2483d7 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/index/GeometryTransformIndex.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/index/GeometryTransformIndex.java
@@ -60,7 +60,8 @@ public class GeometryTransformIndex {
     public static final GeometryWrapper transform(GeometryWrapper 
sourceGeometryWrapper, String srsURI, Boolean storeSRSTransform) throws 
TransformException, FactoryException {
 
         GeometryWrapper transformedGeometryWrapper;
-        IndexKey key = new IndexKey(sourceGeometryWrapper.getLexicalForm(), 
srsURI);
+        IndexKey key = new 
IndexKey(sourceGeometryWrapper.getGeometryDatatypeURI(),
+                sourceGeometryWrapper.getLexicalForm(), srsURI);
 
         if (INDEX_ACTIVE && storeSRSTransform) {
 
@@ -137,10 +138,12 @@ public class GeometryTransformIndex {
 
     private static class IndexKey {
 
-         private final String sourceGeometryLiteral;
-         private final String srsURI;
+        private final String sourceDatatypeURI;
+        private final String sourceGeometryLiteral;
+        private final String srsURI;
 
-        public IndexKey(String sourceGeometryLiteral, String srsURI) {
+        public IndexKey(String sourceDatatypeURI, String 
sourceGeometryLiteral, String srsURI) {
+            this.sourceDatatypeURI = sourceDatatypeURI;
             this.sourceGeometryLiteral = sourceGeometryLiteral;
             this.srsURI = srsURI;
         }
@@ -148,6 +151,7 @@ public class GeometryTransformIndex {
         @Override
         public int hashCode() {
             int hash = 7;
+            hash = 47 * hash + Objects.hashCode(this.sourceDatatypeURI);
             hash = 47 * hash + Objects.hashCode(this.sourceGeometryLiteral);
             hash = 47 * hash + Objects.hashCode(this.srsURI);
             return hash;
@@ -165,6 +169,9 @@ public class GeometryTransformIndex {
                 return false;
             }
             final IndexKey other = (IndexKey) obj;
+            if (!Objects.equals(this.sourceDatatypeURI, 
other.sourceDatatypeURI)) {
+                return false;
+            }
             if (!Objects.equals(this.sourceGeometryLiteral, 
other.sourceGeometryLiteral)) {
                 return false;
             }
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLReader.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLReader.java
index 085f32a580..90f0712b1b 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLReader.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/parsers/gml/GMLReader.java
@@ -704,6 +704,18 @@ public class GMLReader implements ParserReader {
     private static final String EMPTY_GML_TEXT = "<gml:Point 
xmlns:gml='http://www.opengis.net/gml/3.2' 
srsName=\"http://www.opengis.net/def/crs/OGC/1.3/CRS84\"; />";
 
     public static GMLReader extract(String gmlText) throws JDOMException, 
IOException {
+        return new GMLReader(readRootElement(gmlText));
+    }
+
+    /**
+     * Returns the local name of a GML literal's root element. An empty
+     * literal represents an empty Point.
+     */
+    public static String readGeometryType(String gmlText) throws 
JDOMException, IOException {
+        return readRootElement(gmlText).getName();
+    }
+
+    private static Element readRootElement(String gmlText) throws 
JDOMException, IOException {
 
         if (gmlText.isEmpty()) {
             gmlText = EMPTY_GML_TEXT;
@@ -712,8 +724,7 @@ public class GMLReader implements ParserReader {
         SAXBuilder jdomBuilder = newSAXBuilder();
         InputStream stream = new 
ByteArrayInputStream(gmlText.getBytes(StandardCharsets.UTF_8));
         Document xmlDoc = jdomBuilder.build(stream);
-        Element gmlElement = xmlDoc.getRootElement();
-        return new GMLReader(gmlElement);
+        return xmlDoc.getRootElement();
     }
 
     // ---- XXE safe SAXBuilder
diff --git 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/vocabulary/Geof.java
 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/vocabulary/Geof.java
index abcb958a99..aa3384ed29 100644
--- 
a/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/vocabulary/Geof.java
+++ 
b/jena-geosparql/src/main/java/org/apache/jena/geosparql/implementation/vocabulary/Geof.java
@@ -76,6 +76,7 @@ public interface Geof {
     public static final String GETSRID_NAME = GEOF_URI + "getSRID";
 
     // Geometry property function symbols:
+    public static final String GEOMETRY_TYPE = GEOF_URI + "geometryType";
     public static final String IS_3D = GEOF_URI + "is3D";
     public static final String IS_MEASURED = GEOF_URI + "isMeasured";
     public static final String NUM_GEOMETRIES = GEOF_URI + "numGeometries";
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryTypeFFTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryTypeFFTest.java
new file mode 100644
index 0000000000..45b85d1cc7
--- /dev/null
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geof/topological/filter_functions/geometry_property/GeometryTypeFFTest.java
@@ -0,0 +1,165 @@
+/*
+ * 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
+ *
+ *   https://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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package 
org.apache.jena.geosparql.geof.topological.filter_functions.geometry_property;
+
+import static org.junit.Assert.*;
+
+import org.apache.jena.datatypes.xsd.XSDDatatype;
+import org.apache.jena.geosparql.configuration.GeoSPARQLConfig;
+import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
+import org.apache.jena.geosparql.implementation.index.GeometryLiteralIndex;
+import org.apache.jena.graph.Node;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.query.QueryBuildException;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.query.QuerySolution;
+import org.apache.jena.query.ResultSet;
+import org.apache.jena.rdf.model.ModelFactory;
+import org.apache.jena.sparql.expr.ExprEvalException;
+import org.apache.jena.sparql.expr.NodeValue;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+public class GeometryTypeFFTest {
+    private static final String GML = "http://www.opengis.net/ont/gml#";;
+    private static final String SF = "http://www.opengis.net/ont/sf#";;
+
+    @BeforeClass
+    public static void setup() {
+        GeoSPARQLConfig.setupNoIndex();
+    }
+
+    @Test
+    public void wktReturnsTypedSubtypeUriIncludingEmptyGeometries() {
+        assertType("'POINT (1 2)'^^geo:wktLiteral", SF + "Point");
+        assertType("'LINESTRING (0 0, 1 1)'^^geo:wktLiteral", SF + 
"LineString");
+        assertType("'LINEARRING EMPTY'^^geo:wktLiteral", SF + "LinearRing");
+        assertType("'POLYGON ((0 0, 1 0, 0 1, 0 0))'^^geo:wktLiteral", SF + 
"Polygon");
+        assertType("'MULTIPOINT ((1 2), (3 4))'^^geo:wktLiteral", SF + 
"MultiPoint");
+        assertType("'MULTILINESTRING ((0 0, 1 1))'^^geo:wktLiteral", SF + 
"MultiLineString");
+        assertType("'MULTIPOLYGON (((0 0, 1 0, 0 1, 0 0)))'^^geo:wktLiteral", 
SF + "MultiPolygon");
+        assertType("'GEOMETRYCOLLECTION EMPTY'^^geo:wktLiteral", SF + 
"GeometryCollection");
+    }
+
+    @Test
+    public void memoryIndexKeepsWktAndGmlTypesSeparate() {
+        GeoSPARQLConfig.setupMemoryIndex();
+        try {
+            assertType("''^^geo:wktLiteral", SF + "Point");
+            assertType("''^^geo:gmlLiteral", GML + "Point");
+
+            GeometryLiteralIndex.clear();
+            assertType("''^^geo:gmlLiteral", GML + "Point");
+            assertType("''^^geo:wktLiteral", SF + "Point");
+        } finally {
+            GeoSPARQLConfig.setupNoIndex();
+        }
+    }
+
+    @Test
+    public void gmlRetainsSourceSubtype() {
+        assertGmlType("Curve", "<g:segments><g:Arc><g:posList>5 0 0 5 -5 
0</g:posList>"
+                + "</g:Arc></g:segments>");
+        assertGmlType("Surface", "<g:patches><g:PolygonPatch>" + polygonRing()
+                + "</g:PolygonPatch></g:patches>");
+        assertGmlType("MultiCurve", "<g:curveMember><g:LineString><g:posList>0 
0 1 1</g:posList>"
+                + "</g:LineString></g:curveMember>");
+        assertGmlType("MultiSurface", "<g:surfaceMember><g:Polygon>" + 
polygonRing()
+                + "</g:Polygon></g:surfaceMember>");
+    }
+
+    @Test
+    public void acceptedEmptyGmlElementsRetainTheirTypes() {
+        for (String type : new String[] {
+                "Point", "LineString", "Curve", "MultiCurve", "MultiSurface", 
"MultiGeometry" }) {
+            assertGmlType(type, "");
+        }
+    }
+
+    @Test
+    public void derivedGeometryUsesItsOwnType() {
+        String curve = gml("Curve", "<g:segments><g:LineStringSegment>"
+                + "<g:posList>0 0 10 10 20 
0</g:posList></g:LineStringSegment></g:segments>");
+        assertEquals(NodeFactory.createLiteralDT(GML + "Polygon", 
XSDDatatype.XSDanyURI),
+                     evaluate("geof:geometryType(geof:envelope('" + curve + 
"'^^geo:gmlLiteral))"));
+    }
+
+    @Test
+    public void invalidArgumentsRaiseExpressionErrorsAndLeaveBindUnbound() {
+        GeometryTypeFF function = new GeometryTypeFF();
+        Node nonLiteral = NodeFactory.createURI("urn:not-a-literal");
+        assertThrows(ExprEvalException.class,
+                () -> function.exec(NodeValue.makeNode(nonLiteral)));
+        assertThrows(ExprEvalException.class, () -> 
function.exec(NodeValue.makeInteger(42)));
+        assertThrows(ExprEvalException.class,
+                () -> function.exec(NodeValue.makeString("POINT (1 2)")));
+        assertThrows(ExprEvalException.class,
+                () -> function.exec(NodeValue.makeNode("invalid", 
WKTDatatype.INSTANCE)));
+        for (String argument : new String[] {
+                "<urn:not-a-literal>", "42", "'POINT (1 2)'",
+                "'invalid'^^geo:wktLiteral" }) {
+            assertNull(argument, evaluate("geof:geometryType(" + argument + 
")"));
+        }
+    }
+
+    @Test
+    public void wrongArityIsRejected() {
+        assertThrows(QueryBuildException.class, () -> 
evaluate("geof:geometryType()"));
+        assertThrows(QueryBuildException.class,
+                     () -> evaluate("geof:geometryType('POINT 
EMPTY'^^geo:wktLiteral, 1)"));
+    }
+
+    private static void assertGmlType(String type, String contents) {
+        assertType("'" + gml(type, contents) + "'^^geo:gmlLiteral", GML + 
type);
+    }
+
+    private static void assertType(String geometry, String expectedUri) {
+        assertEquals(geometry, NodeFactory.createLiteralDT(expectedUri, 
XSDDatatype.XSDanyURI),
+                     evaluate("geof:geometryType(" + geometry + ")"));
+    }
+
+    private static String polygonRing() {
+        return "<g:exterior><g:LinearRing><g:posList>0 0 10 0 10 10 0 
0</g:posList>"
+                + "</g:LinearRing></g:exterior>";
+    }
+
+    private static String gml(String type, String contents) {
+        return "<g:" + type + " xmlns:g=\"http://www.opengis.net/gml/3.2\"; "
+               + "srsName=\"http://www.opengis.net/def/crs/EPSG/0/27700\";>"
+               + contents + "</g:" + type + ">";
+    }
+
+    private static Node evaluate(String expression) {
+        String query = """
+            PREFIX geof: <http://www.opengis.net/def/function/geosparql/>
+            PREFIX geo: <http://www.opengis.net/ont/geosparql#>
+            SELECT ?result WHERE { BIND(%s AS ?result) }
+            """.formatted(expression);
+        try (QueryExecution execution = QueryExecution.create(
+                query, ModelFactory.createDefaultModel())) {
+            ResultSet results = execution.execSelect();
+            assertTrue("Expected one solution for " + expression, 
results.hasNext());
+            QuerySolution solution = results.next();
+            assertFalse("Expected only one solution for " + expression, 
results.hasNext());
+            return solution.contains("result") ? 
solution.get("result").asNode() : null;
+        }
+    }
+}
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryTransformIndexTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryTransformIndexTest.java
new file mode 100644
index 0000000000..434f2711dc
--- /dev/null
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryTransformIndexTest.java
@@ -0,0 +1,61 @@
+/*
+ * 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
+ *
+ *   https://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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package org.apache.jena.geosparql.implementation;
+
+import static org.junit.Assert.assertEquals;
+
+import org.apache.jena.geosparql.configuration.GeoSPARQLConfig;
+import org.apache.jena.geosparql.implementation.datatype.GMLDatatype;
+import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
+import org.apache.jena.geosparql.implementation.index.GeometryTransformIndex;
+import org.junit.Test;
+
+public class GeometryTransformIndexTest {
+    private static final String TARGET_SRS = 
"http://www.opengis.net/def/crs/EPSG/0/27700";;
+    private static final String WKT_POINT = 
"http://www.opengis.net/ont/sf#Point";;
+    private static final String GML_POINT = 
"http://www.opengis.net/ont/gml#Point";;
+
+    @Test
+    public void sameLexicalFormKeepsItsDatatypeInBothCacheOrders() throws 
Exception {
+        GeoSPARQLConfig.setupMemoryIndex();
+        try {
+            GeometryWrapper wkt = GeometryWrapper.extract("", WKTDatatype.URI);
+            GeometryWrapper gml = GeometryWrapper.extract("", GMLDatatype.URI);
+
+            assertTransformedType(wkt, WKTDatatype.URI, WKT_POINT);
+            assertTransformedType(gml, GMLDatatype.URI, GML_POINT);
+
+            GeometryTransformIndex.clear();
+            assertTransformedType(gml, GMLDatatype.URI, GML_POINT);
+            assertTransformedType(wkt, WKTDatatype.URI, WKT_POINT);
+        } finally {
+            GeoSPARQLConfig.setupNoIndex();
+        }
+    }
+
+    private static void assertTransformedType(GeometryWrapper source, String 
datatypeURI,
+            String geometryTypeURI) throws Exception {
+        GeometryWrapper transformed = source.transform(TARGET_SRS);
+        assertEquals(datatypeURI, transformed.getGeometryDatatypeURI());
+        assertEquals(datatypeURI, 
transformed.asNodeValue().asNode().getLiteralDatatypeURI());
+        assertEquals(geometryTypeURI, transformed.getGeometryTypeURI());
+    }
+}
diff --git 
a/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryTypeSerializationTest.java
 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryTypeSerializationTest.java
new file mode 100644
index 0000000000..5c97558a7c
--- /dev/null
+++ 
b/jena-geosparql/src/test/java/org/apache/jena/geosparql/implementation/GeometryTypeSerializationTest.java
@@ -0,0 +1,230 @@
+/*
+ * 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
+ *
+ *   https://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.
+ *
+ *   SPDX-License-Identifier: Apache-2.0
+ */
+package org.apache.jena.geosparql.implementation;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertThrows;
+
+import org.apache.jena.datatypes.DatatypeFormatException;
+import org.apache.jena.datatypes.TypeMapper;
+import org.apache.jena.geosparql.implementation.datatype.GMLDatatype;
+import org.apache.jena.geosparql.implementation.datatype.GeometryDatatype;
+import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
+import org.junit.Test;
+
+public class GeometryTypeSerializationTest {
+    private static final String GML = "http://www.opengis.net/ont/gml#";;
+    private static final String SF = "http://www.opengis.net/ont/sf#";;
+
+    @Test
+    public void parsedGmlTypeIsResolvedOnlyOnceAndCopied() {
+        CountingGeometryWrapper geometry = new 
CountingGeometryWrapper(curve());
+        assertEquals(GML + "Curve", type(geometry));
+        assertEquals(GML + "Curve", type(geometry));
+        assertEquals(1, geometry.lexicalReads);
+
+        CountingGeometryWrapper copy = new CountingGeometryWrapper(geometry);
+        assertEquals(GML + "Curve", type(copy));
+        assertEquals(0, copy.lexicalReads);
+    }
+
+    @Test
+    public void derivedGeometryHasItsOwnType() {
+        GeometryWrapper source = curve();
+        assertEquals(GML + "Curve", type(source));
+        assertEquals(GML + "Polygon", type(source.envelope()));
+        assertEquals(GML + "Curve", type(source));
+    }
+
+    @Test
+    public void conversionToWktUsesTheConvertedTypeWithoutChangingSource() {
+        GeometryWrapper source = curve();
+        assertEquals(GML + "Curve", type(source));
+        GeometryWrapper converted = 
GeometryWrapper.extract(source.asLiteral(WKTDatatype.URI));
+        assertEquals(SF + "LineString", type(converted));
+        assertEquals(GML + "Curve", type(source));
+    }
+
+    @Test
+    public void constructedGmlUsesWriterElementNames() {
+        String[][] cases = {
+            { "POINT (1 2)", "Point" },
+            { "LINESTRING (0 0, 1 1)", "LineString" },
+            { "POLYGON ((0 0, 1 0, 0 1, 0 0))", "Polygon" },
+            { "MULTIPOINT ((1 2), (3 4))", "MultiPoint" },
+            { "MULTILINESTRING ((0 0, 1 1))", "MultiCurve" },
+            { "MULTIPOLYGON (((0 0, 1 0, 0 1, 0 0)))", "MultiSurface" },
+            { "GEOMETRYCOLLECTION (POINT (1 2))", "MultiGeometry" },
+            { "MULTIPOLYGON EMPTY", "MultiSurface" }
+        };
+        for (String[] example : cases) {
+            GeometryWrapper wkt = GeometryWrapper.extract(example[0], 
WKTDatatype.URI);
+            GeometryWrapper constructed = new GeometryWrapper(
+                    wkt.getParsingGeometry(), wkt.getSrsURI(), GMLDatatype.URI,
+                    wkt.getDimensionInfo());
+            assertFalse(example[0], constructed.hasLexicalForm());
+            assertEquals(example[0], GML + example[1], type(constructed));
+            assertFalse(example[0], constructed.hasLexicalForm());
+            assertEquals(example[0], GML + example[1],
+                    type(GeometryWrapper.extract(constructed.asLiteral())));
+            GeometryWrapper serializedFirst = new GeometryWrapper(
+                    wkt.getParsingGeometry(), wkt.getSrsURI(), GMLDatatype.URI,
+                    wkt.getDimensionInfo());
+            serializedFirst.asLiteral();
+            assertEquals(example[0], GML + example[1], type(serializedFirst));
+        }
+    }
+
+    @Test
+    public void emptyGmlTextUsesJenasPointInterpretation() {
+        assertEquals(GML + "Point", type(GeometryWrapper.extract("", 
GMLDatatype.URI)));
+    }
+
+    @Test
+    public void constructedGmlTypeDoesNotSerializeOrRecompute() {
+        GeometryWrapper wkt = WKTDatatype.INSTANCE.read("MULTILINESTRING ((0 
0, 1 1))");
+        CountingGeometryWrapper geometry = new CountingGeometryWrapper(new 
GeometryWrapper(
+            wkt.getParsingGeometry(), wkt.getSrsURI(), GMLDatatype.URI, 
wkt.getDimensionInfo()));
+        assertEquals(GML + "MultiCurve", type(geometry));
+        assertEquals(GML + "MultiCurve", type(geometry));
+        assertEquals(0, geometry.lexicalReads);
+        assertFalse(geometry.hasLexicalForm());
+
+        CountingGeometryWrapper copy = new CountingGeometryWrapper(geometry);
+        assertEquals(GML + "MultiCurve", type(copy));
+        assertEquals(0, copy.lexicalReads);
+        assertFalse(copy.hasLexicalForm());
+    }
+
+    @Test
+    public void publicLexicalConstructorPreservesSourceSubtype() {
+        GeometryWrapper source = curve();
+        GeometryWrapper geometry = new GeometryWrapper(
+                source.getParsingGeometry(), source.getSrsURI(),
+                GMLDatatype.URI, source.getDimensionInfo(), 
source.getLexicalForm());
+        geometry.asLiteral();
+        assertEquals(GML + "Curve", type(geometry));
+        assertEquals(GML + "Curve", type(new GeometryWrapper(geometry)));
+    }
+
+    @Test
+    public void protectedLexicalConstructorPreservesSourceSubtype() {
+        GeometryWrapper source = curve();
+        GeometryWrapper geometry = new GeometryWrapper(
+                source.getParsingGeometry(), source.getXYGeometry(), 
source.getSrsURI(),
+                GMLDatatype.URI, source.getDimensionInfo(), 
source.getLexicalForm());
+        assertEquals(GML + "Curve", type(new GeometryWrapper(geometry)));
+    }
+
+    @Test
+    public void constructedGmlRejectsTypesTheWriterCannotRepresent() {
+        GeometryWrapper ring = WKTDatatype.INSTANCE.read("LINEARRING (0 0, 1 
0, 1 1, 0 0)");
+        GeometryWrapper gml = new GeometryWrapper(ring.getParsingGeometry(), 
ring.getSrsURI(),
+                                                  GMLDatatype.URI, 
ring.getDimensionInfo());
+        assertThrows(DatatypeFormatException.class, gml::getGeometryTypeURI);
+        assertThrows(DatatypeFormatException.class, gml::asLiteral);
+    }
+
+    @Test
+    public void unsupportedSimpleFeaturesTypeRaisesDatatypeError() {
+        GeometryWrapper geometry = new 
GeometryWrapper(WKTDatatype.INSTANCE.read("POINT (1 2)")) {
+            @Override
+            public String getGeometryType() {
+                return "UnsupportedGeometry";
+            }
+        };
+        assertThrows(DatatypeFormatException.class, 
geometry::getGeometryTypeURI);
+    }
+
+    @Test
+    public void datatypeExtensionsWithoutTypeResolutionRaiseDatatypeError() {
+        GeometryDatatype datatype = new 
GeometryDatatype("urn:test:geometry-type:default") {
+            @Override
+            public GeometryWrapper read(String text) {
+                GeometryWrapper wkt = WKTDatatype.INSTANCE.read(text);
+                return new GeometryWrapper(
+                        wkt.getParsingGeometry(), wkt.getSrsURI(), getURI(),
+                        wkt.getDimensionInfo());
+            }
+        };
+        TypeMapper.getInstance().registerDatatype(datatype);
+        try {
+            GeometryWrapper geometry = datatype.read("POINT (1 2)");
+            assertThrows(DatatypeFormatException.class, 
geometry::getGeometryTypeURI);
+        } finally {
+            TypeMapper.getInstance().unregisterDatatype(datatype);
+        }
+    }
+
+    @Test
+    public void datatypeExtensionsCanResolveTheirOwnTypes() {
+        GeometryDatatype datatype = new 
GeometryDatatype("urn:test:geometry-type:custom") {
+            @Override
+            public GeometryWrapper read(String text) {
+                GeometryWrapper wkt = WKTDatatype.INSTANCE.read(text);
+                return new GeometryWrapper(
+                        wkt.getParsingGeometry(), wkt.getSrsURI(), getURI(),
+                        wkt.getDimensionInfo());
+            }
+
+            @Override
+            public String getGeometryTypeURI(GeometryWrapper geometry) {
+                return "urn:test:Point";
+            }
+        };
+        TypeMapper.getInstance().registerDatatype(datatype);
+        try {
+            assertEquals("urn:test:Point", type(datatype.read("POINT (1 2)")));
+        } finally {
+            TypeMapper.getInstance().unregisterDatatype(datatype);
+        }
+    }
+
+    private static GeometryWrapper curve() {
+        return GMLDatatype.INSTANCE.read("""
+            <Curve xmlns="http://www.opengis.net/gml/3.2";
+                   srsName="http://www.opengis.net/def/crs/EPSG/0/27700";>
+              <segments><LineStringSegment>
+                <posList>0 0 10 10 20 0</posList>
+              </LineStringSegment></segments>
+            </Curve>
+            """);
+    }
+
+    private static String type(GeometryWrapper geometry) {
+        return geometry.getGeometryTypeURI();
+    }
+
+    private static class CountingGeometryWrapper extends GeometryWrapper {
+        private int lexicalReads;
+
+        CountingGeometryWrapper(GeometryWrapper geometry) {
+            super(geometry);
+        }
+
+        @Override
+        public String getLexicalForm() {
+            lexicalReads++;
+            return super.getLexicalForm();
+        }
+    }
+}

Reply via email to