View Javadoc
1   /**
2    * Copyright (C) 2014-2016 Philip Helger (www.helger.com)
3    * philip[at]helger[dot]com
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    *         http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
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   * The default XPath binding for the pure Schematron implementation.
72   *
73   * @author Philip Helger
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     * Compile an XPath expression string to an {@link XPathExpressionException}
85     * object. If expression contains any variables, the
86     * {@link XPathVariableResolver} will be used to resolve them within this
87     * method!
88     *
89     * @param aXPathContext
90     *        Context to use. May not be <code>null</code>.
91     * @param sXPathExpression
92     *        The expression to be compiled. May not be <code>null</code>.
93     * @return The precompiled {@link XPathExpression}
94     * @throws XPathExpressionException
95     *         If expression cannot be compiled.
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       // Do something with it
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           // Replace all variables
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           // No XPath required
145           ret.add (new PSXPathBoundElement (aName));
146         }
147       }
148       else
149         if (aContentElement instanceof PSValueOf)
150         {
151           final PSValueOf aValueOf = (PSValueOf) aContentElement;
152 
153           // Replace variables
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           // No XPath compilation necessary
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       // For all contained diagnostic elements
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           // error already emitted
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    * Pre-compile all patterns incl. their content
223    *
224    * @param aXPathContext
225    * @param aXPathContext
226    *        Global XPath object to use. May not be <code>null</code>.
227    * @param aBoundDiagnostics
228    *        A map from DiagnosticID to its mapped counterpart. May not be
229    *        <code>null</code>.
230    * @param aGlobalVariables
231    *        The global Schematron-let variables. May not be <code>null</code>.
232    * @return <code>null</code> if an XPath error is contained
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     // For all relevant patterns
243     for (final PSPattern aPattern : getAllRelevantPatterns ())
244     {
245       // Handle pattern specific variables
246       final PSXPathVariables aPatternVariables = aGlobalVariables.getClone ();
247 
248       if (aPattern.hasAnyLet ())
249       {
250         // The pattern has special variables, so we need to extend the variable
251         // map
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       // For all rules of the current pattern
258       final ICommonsList <PSXPathBoundRule> aBoundRules = new CommonsArrayList <> ();
259       for (final PSRule aRule : aPattern.getAllRules ())
260       {
261         // Handle rule specific variables
262         final PSXPathVariables aRuleVariables = aPatternVariables.getClone ();
263         if (aRule.hasAnyLet ())
264         {
265           // The rule has special variables, so we need to extend the
266           // variable map
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         // For all contained assert and reports within the current rule
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               // Error already emitted
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         // Evaluate base node set for this rule
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       // Create the bound pattern
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     // The XPath object used to compile the expressions
347     XPathFactory aXPathFactory;
348     try
349     {
350       // First try to use Saxon, using the context class loader
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       // Must be Throwable because of e.g. IllegalAccessError (see issue #19)
358       // Seems like Saxon is not in the class path - fall back to default JAXP
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    * Create a new bound schema. All the XPath pre-compilation happens inside
373    * this constructor, so that the {@link #validate(Node, IPSValidationHandler)}
374    * method can be called many times without compiling the XPath statements
375    * again and again.
376    *
377    * @param aQueryBinding
378    *        The query binding to be used. May not be <code>null</code>.
379    * @param aOrigSchema
380    *        The original schema that should be bound. May not be
381    *        <code>null</code>.
382    * @param sPhase
383    *        The selected phase. May be <code>null</code> indicating that the
384    *        default phase of the schema should be used (if present) or all
385    *        patterns should be evaluated if no default phase is present.
386    * @param aCustomErrorListener
387    *        A custom error listener to be used. May be <code>null</code> in
388    *        which case a
389    *        {@link com.helger.schematron.pure.errorhandler.LoggingPSErrorHandler}
390    *        is used internally.
391    * @param aXPathVariableResolver
392    *        Custom XPath variable resolver. May be <code>null</code>.
393    * @param aXPathFunctionResolver
394    *        Custom XPath function resolver. May be <code>null</code>.
395    * @throws SchematronBindException
396    *         In case XPath expressions are incorrect and pre-compilation fails
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       // Saxon implementation special handling
423       final XPathEvaluator aSaxonXPath = (XPathEvaluator) aXPathContext;
424       if (false)
425       {
426         // Enable this to debug Saxon function resolving
427         aSaxonXPath.getConfiguration ().setBooleanProperty (FeatureKeys.TRACE_EXTERNAL_FUNCTIONS, true);
428       }
429 
430       // Since 9.7.0-4 it must implement NamespaceResolver
431       aSaxonXPath.setNamespaceContext (new SaxonNamespaceContext (aNamespaceContext));
432 
433       // Wrap the PSErrorHandler to a ErrorListener
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     // Get all "global" variables that are defined in the schema
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       // Get all variables that are defined in the specified phase
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     // Pre-compile all diagnostics first
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     // Perform the pre-compilation of all XPath expressions in the patterns,
475     // rules, asserts/reports and the content elements
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     // Do we already have an absolute XPath?
498     if (sRuleContext.startsWith ("/"))
499       return sRuleContext;
500 
501     // Create an absolute XPath expression!
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     // Call the "start" callback method
518     aValidationHandler.onStart (aSchema, aPhase);
519 
520     // For all bound patterns
521     for (final PSXPathBoundPattern aBoundPattern : m_aBoundPatterns)
522     {
523       final PSPattern aPattern = aBoundPattern.getPattern ();
524       aValidationHandler.onPattern (aPattern);
525 
526       // For all bound rules
527       rules: for (final PSXPathBoundRule aBoundRule : aBoundPattern.getAllBoundRules ())
528       {
529         final PSRule aRule = aBoundRule.getRule ();
530 
531         // Find all nodes matching the rules
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           // For all contained assert and report elements
549           for (final PSXPathBoundAssertReport aBoundAssertReport : aBoundRule.getAllBoundAssertReports ())
550           {
551             // XSLT does "fired-rule" for each node
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             // Check each node, if it matches the assert/report
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                   // It's an assert
569                   if (!bTestResult)
570                   {
571                     // Assert failed
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                   // It's a report
586                   if (bTestResult)
587                   {
588                     // Successful report
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             // The rule matched at least one node. In this case continue with
615             // the next pattern
616             break rules;
617           }
618         }
619       }
620     }
621 
622     // Call the "end" callback method
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 }