1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package com.helger.schematron.pure.bound.xpath;
18
19 import java.util.Map;
20
21 import javax.annotation.Nonnull;
22 import javax.annotation.Nullable;
23 import javax.annotation.concurrent.NotThreadSafe;
24 import javax.xml.xpath.XPath;
25 import javax.xml.xpath.XPathConstants;
26 import javax.xml.xpath.XPathExpression;
27 import javax.xml.xpath.XPathExpressionException;
28 import javax.xml.xpath.XPathFactory;
29 import javax.xml.xpath.XPathFunctionResolver;
30 import javax.xml.xpath.XPathVariableResolver;
31
32 import org.slf4j.Logger;
33 import org.slf4j.LoggerFactory;
34 import org.w3c.dom.Node;
35 import org.w3c.dom.NodeList;
36
37 import com.helger.commons.ValueEnforcer;
38 import com.helger.commons.collection.impl.CommonsArrayList;
39 import com.helger.commons.collection.impl.CommonsHashMap;
40 import com.helger.commons.collection.impl.ICommonsList;
41 import com.helger.commons.collection.impl.ICommonsMap;
42 import com.helger.commons.string.ToStringGenerator;
43 import com.helger.schematron.pure.binding.IPSQueryBinding;
44 import com.helger.schematron.pure.binding.SchematronBindException;
45 import com.helger.schematron.pure.binding.xpath.IPSXPathVariables;
46 import com.helger.schematron.pure.binding.xpath.PSXPathVariables;
47 import com.helger.schematron.pure.bound.AbstractPSBoundSchema;
48 import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
49 import com.helger.schematron.pure.model.IPSElement;
50 import com.helger.schematron.pure.model.IPSHasMixedContent;
51 import com.helger.schematron.pure.model.PSAssertReport;
52 import com.helger.schematron.pure.model.PSDiagnostic;
53 import com.helger.schematron.pure.model.PSName;
54 import com.helger.schematron.pure.model.PSPattern;
55 import com.helger.schematron.pure.model.PSPhase;
56 import com.helger.schematron.pure.model.PSRule;
57 import com.helger.schematron.pure.model.PSSchema;
58 import com.helger.schematron.pure.model.PSValueOf;
59 import com.helger.schematron.pure.validation.IPSValidationHandler;
60 import com.helger.schematron.pure.validation.SchematronValidationException;
61 import com.helger.schematron.saxon.SaxonNamespaceContext;
62 import com.helger.schematron.xpath.XPathEvaluationHelper;
63 import com.helger.schematron.xslt.util.PSErrorListener;
64 import com.helger.xml.namespace.MapBasedNamespaceContext;
65 import com.helger.xml.xpath.XPathHelper;
66
67 import net.sf.saxon.xpath.XPathEvaluator;
68
69
70
71
72
73
74 @NotThreadSafe
75 public class PSXPathBoundSchema extends AbstractPSBoundSchema
76 {
77 private static final Logger LOGGER = LoggerFactory.getLogger (PSXPathBoundSchema.class);
78
79 private final XPathVariableResolver m_aXPathVariableResolver;
80 private final XPathFunctionResolver m_aXPathFunctionResolver;
81 private final XPathFactory m_aXPathFactory;
82 private ICommonsList <PSXPathBoundPattern> m_aBoundPatterns;
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98 @Nullable
99 private static XPathExpression _compileXPath (@Nonnull final XPath aXPathContext,
100 @Nonnull final String sXPathExpression) throws XPathExpressionException
101 {
102 XPathExpression ret = null;
103 try
104 {
105 ret = aXPathContext.compile (sXPathExpression);
106 }
107 catch (final XPathExpressionException ex)
108 {
109
110 throw ex;
111 }
112 return ret;
113 }
114
115 @Nullable
116 private ICommonsList <PSXPathBoundElement> _createBoundElements (@Nonnull final IPSHasMixedContent aMixedContent,
117 @Nonnull final XPath aXPathContext,
118 @Nonnull final IPSXPathVariables aVariables)
119 {
120 final ICommonsList <PSXPathBoundElement> ret = new CommonsArrayList <> ();
121 boolean bHasAnyError = false;
122
123 for (final Object aContentElement : aMixedContent.getAllContentElements ())
124 {
125 if (aContentElement instanceof PSName)
126 {
127 final PSName"../../../../../../com/helger/schematron/pure/model/PSName.html#PSName">PSName aName = (PSName) aContentElement;
128 if (aName.hasPath ())
129 {
130
131 final String sPath = aVariables.getAppliedReplacement (aName.getPath ());
132 try
133 {
134 final XPathExpression aXpathExpression = _compileXPath (aXPathContext, sPath);
135 ret.add (new PSXPathBoundElement (aName, sPath, aXpathExpression));
136 }
137 catch (final XPathExpressionException ex)
138 {
139 error (aName,
140 "Failed to compile XPath expression in <name>: '" + sPath + "'",
141 ex.getCause () != null ? ex.getCause () : ex);
142 bHasAnyError = true;
143 }
144 }
145 else
146 {
147
148 ret.add (new PSXPathBoundElement (aName));
149 }
150 }
151 else
152 if (aContentElement instanceof PSValueOf)
153 {
154 final PSValueOf/../../../../com/helger/schematron/pure/model/PSValueOf.html#PSValueOf">PSValueOf aValueOf = (PSValueOf) aContentElement;
155
156
157 final String sSelect = aVariables.getAppliedReplacement (aValueOf.getSelect ());
158 try
159 {
160 final XPathExpression aXPathExpression = _compileXPath (aXPathContext, sSelect);
161 ret.add (new PSXPathBoundElement (aValueOf, sSelect, aXPathExpression));
162 }
163 catch (final XPathExpressionException ex)
164 {
165 error (aValueOf, "Failed to compile XPath expression in <value-of>: '" + sSelect + "'", ex);
166 bHasAnyError = true;
167 }
168 }
169 else
170 {
171
172 if (aContentElement instanceof String)
173 ret.add (new PSXPathBoundElement ((String) aContentElement));
174 else
175 ret.add (new PSXPathBoundElement ((IPSElement) aContentElement));
176 }
177 }
178
179 if (bHasAnyError)
180 return null;
181
182 return ret;
183 }
184
185 @Nullable
186 private ICommonsMap <String, PSXPathBoundDiagnostic> _createBoundDiagnostics (@Nonnull final XPath aXPathContext,
187 @Nonnull final IPSXPathVariables aGlobalVariables)
188 {
189 final ICommonsMap <String, PSXPathBoundDiagnostic> ret = new CommonsHashMap <> ();
190 boolean bHasAnyError = false;
191
192 final PSSchema aSchema = getOriginalSchema ();
193 if (aSchema.hasDiagnostics ())
194 {
195
196 for (final PSDiagnostic aDiagnostic : aSchema.getDiagnostics ().getAllDiagnostics ())
197 {
198 final ICommonsList <PSXPathBoundElement> aBoundElements = _createBoundElements (aDiagnostic,
199 aXPathContext,
200 aGlobalVariables);
201 if (aBoundElements == null)
202 {
203
204 bHasAnyError = true;
205 }
206 else
207 {
208 final PSXPathBoundDiagnosticundDiagnostic.html#PSXPathBoundDiagnostic">PSXPathBoundDiagnostic aBoundDiagnostic = new PSXPathBoundDiagnostic (aDiagnostic, aBoundElements);
209 if (ret.put (aDiagnostic.getID (), aBoundDiagnostic) != null)
210 {
211 error (aDiagnostic, "A diagnostic element with ID '" + aDiagnostic.getID () + "' was overwritten!");
212 bHasAnyError = true;
213 }
214 }
215 }
216 }
217
218 if (bHasAnyError)
219 return null;
220
221 return ret;
222 }
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237 @Nullable
238 private ICommonsList <PSXPathBoundPattern> _createBoundPatterns (@Nonnull final XPath aXPathContext,
239 @Nonnull final ICommonsMap <String, PSXPathBoundDiagnostic> aBoundDiagnostics,
240 @Nonnull final IPSXPathVariables aGlobalVariables)
241 {
242 final ICommonsList <PSXPathBoundPattern> ret = new CommonsArrayList <> ();
243 boolean bHasAnyError = false;
244
245
246 for (final PSPattern aPattern : getAllRelevantPatterns ())
247 {
248
249 final PSXPathVariables aPatternVariables = aGlobalVariables.getClone ();
250
251 if (aPattern.hasAnyLet ())
252 {
253
254
255 for (final Map.Entry <String, String> aEntry : aPattern.getAllLetsAsMap ().entrySet ())
256 if (aPatternVariables.add (aEntry).isUnchanged ())
257 error (aPattern, "Duplicate <let> with name '" + aEntry.getKey () + "' in <pattern>");
258 }
259
260
261 final ICommonsList <PSXPathBoundRule> aBoundRules = new CommonsArrayList <> ();
262 for (final PSRule aRule : aPattern.getAllRules ())
263 {
264
265 final PSXPathVariables aRuleVariables = aPatternVariables.getClone ();
266 if (aRule.hasAnyLet ())
267 {
268
269
270 for (final Map.Entry <String, String> aEntry : aRule.getAllLetsAsMap ().entrySet ())
271 if (aRuleVariables.add (aEntry).isUnchanged ())
272 error (aRule, "Duplicate <let> with name '" + aEntry.getKey () + "' in <rule>");
273 }
274
275
276 final ICommonsList <PSXPathBoundAssertReport> aBoundAssertReports = new CommonsArrayList <> ();
277 for (final PSAssertReport aAssertReport : aRule.getAllAssertReports ())
278 {
279 final String sTest = aRuleVariables.getAppliedReplacement (aAssertReport.getTest ());
280 try
281 {
282 final XPathExpression aTestExpr = _compileXPath (aXPathContext, sTest);
283 final ICommonsList <PSXPathBoundElement> aBoundElements = _createBoundElements (aAssertReport,
284 aXPathContext,
285 aRuleVariables);
286 if (aBoundElements == null)
287 {
288
289 bHasAnyError = true;
290 }
291 else
292 {
293 final PSXPathBoundAssertReportssertReport.html#PSXPathBoundAssertReport">PSXPathBoundAssertReport aBoundAssertReport = new PSXPathBoundAssertReport (aAssertReport,
294 sTest,
295 aTestExpr,
296 aBoundElements,
297 aBoundDiagnostics);
298 aBoundAssertReports.add (aBoundAssertReport);
299 }
300 }
301 catch (final Throwable t)
302 {
303 error (aAssertReport,
304 "Failed to compile XPath expression in <" +
305 (aAssertReport.isAssert () ? "assert" : "report") +
306 ">: '" +
307 sTest +
308 "' with the following variables: " +
309 aRuleVariables.getAll (),
310 t);
311 bHasAnyError = true;
312 }
313 }
314
315
316 final String sRuleContext = aGlobalVariables.getAppliedReplacement (getValidationContext (aRule.getContext ()));
317 PSXPathBoundRule aBoundRule = null;
318 try
319 {
320 final XPathExpression aRuleContext = _compileXPath (aXPathContext, sRuleContext);
321 aBoundRule = new PSXPathBoundRule (aRule, sRuleContext, aRuleContext, aBoundAssertReports);
322 aBoundRules.add (aBoundRule);
323 }
324 catch (final XPathExpressionException ex)
325 {
326 error (aRule,
327 "Failed to compile XPath expression in <rule>: '" + sRuleContext + "'",
328 ex.getCause () != null ? ex.getCause () : ex);
329 bHasAnyError = true;
330 }
331 }
332
333
334 final PSXPathBoundPatternPathBoundPattern.html#PSXPathBoundPattern">PSXPathBoundPattern aBoundPattern = new PSXPathBoundPattern (aPattern, aBoundRules);
335 ret.add (aBoundPattern);
336 }
337
338 if (bHasAnyError)
339 return null;
340
341 return ret;
342 }
343
344 @Nonnull
345 public static XPathFactory createXPathFactorySaxonFirst () throws SchematronBindException
346 {
347 try
348 {
349 return XPathHelper.createXPathFactorySaxonFirst ();
350 }
351 catch (final IllegalStateException ex)
352 {
353 throw new SchematronBindException (ex.getMessage (), ex.getCause ());
354 }
355 }
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384 public PSXPathBoundSchema (@Nonnull final IPSQueryBinding aQueryBinding,
385 @Nonnull final PSSchema aOrigSchema,
386 @Nullable final String sPhase,
387 @Nullable final IPSErrorHandler aCustomErrorListener,
388 @Nullable final XPathVariableResolver aXPathVariableResolver,
389 @Nullable final XPathFunctionResolver aXPathFunctionResolver) throws SchematronBindException
390 {
391 super (aQueryBinding, aOrigSchema, sPhase, aCustomErrorListener);
392 m_aXPathVariableResolver = aXPathVariableResolver;
393 m_aXPathFunctionResolver = aXPathFunctionResolver;
394 m_aXPathFactory = createXPathFactorySaxonFirst ();
395 }
396
397 @Nonnull
398 private XPath _createXPathContext ()
399 {
400 final MapBasedNamespaceContext aNamespaceContext = getNamespaceContext ();
401 final XPath aXPathContext = XPathHelper.createNewXPath (m_aXPathFactory,
402 m_aXPathVariableResolver,
403 m_aXPathFunctionResolver,
404 aNamespaceContext);
405
406 if ("net.sf.saxon.xpath.XPathEvaluator".equals (aXPathContext.getClass ().getName ()))
407 {
408
409 final XPathEvaluator aSaxonXPath = (XPathEvaluator) aXPathContext;
410
411
412 aSaxonXPath.setNamespaceContext (new SaxonNamespaceContext (aNamespaceContext));
413
414
415 aSaxonXPath.getConfiguration ().setErrorListener (new PSErrorListener (getErrorHandler ()));
416 }
417 return aXPathContext;
418 }
419
420 @Nonnull
421 public PSXPathBoundSchema bind () throws SchematronBindException
422 {
423 if (LOGGER.isDebugEnabled ())
424 LOGGER.debug ("Binding pure Schematron");
425
426 if (m_aBoundPatterns != null)
427 throw new IllegalStateException ("bind must only be called once!");
428
429 final PSSchema aSchema = getOriginalSchema ();
430 final PSPhase aPhase = getPhase ();
431
432
433 final PSXPathVariablesSXPathVariables.html#PSXPathVariables">PSXPathVariables aGlobalVariables = new PSXPathVariables ();
434 if (aSchema.hasAnyLet ())
435 for (final Map.Entry <String, String> aEntry : aSchema.getAllLetsAsMap ().entrySet ())
436 if (aGlobalVariables.add (aEntry).isUnchanged ())
437 error (aSchema, "Duplicate <let> with name '" + aEntry.getKey () + "' in global <schema>");
438
439 if (aPhase != null)
440 {
441
442 for (final Map.Entry <String, String> aEntry : aPhase.getAllLetsAsMap ().entrySet ())
443 if (aGlobalVariables.add (aEntry).isUnchanged ())
444 error (aSchema,
445 "Duplicate <let> with name '" + aEntry.getKey () + "' in <phase> with name '" + getPhaseID () + "'");
446 }
447
448 final XPath aXPathContext = _createXPathContext ();
449
450
451 final ICommonsMap <String, PSXPathBoundDiagnostic> aBoundDiagnostics = _createBoundDiagnostics (aXPathContext,
452 aGlobalVariables);
453 if (aBoundDiagnostics == null)
454 throw new SchematronBindException ("Failed to precompile the diagnostics of the supplied schema. Check the " +
455 (isDefaultErrorHandler () ? "log output" : "error listener") +
456 " for XPath errors!");
457
458
459
460 m_aBoundPatterns = _createBoundPatterns (aXPathContext, aBoundDiagnostics, aGlobalVariables);
461 if (m_aBoundPatterns == null)
462 throw new SchematronBindException ("Failed to precompile the supplied schema.");
463 return this;
464 }
465
466 @Nullable
467 public XPathVariableResolver getXPathVariableResolver ()
468 {
469 return m_aXPathVariableResolver;
470 }
471
472 @Nullable
473 public XPathFunctionResolver getXPathFunctionResolver ()
474 {
475 return m_aXPathFunctionResolver;
476 }
477
478 @Nonnull
479 public String getValidationContext (@Nonnull final String sRuleContext)
480 {
481
482 if (sRuleContext.startsWith ("/"))
483 return sRuleContext;
484
485
486 return "//" + sRuleContext;
487 }
488
489 public void validate (@Nonnull final Node aNode,
490 @Nullable final String sBaseURI,
491 @Nonnull final IPSValidationHandler aValidationHandler) throws SchematronValidationException
492 {
493 ValueEnforcer.notNull (aNode, "Node");
494 ValueEnforcer.notNull (aValidationHandler, "ValidationHandler");
495
496 if (m_aBoundPatterns == null)
497 throw new IllegalStateException ("bind was never called!");
498
499 final PSSchema aSchema = getOriginalSchema ();
500 final PSPhase aPhase = getPhase ();
501
502
503 aValidationHandler.onStart (aSchema, aPhase, sBaseURI);
504
505
506 for (final PSXPathBoundPattern aBoundPattern : m_aBoundPatterns)
507 {
508 final PSPattern aPattern = aBoundPattern.getPattern ();
509 aValidationHandler.onPattern (aPattern);
510
511
512 rules: for (final PSXPathBoundRule aBoundRule : aBoundPattern.getAllBoundRules ())
513 {
514 final PSRule aRule = aBoundRule.getRule ();
515
516
517 NodeList aRuleMatchingNodes = null;
518 try
519 {
520 aRuleMatchingNodes = XPathEvaluationHelper.evaluate (aBoundRule.getBoundRuleExpression (),
521 aNode,
522 XPathConstants.NODESET,
523 sBaseURI);
524 }
525 catch (final XPathExpressionException ex)
526 {
527
528 error (aRule,
529 "Failed to evaluate XPath expression to a nodeset: '" + aBoundRule.getRuleExpression () + "'",
530 ex.getCause () != null ? ex.getCause () : ex);
531 continue rules;
532 }
533
534 final int nRuleMatchingNodes = aRuleMatchingNodes.getLength ();
535 if (nRuleMatchingNodes > 0)
536 {
537
538 for (final PSXPathBoundAssertReport aBoundAssertReport : aBoundRule.getAllBoundAssertReports ())
539 {
540
541 aValidationHandler.onRule (aRule, aBoundRule.getRuleExpression ());
542
543 final PSAssertReport aAssertReport = aBoundAssertReport.getAssertReport ();
544 final boolean bIsAssert = aAssertReport.isAssert ();
545 final XPathExpression aTestExpression = aBoundAssertReport.getBoundTestExpression ();
546
547
548 for (int i = 0; i < nRuleMatchingNodes; ++i)
549 {
550 final Node aRuleMatchingNode = aRuleMatchingNodes.item (i);
551 try
552 {
553 final boolean bTestResult = ((Boolean) XPathEvaluationHelper.evaluate (aTestExpression,
554 aRuleMatchingNode,
555 XPathConstants.BOOLEAN,
556 sBaseURI)).booleanValue ();
557 if (bIsAssert)
558 {
559
560 if (!bTestResult)
561 {
562
563 if (aValidationHandler.onFailedAssert (aAssertReport,
564 aBoundAssertReport.getTestExpression (),
565 aRuleMatchingNode,
566 i,
567 aBoundAssertReport)
568 .isBreak ())
569 {
570 return;
571 }
572 }
573 }
574 else
575 {
576
577 if (bTestResult)
578 {
579
580 if (aValidationHandler.onSuccessfulReport (aAssertReport,
581 aBoundAssertReport.getTestExpression (),
582 aRuleMatchingNode,
583 i,
584 aBoundAssertReport)
585 .isBreak ())
586 {
587 return;
588 }
589 }
590 }
591 }
592 catch (final XPathExpressionException ex)
593 {
594 error (aRule,
595 "Failed to evaluate XPath expression to a boolean: '" +
596 aBoundAssertReport.getTestExpression () +
597 "'",
598 ex.getCause () != null ? ex.getCause () : ex);
599 }
600 }
601 }
602
603 if (false)
604 {
605
606
607 break rules;
608 }
609 }
610 }
611 }
612
613
614 aValidationHandler.onEnd (aSchema, aPhase);
615 }
616
617 @Override
618 public String toString ()
619 {
620 return ToStringGenerator.getDerived (super.toString ()).append ("boundPatterns", m_aBoundPatterns).getToString ();
621 }
622 }