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