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.w3c.dom.Node;
34 import org.w3c.dom.NodeList;
35
36 import com.helger.commons.ValueEnforcer;
37 import com.helger.commons.collection.ext.CommonsArrayList;
38 import com.helger.commons.collection.ext.CommonsHashMap;
39 import com.helger.commons.collection.ext.ICommonsList;
40 import com.helger.commons.collection.ext.ICommonsMap;
41 import com.helger.commons.lang.ClassLoaderHelper;
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.pure.validation.xpath.PSXPathValidationHandlerSVRL;
62 import com.helger.schematron.saxon.SaxonNamespaceContext;
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.lib.FeatureKeys;
68 import net.sf.saxon.xpath.XPathEvaluator;
69
70
71
72
73
74
75 @Immutable
76 public class PSXPathBoundSchema extends AbstractPSBoundSchema
77 {
78 private final XPathVariableResolver m_aXPathVariableResolver;
79 private final XPathFunctionResolver m_aXPathFunctionResolver;
80 private final XPathFactory m_aXPathFactory;
81 private ICommonsList <PSXPathBoundPattern> m_aBoundPatterns;
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97 @Nullable
98 private static XPathExpression _compileXPath (@Nonnull final XPath aXPathContext,
99 @Nonnull final String sXPathExpression) throws XPathExpressionException
100 {
101 XPathExpression ret = null;
102 try
103 {
104 ret = aXPathContext.compile (sXPathExpression);
105 }
106 catch (final XPathExpressionException ex)
107 {
108
109 throw ex;
110 }
111 return ret;
112 }
113
114 @Nullable
115 private ICommonsList <PSXPathBoundElement> _createBoundElements (@Nonnull final IPSHasMixedContent aMixedContent,
116 @Nonnull final XPath aXPathContext,
117 @Nonnull final IPSXPathVariables aVariables)
118 {
119 final ICommonsList <PSXPathBoundElement> ret = new CommonsArrayList <> ();
120 boolean bHasAnyError = false;
121
122 for (final Object aContentElement : aMixedContent.getAllContentElements ())
123 {
124 if (aContentElement instanceof PSName)
125 {
126 final PSName aName = (PSName) aContentElement;
127 if (aName.hasPath ())
128 {
129
130 final String sPath = aVariables.getAppliedReplacement (aName.getPath ());
131 try
132 {
133 final XPathExpression aXpathExpression = _compileXPath (aXPathContext, sPath);
134 ret.add (new PSXPathBoundElement (aName, sPath, aXpathExpression));
135 }
136 catch (final XPathExpressionException ex)
137 {
138 error (aName, "Failed to compile XPath expression in <name>: '" + sPath + "'", ex);
139 bHasAnyError = true;
140 }
141 }
142 else
143 {
144
145 ret.add (new PSXPathBoundElement (aName));
146 }
147 }
148 else
149 if (aContentElement instanceof PSValueOf)
150 {
151 final PSValueOf aValueOf = (PSValueOf) aContentElement;
152
153
154 final String sSelect = aVariables.getAppliedReplacement (aValueOf.getSelect ());
155 try
156 {
157 final XPathExpression aXPathExpression = _compileXPath (aXPathContext, sSelect);
158 ret.add (new PSXPathBoundElement (aValueOf, sSelect, aXPathExpression));
159 }
160 catch (final XPathExpressionException ex)
161 {
162 error (aValueOf, "Failed to compile XPath expression in <value-of>: '" + sSelect + "'", ex);
163 bHasAnyError = true;
164 }
165 }
166 else
167 {
168
169 if (aContentElement instanceof String)
170 ret.add (new PSXPathBoundElement ((String) aContentElement));
171 else
172 ret.add (new PSXPathBoundElement ((IPSElement) aContentElement));
173 }
174 }
175
176 if (bHasAnyError)
177 return null;
178
179 return ret;
180 }
181
182 @Nullable
183 private ICommonsMap <String, PSXPathBoundDiagnostic> _createBoundDiagnostics (@Nonnull final XPath aXPathContext,
184 @Nonnull final IPSXPathVariables aGlobalVariables)
185 {
186 final ICommonsMap <String, PSXPathBoundDiagnostic> ret = new CommonsHashMap <> ();
187 boolean bHasAnyError = false;
188
189 final PSSchema aSchema = getOriginalSchema ();
190 if (aSchema.hasDiagnostics ())
191 {
192
193 for (final PSDiagnostic aDiagnostic : aSchema.getDiagnostics ().getAllDiagnostics ())
194 {
195 final ICommonsList <PSXPathBoundElement> aBoundElements = _createBoundElements (aDiagnostic,
196 aXPathContext,
197 aGlobalVariables);
198 if (aBoundElements == null)
199 {
200
201 bHasAnyError = true;
202 }
203 else
204 {
205 final PSXPathBoundDiagnostic aBoundDiagnostic = new PSXPathBoundDiagnostic (aDiagnostic, aBoundElements);
206 if (ret.put (aDiagnostic.getID (), aBoundDiagnostic) != null)
207 {
208 error (aDiagnostic, "A diagnostic element with ID '" + aDiagnostic.getID () + "' was overwritten!");
209 bHasAnyError = true;
210 }
211 }
212 }
213 }
214
215 if (bHasAnyError)
216 return null;
217
218 return ret;
219 }
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234 @Nullable
235 private ICommonsList <PSXPathBoundPattern> _createBoundPatterns (@Nonnull final XPath aXPathContext,
236 @Nonnull final ICommonsMap <String, PSXPathBoundDiagnostic> aBoundDiagnostics,
237 @Nonnull final IPSXPathVariables aGlobalVariables)
238 {
239 final ICommonsList <PSXPathBoundPattern> ret = new CommonsArrayList <> ();
240 boolean bHasAnyError = false;
241
242
243 for (final PSPattern aPattern : getAllRelevantPatterns ())
244 {
245
246 final PSXPathVariables aPatternVariables = aGlobalVariables.getClone ();
247
248 if (aPattern.hasAnyLet ())
249 {
250
251
252 for (final Map.Entry <String, String> aEntry : aPattern.getAllLetsAsMap ().entrySet ())
253 if (aPatternVariables.add (aEntry).isUnchanged ())
254 error (aPattern, "Duplicate <let> with name '" + aEntry.getKey () + "' in <pattern>");
255 }
256
257
258 final ICommonsList <PSXPathBoundRule> aBoundRules = new CommonsArrayList <> ();
259 for (final PSRule aRule : aPattern.getAllRules ())
260 {
261
262 final PSXPathVariables aRuleVariables = aPatternVariables.getClone ();
263 if (aRule.hasAnyLet ())
264 {
265
266
267 for (final Map.Entry <String, String> aEntry : aRule.getAllLetsAsMap ().entrySet ())
268 {
269 if (aRuleVariables.add (aEntry).isUnchanged ())
270 {
271 error (aRule, "Duplicate <let> with name '" + aEntry.getKey () + "' in <rule>");
272 }
273 }
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 (m_aBoundPatterns != null)
443 throw new IllegalStateException ("bind must only be called once!");
444
445 final PSSchema aSchema = getOriginalSchema ();
446 final PSPhase aPhase = getPhase ();
447
448
449 final PSXPathVariables aGlobalVariables = new PSXPathVariables ();
450 if (aSchema.hasAnyLet ())
451 for (final Map.Entry <String, String> aEntry : aSchema.getAllLetsAsMap ().entrySet ())
452 if (aGlobalVariables.add (aEntry).isUnchanged ())
453 error (aSchema, "Duplicate <let> with name '" + aEntry.getKey () + "' in global <schema>");
454
455 if (aPhase != null)
456 {
457
458 for (final Map.Entry <String, String> aEntry : aPhase.getAllLetsAsMap ().entrySet ())
459 if (aGlobalVariables.add (aEntry).isUnchanged ())
460 error (aSchema,
461 "Duplicate <let> with name '" + aEntry.getKey () + "' in <phase> with name '" + getPhaseID () + "'");
462 }
463
464 final XPath aXPathContext = _createXPathContext ();
465
466
467 final ICommonsMap <String, PSXPathBoundDiagnostic> aBoundDiagnostics = _createBoundDiagnostics (aXPathContext,
468 aGlobalVariables);
469 if (aBoundDiagnostics == null)
470 throw new SchematronBindException ("Failed to precompile the diagnostics of the supplied schema. Check the " +
471 (isDefaultErrorHandler () ? "log output" : "error listener") +
472 " for XPath errors!");
473
474
475
476 m_aBoundPatterns = _createBoundPatterns (aXPathContext, aBoundDiagnostics, aGlobalVariables);
477 if (m_aBoundPatterns == null)
478 throw new SchematronBindException ("Failed to precompile the supplied schema.");
479 return this;
480 }
481
482 @Nullable
483 public XPathVariableResolver getXPathVariableResolver ()
484 {
485 return m_aXPathVariableResolver;
486 }
487
488 @Nullable
489 public XPathFunctionResolver getXPathFunctionResolver ()
490 {
491 return m_aXPathFunctionResolver;
492 }
493
494 @Nonnull
495 public String getValidationContext (@Nonnull final String sRuleContext)
496 {
497
498 if (sRuleContext.startsWith ("/"))
499 return sRuleContext;
500
501
502 return "//" + sRuleContext;
503 }
504
505 public void validate (@Nonnull final Node aNode,
506 @Nonnull final IPSValidationHandler aValidationHandler) throws SchematronValidationException
507 {
508 ValueEnforcer.notNull (aNode, "Node");
509 ValueEnforcer.notNull (aValidationHandler, "ValidationHandler");
510
511 if (m_aBoundPatterns == null)
512 throw new IllegalStateException ("bind was never called!");
513
514 final PSSchema aSchema = getOriginalSchema ();
515 final PSPhase aPhase = getPhase ();
516
517
518 aValidationHandler.onStart (aSchema, aPhase);
519
520
521 for (final PSXPathBoundPattern aBoundPattern : m_aBoundPatterns)
522 {
523 final PSPattern aPattern = aBoundPattern.getPattern ();
524 aValidationHandler.onPattern (aPattern);
525
526
527 rules: for (final PSXPathBoundRule aBoundRule : aBoundPattern.getAllBoundRules ())
528 {
529 final PSRule aRule = aBoundRule.getRule ();
530
531
532 NodeList aRuleMatchingNodes = null;
533 try
534 {
535 aRuleMatchingNodes = (NodeList) aBoundRule.getBoundRuleExpression ().evaluate (aNode, XPathConstants.NODESET);
536 }
537 catch (final XPathExpressionException ex)
538 {
539 error (aRule,
540 "Failed to evaluate XPath expression to a nodeset: '" + aBoundRule.getRuleExpression () + "'",
541 ex);
542 continue rules;
543 }
544
545 final int nRuleMatchingNodes = aRuleMatchingNodes.getLength ();
546 if (nRuleMatchingNodes > 0)
547 {
548
549 for (final PSXPathBoundAssertReport aBoundAssertReport : aBoundRule.getAllBoundAssertReports ())
550 {
551
552 aValidationHandler.onRule (aRule, aBoundRule.getRuleExpression ());
553
554 final PSAssertReport aAssertReport = aBoundAssertReport.getAssertReport ();
555 final boolean bIsAssert = aAssertReport.isAssert ();
556 final XPathExpression aTestExpression = aBoundAssertReport.getBoundTestExpression ();
557
558
559 for (int i = 0; i < nRuleMatchingNodes; ++i)
560 {
561 final Node aRuleMatchingNode = aRuleMatchingNodes.item (i);
562 try
563 {
564 final boolean bTestResult = ((Boolean) aTestExpression.evaluate (aRuleMatchingNode,
565 XPathConstants.BOOLEAN)).booleanValue ();
566 if (bIsAssert)
567 {
568
569 if (!bTestResult)
570 {
571
572 if (aValidationHandler.onFailedAssert (aAssertReport,
573 aBoundAssertReport.getTestExpression (),
574 aRuleMatchingNode,
575 i,
576 aBoundAssertReport)
577 .isBreak ())
578 {
579 return;
580 }
581 }
582 }
583 else
584 {
585
586 if (bTestResult)
587 {
588
589 if (aValidationHandler.onSuccessfulReport (aAssertReport,
590 aBoundAssertReport.getTestExpression (),
591 aRuleMatchingNode,
592 i,
593 aBoundAssertReport)
594 .isBreak ())
595 {
596 return;
597 }
598 }
599 }
600 }
601 catch (final XPathExpressionException ex)
602 {
603 error (aRule,
604 "Failed to evaluate XPath expression to a boolean: '" +
605 aBoundAssertReport.getTestExpression () +
606 "'",
607 ex);
608 }
609 }
610 }
611
612 if (false)
613 {
614
615
616 break rules;
617 }
618 }
619 }
620 }
621
622
623 aValidationHandler.onEnd (aSchema, aPhase);
624 }
625
626 @Nonnull
627 public SchematronOutputType validateComplete (@Nonnull final Node aNode) throws SchematronValidationException
628 {
629 final PSXPathValidationHandlerSVRL aValidationHandler = new PSXPathValidationHandlerSVRL (getErrorHandler ());
630 validate (aNode, aValidationHandler);
631 return aValidationHandler.getSVRL ();
632 }
633
634 @Override
635 public String toString ()
636 {
637 return ToStringGenerator.getDerived (super.toString ()).append ("boundPatterns", m_aBoundPatterns).toString ();
638 }
639 }