Repository: apache/jena (java)

Issue:
Cancellable spatial property functions
### Version

5.3.0-SNAPSHOT

### Feature

In `jena-geosparql`, many (if not all) property functions do not respect query cancellation.
For example, accidentally executing a query such as below creates a cross product of all geometries in the store which may run for days, weeks or longer, regardless of the query timeout setting.

```sparql
PREFIX geo: <http://www.opengis.net/ont/geosparql#> SELECT * { ?a geo:sfIntersects ?b . }
```

The proposed solution is to improve the implementations in  `jena-geosparql` to make more use of the `QueryIter` machinery which provides the cancellation system.


### Are you interested in contributing a solution yourself?

Yes

Pull request description:
GH-2930: Support for query cancellation for spatial property functions
GitHub issue resolved #2930 

Pull request Description: Improves base implementation of `GenericPropertyFunction` to operate lazily using the `QueryIter` machinery and its cancellation features. The class `CancelQueryTest` tests for successful cancellation.

Disclaimer: The core work was done by my colleague @LorenzBuehmann - He suggested that I should take care of the PR.

~~Set to draft because perhaps some more regression testing should be added.~~

There are several tests in the geosparql module. I experimented by introducing intentional errors into the PR, such as setting isSubjectBound hard to false/false or returning empty/unit result sets and they causes failures - so the tests already seem to sufficiently capture cases where bogus results are returned.

----

 - [x] Tests are included.
 - [x] Commits have been squashed to remove intermediate development commit messages.
 - [x] Key commit messages start with the issue number (GH-xxxx)

By submitting this pull request, I acknowledge that I am making a contribution to the Apache Software Foundation under the terms and conditions of the [Contributor's Agreement](https://www.apache.org/licenses/contributor-agreements.html).

----

See the [Apache Jena "Contributing" guide](https://github.com/apache/jena/blob/main/CONTRIBUTING.md).


Patch:
diff --git a/.github/workflows/pr-dependencies.yml b/.github/workflows/pr.yml
similarity index 95%
rename from .github/workflows/pr-dependencies.yml
rename to .github/workflows/pr.yml
index 2e6790f2b6..bac438e7c2 100644
--- a/.github/workflows/pr-dependencies.yml
+++ b/.github/workflows/pr.yml
@@ -17,7 +17,7 @@ jobs:
       
   build-pr:
     ## Dependabot only
-    if: github.actor == 'dependabot[bot]'
+    ## if: github.actor == 'dependabot[bot]'
     
     strategy:
       fail-fast: false
diff --git a/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx/ParserRRX_SAX.java b/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx/ParserRRX_SAX.java
index 30daf48700..6225282a89 100644
--- a/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx/ParserRRX_SAX.java
+++ b/jena-arq/src/main/java/org/apache/jena/riot/lang/rdfxml/rrx/ParserRRX_SAX.java
@@ -42,7 +42,6 @@ import org.apache.jena.irix.IRIx;
 import org.apache.jena.riot.RiotException;
 import org.apache.jena.riot.lang.rdfxml.RDFXMLParseException;
 import org.apache.jena.riot.out.NodeFmtLib;
-import org.apache.jena.riot.system.FactoryRDF;
 import org.apache.jena.riot.system.ParserProfile;
 import org.apache.jena.riot.system.StreamRDF;
 import org.apache.jena.sparql.util.Context;
@@ -382,7 +381,7 @@ class ParserRRX_SAX
 
     // ---- Parser Setup
     private final ParserProfile parserProfile;
-    private final FactoryRDF factory;
+  //  private final FactoryRDF factory;
     private final Context context;
     private final org.apache.jena.riot.system.ErrorHandler errorHandler;
     private final String initialXmlBase;
@@ -497,7 +496,6 @@ class ParserRRX_SAX
         // Debug
 
         this.parserProfile = parserProfile;
-        this.factory = parserProfile.getFactorRDF();
         this.errorHandler = parserProfile.getErrorHandler();
         this.context = context;
         this.initialXmlBase = xmlBase;
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDataset.java b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDataset.java
index f79786f499..8f0de2b0b1 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDataset.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/exec/QueryExecDataset.java
@@ -111,7 +111,7 @@ public class QueryExecDataset implements QueryExec
         // See also query substitution handled in QueryExecBuilder
         this.initialBinding = initialToEngine;
 
-        // Cancel signal may originate from an e.c. an update execution.
+        // Cancel signal may originate from e.g. an update execution.
         this.cancelSignal = Context.getOrSetCancelSignal(context);
 
         init();
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/graph/NodeTransformLib.java b/jena-arq/src/main/java/org/apache/jena/sparql/graph/NodeTransformLib.java
index fa38705ee9..cd7435b6bd 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/graph/NodeTransformLib.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/graph/NodeTransformLib.java
@@ -40,8 +40,8 @@ import org.apache.jena.sparql.expr.ExprTransform;
 
 public class NodeTransformLib {
     /**
-     * Do a node{@literal ->}node conversion of an Op - return
-     * original BGP for "no change"
+     * Do a node{@literal ->}node conversion of an {@link Op} - return
+     * original {@link Op} java object for "no change"
      */
     public static Op transform(NodeTransform nodeTransform, Op op) {
         Transform opTransform = new NodeTransformOp(nodeTransform);
@@ -51,7 +51,7 @@ public class NodeTransformLib {
 
     /**
      * Do a node{@literal ->}node conversion of a BGP - return
-     * original BGP for "no change".
+     * original BGP java object for "no change".
      */
     public static BasicPattern transform(NodeTransform nodeTransform, BasicPattern pattern) {
         BasicPattern bgp2 = new BasicPattern();
diff --git a/jena-core/src/main/java/org/apache/jena/util/iterator/NiceIterator.java b/jena-core/src/main/java/org/apache/jena/util/iterator/NiceIterator.java
index 4a6871ac78..a30cb7c117 100644
--- a/jena-core/src/main/java/org/apache/jena/util/iterator/NiceIterator.java
+++ b/jena-core/src/main/java/org/apache/jena/util/iterator/NiceIterator.java
@@ -227,7 +227,8 @@ public class NiceIterator<T> implements ExtendedIterator<T>
     public static <T> Set<T> asSet( ExtendedIterator<T> it )
         {
         Set<T> result = new HashSet<>();
-        it.forEachRemaining(result::add);
+        try { it.forEachRemaining(result::add); }
+        finally { it.close(); }
         return result;
         }
 
@@ -238,7 +239,8 @@ public class NiceIterator<T> implements ExtendedIterator<T>
     public static <T> List<T> asList( ExtendedIterator<T> it )
         {
         List<T> result = new ArrayList<>();
-        it.forEachRemaining(result::add);
+        try { it.forEachRemaining(result::add); }
+        finally { it.close(); }
         return result;
         }
     }
diff --git a/jena-core/src/main/java/org/apache/jena/rdfxml/xmlinput0/NTriple.java b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/NTripleARP0.java
similarity index 99%
rename from jena-core/src/main/java/org/apache/jena/rdfxml/xmlinput0/NTriple.java
rename to jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/NTripleARP0.java
index 22f9a64bdc..6b3f0d7740 100644
--- a/jena-core/src/main/java/org/apache/jena/rdfxml/xmlinput0/NTriple.java
+++ b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/NTripleARP0.java
@@ -85,8 +85,8 @@ import org.xml.sax.SAXParseException;
  * </dl>
  * @deprecated Legacy. To be removed.
  */
-@Deprecated
-public class NTriple implements ARPErrorNumbers {
+@Deprecated(forRemoval = true)
+public class NTripleARP0 implements ARPErrorNumbers {
 
 	private static StringBuilder line = new StringBuilder();
     private static ARP0 arp;
@@ -198,7 +198,7 @@ public class NTriple implements ARPErrorNumbers {
 	static void usage() {
 		System.err.println(
 			"java <class-path> "
-				+ NTriple.class.getName()
+				+ NTripleARP0.class.getName()
 				+ " ( [ -[xstfurR]][ -b xmlBase -[eiw] NNN[,NNN...] ] [ file ] [ url ] )... ");
 		System.err.println(
 			"    All options, files and URLs can be intemingled in any order.");
diff --git a/jena-core/src/main/java/org/apache/jena/rdfxml/xmlinput1/NTriple.java b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/NTripleARP1.java
similarity index 98%
rename from jena-core/src/main/java/org/apache/jena/rdfxml/xmlinput1/NTriple.java
rename to jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/NTripleARP1.java
index f15e283b81..3b57f89355 100644
--- a/jena-core/src/main/java/org/apache/jena/rdfxml/xmlinput1/NTriple.java
+++ b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/NTripleARP1.java
@@ -83,20 +83,15 @@ import org.xml.sax.SAXParseException;
  * </dt><dd>
  * Ignores numbered error/warning conditions.
  * </dl>
+ * @deprecated Internaluse only To be removed.
  */
-@SuppressWarnings("deprecation")
-public class NTriple implements ARPErrorNumbers {
+@Deprecated(forRemoval = true)
+public class NTripleARP1 implements ARPErrorNumbers {
 
 	private static StringBuilder line = new StringBuilder();
 	private static ARP arp;
 	private static String xmlBase = null;
 	private static boolean numbers = false;
-	/** Starts an RDF/XML to NTriple converter.
-	 * @param args The command-line arguments.
-	 */
-	static public void main(String args[]) {
-		mainEh(args, null, null);
-	}
 	static StatementHandler andMeToo = null;
 	/** Starts an RDF/XML to NTriple converter,
 	 * using an error handler, and an ARPHandler.
@@ -197,7 +192,7 @@ public class NTriple implements ARPErrorNumbers {
 	static void usage() {
 		System.err.println(
 			"java <class-path> "
-				+ NTriple.class.getName()
+				+ NTripleARP1.class.getName()
 				+ " ( [ -[xstfurR]][ -b xmlBase -[eiw] NNN[,NNN...] ] [ file ] [ url ] )... ");
 		System.err.println(
 			"    All options, files and URLs can be intemingled in any order.");
diff --git a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/GenericPropertyFunction.java b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/GenericPropertyFunction.java
index 2c2d4e7f65..e389a681c6 100644
--- a/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/GenericPropertyFunction.java
+++ b/jena-geosparql/src/main/java/org/apache/jena/geosparql/geo/topological/GenericPropertyFunction.java
@@ -17,10 +17,11 @@
  */
 package org.apache.jena.geosparql.geo.topological;
 
-import java.util.ArrayList;
-import java.util.Collections;
+import java.util.Collection;
 import java.util.HashSet;
 import java.util.List;
+
+import org.apache.jena.atlas.iterator.Iter;
 import org.apache.jena.geosparql.geof.topological.GenericFilterFunction;
 import org.apache.jena.geosparql.implementation.GeometryWrapper;
 import org.apache.jena.geosparql.implementation.index.QueryRewriteIndex;
@@ -37,8 +38,10 @@ import org.apache.jena.sparql.engine.ExecutionContext;
 import org.apache.jena.sparql.engine.QueryIterator;
 import org.apache.jena.sparql.engine.binding.Binding;
 import org.apache.jena.sparql.engine.binding.BindingFactory;
+import org.apache.jena.sparql.engine.iterator.QueryIter;
 import org.apache.jena.sparql.engine.iterator.QueryIterConcat;
 import org.apache.jena.sparql.engine.iterator.QueryIterNullIterator;
+import org.apache.jena.sparql.engine.iterator.QueryIterPlainWrapper;
 import org.apache.jena.sparql.engine.iterator.QueryIterSingleton;
 import org.apache.jena.sparql.expr.ExprEvalException;
 import org.apache.jena.sparql.pfunction.PFuncSimple;
@@ -83,14 +86,18 @@ public abstract class GenericPropertyFunction extends PFuncSimple {
             //One bound and one unbound.
             return oneBound(binding, subject, predicate, object, execCxt);
         }
+    }
 
+    private QueryIterator bothBound(Binding binding, boolean isSubjectBound, Node subject, Node predicate, Node object, ExecutionContext execCxt) {
+        QueryIterator iter = isSubjectBound
+                ? bothBound(binding, subject, predicate, object, execCxt)
+                : bothBound(binding, object, predicate, subject, execCxt);
+        return iter;
     }
 
     private QueryIterator bothBound(Binding binding, Node subject, Node predicate, Node object, ExecutionContext execCxt) {
-
         Graph graph = execCxt.getActiveGraph();
         QueryRewriteIndex queryRewriteIndex = QueryRewriteIndex.retrieve(execCxt);
-
         Boolean isPositiveResult = queryRewrite(graph, subject, predicate, object, queryRewriteIndex);
         if (isPositiveResult) {
             //Filter function test succeded so retain binding.
@@ -99,39 +106,25 @@ public abstract class GenericPropertyFunction extends PFuncSimple {
             //Filter function test failed so null result.
             return QueryIterNullIterator.create(execCxt);
         }
-
     }
 
     private QueryIterator bothUnbound(Binding binding, Node subject, Node predicate, Node object, ExecutionContext execCxt) {
-
-        QueryIterConcat queryIterConcat = new QueryIterConcat(execCxt);
         Var subjectVar = Var.alloc(subject.getName());
 
         Graph graph = execCxt.getActiveGraph();
 
         //Search for both Features and Geometry in the Graph. Reliant upon consistent usage of SpatialObject (which is base class of Feature and Geometry) if present.
-        ExtendedIterator<Triple> subjectTriples;
-        if (graph.contains(null, RDF.type.asNode(), Geo.SPATIAL_OBJECT_NODE)) {
-            subjectTriples = graph.find(null, RDF.type.asNode(), Geo.SPATIAL_OBJECT_NODE);
-        } else if (graph.contains(null, RDF.type.asNode(), Geo.FEATURE_NODE) || graph.contains(null, RDF.type.asNode(), Geo.GEOMETRY_NODE)) {
-            ExtendedIterator<Triple> featureTriples = graph.find(null, RDF.type.asNode(), Geo.FEATURE_NODE);
-            ExtendedIterator<Triple> geometryTriples = graph.find(null, RDF.type.asNode(), Geo.GEOMETRY_NODE);
-            subjectTriples = featureTriples.andThen(geometryTriples);
-        } else {
-            //Check for Geo Predicate Features in the Graph if no GeometryLiterals found.
-            subjectTriples = graph.find(null, SpatialExtension.GEO_LAT_NODE, null);
-        }
-
-        //Bind all the Spatial Objects or Geo Predicates once as the subject and search for corresponding Objects.
-        while (subjectTriples.hasNext()) {
-            Triple subjectTriple = subjectTriples.next();
-            Node boundSubject = subjectTriple.getSubject();
-            Binding subjectBind = BindingFactory.binding(binding, subjectVar, boundSubject);
-            QueryIterator queryIter = oneBound(subjectBind, boundSubject, predicate, object, execCxt);
-            queryIterConcat.add(queryIter);
-        }
-
-        return queryIterConcat;
+        ExtendedIterator<Triple> spatialTriples = findSpatialTriples(graph);
+        ExtendedIterator<Binding> iterator = spatialTriples
+            .mapWith(Triple::getSubject)
+            .mapWith(node -> BindingFactory.binding(binding, subjectVar, node));
+
+        QueryIter queryIter = QueryIter.flatMap(
+            QueryIterPlainWrapper.create(iterator, execCxt),
+            b -> oneBound(b, b.get(subjectVar), predicate, object, execCxt),
+            execCxt
+        );
+        return queryIter;
     }
 
     private QueryIterator oneBound(Binding binding, Node subject, Node predicate, Node object, ExecutionContext execCxt) {
@@ -139,7 +132,7 @@ public abstract class GenericPropertyFunction extends PFuncSimple {
         Graph graph = execCxt.getActiveGraph();
         Node boundNode;
         Node unboundNode;
-        Boolean isSubjectBound;
+        boolean isSubjectBound;
         if (subject.isConcrete()) {
             //Subject is bound, object is unbound.
             boundNode = subject;
@@ -152,7 +145,10 @@ public abstract class GenericPropertyFunction extends PFuncSimple {
             isSubjectBound = false;
         }
 
-        if (!(boundNode.isLiteral() || graph.contains(boundNode, RDF.type.asNode(), Geo.SPATIAL_OBJECT_NODE) || graph.contains(boundNode, RDF.type.asNode(), Geo.FEATURE_NODE) || graph.contains(boundNode, RDF.type.asNode(), Geo.GEOMETRY_NODE))) {
+        if (!(boundNode.isLiteral() ||
+                graph.contains(boundNode, RDF.type.asNode(), Geo.SPATIAL_OBJECT_NODE) ||
+                graph.contains(boundNode, RDF.type.asNode(), Geo.FEATURE_NODE) ||
+                graph.contains(boundNode, RDF.type.asNode(), Geo.GEOMETRY_NODE))) {
             if (!graph.contains(boundNode, SpatialExtension.GEO_LAT_NODE, null)) {
                 //Bound node is not a Feature or a Geometry or has Geo predicates so exit.
                 return QueryIterNullIterator.create(execCxt);
@@ -160,25 +156,40 @@ public abstract class GenericPropertyFunction extends PFuncSimple {
         }
 
         boolean isSpatialIndex = SpatialIndex.isDefined(execCxt);
-        QueryIterConcat queryIterConcat;
+        QueryIterator result;
         if (!isSpatialIndex || filterFunction.isDisjoint() || filterFunction.isDisconnected()) {
             //Disjointed so retrieve all cases.
-            queryIterConcat = findAll(graph, boundNode, unboundNode, binding, isSubjectBound, predicate, execCxt);
+            result = findAll(graph, boundNode, unboundNode, binding, isSubjectBound, predicate, execCxt);
         } else {
             //Only retrieve those in the spatial index which are within same bounding box.
-            queryIterConcat = findIndex(graph, boundNode, unboundNode, binding, isSubjectBound, predicate, execCxt);
+            result = findIndex(graph, boundNode, unboundNode, binding, isSubjectBound, predicate, execCxt);
         }
-
-        return queryIterConcat;
+        return result;
     }
 
-    private QueryIterConcat findAll(Graph graph, Node boundNode, Node unboundNode, Binding binding, boolean isSubjectBound, Node predicate, ExecutionContext execCxt) {
+    private QueryIterator findAll(Graph graph, Node boundNode, Node unboundNode, Binding binding, boolean isSubjectBound, Node predicate, ExecutionContext execCxt) {
 
         //Prepare the results.
         Var unboundVar = Var.alloc(unboundNode.getName());
-        QueryIterConcat queryIterConcat = new QueryIterConcat(execCxt);
 
         //Search for both Features and Geometry in the Graph. Reliant upon consistent usage of SpatialObject (which is base class of Feature and Geometry) if present.
+        ExtendedIterator<Triple> spatialTriples = findSpatialTriples(graph);
+
+        ExtendedIterator<Binding> iterator = spatialTriples
+            .mapWith(Triple::getSubject)
+            .mapWith(node -> BindingFactory.binding(binding, unboundVar, node));
+
+        return QueryIter.flatMap(
+            QueryIterPlainWrapper.create(iterator, execCxt),
+            b -> {
+                Node spatialNode = b.get(unboundVar);
+                QueryIterator iter = bothBound(b, isSubjectBound, boundNode, predicate, spatialNode, execCxt);
+                return iter;
+            },
+            execCxt);
+    }
+
+    private static ExtendedIterator<Triple> findSpatialTriples(Graph graph) {
         ExtendedIterator<Triple> spatialTriples;
         if (graph.contains(null, RDF.type.asNode(), Geo.SPATIAL_OBJECT_NODE)) {
             spatialTriples = graph.find(null, RDF.type.asNode(), Geo.SPATIAL_OBJECT_NODE);
@@ -190,45 +201,33 @@ public abstract class GenericPropertyFunction extends PFuncSimple {
             //Check for Geo Predicate Features in the Graph if no GeometryLiterals found.
             spatialTriples = graph.find(null, SpatialExtension.GEO_LAT_NODE, null);
         }
-
-        while (spatialTriples.hasNext()) {
-            Triple spatialTriple = spatialTriples.next();
-            Node spatialNode = spatialTriple.getSubject();
-            Binding newBind = BindingFactory.binding(binding, unboundVar, spatialNode);
-            QueryIterator queryIter;
-            if (isSubjectBound) {
-                queryIter = bothBound(newBind, boundNode, predicate, spatialNode, execCxt);
-            } else {
-                queryIter = bothBound(newBind, spatialNode, predicate, boundNode, execCxt);
-            }
-            queryIterConcat.add(queryIter);
-        }
-
-        return queryIterConcat;
+        return spatialTriples;
     }
 
-    private QueryIterConcat findIndex(Graph graph, Node boundNode, Node unboundNode, Binding binding, boolean isSubjectBound, Node predicate, ExecutionContext execCxt) throws ExprEvalException {
-
+    private QueryIterator findIndex(Graph graph, Node boundNode, Node unboundNode, Binding binding, boolean isSubjectBound, Node predicate, ExecutionContext execCxt) throws ExprEvalException {
         try {
             //Prepare for results.
-            Var unboundVar = Var.alloc(unboundNode.getName());
-            QueryIterConcat queryIterConcat = new QueryIterConcat(execCxt);
+            Var unboundVar = Var.alloc(unboundNode);
 
             //Find the asserted triples.
-            List<Node> assertedNodes = !isSubjectBound || !boundNode.isLiteral() ? findAsserted(graph, boundNode, isSubjectBound, predicate) : Collections.emptyList();
-            for (Node node : assertedNodes) {
-                Binding newBind = BindingFactory.binding(binding, unboundVar, node);
-                QueryIterator queryIter = QueryIterSingleton.create(newBind, execCxt);
-                queryIterConcat.add(queryIter);
-            }
+            Collection<Node> assertedNodes = !isSubjectBound || !boundNode.isLiteral()
+                ? findAsserted(graph, boundNode, isSubjectBound, predicate)
+                : List.of();
+
+            QueryIterator assertedNodesIter = QueryIterPlainWrapper.create(
+                Iter.map(assertedNodes.iterator(), node -> BindingFactory.binding(binding, unboundVar, node)),
+                execCxt);
 
             //Find the GeometryLiteral of the Bound Node.
             SpatialObjectGeometryLiteral boundGeometryLiteral = SpatialObjectGeometryLiteral.retrieve(graph, boundNode);
             if (!boundGeometryLiteral.isValid()) {
                 //Bound Node is not a Feature or a Geometry or there is no GeometryLiteral so exit.
-                return queryIterConcat;
+                return assertedNodesIter;
             }
 
+            QueryIterConcat queryIterConcat = new QueryIterConcat(execCxt);
+            queryIterConcat.add(assertedNodesIter);
+
             Node geometryLiteral = boundGeometryLiteral.getGeometryLiteral();
 
             //Perform the search of the Spatial Index of the Dataset.
@@ -238,39 +237,19 @@ public abstract class GenericPropertyFunction extends PFuncSimple {
             Envelope searchEnvelope = transformedGeom.getEnvelope();
             HashSet<Resource> features = spatialIndex.query(searchEnvelope);
 
-            //Check each of the Features that match the search.
-            for (Resource feature : features) {
-                Node featureNode = feature.asNode();
-
-                //Ensure not already an asserted node.
-                if (!assertedNodes.contains(featureNode)) {
-
-                    Binding newBind = BindingFactory.binding(binding, unboundVar, featureNode);
-                    QueryIterator queryIter;
-                    if (isSubjectBound) {
-                        queryIter = bothBound(newBind, boundNode, predicate, featureNode, execCxt);
-                    } else {
-                        queryIter = bothBound(newBind, featureNode, predicate, boundNode, execCxt);
-                    }
-                    queryIterConcat.add(queryIter);
-                }
-
-                //Also test all Geometry of the Features. All, some or one Geometry may have matched.
-                List<Node> featureGeometryTriples = G.listSP(graph, feature.asNode(), Geo.HAS_GEOMETRY_NODE);
-                for ( Node geomNode : featureGeometryTriples) {
-                    //Ensure not already an asserted node.
-                    if (!assertedNodes.contains(geomNode)) {
-                        Binding newBind = BindingFactory.binding(binding, unboundVar, geomNode);
-                        QueryIterator queryIter;
-                        if (isSubjectBound) {
-                            queryIter = bothBound(newBind, boundNode, predicate, geomNode, execCxt);
-                        } else {
-                            queryIter = bothBound(newBind, geomNode, predicate, boundNode, execCxt);
-                        }
-                        queryIterConcat.add(queryIter);
-                    }
-                }
-            }
+            // Check each of the Features that match the search.
+            QueryIterator featuresIter = QueryIterPlainWrapper.create(
+                    Iter.map(features.iterator(), feature -> BindingFactory.binding(binding, unboundVar, feature.asNode())),
+                    execCxt);
+
+            QueryIterator queryIterator = QueryIter.flatMap(featuresIter,
+                featureBinding -> {
+                    return findByFeature(graph, binding, featureBinding,
+                            isSubjectBound, boundNode, predicate, unboundVar,
+                            execCxt, assertedNodes);
+                },
+                execCxt);
+            queryIterConcat.add(queryIterator);
 
             return queryIterConcat;
         } catch (MismatchedDimensionException | TransformException | FactoryException | SpatialIndexException ex) {
@@ -278,15 +257,46 @@ public abstract class GenericPropertyFunction extends PFuncSimple {
         }
     }
 
-    private List<Node> findAsserted(Graph graph, Node boundNode, boolean isSubjectBound, Node predicate) {
-        List<Node> assertedNodes = new ArrayList<>();
-        if (isSubjectBound) {
-            List<Node> x = G.listSP(graph, boundNode, predicate);
-            assertedNodes.addAll(x);
-        } else {
-            List<Node> x = G.listPO(graph, predicate, boundNode);
-            assertedNodes.addAll(x);
+    private QueryIterator findByFeature(Graph graph, Binding binding, Binding featureBinding,
+            boolean isSubjectBound, Node boundNode, Node predicate, Var unboundVar,
+            ExecutionContext execCxt, Collection<Node> assertedNodes) {
+
+        Node featureNode = featureBinding.get(unboundVar);
+        QueryIterConcat featureIterConcat = new QueryIterConcat(execCxt);
+
+        // Check Features directly if not already asserted
+        if (!assertedNodes.contains(featureNode)) {
+            QueryIterator tmpIter = bothBound(featureBinding, isSubjectBound, boundNode, predicate, featureNode, execCxt);
+            featureIterConcat.add(tmpIter);
         }
+
+        // Also test all Geometry of the Features. All, some or one Geometry may have matched.
+        ExtendedIterator<Node> featureGeometries = G.iterSP(graph, featureNode, Geo.HAS_GEOMETRY_NODE);
+        QueryIterator geometriesQueryIterator = QueryIterPlainWrapper.create(
+            Iter.map(
+                Iter.filter( // omit asserted
+                    featureGeometries,
+                    geometry -> !assertedNodes.contains(geometry)
+                ),
+                geometryNode -> BindingFactory.binding(binding, unboundVar, geometryNode)),
+            execCxt);
+
+        geometriesQueryIterator = QueryIter.flatMap(
+            geometriesQueryIterator,
+            b2 -> {
+                Node geomNode = b2.get(unboundVar);
+                return bothBound(b2, isSubjectBound, boundNode, predicate, geomNode, execCxt);
+            },
+            execCxt);
+
+        featureIterConcat.add(geometriesQueryIterator);
+        return featureIterConcat;
+    }
+
+    private List<Node> findAsserted(Graph graph, Node boundNode, boolean isSubjectBound, Node predicate) {
+        List<Node> assertedNodes = isSubjectBound
+            ? G.listSP(graph, boundNode, predicate)
+            : G.listPO(graph, predicate, boundNode);
         return assertedNodes;
     }
 
@@ -323,5 +333,4 @@ public abstract class GenericPropertyFunction extends PFuncSimple {
     public Boolean testFilterFunction(Node subjectGeometryLiteral, Node objectGeometryLiteral) {
         return filterFunction.exec(subjectGeometryLiteral, objectGeometryLiteral);
     }
-
 }
diff --git a/jena-iri3986/src/main/java/org/apache/jena/rfc3986/ParseDID.java b/jena-iri3986/src/main/java/org/apache/jena/rfc3986/ParseDID.java
index adc38247e7..564677de79 100644
--- a/jena-iri3986/src/main/java/org/apache/jena/rfc3986/ParseDID.java
+++ b/jena-iri3986/src/main/java/org/apache/jena/rfc3986/ParseDID.java
@@ -43,7 +43,7 @@ public class ParseDID {
 
     April 2021: At-risk : add: empty  method-specific-id
 */
-    private static final Pattern DID_PATTERN = Pattern.compile("^did:[a-z]+:(?:(?:[A-Za-z0-9.-_]|%[0-9]A-Fa-f]{2})+)$");
+    private static final Pattern DID_PATTERN = Pattern.compile("^did:[a-z]+:(?:(?:[a-z0-9.-_]|%[0-9]A-Fa-f]{2})+)$");
     private static final int DIDStart = "did:".length();
 
     static
@@ -55,10 +55,6 @@ public class ParseDID {
         int end = string.length();
         int p = DIDStart;
 
-        // XXX if string is empty or single space.
-        // XXX if string starts "."
-        // XXX if string ends ":"
-
         int q = methodName(string, p);
         if ( q <= p+1)
             error(string, "No method name");
@@ -84,6 +80,7 @@ public class ParseDID {
                 p++;
                 return p;
             }
+            // Special upper case for better error message?
             if ( ! methodChar(ch) )
                 error(str, "Bad character: '"+Character.toString(ch)+"'");
             p++;
diff --git a/jena-iri3986/src/main/java/org/apache/jena/rfc3986/ParseDNS.java b/jena-iri3986/src/main/java/org/apache/jena/rfc3986/ParseDNS.java
index c52360d081..8d7c3da981 100644
--- a/jena-iri3986/src/main/java/org/apache/jena/rfc3986/ParseDNS.java
+++ b/jena-iri3986/src/main/java/org/apache/jena/rfc3986/ParseDNS.java
@@ -52,7 +52,7 @@ public class ParseDNS {
      *   Internationalization
      */
 
-    /* RFC952:
+    /* RFC 952:
      *
      * <hname> ::= <name>*["."<name>]
      * <name> ::=  <let>[*[<let-or-digit-or-hyphen>]<let-or-digit>]
diff --git a/pom.xml b/pom.xml
index 27ee51653f..00ac5f43fc 100644
--- a/pom.xml
+++ b/pom.xml
@@ -63,7 +63,7 @@
     <ver.log4j2>2.24.3</ver.log4j2>
 
     <!-- JSON-LD 1.1 -->
-    <ver.titanium-json-ld>1.4.1</ver.titanium-json-ld>
+    <ver.titanium-json-ld>1.5.0</ver.titanium-json-ld>
     <ver.jakarta.json>2.0.1</ver.jakarta.json>
 
     <!-- These two must be in-step -->


Test patch:
diff --git a/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/ARPTests2.java b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/ARPTests2.java
index dfb86f3ce8..9aecef2887 100644
--- a/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/ARPTests2.java
+++ b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/ARPTests2.java
@@ -545,6 +545,7 @@ public class ARPTests2 extends TestCase implements RDFErrorHandler, ARPErrorNumb
 //        checkExpected() ;
 //    }
 
+    @SuppressWarnings("removal")
     public void testNTripleEscaping() {
     	String data[][] = {
     			{ "foo", "foo" },
@@ -555,7 +556,7 @@ public class ARPTests2 extends TestCase implements RDFErrorHandler, ARPErrorNumb
     	};
     	for (String p[] : data ) {
 //    		System.err.println(NTriple.escapeNTriple(p[0]));
-    		Assert.assertEquals("NTriple escaping", p[1],NTriple.escapeNTriple(p[0]) );
+    		Assert.assertEquals("NTriple escaping", p[1],NTripleARP0.escapeNTriple(p[0]) );
     	}
 
 
diff --git a/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/NTripleTestSuite.java b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/NTripleTestSuite.java
index ca23ed1902..9f489d37a9 100644
--- a/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/NTripleTestSuite.java
+++ b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/NTripleTestSuite.java
@@ -242,7 +242,7 @@ class NTripleTestSuite extends WGTestSuite {
 		return loadRDFx(in, eh, base, true, 0);
 	}
 
-    @SuppressWarnings("deprecation")
+    @SuppressWarnings("removal")
     static Model loadRDFx(
 		InFactoryX in,
 		RDFErrorHandler eh,
@@ -274,7 +274,7 @@ class NTripleTestSuite extends WGTestSuite {
 			System.setIn(in.open());
 			System.setOut(out);
 			try {
-				NTriple.mainEh(new String[] { "-b", base, "-s" }, th, th);
+				NTripleARP0.mainEh(new String[] { "-b", base, "-s" }, th, th);
 			} catch (SimulatedException e) {
 				if (wantModel)
 					throw e;
diff --git a/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/TaintingTests.java b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/TaintingTests.java
index 20ce16401c..e8b31bd39e 100644
--- a/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/TaintingTests.java
+++ b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput0/TaintingTests.java
@@ -178,7 +178,7 @@ public class TaintingTests extends TestCase implements ErrorHandler,
 	}
 
     @Override
-    @SuppressWarnings("deprecation")
+    @SuppressWarnings("removal")
     public void runTest() throws IOException {
         ByteArrayOutputStream goodBytes = new ByteArrayOutputStream();
         ByteArrayOutputStream badBytes = new ByteArrayOutputStream();
@@ -187,7 +187,7 @@ public class TaintingTests extends TestCase implements ErrorHandler,
         try (PrintStream out = new PrintStream(goodBytes); PrintStream err = new PrintStream(badBytes); ) {
             System.setOut(out);
             System.setErr(err);
-            NTriple.mainEh(new String[]{"-e","102,136,105,103,108,107,116,106,004,131",
+            NTripleARP0.mainEh(new String[]{"-e","102,136,105,103,108,107,116,106,004,131",
                 "-E","-b",base,fileName},this,null);
         }
         finally {
diff --git a/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/NTripleTestSuite.java b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/NTripleTestSuite.java
index 7f73ba0049..1675c804e5 100644
--- a/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/NTripleTestSuite.java
+++ b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/NTripleTestSuite.java
@@ -226,6 +226,7 @@ class NTripleTestSuite extends WGTestSuite {
 		return loadRDFx(in, eh, base, true, 0);
 	}
 
+    @SuppressWarnings("removal")
     static Model loadRDFx(
 		InputSupplier in,
 		RDFErrorHandler eh,
@@ -257,7 +258,7 @@ class NTripleTestSuite extends WGTestSuite {
 			System.setIn(in.open());
 			System.setOut(out);
 			try {
-				NTriple.mainEh(new String[] { "-b", base, "-s" }, th, th);
+				NTripleARP1.mainEh(new String[] { "-b", base, "-s" }, th, th);
 			} catch (SimulatedException e) {
 				if (wantModel)
 					throw e;
diff --git a/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/TestsARP.java b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/TestsARP.java
index cdd10d6ff9..e99533e721 100644
--- a/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/TestsARP.java
+++ b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/TestsARP.java
@@ -495,6 +495,7 @@ public class TestsARP extends TestCase implements RDFErrorHandler, ARPErrorNumbe
 //        checkExpected() ;
 //    }
 
+    @SuppressWarnings("removal")
     public void testNTripleEscaping() {
     	String data[][] = {
     			{ "foo", "foo" },
@@ -505,7 +506,7 @@ public class TestsARP extends TestCase implements RDFErrorHandler, ARPErrorNumbe
     	};
     	for (String p[] : data ) {
 //    		System.err.println(NTriple.escapeNTriple(p[0]));
-    		Assert.assertEquals("NTriple escaping", p[1],NTriple.escapeNTriple(p[0]) );
+    		Assert.assertEquals("NTriple escaping", p[1],NTripleARP1.escapeNTriple(p[0]) );
     	}
 
 
diff --git a/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/TestsTainting.java b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/TestsTainting.java
index b51e4e0b86..ba472f50cb 100644
--- a/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/TestsTainting.java
+++ b/jena-core/src/test/java/org/apache/jena/rdfxml/xmlinput1/TestsTainting.java
@@ -183,6 +183,7 @@ public class TestsTainting extends TestCase implements ErrorHandler,
 		OntDocumentManager.getInstance().reset(true);
 	}
 
+    @SuppressWarnings("removal")
     @Override
     public void runTest() throws IOException {
         ByteArrayOutputStream goodBytes = new ByteArrayOutputStream();
@@ -192,7 +193,7 @@ public class TestsTainting extends TestCase implements ErrorHandler,
         try (PrintStream out = new PrintStream(goodBytes); PrintStream err = new PrintStream(badBytes); ) {
             System.setOut(out);
             System.setErr(err);
-            NTriple.mainEh(new String[]{"-e","102,136,105,103,108,107,116,106,004,131",
+            NTripleARP1.mainEh(new String[]{"-e","102,136,105,103,108,107,116,106,004,131",
                 "-E","-b",base,fileName},this,null);
         }
         finally {
diff --git a/jena-geosparql/src/test/java/org/apache/jena/geosparql/geo/topological/CancelQueryTest.java b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geo/topological/CancelQueryTest.java
new file mode 100644
index 0000000000..7b54ebb904
--- /dev/null
+++ b/jena-geosparql/src/test/java/org/apache/jena/geosparql/geo/topological/CancelQueryTest.java
@@ -0,0 +1,166 @@
+/*
+ * 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.jena.geosparql.geo.topological;
+
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.function.Function;
+import java.util.stream.IntStream;
+
+import org.apache.jena.geosparql.configuration.GeoSPARQLConfig;
+import org.apache.jena.geosparql.implementation.datatype.WKTDatatype;
+import org.apache.jena.geosparql.implementation.index.IndexConfiguration;
+import org.apache.jena.geosparql.implementation.vocabulary.Geo;
+import org.apache.jena.geosparql.spatial.SpatialIndex;
+import org.apache.jena.geosparql.spatial.SpatialIndexException;
+import org.apache.jena.graph.Graph;
+import org.apache.jena.graph.NodeFactory;
+import org.apache.jena.query.Dataset;
+import org.apache.jena.query.DatasetFactory;
+import org.apache.jena.query.Query;
+import org.apache.jena.query.QueryCancelledException;
+import org.apache.jena.query.QueryExecution;
+import org.apache.jena.query.QueryFactory;
+import org.apache.jena.query.ResultSet;
+import org.apache.jena.rdf.model.Model;
+import org.apache.jena.rdf.model.ModelFactory;
+import org.apache.jena.sparql.graph.GraphFactory;
+import org.apache.jena.vocabulary.RDF;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+@RunWith(Parameterized.class)
+public class CancelQueryTest {
+
+    private final int numGeometries;
+
+    public CancelQueryTest(int numGeometries) {
+        this.numGeometries = numGeometries;
+    }
+
+    @Parameterized.Parameters(name = "number of geometries: {0}")
+    public static List<Integer> sizes() {
+        return List.of(
+            31_623 // The square root of 1 billion approximated as an integer.
+        );
+    }
+
+    public static Graph createSpatialGraph(int numGeometries) {
+        Graph graph = GraphFactory.createDefaultGraph();
+
+        IntStream.range(0, numGeometries).forEach(i -> {
+            // features
+            graph.add(NodeFactory.createURI("http://www.example.org/r" + i), RDF.type.asNode(), Geo.FEATURE_NODE);
+            // geometries
+            graph.add(NodeFactory.createURI("http://www.example.org/r" + i),
+                    Geo.HAS_GEOMETRY_PROP.asNode(),
+                    NodeFactory.createURI("http://www.example.org/r" + i + "/geometry"));
+            // geo:Geometry type triples
+            graph.add(NodeFactory.createURI("http://www.example.org/r" + i + "/geometry"),
+                    RDF.type.asNode(),
+                    Geo.GEOMETRY_NODE);
+            // geometry WKT literals
+            graph.add(NodeFactory.createURI("http://www.example.org/r" + i + "/geometry"),
+                    Geo.AS_WKT_PROP.asNode(),
+                    NodeFactory.createLiteralDT("POINT(2 2)", WKTDatatype.INSTANCE));
+        });
+        // System.out.printf("created graph with %d triples and %d geometries\n", graph.size(), numGeometries);
+
+        return graph;
+    }
+
+    @Test(timeout = 10000)
+    public void test_cancel_spatial_property_function1() {
+        GeoSPARQLConfig.setup(IndexConfiguration.IndexOption.MEMORY, Boolean.TRUE);
+
+        long cancelDelayMillis = 1000;
+        boolean useIndex = true;
+
+        // Create a dataset with spatial triples
+        Graph graph = createSpatialGraph(numGeometries);
+        Model model = ModelFactory.createModelForGraph(graph);
+        Dataset ds = DatasetFactory.create(model);
+
+        // create spatial index
+        if (useIndex){
+            try {
+                SpatialIndex index = SpatialIndex.buildSpatialIndex(ds);
+                SpatialIndex.setSpatialIndex(ds, index);
+            } catch (SpatialIndexException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        // Create a query that queries for spatial relation with both sides being unbound, thus, pairwise comparison would be needed
+        Query query = QueryFactory.create("PREFIX geo: <http://www.opengis.net/ont/geosparql#> SELECT * { ?a geo:sfIntersects ?b . }");
+        Callable<QueryExecution> qeFactory = () -> QueryExecution.dataset(ds).query(query).build();//.timeout(2000, TimeUnit.MILLISECONDS).build();
+
+        runAsyncAbort(cancelDelayMillis, qeFactory, CancelQueryTest::doCount);
+    }
+
+    private static long doCount(QueryExecution qe) {
+        // System.out.println("Executing query ...");
+        long counter = 0;
+        try (QueryExecution qe2 = qe) {
+            ResultSet rs = qe2.execSelect();
+            while (rs.hasNext()) {
+                rs.next();
+                ++counter;
+            }
+        } finally {
+            // System.out.println("Aborted after seeing " + counter + " bindings");
+        }
+        return counter;
+    }
+
+    public static void runAsyncAbort(long cancelDelayMillis, Callable<QueryExecution> qeFactory, Function<QueryExecution, ?> processor) {
+        ExecutorService executorService = Executors.newSingleThreadExecutor();
+
+        try (QueryExecution qe = qeFactory.call()){
+            Future<?> future = executorService.submit(() -> processor.apply(qe));
+            try {
+                Thread.sleep(cancelDelayMillis);
+            } catch (InterruptedException e) {
+                throw new RuntimeException(e);
+            }
+            // System.out.println("Aborting query execution: " + qe);
+            qe.abort();
+            try {
+                future.get();
+            } catch (ExecutionException e) {
+                Throwable cause = e.getCause();
+                if (!(cause instanceof QueryCancelledException)) {
+                    e.printStackTrace();
+                }
+                Assert.assertEquals(QueryCancelledException.class, cause.getClass());
+            }
+        } catch (Exception e) {
+            throw new RuntimeException("Failed to build a query execution", e);
+        } finally {
+            // System.out.println("Completed: " + qe);
+            executorService.shutdownNow();
+        }
+    }
+}
diff --git a/jena-shex/src/test/files/spec/validation/Q.rq b/jena-shex/src/test/files/spec/validation/Q.rq
deleted file mode 100644
index 9c683906da..0000000000
--- a/jena-shex/src/test/files/spec/validation/Q.rq
+++ /dev/null
@@ -1,6 +0,0 @@
-PREFIX sht: <http://www.w3.org/ns/shacl/test-suite#>
-
-SELECT DISTINCT ?o {
-    ?s a ?o .
-    FILTER(STRSTARTS( str(?o) , str(sht:) ))
-}