@@ -94,6 +94,8 @@ public class ShaclSailConnection extends NotifyingSailConnectionWrapper implemen
9494 private final HashSet <Statement > removedStatementsInferredSet = new HashSet <>();
9595
9696 private boolean shapeRefreshNeeded = false ;
97+ private boolean legacyStatementAddedWithoutInferredFlagObserved = false ;
98+ private boolean legacyStatementRemovedWithoutInferredFlagObserved = false ;
9799 private boolean shapesModifiedInCurrentTransaction = false ;
98100
99101 public final ShaclSail sail ;
@@ -316,7 +318,7 @@ public void addStatement(UpdateContext modify, Resource subj, IRI pred, Value ob
316318 throws SailException {
317319 if (useDefaultShapesGraph && contexts .length == 1 && RDF4J .SHACL_SHAPE_GRAPH .equals (contexts [0 ])) {
318320 shapesRepoConnection .add (subj , pred , obj , contexts );
319- shapeRefreshNeeded = true ;
321+ markShapesRefreshNeeded () ;
320322 } else {
321323 super .addStatement (modify , subj , pred , obj , contexts );
322324 }
@@ -327,7 +329,7 @@ public void removeStatement(UpdateContext modify, Resource subj, IRI pred, Value
327329 throws SailException {
328330 if (useDefaultShapesGraph && contexts .length == 1 && RDF4J .SHACL_SHAPE_GRAPH .equals (contexts [0 ])) {
329331 shapesRepoConnection .remove (subj , pred , obj , contexts );
330- shapeRefreshNeeded = true ;
332+ markShapesRefreshNeeded () ;
331333 } else {
332334 super .removeStatement (modify , subj , pred , obj , contexts );
333335 }
@@ -337,7 +339,7 @@ public void removeStatement(UpdateContext modify, Resource subj, IRI pred, Value
337339 public void addStatement (Resource subj , IRI pred , Value obj , Resource ... contexts ) throws SailException {
338340 if (useDefaultShapesGraph && contexts .length == 1 && RDF4J .SHACL_SHAPE_GRAPH .equals (contexts [0 ])) {
339341 shapesRepoConnection .add (subj , pred , obj , contexts );
340- shapeRefreshNeeded = true ;
342+ markShapesRefreshNeeded () ;
341343 } else {
342344 super .addStatement (subj , pred , obj , contexts );
343345 }
@@ -347,7 +349,7 @@ public void addStatement(Resource subj, IRI pred, Value obj, Resource... context
347349 public void removeStatements (Resource subj , IRI pred , Value obj , Resource ... contexts ) throws SailException {
348350 if (useDefaultShapesGraph && contexts .length == 1 && RDF4J .SHACL_SHAPE_GRAPH .equals (contexts [0 ])) {
349351 shapesRepoConnection .remove (subj , pred , obj , contexts );
350- shapeRefreshNeeded = true ;
352+ markShapesRefreshNeeded () ;
351353 } else {
352354 super .removeStatements (subj , pred , obj , contexts );
353355 }
@@ -357,7 +359,7 @@ public void removeStatements(Resource subj, IRI pred, Value obj, Resource... con
357359 public void clear (Resource ... contexts ) throws SailException {
358360 if (Arrays .asList (contexts ).contains (RDF4J .SHACL_SHAPE_GRAPH )) {
359361 shapesRepoConnection .clear ();
360- shapeRefreshNeeded = true ;
362+ markShapesRefreshNeeded () ;
361363 }
362364 super .clear (contexts );
363365 }
@@ -488,6 +490,8 @@ private void cleanup() {
488490 stats = null ;
489491 prepareHasBeenCalled = false ;
490492 shapeRefreshNeeded = false ;
493+ legacyStatementAddedWithoutInferredFlagObserved = false ;
494+ legacyStatementRemovedWithoutInferredFlagObserved = false ;
491495 shapesModifiedInCurrentTransaction = false ;
492496
493497 currentIsolationLevel = null ;
@@ -1375,6 +1379,7 @@ public void prepare() throws SailException {
13751379 }
13761380
13771381 List <ContextWithShape > shapesToValidate = shapesAfterRefresh != null ? shapesAfterRefresh : currentShapes ;
1382+ validateLegacyCallbackInferredSupport (shapesToValidate );
13781383 boolean requiresRdfsSubClassReasoner = sail .isRdfsSubClassReasoning ()
13791384 || requiresRdfsSubClassReasoner (shapesToValidate );
13801385
@@ -1495,6 +1500,7 @@ private ValidationReport serializableValidation(List<ContextWithShape> shapesAft
14951500
14961501 @ Override
14971502 public void statementAdded (Statement statement ) {
1503+ legacyStatementAddedWithoutInferredFlagObserved = true ;
14981504 statementAdded (statement , false );
14991505 }
15001506
@@ -1522,6 +1528,7 @@ public void statementAdded(Statement statement, boolean inferred) {
15221528
15231529 @ Override
15241530 public void statementRemoved (Statement statement ) {
1531+ legacyStatementRemovedWithoutInferredFlagObserved = true ;
15251532 statementRemoved (statement , false );
15261533 }
15271534
@@ -1552,13 +1559,90 @@ private void checkIfShapesRefreshIsNeeded(Statement statement) {
15521559 if (!shapeRefreshNeeded ) {
15531560 for (IRI shapesGraph : shapesGraphs ) {
15541561 if (Objects .equals (statement .getContext (), shapesGraph )) {
1555- shapeRefreshNeeded = true ;
1562+ markShapesRefreshNeeded () ;
15561563 break ;
15571564 }
15581565 }
15591566 }
15601567 }
15611568
1569+ private void markShapesRefreshNeeded () {
1570+ shapeRefreshNeeded = true ;
1571+ }
1572+
1573+ private Boolean inferInferredFromStatementMetadata (Statement statement ) {
1574+ try {
1575+ Boolean inferred = invokeBooleanStatementMethod (statement , "isInferred" );
1576+ if (inferred != null ) {
1577+ return inferred ;
1578+ }
1579+ Boolean explicit = invokeBooleanStatementMethod (statement , "isExplicit" );
1580+ if (explicit != null ) {
1581+ return !explicit ;
1582+ }
1583+ return null ;
1584+ } catch (ReflectiveOperationException e ) {
1585+ if (logger .isDebugEnabled ()) {
1586+ logger .debug ("Unable to infer inferred-flag from legacy callback statement metadata." , e );
1587+ }
1588+ return null ;
1589+ }
1590+ }
1591+
1592+ private Boolean invokeBooleanStatementMethod (Statement statement , String methodName )
1593+ throws ReflectiveOperationException {
1594+ var method = statement .getClass ().getMethod (methodName );
1595+ Class <?> returnType = method .getReturnType ();
1596+ if (returnType != boolean .class && returnType != Boolean .class ) {
1597+ return null ;
1598+ }
1599+ Object value = method .invoke (statement );
1600+ return value == null ? null : (Boolean ) value ;
1601+ }
1602+
1603+ /**
1604+ * Reject legacy no-flag callbacks only when a shape explicitly disables inferred statements using
1605+ * rsx:includeInferredStatements=false. In that case, inferred-vs-explicit classification is required for correct
1606+ * validation and stores must use statementAdded/Removed callbacks with the inferred boolean argument.
1607+ */
1608+ private void validateLegacyCallbackInferredSupport (List <ContextWithShape > shapesToValidate ) {
1609+ if (!legacyStatementAddedWithoutInferredFlagObserved && !legacyStatementRemovedWithoutInferredFlagObserved ) {
1610+ return ;
1611+ }
1612+
1613+ boolean hasExplicitIncludeInferredDisabled = shapesToValidate .stream ()
1614+ .filter (ContextWithShape ::hasShape )
1615+ .map (ContextWithShape ::getShape )
1616+ .map (Shape ::getIncludeInferredStatementsOverride )
1617+ .anyMatch (Boolean .FALSE ::equals );
1618+
1619+ if (!hasExplicitIncludeInferredDisabled ) {
1620+ return ;
1621+ }
1622+
1623+ String callbackDetails = getObservedLegacyCallbacksWithoutInferredFlag ();
1624+ String message = "Underlying Sail does not support shapes that explicitly set "
1625+ + "rsx:includeInferredStatements=false because it emits deprecated "
1626+ + "SailConnectionListener callbacks without inferred flags (" + callbackDetails + "). "
1627+ + "Implement statementAdded(Statement, boolean inferred) and "
1628+ + "statementRemoved(Statement, boolean inferred)." ;
1629+ logger .error (message );
1630+ throw new ShaclSailValidationException (message );
1631+ }
1632+
1633+ private String getObservedLegacyCallbacksWithoutInferredFlag () {
1634+ if (legacyStatementAddedWithoutInferredFlagObserved && legacyStatementRemovedWithoutInferredFlagObserved ) {
1635+ return "statementAdded(Statement), statementRemoved(Statement)" ;
1636+ }
1637+ if (legacyStatementAddedWithoutInferredFlagObserved ) {
1638+ return "statementAdded(Statement)" ;
1639+ }
1640+ if (legacyStatementRemovedWithoutInferredFlagObserved ) {
1641+ return "statementRemoved(Statement)" ;
1642+ }
1643+ return "<none>" ;
1644+ }
1645+
15621646 private void checkTransactionalValidationLimit () {
15631647 int changeCount = addedStatementsSet .size () + removedStatementsSet .size ()
15641648 + addedStatementsInferredSet .size () + removedStatementsInferredSet .size ();
0 commit comments