1212package org .eclipse .rdf4j .sail .shacl .ast .planNodes ;
1313
1414import java .util .Arrays ;
15+ import java .util .List ;
1516import java .util .Objects ;
17+ import java .util .regex .Matcher ;
18+ import java .util .regex .Pattern ;
19+ import java .util .stream .Collectors ;
1620
1721import org .eclipse .rdf4j .common .iteration .CloseableIteration ;
22+ import org .eclipse .rdf4j .model .Literal ;
1823import org .eclipse .rdf4j .model .Resource ;
1924import org .eclipse .rdf4j .model .Value ;
2025import org .eclipse .rdf4j .model .impl .BooleanLiteral ;
26+ import org .eclipse .rdf4j .model .util .Values ;
2127import org .eclipse .rdf4j .query .BindingSet ;
2228import org .eclipse .rdf4j .query .Dataset ;
2329import org .eclipse .rdf4j .query .MalformedQueryException ;
2430import org .eclipse .rdf4j .query .QueryLanguage ;
31+ import org .eclipse .rdf4j .query .impl .MapBindingSet ;
2532import org .eclipse .rdf4j .query .parser .ParsedQuery ;
2633import org .eclipse .rdf4j .query .parser .QueryParserFactory ;
2734import org .eclipse .rdf4j .query .parser .QueryParserRegistry ;
4148public class SparqlConstraintSelect implements PlanNode {
4249
4350 private static final Logger logger = LoggerFactory .getLogger (SparqlConstraintSelect .class );
51+ private static final Pattern MESSAGE_TEMPLATE_PATTERN = Pattern .compile ("\\ {[?$]([A-Za-z_][A-Za-z0-9_]*)\\ }" );
4452
4553 private final SailConnection connection ;
4654
@@ -55,6 +63,9 @@ public class SparqlConstraintSelect implements PlanNode {
5563 private final Dataset dataset ;
5664 private final ParsedQuery parsedQuery ;
5765 private final boolean printed = false ;
66+ private final Value shapesGraphBinding ;
67+ private final Value currentShapeBinding ;
68+ private final Value pathForMessageBinding ;
5869 private ValidationExecutionLogger validationExecutionLogger ;
5970
6071 public SparqlConstraintSelect (SailConnection connection , PlanNode targets , String query ,
@@ -72,6 +83,9 @@ public SparqlConstraintSelect(SailConnection connection, PlanNode targets, Strin
7283 this .variables = new String [] { "$this" };
7384 this .scope = scope ;
7485 this .dataset = PlanNodeHelper .asDefaultGraphDataset (dataGraph );
86+ this .currentShapeBinding = shape != null ? shape .getId () : null ;
87+ this .shapesGraphBinding = determineShapesGraphBinding (shape );
88+ this .pathForMessageBinding = determinePathForMessageBinding (constraintComponent , scope );
7589
7690 QueryParserFactory queryParserFactory = QueryParserRegistry .getInstance ()
7791 .get (QueryLanguage .SPARQL )
@@ -86,6 +100,33 @@ public SparqlConstraintSelect(SailConnection connection, PlanNode targets, Strin
86100
87101 }
88102
103+ private static Value determineShapesGraphBinding (Shape shape ) {
104+ if (shape == null ) {
105+ return null ;
106+ }
107+ Resource [] contexts = shape .getContexts ();
108+ if (contexts == null ) {
109+ return null ;
110+ }
111+ for (Resource context : contexts ) {
112+ if (context != null && context .isIRI ()) {
113+ return context ;
114+ }
115+ }
116+ return null ;
117+ }
118+
119+ private static Value determinePathForMessageBinding (SparqlConstraintComponent constraintComponent ,
120+ ConstraintComponent .Scope scope ) {
121+ if (constraintComponent == null || scope != ConstraintComponent .Scope .propertyShape ) {
122+ return null ;
123+ }
124+ return constraintComponent .getTargetChain ()
125+ .getPath ()
126+ .map (p -> Values .literal (p .toSparqlPathString ()))
127+ .orElse (null );
128+ }
129+
89130 @ Override
90131 public CloseableIteration <? extends ValidationTuple > iterator () {
91132 return new LoggingCloseableIteration (this , validationExecutionLogger ) {
@@ -108,7 +149,14 @@ private void calculateNext() {
108149
109150 if (results == null && targetIterator .hasNext ()) {
110151 nextTarget = targetIterator .next ();
111- SingletonBindingSet bindings = new SingletonBindingSet ("this" , nextTarget .getActiveTarget ());
152+ MapBindingSet bindings = new MapBindingSet (3 );
153+ bindings .setBinding ("this" , nextTarget .getActiveTarget ());
154+ if (currentShapeBinding != null ) {
155+ bindings .setBinding ("currentShape" , currentShapeBinding );
156+ }
157+ if (shapesGraphBinding != null ) {
158+ bindings .setBinding ("shapesGraph" , shapesGraphBinding );
159+ }
112160 results = connection .evaluate (parsedQuery .getTupleExpr (), dataset , bindings , true );
113161 }
114162
@@ -128,6 +176,32 @@ private void calculateNext() {
128176 Value currentValue = value1 ;
129177
130178 Value path = bindingSet .getValue ("path" );
179+ List <Literal > resultMessages = null ;
180+
181+ Value messageValue = bindingSet .getValue ("message" );
182+ if (messageValue != null ) {
183+ if (messageValue .isLiteral ()) {
184+ resultMessages = List .of ((Literal ) messageValue );
185+ } else {
186+ resultMessages = List .of (Values .literal (messageValue .stringValue ()));
187+ }
188+ } else if (produceValidationReports ) {
189+ List <Literal > templates = constraintComponent .getDefaultMessage ();
190+ if (!templates .isEmpty ()) {
191+ if (constraintComponent .hasMessageTemplateVariables ()) {
192+ Value focusNode = nextTarget .getActiveTarget ();
193+ resultMessages = templates .stream ()
194+ .map (t -> substituteMessageTemplate (t , bindingSet , focusNode ,
195+ shapesGraphBinding , currentShapeBinding ,
196+ pathForMessageBinding ))
197+ .collect (Collectors .toList ());
198+ } else {
199+ resultMessages = List .copyOf (templates );
200+ }
201+ }
202+ }
203+
204+ final List <Literal > finalResultMessages = resultMessages ;
131205
132206 if (scope == ConstraintComponent .Scope .nodeShape ) {
133207 next = nextTarget .addValidationResult (t -> {
@@ -137,7 +211,10 @@ private void calculateNext() {
137211 constraintComponent , shape .getSeverity (),
138212 ConstraintComponent .Scope .nodeShape , t .getContexts (),
139213 shape .getContexts ());
140- if (path != null ) {
214+ if (finalResultMessages != null ) {
215+ validationResult .setMessagesOverride (finalResultMessages );
216+ }
217+ if (path != null && path .isIRI ()) {
141218 validationResult .setPathIri (path );
142219 }
143220 return validationResult ;
@@ -154,7 +231,10 @@ private void calculateNext() {
154231 constraintComponent , shape .getSeverity (),
155232 ConstraintComponent .Scope .propertyShape , t .getContexts (),
156233 shape .getContexts ());
157- if (path != null ) {
234+ if (finalResultMessages != null ) {
235+ validationResult .setMessagesOverride (finalResultMessages );
236+ }
237+ if (path != null && path .isIRI ()) {
158238 validationResult .setPathIri (path );
159239 }
160240 return validationResult ;
@@ -225,6 +305,41 @@ public void getPlanAsGraphvizDot(StringBuilder stringBuilder) {
225305
226306 }
227307
308+ private static Literal substituteMessageTemplate (Literal template , BindingSet bindingSet , Value focusNode ,
309+ Value shapesGraphBinding , Value currentShapeBinding , Value pathForMessageBinding ) {
310+ String label = template .getLabel ();
311+ Matcher matcher = MESSAGE_TEMPLATE_PATTERN .matcher (label );
312+ if (!matcher .find ()) {
313+ return template ;
314+ }
315+
316+ matcher .reset ();
317+ StringBuffer sb = new StringBuffer ();
318+ while (matcher .find ()) {
319+ String varName = matcher .group (1 );
320+ Value value = bindingSet .getValue (varName );
321+ if (value == null ) {
322+ if ("this" .equals (varName )) {
323+ value = focusNode ;
324+ } else if ("shapesGraph" .equals (varName )) {
325+ value = shapesGraphBinding ;
326+ } else if ("currentShape" .equals (varName )) {
327+ value = currentShapeBinding ;
328+ } else if ("PATH" .equals (varName )) {
329+ value = pathForMessageBinding ;
330+ }
331+ }
332+ String replacement = value != null ? value .stringValue () : matcher .group (0 );
333+ matcher .appendReplacement (sb , Matcher .quoteReplacement (replacement ));
334+ }
335+ matcher .appendTail (sb );
336+
337+ if (template .getLanguage ().isPresent ()) {
338+ return Values .literal (sb .toString (), template .getLanguage ().get ());
339+ }
340+ return Values .literal (sb .toString (), template .getDatatype ());
341+ }
342+
228343 @ Override
229344 public String getId () {
230345 return System .identityHashCode (this ) + "" ;
0 commit comments