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