View Javadoc
1   /**
2    * Copyright (C) 2014-2017 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.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   * The default XPath binding for the pure Schematron implementation.
74   *
75   * @author Philip Helger
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     * Compile an XPath expression string to an {@link XPathExpressionException}
89     * object. If expression contains any variables, the
90     * {@link XPathVariableResolver} will be used to resolve them within this
91     * method!
92     *
93     * @param aXPathContext
94     *        Context to use. May not be <code>null</code>.
95     * @param sXPathExpression
96     *        The expression to be compiled. May not be <code>null</code>.
97     * @return The precompiled {@link XPathExpression}
98     * @throws XPathExpressionException
99     *         If expression cannot be compiled.
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       // Do something with it
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           // Replace all variables
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           // No XPath required
149           ret.add (new PSXPathBoundElement (aName));
150         }
151       }
152       else
153         if (aContentElement instanceof PSValueOf)
154         {
155           final PSValueOf aValueOf = (PSValueOf) aContentElement;
156 
157           // Replace variables
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           // No XPath compilation necessary
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       // For all contained diagnostic elements
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           // error already emitted
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    * Pre-compile all patterns incl. their content
227    *
228    * @param aXPathContext
229    * @param aXPathContext
230    *        Global XPath object to use. May not be <code>null</code>.
231    * @param aBoundDiagnostics
232    *        A map from DiagnosticID to its mapped counterpart. May not be
233    *        <code>null</code>.
234    * @param aGlobalVariables
235    *        The global Schematron-let variables. May not be <code>null</code>.
236    * @return <code>null</code> if an XPath error is contained
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     // For all relevant patterns
247     for (final PSPattern aPattern : getAllRelevantPatterns ())
248     {
249       // Handle pattern specific variables
250       final PSXPathVariables aPatternVariables = aGlobalVariables.getClone ();
251 
252       if (aPattern.hasAnyLet ())
253       {
254         // The pattern has special variables, so we need to extend the variable
255         // map
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       // For all rules of the current pattern
262       final ICommonsList <PSXPathBoundRule> aBoundRules = new CommonsArrayList <> ();
263       for (final PSRule aRule : aPattern.getAllRules ())
264       {
265         // Handle rule specific variables
266         final PSXPathVariables aRuleVariables = aPatternVariables.getClone ();
267         if (aRule.hasAnyLet ())
268         {
269           // The rule has special variables, so we need to extend the
270           // variable map
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         // 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 (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     // Get all "global" variables that are defined in the schema
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       // Get all variables that are defined in the specified phase
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     // Pre-compile all diagnostics first
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     // Perform the pre-compilation of all XPath expressions in the patterns,
478     // rules, asserts/reports and the content elements
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     // Do we already have an absolute XPath?
501     if (sRuleContext.startsWith ("/"))
502       return sRuleContext;
503 
504     // Create an absolute XPath expression!
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     // Call the "start" callback method
521     aValidationHandler.onStart (aSchema, aPhase);
522 
523     // For all bound patterns
524     for (final PSXPathBoundPattern aBoundPattern : m_aBoundPatterns)
525     {
526       final PSPattern aPattern = aBoundPattern.getPattern ();
527       aValidationHandler.onPattern (aPattern);
528 
529       // For all bound rules
530       rules: for (final PSXPathBoundRule aBoundRule : aBoundPattern.getAllBoundRules ())
531       {
532         final PSRule aRule = aBoundRule.getRule ();
533 
534         // Find all nodes matching the rules
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           // For all contained assert and report elements
552           for (final PSXPathBoundAssertReport aBoundAssertReport : aBoundRule.getAllBoundAssertReports ())
553           {
554             // XSLT does "fired-rule" for each node
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             // Check each node, if it matches the assert/report
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                   // It's an assert
572                   if (!bTestResult)
573                   {
574                     // Assert failed
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                   // It's a report
589                   if (bTestResult)
590                   {
591                     // Successful report
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             // The rule matched at least one node. In this case continue with
618             // the next pattern
619             break rules;
620           }
621         }
622       }
623     }
624 
625     // Call the "end" callback method
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 }