View Javadoc
1   /**
2    * Copyright (C) 2014-2018 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.NotThreadSafe;
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.slf4j.Logger;
33  import org.slf4j.LoggerFactory;
34  import org.w3c.dom.Node;
35  import org.w3c.dom.NodeList;
36  
37  import com.helger.commons.ValueEnforcer;
38  import com.helger.commons.collection.impl.CommonsArrayList;
39  import com.helger.commons.collection.impl.CommonsHashMap;
40  import com.helger.commons.collection.impl.ICommonsList;
41  import com.helger.commons.collection.impl.ICommonsMap;
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.saxon.SaxonNamespaceContext;
62  import com.helger.schematron.xpath.XPathEvaluationHelper;
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.xpath.XPathEvaluator;
68  
69  /**
70   * The default XPath binding for the pure Schematron implementation.
71   *
72   * @author Philip Helger
73   */
74  @NotThreadSafe
75  public class PSXPathBoundSchema extends AbstractPSBoundSchema
76  {
77    private static final Logger LOGGER = LoggerFactory.getLogger (PSXPathBoundSchema.class);
78  
79    private final XPathVariableResolver m_aXPathVariableResolver;
80    private final XPathFunctionResolver m_aXPathFunctionResolver;
81    private final XPathFactory m_aXPathFactory;
82    private ICommonsList <PSXPathBoundPattern> m_aBoundPatterns;
83  
84    /**
85     * Compile an XPath expression string to an {@link XPathExpressionException}
86     * object. If expression contains any variables, the
87     * {@link XPathVariableResolver} will be used to resolve them within this
88     * method!
89     *
90     * @param aXPathContext
91     *        Context to use. May not be <code>null</code>.
92     * @param sXPathExpression
93     *        The expression to be compiled. May not be <code>null</code>.
94     * @return The precompiled {@link XPathExpression}
95     * @throws XPathExpressionException
96     *         If expression cannot be compiled.
97     */
98    @Nullable
99    private static XPathExpression _compileXPath (@Nonnull final XPath aXPathContext,
100                                                 @Nonnull final String sXPathExpression) throws XPathExpressionException
101   {
102     XPathExpression ret = null;
103     try
104     {
105       ret = aXPathContext.compile (sXPathExpression);
106     }
107     catch (final XPathExpressionException ex)
108     {
109       // Do something with it
110       throw ex;
111     }
112     return ret;
113   }
114 
115   @Nullable
116   private ICommonsList <PSXPathBoundElement> _createBoundElements (@Nonnull final IPSHasMixedContent aMixedContent,
117                                                                    @Nonnull final XPath aXPathContext,
118                                                                    @Nonnull final IPSXPathVariables aVariables)
119   {
120     final ICommonsList <PSXPathBoundElement> ret = new CommonsArrayList <> ();
121     boolean bHasAnyError = false;
122 
123     for (final Object aContentElement : aMixedContent.getAllContentElements ())
124     {
125       if (aContentElement instanceof PSName)
126       {
127         final PSName"../../../../../../com/helger/schematron/pure/model/PSName.html#PSName">PSName aName = (PSName) aContentElement;
128         if (aName.hasPath ())
129         {
130           // Replace all variables
131           final String sPath = aVariables.getAppliedReplacement (aName.getPath ());
132           try
133           {
134             final XPathExpression aXpathExpression = _compileXPath (aXPathContext, sPath);
135             ret.add (new PSXPathBoundElement (aName, sPath, aXpathExpression));
136           }
137           catch (final XPathExpressionException ex)
138           {
139             error (aName,
140                    "Failed to compile XPath expression in <name>: '" + sPath + "'",
141                    ex.getCause () != null ? ex.getCause () : ex);
142             bHasAnyError = true;
143           }
144         }
145         else
146         {
147           // No XPath required
148           ret.add (new PSXPathBoundElement (aName));
149         }
150       }
151       else
152         if (aContentElement instanceof PSValueOf)
153         {
154           final PSValueOf/../../../../com/helger/schematron/pure/model/PSValueOf.html#PSValueOf">PSValueOf aValueOf = (PSValueOf) aContentElement;
155 
156           // Replace variables
157           final String sSelect = aVariables.getAppliedReplacement (aValueOf.getSelect ());
158           try
159           {
160             final XPathExpression aXPathExpression = _compileXPath (aXPathContext, sSelect);
161             ret.add (new PSXPathBoundElement (aValueOf, sSelect, aXPathExpression));
162           }
163           catch (final XPathExpressionException ex)
164           {
165             error (aValueOf, "Failed to compile XPath expression in <value-of>: '" + sSelect + "'", ex);
166             bHasAnyError = true;
167           }
168         }
169         else
170         {
171           // No XPath compilation necessary
172           if (aContentElement instanceof String)
173             ret.add (new PSXPathBoundElement ((String) aContentElement));
174           else
175             ret.add (new PSXPathBoundElement ((IPSElement) aContentElement));
176         }
177     }
178 
179     if (bHasAnyError)
180       return null;
181 
182     return ret;
183   }
184 
185   @Nullable
186   private ICommonsMap <String, PSXPathBoundDiagnostic> _createBoundDiagnostics (@Nonnull final XPath aXPathContext,
187                                                                                 @Nonnull final IPSXPathVariables aGlobalVariables)
188   {
189     final ICommonsMap <String, PSXPathBoundDiagnostic> ret = new CommonsHashMap <> ();
190     boolean bHasAnyError = false;
191 
192     final PSSchema aSchema = getOriginalSchema ();
193     if (aSchema.hasDiagnostics ())
194     {
195       // For all contained diagnostic elements
196       for (final PSDiagnostic aDiagnostic : aSchema.getDiagnostics ().getAllDiagnostics ())
197       {
198         final ICommonsList <PSXPathBoundElement> aBoundElements = _createBoundElements (aDiagnostic,
199                                                                                         aXPathContext,
200                                                                                         aGlobalVariables);
201         if (aBoundElements == null)
202         {
203           // error already emitted
204           bHasAnyError = true;
205         }
206         else
207         {
208           final PSXPathBoundDiagnosticundDiagnostic.html#PSXPathBoundDiagnostic">PSXPathBoundDiagnostic aBoundDiagnostic = new PSXPathBoundDiagnostic (aDiagnostic, aBoundElements);
209           if (ret.put (aDiagnostic.getID (), aBoundDiagnostic) != null)
210           {
211             error (aDiagnostic, "A diagnostic element with ID '" + aDiagnostic.getID () + "' was overwritten!");
212             bHasAnyError = true;
213           }
214         }
215       }
216     }
217 
218     if (bHasAnyError)
219       return null;
220 
221     return ret;
222   }
223 
224   /**
225    * Pre-compile all patterns incl. their content
226    *
227    * @param aXPathContext
228    * @param aXPathContext
229    *        Global XPath object to use. May not be <code>null</code>.
230    * @param aBoundDiagnostics
231    *        A map from DiagnosticID to its mapped counterpart. May not be
232    *        <code>null</code>.
233    * @param aGlobalVariables
234    *        The global Schematron-let variables. May not be <code>null</code>.
235    * @return <code>null</code> if an XPath error is contained
236    */
237   @Nullable
238   private ICommonsList <PSXPathBoundPattern> _createBoundPatterns (@Nonnull final XPath aXPathContext,
239                                                                    @Nonnull final ICommonsMap <String, PSXPathBoundDiagnostic> aBoundDiagnostics,
240                                                                    @Nonnull final IPSXPathVariables aGlobalVariables)
241   {
242     final ICommonsList <PSXPathBoundPattern> ret = new CommonsArrayList <> ();
243     boolean bHasAnyError = false;
244 
245     // For all relevant patterns
246     for (final PSPattern aPattern : getAllRelevantPatterns ())
247     {
248       // Handle pattern specific variables
249       final PSXPathVariables aPatternVariables = aGlobalVariables.getClone ();
250 
251       if (aPattern.hasAnyLet ())
252       {
253         // The pattern has special variables, so we need to extend the variable
254         // map
255         for (final Map.Entry <String, String> aEntry : aPattern.getAllLetsAsMap ().entrySet ())
256           if (aPatternVariables.add (aEntry).isUnchanged ())
257             error (aPattern, "Duplicate <let> with name '" + aEntry.getKey () + "' in <pattern>");
258       }
259 
260       // For all rules of the current pattern
261       final ICommonsList <PSXPathBoundRule> aBoundRules = new CommonsArrayList <> ();
262       for (final PSRule aRule : aPattern.getAllRules ())
263       {
264         // Handle rule specific variables
265         final PSXPathVariables aRuleVariables = aPatternVariables.getClone ();
266         if (aRule.hasAnyLet ())
267         {
268           // The rule has special variables, so we need to extend the
269           // variable map
270           for (final Map.Entry <String, String> aEntry : aRule.getAllLetsAsMap ().entrySet ())
271             if (aRuleVariables.add (aEntry).isUnchanged ())
272               error (aRule, "Duplicate <let> with name '" + aEntry.getKey () + "' in <rule>");
273         }
274 
275         // For all contained assert and reports within the current rule
276         final ICommonsList <PSXPathBoundAssertReport> aBoundAssertReports = new CommonsArrayList <> ();
277         for (final PSAssertReport aAssertReport : aRule.getAllAssertReports ())
278         {
279           final String sTest = aRuleVariables.getAppliedReplacement (aAssertReport.getTest ());
280           try
281           {
282             final XPathExpression aTestExpr = _compileXPath (aXPathContext, sTest);
283             final ICommonsList <PSXPathBoundElement> aBoundElements = _createBoundElements (aAssertReport,
284                                                                                             aXPathContext,
285                                                                                             aRuleVariables);
286             if (aBoundElements == null)
287             {
288               // Error already emitted
289               bHasAnyError = true;
290             }
291             else
292             {
293               final PSXPathBoundAssertReportssertReport.html#PSXPathBoundAssertReport">PSXPathBoundAssertReport aBoundAssertReport = new PSXPathBoundAssertReport (aAssertReport,
294                                                                                                 sTest,
295                                                                                                 aTestExpr,
296                                                                                                 aBoundElements,
297                                                                                                 aBoundDiagnostics);
298               aBoundAssertReports.add (aBoundAssertReport);
299             }
300           }
301           catch (final Throwable t)
302           {
303             error (aAssertReport,
304                    "Failed to compile XPath expression in <" +
305                                   (aAssertReport.isAssert () ? "assert" : "report") +
306                                   ">: '" +
307                                   sTest +
308                                   "' with the following variables: " +
309                                   aRuleVariables.getAll (),
310                    t);
311             bHasAnyError = true;
312           }
313         }
314 
315         // Evaluate base node set for this rule
316         final String sRuleContext = aGlobalVariables.getAppliedReplacement (getValidationContext (aRule.getContext ()));
317         PSXPathBoundRule aBoundRule = null;
318         try
319         {
320           final XPathExpression aRuleContext = _compileXPath (aXPathContext, sRuleContext);
321           aBoundRule = new PSXPathBoundRule (aRule, sRuleContext, aRuleContext, aBoundAssertReports);
322           aBoundRules.add (aBoundRule);
323         }
324         catch (final XPathExpressionException ex)
325         {
326           error (aRule,
327                  "Failed to compile XPath expression in <rule>: '" + sRuleContext + "'",
328                  ex.getCause () != null ? ex.getCause () : ex);
329           bHasAnyError = true;
330         }
331       }
332 
333       // Create the bound pattern
334       final PSXPathBoundPatternPathBoundPattern.html#PSXPathBoundPattern">PSXPathBoundPattern aBoundPattern = new PSXPathBoundPattern (aPattern, aBoundRules);
335       ret.add (aBoundPattern);
336     }
337 
338     if (bHasAnyError)
339       return null;
340 
341     return ret;
342   }
343 
344   @Nonnull
345   public static XPathFactory createXPathFactorySaxonFirst () throws SchematronBindException
346   {
347     try
348     {
349       return XPathHelper.createXPathFactorySaxonFirst ();
350     }
351     catch (final IllegalStateException ex)
352     {
353       throw new SchematronBindException (ex.getMessage (), ex.getCause ());
354     }
355   }
356 
357   /**
358    * Create a new bound schema. All the XPath pre-compilation happens inside
359    * this constructor, so that the
360    * {@link #validate(Node, String, IPSValidationHandler)} method can be called
361    * many times without compiling the XPath statements again and again.
362    *
363    * @param aQueryBinding
364    *        The query binding to be used. May not be <code>null</code>.
365    * @param aOrigSchema
366    *        The original schema that should be bound. May not be
367    *        <code>null</code>.
368    * @param sPhase
369    *        The selected phase. May be <code>null</code> indicating that the
370    *        default phase of the schema should be used (if present) or all
371    *        patterns should be evaluated if no default phase is present.
372    * @param aCustomErrorListener
373    *        A custom error listener to be used. May be <code>null</code> in
374    *        which case a
375    *        {@link com.helger.schematron.pure.errorhandler.LoggingPSErrorHandler}
376    *        is used internally.
377    * @param aXPathVariableResolver
378    *        Custom XPath variable resolver. May be <code>null</code>.
379    * @param aXPathFunctionResolver
380    *        Custom XPath function resolver. May be <code>null</code>.
381    * @throws SchematronBindException
382    *         In case XPath expressions are incorrect and pre-compilation fails
383    */
384   public PSXPathBoundSchema (@Nonnull final IPSQueryBinding aQueryBinding,
385                              @Nonnull final PSSchema aOrigSchema,
386                              @Nullable final String sPhase,
387                              @Nullable final IPSErrorHandler aCustomErrorListener,
388                              @Nullable final XPathVariableResolver aXPathVariableResolver,
389                              @Nullable final XPathFunctionResolver aXPathFunctionResolver) throws SchematronBindException
390   {
391     super (aQueryBinding, aOrigSchema, sPhase, aCustomErrorListener);
392     m_aXPathVariableResolver = aXPathVariableResolver;
393     m_aXPathFunctionResolver = aXPathFunctionResolver;
394     m_aXPathFactory = createXPathFactorySaxonFirst ();
395   }
396 
397   @Nonnull
398   private XPath _createXPathContext ()
399   {
400     final MapBasedNamespaceContext aNamespaceContext = getNamespaceContext ();
401     final XPath aXPathContext = XPathHelper.createNewXPath (m_aXPathFactory,
402                                                             m_aXPathVariableResolver,
403                                                             m_aXPathFunctionResolver,
404                                                             aNamespaceContext);
405 
406     if ("net.sf.saxon.xpath.XPathEvaluator".equals (aXPathContext.getClass ().getName ()))
407     {
408       // Saxon implementation special handling
409       final XPathEvaluator aSaxonXPath = (XPathEvaluator) aXPathContext;
410 
411       // Since 9.7.0-4 it must implement NamespaceResolver
412       aSaxonXPath.setNamespaceContext (new SaxonNamespaceContext (aNamespaceContext));
413 
414       // Wrap the PSErrorHandler to a ErrorListener
415       aSaxonXPath.getConfiguration ().setErrorListener (new PSErrorListener (getErrorHandler ()));
416     }
417     return aXPathContext;
418   }
419 
420   @Nonnull
421   public PSXPathBoundSchema bind () throws SchematronBindException
422   {
423     if (LOGGER.isDebugEnabled ())
424       LOGGER.debug ("Binding pure Schematron");
425 
426     if (m_aBoundPatterns != null)
427       throw new IllegalStateException ("bind must only be called once!");
428 
429     final PSSchema aSchema = getOriginalSchema ();
430     final PSPhase aPhase = getPhase ();
431 
432     // Get all "global" variables that are defined in the schema
433     final PSXPathVariablesSXPathVariables.html#PSXPathVariables">PSXPathVariables aGlobalVariables = new PSXPathVariables ();
434     if (aSchema.hasAnyLet ())
435       for (final Map.Entry <String, String> aEntry : aSchema.getAllLetsAsMap ().entrySet ())
436         if (aGlobalVariables.add (aEntry).isUnchanged ())
437           error (aSchema, "Duplicate <let> with name '" + aEntry.getKey () + "' in global <schema>");
438 
439     if (aPhase != null)
440     {
441       // Get all variables that are defined in the specified phase
442       for (final Map.Entry <String, String> aEntry : aPhase.getAllLetsAsMap ().entrySet ())
443         if (aGlobalVariables.add (aEntry).isUnchanged ())
444           error (aSchema,
445                  "Duplicate <let> with name '" + aEntry.getKey () + "' in <phase> with name '" + getPhaseID () + "'");
446     }
447 
448     final XPath aXPathContext = _createXPathContext ();
449 
450     // Pre-compile all diagnostics first
451     final ICommonsMap <String, PSXPathBoundDiagnostic> aBoundDiagnostics = _createBoundDiagnostics (aXPathContext,
452                                                                                                     aGlobalVariables);
453     if (aBoundDiagnostics == null)
454       throw new SchematronBindException ("Failed to precompile the diagnostics of the supplied schema. Check the " +
455                                          (isDefaultErrorHandler () ? "log output" : "error listener") +
456                                          " for XPath errors!");
457 
458     // Perform the pre-compilation of all XPath expressions in the patterns,
459     // rules, asserts/reports and the content elements
460     m_aBoundPatterns = _createBoundPatterns (aXPathContext, aBoundDiagnostics, aGlobalVariables);
461     if (m_aBoundPatterns == null)
462       throw new SchematronBindException ("Failed to precompile the supplied schema.");
463     return this;
464   }
465 
466   @Nullable
467   public XPathVariableResolver getXPathVariableResolver ()
468   {
469     return m_aXPathVariableResolver;
470   }
471 
472   @Nullable
473   public XPathFunctionResolver getXPathFunctionResolver ()
474   {
475     return m_aXPathFunctionResolver;
476   }
477 
478   @Nonnull
479   public String getValidationContext (@Nonnull final String sRuleContext)
480   {
481     // Do we already have an absolute XPath?
482     if (sRuleContext.startsWith ("/"))
483       return sRuleContext;
484 
485     // Create an absolute XPath expression!
486     return "//" + sRuleContext;
487   }
488 
489   public void validate (@Nonnull final Node aNode,
490                         @Nullable final String sBaseURI,
491                         @Nonnull final IPSValidationHandler aValidationHandler) throws SchematronValidationException
492   {
493     ValueEnforcer.notNull (aNode, "Node");
494     ValueEnforcer.notNull (aValidationHandler, "ValidationHandler");
495 
496     if (m_aBoundPatterns == null)
497       throw new IllegalStateException ("bind was never called!");
498 
499     final PSSchema aSchema = getOriginalSchema ();
500     final PSPhase aPhase = getPhase ();
501 
502     // Call the "start" callback method
503     aValidationHandler.onStart (aSchema, aPhase, sBaseURI);
504 
505     // For all bound patterns
506     for (final PSXPathBoundPattern aBoundPattern : m_aBoundPatterns)
507     {
508       final PSPattern aPattern = aBoundPattern.getPattern ();
509       aValidationHandler.onPattern (aPattern);
510 
511       // For all bound rules
512       rules: for (final PSXPathBoundRule aBoundRule : aBoundPattern.getAllBoundRules ())
513       {
514         final PSRule aRule = aBoundRule.getRule ();
515 
516         // Find all nodes matching the rules
517         NodeList aRuleMatchingNodes = null;
518         try
519         {
520           aRuleMatchingNodes = XPathEvaluationHelper.evaluate (aBoundRule.getBoundRuleExpression (),
521                                                                aNode,
522                                                                XPathConstants.NODESET,
523                                                                sBaseURI);
524         }
525         catch (final XPathExpressionException ex)
526         {
527           // Handle the cause, because it is usually a wrapper only
528           error (aRule,
529                  "Failed to evaluate XPath expression to a nodeset: '" + aBoundRule.getRuleExpression () + "'",
530                  ex.getCause () != null ? ex.getCause () : ex);
531           continue rules;
532         }
533 
534         final int nRuleMatchingNodes = aRuleMatchingNodes.getLength ();
535         if (nRuleMatchingNodes > 0)
536         {
537           // For all contained assert and report elements
538           for (final PSXPathBoundAssertReport aBoundAssertReport : aBoundRule.getAllBoundAssertReports ())
539           {
540             // XSLT does "fired-rule" for each node
541             aValidationHandler.onRule (aRule, aBoundRule.getRuleExpression ());
542 
543             final PSAssertReport aAssertReport = aBoundAssertReport.getAssertReport ();
544             final boolean bIsAssert = aAssertReport.isAssert ();
545             final XPathExpression aTestExpression = aBoundAssertReport.getBoundTestExpression ();
546 
547             // Check each node, if it matches the assert/report
548             for (int i = 0; i < nRuleMatchingNodes; ++i)
549             {
550               final Node aRuleMatchingNode = aRuleMatchingNodes.item (i);
551               try
552               {
553                 final boolean bTestResult = ((Boolean) XPathEvaluationHelper.evaluate (aTestExpression,
554                                                                                        aRuleMatchingNode,
555                                                                                        XPathConstants.BOOLEAN,
556                                                                                        sBaseURI)).booleanValue ();
557                 if (bIsAssert)
558                 {
559                   // It's an assert
560                   if (!bTestResult)
561                   {
562                     // Assert failed
563                     if (aValidationHandler.onFailedAssert (aAssertReport,
564                                                            aBoundAssertReport.getTestExpression (),
565                                                            aRuleMatchingNode,
566                                                            i,
567                                                            aBoundAssertReport)
568                                           .isBreak ())
569                     {
570                       return;
571                     }
572                   }
573                 }
574                 else
575                 {
576                   // It's a report
577                   if (bTestResult)
578                   {
579                     // Successful report
580                     if (aValidationHandler.onSuccessfulReport (aAssertReport,
581                                                                aBoundAssertReport.getTestExpression (),
582                                                                aRuleMatchingNode,
583                                                                i,
584                                                                aBoundAssertReport)
585                                           .isBreak ())
586                     {
587                       return;
588                     }
589                   }
590                 }
591               }
592               catch (final XPathExpressionException ex)
593               {
594                 error (aRule,
595                        "Failed to evaluate XPath expression to a boolean: '" +
596                               aBoundAssertReport.getTestExpression () +
597                               "'",
598                        ex.getCause () != null ? ex.getCause () : ex);
599               }
600             }
601           }
602 
603           if (false)
604           {
605             // The rule matched at least one node. In this case continue with
606             // the next pattern
607             break rules;
608           }
609         }
610       }
611     }
612 
613     // Call the "end" callback method
614     aValidationHandler.onEnd (aSchema, aPhase);
615   }
616 
617   @Override
618   public String toString ()
619   {
620     return ToStringGenerator.getDerived (super.toString ()).append ("boundPatterns", m_aBoundPatterns).getToString ();
621   }
622 }