Repository: apache/jena (java)

Issue:
XSD DataTime with T24:00:00 not adjusted 
### Version

5.2.0

### What happened?

```sh
qexpr '"1999-12-31T24:00:00"^^xsd:dateTime = "2000-01-01T00:00:00"^^xsd:dateTime'
```
should be true.



### Relevant output and stacktrace

_No response_

### Are you interested in making a pull request?

Yes

Pull request:
GH-2908: Adjust for T24:00:00 in xsd:dateTime
GitHub issue resolved #2908

----

 - [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/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/Table1.java b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/Table1.java
index a7cc31ea1a..1f21ad409b 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/Table1.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/algebra/table/Table1.java
@@ -24,7 +24,6 @@ import java.util.List ;
 
 import org.apache.jena.atlas.iterator.Iter ;
 import org.apache.jena.graph.Node ;
-import org.apache.jena.riot.out.NodeFmtLib ;
 import org.apache.jena.sparql.core.Var ;
 import org.apache.jena.sparql.engine.ExecutionContext ;
 import org.apache.jena.sparql.engine.QueryIterator ;
@@ -34,25 +33,24 @@ import org.apache.jena.sparql.engine.iterator.QueryIterSingleton ;
 
 /** A table of one row of one binding */
 public class Table1 extends TableBase {
-    private Var  var ;
-    private Node value ;
+    private final Binding row;
 
     public Table1(Var var, Node value) {
-        this.var = var ;
-        this.value = value ;
+        this.row = BindingFactory.binding(var, value);
+    }
+
+    public Table1(Binding row) {
+        this.row = row;
     }
 
     @Override
     public Iterator<Binding> rows() {
-        Binding b = BindingFactory.binding(var, value) ;
-        return Iter.singletonIterator(b) ;
+        return Iter.singletonIterator(row) ;
     }
 
     @Override
     public QueryIterator iterator(ExecutionContext execCxt) {
-        // Root binding?
-        Binding binding = BindingFactory.binding(var, value) ;
-        QueryIterator qIter = QueryIterSingleton.create(null, var, value, execCxt) ;
+        QueryIterator qIter = QueryIterSingleton.create(row, execCxt) ;
         return qIter ;
     }
 
@@ -61,15 +59,15 @@ public class Table1 extends TableBase {
 
     @Override
     public List<Var> getVars() {
-        List<Var> x = new ArrayList<>() ;
-        x.add(var) ;
-        return x ;
+        List<Var> x = new ArrayList<>();
+        row.forEach((v,n)->x.add(v));
+        return x;
     }
 
     @Override
     public List<String> getVarNames() {
         List<String> x = new ArrayList<>() ;
-        x.add(var.getVarName()) ;
+        row.forEach((v,n)->x.add(v.getName()));
         return x ;
     }
 
@@ -85,6 +83,6 @@ public class Table1 extends TableBase {
 
     @Override
     public String toString() {
-        return "Table1(" + var + "," + NodeFmtLib.displayStr(value) + ")" ;
+        return "Table1(" + row + ")" ;
     }
 }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/core/Substitute.java b/jena-arq/src/main/java/org/apache/jena/sparql/core/Substitute.java
index 19cbadc296..e741351417 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/core/Substitute.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/core/Substitute.java
@@ -49,7 +49,7 @@ import org.apache.jena.sparql.syntax.syntaxtransform.UpdateTransformOps;
 public class Substitute {
     /**
      * Inject takes an {@link Op} to transform using a {Binding binding}. The
-     * transformation assumes the Ope structure is legal for the operation. The
+     * transformation assumes the Op structure is legal for the operation. The
      * transformation is to wrap each place a variable is used (BGP, GRAPH, Path and
      * some equivalent operations) with a {@code BIND} to restrict the vartibale to a specific value
      * while still retaining the variable (e.g for FILETERs).
@@ -71,7 +71,7 @@ public class Substitute {
      */
     public static Op inject(Op opInput, Binding binding) {
         Set<Var> injectVars = binding.varsMentioned();
-        Transform transform = new QueryIterLateral.TransformInject(injectVars, binding::get);
+        Transform transform = new QueryIterLateral.TransformInject(injectVars, binding);
         Op opOutput = Transformer.transform(transform, opInput);
         return opOutput;
     }
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterLateral.java b/jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterLateral.java
index 1162c6cb80..9ce2ce85a1 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterLateral.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/engine/iterator/QueryIterLateral.java
@@ -18,6 +18,7 @@
 
 package org.apache.jena.sparql.engine.iterator;
 
+import java.util.ArrayList;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
@@ -27,8 +28,11 @@ import org.apache.jena.atlas.lib.SetUtils;
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
 import org.apache.jena.sparql.algebra.Op;
+import org.apache.jena.sparql.algebra.Table;
 import org.apache.jena.sparql.algebra.TransformCopy;
 import org.apache.jena.sparql.algebra.op.*;
+import org.apache.jena.sparql.algebra.table.Table1;
+import org.apache.jena.sparql.algebra.table.TableN;
 import org.apache.jena.sparql.core.*;
 import org.apache.jena.sparql.engine.ExecutionContext;
 import org.apache.jena.sparql.engine.QueryIterator;
@@ -74,14 +78,14 @@ public class QueryIterLateral extends QueryIterRepeatApply {
         private final Set<Var> injectVars;
         private final Set<Node> varsAsNodes;
         private final Function<Var, Node> replacement;
+        private final Binding binding;
         private static final boolean substitute = true;
 
-        // Replacement becomes binding.??
-        // Or "op call injection"!!
-        public TransformInject(Set<Var> injectVars, Function<Var, Node> replacement) {
+        public TransformInject(Set<Var> injectVars, Binding binding) {
             this.injectVars = injectVars;
             this.varsAsNodes = Set.copyOf(injectVars);
-            this.replacement = replacement;
+            this.replacement = binding::get;
+            this.binding = binding;
         }
 
         @Override
@@ -215,6 +219,7 @@ public class QueryIterLateral extends QueryIterRepeatApply {
 //            Basic Graph Pattern Matching
 //            Property Path Patterns
 //            evaluation of algebra form Graph(var,P) involving a variable (from the syntax GRAPH ?variable {&hellip;})
+        // and also nested (table unit) inside (extend)
 
         @Override
         public Op transform(OpPath opPath) {
@@ -270,6 +275,36 @@ public class QueryIterLateral extends QueryIterRepeatApply {
             return opExec;
         }
 
+        private OpTable tableUnitTransformed = null;
+
+        @Override
+        public Op transform(OpTable opTable) {
+            // Unit table.
+            if ( opTable.isJoinIdentity() ) {
+                if ( tableUnitTransformed == null ) {
+                    Table table2 = new Table1(binding);
+                    // Multiple assignment does not matter!
+                    tableUnitTransformed = OpTable.create(table2);
+                }
+                return tableUnitTransformed;
+            }
+
+            // By the assignment restriction, the binding only needs to be added to each row of the table.
+            Table table = opTable.getTable();
+            // Table vars.
+            List<Var> vars = new ArrayList<>(table.getVars());
+            binding.vars().forEachRemaining(vars::add);
+            TableN table2 = new TableN(vars);
+            BindingBuilder builder = BindingFactory.builder();
+            table.iterator(null).forEachRemaining(row->{
+                builder.reset();
+                builder.addAll(row);
+                builder.addAll(binding);
+                table2.addBinding(builder.build());
+            });
+            return OpTable.create(table2);
+        }
+
         private Triple applyReplacement(Triple triple, Function<Var, Node> replacement) {
             Node s2 = applyReplacement(triple.getSubject(), replacement);
             Node p2 = applyReplacement(triple.getPredicate(), replacement);
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/NodeFunctions.java b/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/NodeFunctions.java
index 829aeb314e..4f015eb903 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/NodeFunctions.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/NodeFunctions.java
@@ -503,10 +503,14 @@ public class NodeFunctions {
         return NodeValue.makeLangString(lex, lang) ;
     }
 
+
+    /** Canonocal duration of 0 -- "P0S" */
+    private static Duration zeroDuration = NodeValue.xmlDatatypeFactory.newDuration(true, null, null, null, null, null, BigDecimal.ZERO) ;
+
     /** A duration, tided */
     public static Duration duration(int seconds) {
         if ( seconds == 0 )
-            return XSDFuncOp.zeroDuration;
+            return zeroDuration;
         Duration dur = NodeValue.xmlDatatypeFactory.newDuration(1000L*seconds);
         // Neaten the duration. Not all the fields are zero.
         dur = NodeValue.xmlDatatypeFactory.newDuration(dur.getSign()>=0,
diff --git a/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/XSDFuncOp.java b/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/XSDFuncOp.java
index 0082091bb5..66c1f24c8f 100644
--- a/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/XSDFuncOp.java
+++ b/jena-arq/src/main/java/org/apache/jena/sparql/expr/nodevalue/XSDFuncOp.java
@@ -1142,6 +1142,13 @@ public class XSDFuncOp
                 dt.setHour(0);
             return dt;
         }
+        if ( nv.isDateTime() ) {
+            if ( dt.getHour() == 24 ) {
+                dt.setHour(0);
+                dt.add(duration1day);
+            }
+            return dt;
+        }
         return dt;
     }
 
@@ -1169,6 +1176,14 @@ public class XSDFuncOp
             if ( dt.getHour() == 24 )
                 // Normalize "24:00:00"
                 dt.setHour(0);
+            return dt;
+        }
+        if ( nv.isDateTime() ) {
+            if ( dt.getHour() == 24 ) {
+                dt.setHour(0);
+                dt.add(duration1day);
+            }
+            return dt;
         }
         return dt;
     }
@@ -1611,7 +1626,9 @@ public class XSDFuncOp
         return NodeValue.makeInteger((BigInteger)x) ;
     }
 
-    public static Duration zeroDuration = NodeValue.xmlDatatypeFactory.newDuration(true, null, null, null, null, null, BigDecimal.ZERO) ;
+    private static Duration duration1day = NodeValue.xmlDatatypeFactory.newDuration(true,
+                                                                                    BigInteger.ZERO, BigInteger.ZERO, BigInteger.ONE,
+                                                                                    BigInteger.ZERO, BigInteger.ZERO, BigDecimal.ZERO) ;
 
     public static int compareDuration(NodeValue nv1, NodeValue nv2) {
         return compareDuration(nv1.getDuration(), nv2.getDuration()) ;
diff --git a/jena-cmds/src/main/java/arq/cmdline/ModEngine.java b/jena-cmds/src/main/java/arq/cmdline/ModEngine.java
index c278ef2f45..bb6b4b6028 100644
--- a/jena-cmds/src/main/java/arq/cmdline/ModEngine.java
+++ b/jena-cmds/src/main/java/arq/cmdline/ModEngine.java
@@ -44,24 +44,25 @@ public class ModEngine extends ModBase
     private boolean timing = false ;
 
     @Override
-    public void registerWith(CmdGeneral cmdLine)
-    {
-        cmdLine.getUsage().startCategory("Query Engine") ;
-        cmdLine.add(engineDecl, "--engine=EngineName", "Register another engine factory[ref]") ;
-        cmdLine.add(unEngineDecl, "--unengine=EngineName", "Unregister an engine factory") ;
+    public void registerWith(CmdGeneral cmdLine) {
+        cmdLine.getUsage().startCategory("Query Engine");
+        cmdLine.add(engineDecl, "--engine=EngineName", "Register another engine factory[ref]");
+        cmdLine.add(unEngineDecl, "--unengine=EngineName", "Unregister an engine factory");
     }
 
-    public void checkCommandLine(CmdGeneral cmdLine)
-    {}
+    public void resetRegistrations() {
+        QueryEngineRef.unregister();
+        QueryEngineRefQuad.unregister();
+        QueryEngineMainQuad.unregister();
+    }
 
     @Override
-    public void processArgs(CmdArgModule cmdLine)
-    {
+    public void processArgs(CmdArgModule cmdLine) {
 
-        List<String> engineDecls = cmdLine.getValues(engineDecl) ;
+        List<String> engineDecls = cmdLine.getValues(engineDecl);
 
-//        if ( x.size() > 0 )
-//            QueryEngineRegistry.get().factories().clear() ;
+        // if ( x.size() > 0 )
+        // QueryEngineRegistry.get().factories().clear() ;
 
         for ( String engineName : engineDecls ) {
             switch (engineName.toLowerCase()) {
@@ -75,7 +76,7 @@ public class ModEngine extends ModBase
             }
         }
 
-        List<String> unEngineDecls = cmdLine.getValues(unEngineDecl) ;
+        List<String> unEngineDecls = cmdLine.getValues(unEngineDecl);
         for ( String engineName : unEngineDecls ) {
             switch (engineName.toLowerCase()) {
                 case "reference", "ref" -> QueryEngineRef.unregister();
diff --git a/jena-cmds/src/main/java/arq/query.java b/jena-cmds/src/main/java/arq/query.java
index e3e7a6c75e..f08f250f52 100644
--- a/jena-cmds/src/main/java/arq/query.java
+++ b/jena-cmds/src/main/java/arq/query.java
@@ -61,28 +61,27 @@ public class query extends CmdARQ
     protected ModResultsOut modResults =  new ModResultsOut() ;
     protected ModEngine     modEngine =   new ModEngine() ;
 
-    public static void main (String... argv)
-    {
-        new query(argv).mainRun() ;
+    public static void main(String...argv) {
+        new query(argv).mainRun();
     }
 
-    public query(String[] argv)
-    {
-        super(argv) ;
-        modQuery = new ModQueryIn(getDefaultSyntax()) ;
-        modDataset = setModDataset() ;
+    public query(String[] argv) {
+        super(argv);
+        modQuery = new ModQueryIn(getDefaultSyntax());
+        modDataset = setModDataset();
         modVersion.addClass(null, Jena.class);
 
-        super.addModule(modQuery) ;
-        super.addModule(modResults) ;
-        super.addModule(modDataset) ;
-        super.addModule(modEngine) ;
-        super.addModule(modTime) ;
+        super.addModule(modQuery);
+        super.addModule(modResults);
+        super.addModule(modDataset);
+        super.addModule(modEngine);
+        super.addModule(modTime);
 
-        super.getUsage().startCategory("Control") ;
-        super.add(argExplain,  "--explain", "Explain and log query execution") ;
-        super.add(argRepeat,   "--repeat=N or N,M", "Do N times or N warmup and then M times (use for timing to overcome start up costs of Java)");
-        super.add(argOptimize, "--optimize=", "Turn the query optimizer on or off (default: on)") ;
+        super.getUsage().startCategory("Control");
+        super.add(argExplain, "--explain", "Explain and log query execution");
+        super.add(argRepeat, "--repeat=N or N,M",
+                  "Do N times or N warmup and then M times (use for timing to overcome start up costs of Java)");
+        super.add(argOptimize, "--optimize=", "Turn the query optimizer on or off (default: on)");
     }
 
     /** Default syntax used when the syntax can not be determined from the command name or file extension
@@ -163,6 +162,7 @@ public class query extends CmdARQ
             String avgStr = modTime.timeStr(avg) ;
             System.err.println("Total time: "+modTime.timeStr(totalTime)+" sec for repeat count of "+repeatCount+ " : average: "+avgStr) ;
         }
+        modEngine.resetRegistrations();
     }
 
     @Override
@@ -211,8 +211,7 @@ public class query extends CmdARQ
     }
 
     protected long totalTime = 0 ;
-    protected void queryExec(boolean timed, ResultsFormat fmt, PrintStream resultsDest)
-    {
+    protected void queryExec(boolean timed, ResultsFormat fmt, PrintStream resultsDest) {
         try {
             Query query = getQuery() ;
             if ( isVerbose() ) {
diff --git a/jena-ontapi/src/main/java/org/apache/jena/ontapi/impl/OntGraphModelImpl.java b/jena-ontapi/src/main/java/org/apache/jena/ontapi/impl/OntGraphModelImpl.java
index ad7bae3c94..c6747bfe92 100644
--- a/jena-ontapi/src/main/java/org/apache/jena/ontapi/impl/OntGraphModelImpl.java
+++ b/jena-ontapi/src/main/java/org/apache/jena/ontapi/impl/OntGraphModelImpl.java
@@ -68,6 +68,7 @@ import org.apache.jena.ontapi.utils.StdModels;
 import org.apache.jena.rdf.model.InfModel;
 import org.apache.jena.rdf.model.Literal;
 import org.apache.jena.rdf.model.Model;
+import org.apache.jena.rdf.model.ModelChangedListener;
 import org.apache.jena.rdf.model.Property;
 import org.apache.jena.rdf.model.RDFList;
 import org.apache.jena.rdf.model.RDFNode;
@@ -355,6 +356,25 @@ public class OntGraphModelImpl extends ModelCom implements OntModel, OntEnhGraph
         return new ModelCom(getBaseGraph());
     }
 
+    @Override
+    public OntGraphModelImpl register(ModelChangedListener listener) {
+        getUnionGraph().getEventManager().register(adapt(listener));
+        return this;
+    }
+
+    @Override
+    public OntGraphModelImpl unregister(ModelChangedListener listener) {
+        getUnionGraph().getEventManager().unregister(adapt(listener));
+        return this;
+    }
+
+    @Override
+    public OntGraphModelImpl notifyEvent(Object event) {
+        var ug = getUnionGraph();
+        ug.getEventManager().notifyEvent(ug, event);
+        return this;
+    }
+
     @Override
     public OntID getID() {
         checkType(OntID.class);
diff --git a/jena-ontapi/src/main/java/org/apache/jena/ontapi/impl/UnionGraphImpl.java b/jena-ontapi/src/main/java/org/apache/jena/ontapi/impl/UnionGraphImpl.java
index 6565221138..f73d964347 100644
--- a/jena-ontapi/src/main/java/org/apache/jena/ontapi/impl/UnionGraphImpl.java
+++ b/jena-ontapi/src/main/java/org/apache/jena/ontapi/impl/UnionGraphImpl.java
@@ -18,17 +18,18 @@
 
 package org.apache.jena.ontapi.impl;
 
-import org.apache.jena.ontapi.UnionGraph;
-import org.apache.jena.ontapi.utils.Graphs;
-import org.apache.jena.ontapi.utils.Iterators;
 import org.apache.jena.graph.Graph;
 import org.apache.jena.graph.GraphEventManager;
 import org.apache.jena.graph.GraphEvents;
 import org.apache.jena.graph.GraphListener;
+import org.apache.jena.graph.GraphUtil;
 import org.apache.jena.graph.Node;
 import org.apache.jena.graph.Triple;
 import org.apache.jena.graph.compose.CompositionBase;
 import org.apache.jena.graph.impl.SimpleEventManager;
+import org.apache.jena.ontapi.UnionGraph;
+import org.apache.jena.ontapi.utils.Graphs;
+import org.apache.jena.ontapi.utils.Iterators;
 import org.apache.jena.shared.PrefixMapping;
 import org.apache.jena.util.iterator.ExtendedIterator;
 
@@ -223,7 +224,7 @@ public class UnionGraphImpl extends CompositionBase implements UnionGraph {
         Triple t = Triple.createMatch(s, p, o);
         UnionGraph.EventManager em = getEventManager();
         em.onDeleteTriple(this, t);
-        super.remove(s, p, o);
+        GraphUtil.remove(this, s, p, o);
         em.notifyEvent(this, GraphEvents.remove(s, p, o));
     }
 
diff --git a/jena-ontapi/src/main/java/org/apache/jena/ontapi/impl/objects/OntSimpleClassImpl.java b/jena-ontapi/src/main/java/org/apache/jena/ontapi/impl/objects/OntSimpleClassImpl.java
index cef800397b..e39cc0869d 100644
--- a/jena-ontapi/src/main/java/org/apache/jena/ontapi/impl/objects/OntSimpleClassImpl.java
+++ b/jena-ontapi/src/main/java/org/apache/jena/ontapi/impl/objects/OntSimpleClassImpl.java
@@ -22,6 +22,7 @@ import org.apache.jena.enhanced.EnhGraph;
 import org.apache.jena.graph.Node;
 import org.apache.jena.ontapi.OntJenaException;
 import org.apache.jena.ontapi.OntModelControls;
+import org.apache.jena.ontapi.common.OntPersonalities;
 import org.apache.jena.ontapi.impl.OntGraphModelImpl;
 import org.apache.jena.ontapi.model.OntClass;
 import org.apache.jena.ontapi.model.OntDataProperty;
@@ -34,6 +35,7 @@ import org.apache.jena.ontapi.model.OntRelationalProperty;
 import org.apache.jena.ontapi.model.OntStatement;
 import org.apache.jena.rdf.model.Resource;
 import org.apache.jena.vocabulary.OWL2;
+import org.apache.jena.vocabulary.RDFS;
 
 import java.util.Arrays;
 import java.util.Collection;
@@ -42,10 +44,9 @@ import java.util.Optional;
 import java.util.stream.Stream;
 
 /**
- * {@code owl:Class} Implementation.
- * Instance of this class as a class with unknown nature is only available in a spec with corresponding permissions
- * ({@link OntModelControls}).
- * Specialized classes have their own implementations ({@link NamedImpl} or {@link OntClassImpl}).
+ * Simple Ontology Class implementation.
+ * Represents RDFS OntClass or OWL OntClass with unknown nature.
+ * Specialized OWL classes have their own implementations ({@link NamedImpl} or {@link OntClassImpl}).
  */
 @SuppressWarnings("WeakerAccess")
 public class OntSimpleClassImpl extends OntObjectImpl implements OntClass {
@@ -56,7 +57,7 @@ public class OntSimpleClassImpl extends OntObjectImpl implements OntClass {
 
     @Override
     public Optional<OntStatement> findRootStatement() {
-        return getOptionalRootStatement(this, OWL2.Class);
+        return getOptionalRootStatement(this, OntPersonalities.isRDFS(getModel().getOntPersonality()) ? RDFS.Class : OWL2.Class);
     }
 
     @Override
diff --git a/jena-ontapi/src/main/java/org/apache/jena/ontapi/model/MutableModel.java b/jena-ontapi/src/main/java/org/apache/jena/ontapi/model/MutableModel.java
index 996d9fb8f2..92d04d229f 100644
--- a/jena-ontapi/src/main/java/org/apache/jena/ontapi/model/MutableModel.java
+++ b/jena-ontapi/src/main/java/org/apache/jena/ontapi/model/MutableModel.java
@@ -21,6 +21,7 @@ package org.apache.jena.ontapi.model;
 import org.apache.jena.datatypes.RDFDatatype;
 import org.apache.jena.rdf.model.Literal;
 import org.apache.jena.rdf.model.Model;
+import org.apache.jena.rdf.model.ModelChangedListener;
 import org.apache.jena.rdf.model.ModelCon;
 import org.apache.jena.rdf.model.Property;
 import org.apache.jena.rdf.model.RDFNode;
@@ -109,4 +110,36 @@ interface MutableModel<R extends Model> extends Model {
 
     @Override
     R add(Resource s, Property p, String lex, String lang);
+
+    /**
+     * Registers a listener for model-changed events on this model.
+     * The methods on the listener will be called when API add/remove calls on the model succeed
+     * [in whole or in part].
+     * The same listener may be registered many times;
+     * if so, its methods will be called as many times as it's registered for each event.
+     *
+     * @param listener {@link ModelChangedListener}, not null
+     * @return this model, for cascading
+     */
+    R register(ModelChangedListener listener);
+
+    /**
+     * Unregisters a listener from model-changed events on this model.
+     * The listener is detached from the model.
+     * The model is returned to permit cascading.
+     * If the listener is not attached to the model, then nothing happens.
+     *
+     * @param listener {@link ModelChangedListener}, not null
+     */
+    R unregister(ModelChangedListener listener);
+
+    /**
+     * Notifies any listeners that the {@code event} has occurred.
+     *
+     * @param event the event, which has occurred, e.g. {@code GraphEvents#startRead}
+     * @return this model, for cascading
+     * @see ModelChangedListener
+     * @see org.apache.jena.graph.GraphEvents
+     */
+    R notifyEvent(Object event);
 }


Test patch:
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/exec/TS_ExecSPARQL.java b/jena-arq/src/test/java/org/apache/jena/sparql/exec/TS_ExecSPARQL.java
index 6ae60d2e42..6a1739a19a 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/exec/TS_ExecSPARQL.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/exec/TS_ExecSPARQL.java
@@ -25,8 +25,9 @@ import org.junit.runners.Suite;
 @Suite.SuiteClasses( {
     TestExecEnvironment.class
     , TestQueryExecDataset.class
+    , TestQueryExecution.class
 } )
- 
+
 public class TS_ExecSPARQL {
 
 }
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/exec/TestQueryExecution.java b/jena-arq/src/test/java/org/apache/jena/sparql/exec/TestQueryExecution.java
new file mode 100644
index 0000000000..258a3a003f
--- /dev/null
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/exec/TestQueryExecution.java
@@ -0,0 +1,50 @@
+/*
+ * 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.sparql.exec;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.Test;
+
+import org.apache.jena.graph.Node;
+import org.apache.jena.sparql.core.DatasetGraph;
+import org.apache.jena.sparql.core.DatasetGraphFactory;
+import org.apache.jena.sparql.engine.binding.Binding;
+
+/** Miscellaneous tests, e.g. from reports. */
+public class TestQueryExecution {
+    @Test public void lateral_with_join() {
+        // GH-2896 LATERAL
+        String qsReport = """
+            SELECT * {
+                BIND( 'x' AS ?xIn )
+                LATERAL {
+                    VALUES ?x { 1 }
+                    { SELECT ?xIn ?xOut { BIND(?xIn AS ?xOut) } }
+                }
+            }
+            """;
+        DatasetGraph dsg = DatasetGraphFactory.empty();
+        RowSet rowSet = QueryExec.dataset(dsg).query(qsReport).select();
+        Binding row = rowSet.next();
+        row.contains("xOut");
+        Node x = row.get("xOut");
+        assertEquals("x", x.getLiteralLexicalForm());
+    }
+}
diff --git a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestExpressions.java b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestExpressions.java
index 245307a982..e1556da5d8 100644
--- a/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestExpressions.java
+++ b/jena-arq/src/test/java/org/apache/jena/sparql/expr/TestExpressions.java
@@ -205,6 +205,17 @@ public class TestExpressions
     @Test(expected=ExprEvalException.class)
     public void boolean_74()       { testBoolean("isNumeric(?x)", true) ; }
 
+    // 24:00:00
+    // Equal
+    static String dateTime1999_24 = "'1999-12-31T24:00:00Z'^^<"+XSDDatatype.XSDdateTime.getURI()+">" ;
+    static String dateTime2000_00 = "'2000-01-01T00:00:00Z'^^<"+XSDDatatype.XSDdateTime.getURI()+">" ;
+
+    static String time_24 = "'24:00:00'^^<"+XSDDatatype.XSDtime.getURI()+">" ;
+    static String time_00 = "'00:00:00'^^<"+XSDDatatype.XSDtime.getURI()+">" ;
+
+    @Test public void dateTime24_01() { testBoolean(dateTime1999_24+" = "+dateTime2000_00 , true) ; }
+    @Test public void time24_01()     { testBoolean(time_24+" = "+time_00 , true) ; }
+
     static String duration1 =  "'P1Y1M1DT1H1M1S"+"'^^<"+XSDDatatype.XSDduration.getURI()+">";
     static String duration2 =  "'P2Y1M1DT1H1M1S"+"'^^<"+XSDDatatype.XSDduration.getURI()+">";
     static String duration3 =  "'P1Y1M1DT1H1M1S"+"'^^<"+XSDDatatype.XSDduration.getURI()+">";
@@ -418,6 +429,7 @@ public class TestExpressions
     static String dateTime3 = "'2005-01-01T12:03:34Z'^^<"+XSDDatatype.XSDdateTime.getURI()+">" ;
     // Later
     static String dateTime4 = "'2005-02-25T13:00:00Z'^^<"+XSDDatatype.XSDdateTime.getURI()+">" ;
+
     static String time1 = "'12:03:34Z'^^<" + XSDDatatype.XSDtime.getURI() + ">";
     static String time2 = "'12:03:34Z'^^<" + XSDDatatype.XSDtime.getURI() + ">";
     static String time3 = "'13:00:00Z'^^<" + XSDDatatype.XSDtime.getURI() + ">";
diff --git a/jena-ontapi/src/test/java/org/apache/jena/ontapi/OntModelMiscTest.java b/jena-ontapi/src/test/java/org/apache/jena/ontapi/OntModelMiscTest.java
index 72cb0acf88..263a537acc 100644
--- a/jena-ontapi/src/test/java/org/apache/jena/ontapi/OntModelMiscTest.java
+++ b/jena-ontapi/src/test/java/org/apache/jena/ontapi/OntModelMiscTest.java
@@ -25,15 +25,19 @@ import org.apache.jena.ontapi.model.OntClass;
 import org.apache.jena.ontapi.model.OntDisjoint;
 import org.apache.jena.ontapi.model.OntModel;
 import org.apache.jena.ontapi.utils.OntModels;
+import org.apache.jena.rdf.listeners.StatementListener;
 import org.apache.jena.rdf.model.Model;
 import org.apache.jena.rdf.model.ModelFactory;
 import org.apache.jena.rdf.model.Resource;
+import org.apache.jena.rdf.model.Statement;
 import org.apache.jena.shared.PrefixMapping;
 import org.apache.jena.vocabulary.OWL;
 import org.apache.jena.vocabulary.OWL2;
 import org.apache.jena.vocabulary.RDF;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.EnumSource;
 
 import java.io.ByteArrayInputStream;
 import java.io.ByteArrayOutputStream;
@@ -154,4 +158,102 @@ public class OntModelMiscTest {
 
         Assertions.assertThrows(IllegalArgumentException.class, ont::createDifferentIndividuals);
     }
+
+    @ParameterizedTest
+    @EnumSource(names = {
+            "OWL2_DL_MEM_RDFS_BUILTIN_INF",
+            "OWL2_DL_MEM",
+            "OWL2_DL_MEM_RDFS_INF",
+            "OWL2_DL_MEM_TRANS_INF",
+            "OWL2_DL_MEM_RULES_INF",
+            "OWL2_MEM",
+            "OWL2_MEM_RDFS_INF",
+            "OWL2_MEM_TRANS_INF",
+            "OWL2_MEM_RULES_INF",
+            "OWL2_MEM_MINI_RULES_INF",
+            "OWL2_MEM_MICRO_RULES_INF",
+            "OWL2_EL_MEM",
+            "OWL2_EL_MEM_RDFS_INF",
+            "OWL2_EL_MEM_TRANS_INF",
+            "OWL2_EL_MEM_RULES_INF",
+            "OWL2_QL_MEM",
+            "OWL2_QL_MEM_RDFS_INF",
+            "OWL2_QL_MEM_TRANS_INF",
+            "OWL2_QL_MEM_RULES_INF",
+            "OWL2_RL_MEM",
+            "OWL2_RL_MEM_RDFS_INF",
+            "OWL2_RL_MEM_TRANS_INF",
+            "OWL2_RL_MEM_RULES_INF",
+            "OWL1_DL_MEM",
+            "OWL1_DL_MEM_RDFS_INF",
+            "OWL1_DL_MEM_TRANS_INF",
+            "OWL1_DL_MEM_RULES_INF",
+            "OWL1_MEM",
+            "OWL1_MEM_RDFS_INF",
+            "OWL1_MEM_TRANS_INF",
+            "OWL1_MEM_RULES_INF",
+            "OWL1_MEM_MINI_RULES_INF",
+            "OWL1_MEM_MICRO_RULES_INF",
+            "OWL1_LITE_MEM",
+            "OWL1_LITE_MEM_RDFS_INF",
+            "OWL1_LITE_MEM_TRANS_INF",
+            "OWL1_LITE_MEM_RULES_INF",
+            "RDFS_MEM",
+            "RDFS_MEM_RDFS_INF",
+            "RDFS_MEM_TRANS_INF",
+    })
+    public void testModelChangeListenerGH2868(TestSpec spec) {
+        var listener = new TestModelChangedListener();
+        var m = OntModelFactory.createModel(spec.inst).register(listener);
+
+        var type1 = m.createOntClass("http://x1");
+        type1.removeProperties();
+        Assertions.assertEquals(1, listener.addedStatements.size());
+        Assertions.assertEquals(1, listener.removedStatements.size());
+        Assertions.assertEquals(1, listener.events.size());
+
+        listener.clear();
+
+        var type2 = m.createOntClass("http://x2");
+        m.remove(type2.getMainStatement());
+        Assertions.assertEquals(1, listener.addedStatements.size());
+        Assertions.assertEquals(1, listener.removedStatements.size());
+        Assertions.assertEquals(0, listener.events.size());
+
+        listener.clear();
+
+        m.createOntClass("http://x3");
+        m.createOntClass("http://x4");
+        m.removeAll(null, RDF.type, null);
+        Assertions.assertEquals(2, listener.addedStatements.size());
+        Assertions.assertEquals(2, listener.removedStatements.size());
+        Assertions.assertEquals(1, listener.events.size());
+    }
+
+    private static class TestModelChangedListener extends StatementListener {
+        private final List<Statement> addedStatements = new ArrayList<>();
+        private final List<Statement> removedStatements = new ArrayList<>();
+        private final List<Object> events = new ArrayList<>();
+
+        @Override
+        public void addedStatement(Statement x) {
+            addedStatements.add(x);
+        }
+
+        @Override
+        public void removedStatement(Statement x) {
+            removedStatements.add(x);
+        }
+
+        @Override
+        public void notifyEvent(Model m, Object event) {
+            events.add(event);
+        }
+
+        private void clear() {
+            addedStatements.clear();
+            removedStatements.clear();
+            events.clear();
+        }
+    }
 }