View Javadoc
1   /**
2    * Copyright (C) 2014-2015 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.validation.xpath;
18  
19  import java.util.List;
20  import java.util.Map;
21  
22  import javax.annotation.Nonnull;
23  import javax.annotation.Nullable;
24  import javax.annotation.concurrent.NotThreadSafe;
25  import javax.xml.xpath.XPathConstants;
26  import javax.xml.xpath.XPathExpressionException;
27  
28  import org.oclc.purl.dsdl.svrl.ActivePattern;
29  import org.oclc.purl.dsdl.svrl.DiagnosticReference;
30  import org.oclc.purl.dsdl.svrl.FailedAssert;
31  import org.oclc.purl.dsdl.svrl.FiredRule;
32  import org.oclc.purl.dsdl.svrl.NsPrefixInAttributeValues;
33  import org.oclc.purl.dsdl.svrl.SchematronOutputType;
34  import org.oclc.purl.dsdl.svrl.SuccessfulReport;
35  import org.w3c.dom.Node;
36  
37  import com.helger.commons.ValueEnforcer;
38  import com.helger.commons.collection.CollectionHelper;
39  import com.helger.commons.state.EContinue;
40  import com.helger.commons.xml.XMLHelper;
41  import com.helger.schematron.pure.bound.xpath.PSXPathBoundAssertReport;
42  import com.helger.schematron.pure.bound.xpath.PSXPathBoundDiagnostic;
43  import com.helger.schematron.pure.bound.xpath.PSXPathBoundElement;
44  import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
45  import com.helger.schematron.pure.model.IPSElement;
46  import com.helger.schematron.pure.model.PSAssertReport;
47  import com.helger.schematron.pure.model.PSDiagnostics;
48  import com.helger.schematron.pure.model.PSDir;
49  import com.helger.schematron.pure.model.PSEmph;
50  import com.helger.schematron.pure.model.PSName;
51  import com.helger.schematron.pure.model.PSPattern;
52  import com.helger.schematron.pure.model.PSPhase;
53  import com.helger.schematron.pure.model.PSRule;
54  import com.helger.schematron.pure.model.PSSchema;
55  import com.helger.schematron.pure.model.PSSpan;
56  import com.helger.schematron.pure.model.PSTitle;
57  import com.helger.schematron.pure.model.PSValueOf;
58  import com.helger.schematron.pure.validation.PSValidationHandlerDefault;
59  import com.helger.schematron.pure.validation.SchematronValidationException;
60  
61  /**
62   * A special validation handler that creates an SVRL document. This class only
63   * works for the XPath binding, as the special {@link PSXPathBoundAssertReport}
64   * class is referenced!
65   *
66   * @author Philip Helger
67   */
68  @NotThreadSafe
69  public class PSXPathValidationHandlerSVRL extends PSValidationHandlerDefault
70  {
71    private final IPSErrorHandler m_aErrorHandler;
72    private SchematronOutputType m_aSchematronOutput;
73    private PSSchema m_aSchema;
74  
75    /**
76     * Constructor
77     *
78     * @param aErrorHandler
79     *        The error handler to be used. May not be <code>null</code>.
80     */
81    public PSXPathValidationHandlerSVRL (@Nonnull final IPSErrorHandler aErrorHandler)
82    {
83      ValueEnforcer.notNull (aErrorHandler, "ErrorHandler");
84      m_aErrorHandler = aErrorHandler;
85    }
86  
87    private void _warn (@Nonnull final IPSElement aSourceElement, @Nonnull final String sMsg)
88    {
89      if (m_aSchema == null)
90        throw new IllegalStateException ("No schema is present!");
91  
92      m_aErrorHandler.warn (m_aSchema.getResource (), aSourceElement, sMsg);
93    }
94  
95    private void _error (@Nonnull final IPSElement aSourceElement, @Nonnull final String sMsg, @Nullable final Throwable t)
96    {
97      if (m_aSchema == null)
98        throw new IllegalStateException ("No schema is present!");
99  
100     m_aErrorHandler.error (m_aSchema.getResource (), aSourceElement, sMsg, t);
101   }
102 
103   @Nullable
104   private static String _getTitleAsString (@Nullable final PSTitle aTitle) throws SchematronValidationException
105   {
106     if (aTitle == null)
107       return null;
108 
109     final StringBuilder aSB = new StringBuilder ();
110     for (final Object aContent : aTitle.getAllContentElements ())
111     {
112       if (aContent instanceof String)
113         aSB.append ((String) aContent);
114       else
115         if (aContent instanceof PSDir)
116           aSB.append (((PSDir) aContent).getAsText ());
117         else
118           throw new SchematronValidationException ("Unsupported title content element: " + aContent);
119     }
120     return aSB.toString ();
121   }
122 
123   @Override
124   public void onStart (@Nonnull final PSSchema aSchema, @Nullable final PSPhase aActivePhase) throws SchematronValidationException
125   {
126     final SchematronOutputType aSchematronOutput = new SchematronOutputType ();
127     if (aActivePhase != null)
128       aSchematronOutput.setPhase (aActivePhase.getID ());
129     aSchematronOutput.setSchemaVersion (aSchema.getSchemaVersion ());
130     aSchematronOutput.setTitle (_getTitleAsString (aSchema.getTitle ()));
131 
132     // Add namespace prefixes
133     for (final Map.Entry <String, String> aEntry : aSchema.getAsNamespaceContext ()
134                                                           .getPrefixToNamespaceURIMap ()
135                                                           .entrySet ())
136     {
137       final NsPrefixInAttributeValues aNsPrefix = new NsPrefixInAttributeValues ();
138       aNsPrefix.setPrefix (aEntry.getKey ());
139       aNsPrefix.setUri (aEntry.getValue ());
140       aSchematronOutput.getNsPrefixInAttributeValues ().add (aNsPrefix);
141     }
142     m_aSchematronOutput = aSchematronOutput;
143     m_aSchema = aSchema;
144   }
145 
146   @Override
147   public void onPattern (@Nonnull final PSPattern aPattern)
148   {
149     final ActivePattern aRetPattern = new ActivePattern ();
150     // TODO document
151     aRetPattern.setId (aPattern.getID ());
152     // TODO name
153     // TODO role
154     m_aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ().add (aRetPattern);
155   }
156 
157   @Override
158   public void onRule (@Nonnull final PSRule aRule, @Nonnull final String sContext)
159   {
160     final FiredRule aRetRule = new FiredRule ();
161     aRetRule.setContext (sContext);
162     aRetRule.setFlag (aRule.getFlag ());
163     aRetRule.setId (aRule.getID ());
164     // TODO role
165     m_aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ().add (aRetRule);
166   }
167 
168   /**
169    * Get the error text from an assert or report element.
170    *
171    * @param aBoundContentElements
172    *        The list of bound elements to be evaluated.
173    * @param aSourceNode
174    *        The XML node of the document currently validated.
175    * @return A non-<code>null</code> String
176    * @throws SchematronValidationException
177    *         In case evaluating an XPath expression fails.
178    */
179   @Nonnull
180   private String _getErrorText (@Nonnull final List <PSXPathBoundElement> aBoundContentElements,
181                                 @Nonnull final Node aSourceNode) throws SchematronValidationException
182   {
183     final StringBuilder aSB = new StringBuilder ();
184     for (final PSXPathBoundElement aBoundElement : aBoundContentElements)
185     {
186       final Object aContent = aBoundElement.getElement ();
187       if (aContent instanceof String)
188         aSB.append ((String) aContent);
189       else
190         if (aContent instanceof PSName)
191         {
192           final PSName aName = (PSName) aContent;
193           if (aName.hasPath ())
194           {
195             // XPath present
196             try
197             {
198               aSB.append ((String) aBoundElement.getBoundExpression ().evaluate (aSourceNode, XPathConstants.STRING));
199             }
200             catch (final XPathExpressionException ex)
201             {
202               _error (aName, "Failed to evaluate XPath expression to a string: '" +
203                              aBoundElement.getExpression () +
204                              "'", ex);
205               // Append the path so that something is present in the output
206               aSB.append (aName.getPath ());
207             }
208           }
209           else
210           {
211             // No XPath present
212             aSB.append (aSourceNode.getNodeName ());
213           }
214         }
215         else
216           if (aContent instanceof PSValueOf)
217           {
218             final PSValueOf aValueOf = (PSValueOf) aContent;
219             try
220             {
221               aSB.append ((String) aBoundElement.getBoundExpression ().evaluate (aSourceNode, XPathConstants.STRING));
222             }
223             catch (final XPathExpressionException ex)
224             {
225               _error (aValueOf, "Failed to evaluate XPath expression to a string: '" +
226                                 aBoundElement.getExpression () +
227                                 "'", ex);
228               // Append the path so that something is present in the output
229               aSB.append (aValueOf.getSelect ());
230             }
231           }
232           else
233             if (aContent instanceof PSEmph)
234               aSB.append (((PSEmph) aContent).getAsText ());
235             else
236               if (aContent instanceof PSDir)
237                 aSB.append (((PSDir) aContent).getAsText ());
238               else
239                 if (aContent instanceof PSSpan)
240                   aSB.append (((PSSpan) aContent).getAsText ());
241                 else
242                   throw new SchematronValidationException ("Unsupported assert/report content element: " + aContent);
243     }
244     return aSB.toString ();
245   }
246 
247   /**
248    * Handle the diagnostic references of a single assert/report element
249    *
250    * @param aSrcDiagnostics
251    *        The list of diagnostic reference IDs in the source assert/report
252    *        element. May be <code>null</code> if no diagnostic references are
253    *        present
254    * @param aDstList
255    *        The diagnostic reference list of the SchematronOutput to be filled.
256    *        May not be <code>null</code>.
257    * @param aBoundAssertReport
258    *        The bound assert report element. Never <code>null</code>.
259    * @param aRuleMatchingNode
260    *        The XML node of the XML document currently validated. Never
261    *        <code>null</code>.
262    * @throws SchematronValidationException
263    */
264   private void _handleDiagnosticReferences (@Nullable final List <String> aSrcDiagnostics,
265                                             @Nonnull final List <DiagnosticReference> aDstList,
266                                             @Nonnull final PSXPathBoundAssertReport aBoundAssertReport,
267                                             @Nonnull final Node aRuleMatchingNode) throws SchematronValidationException
268   {
269     if (CollectionHelper.isNotEmpty (aSrcDiagnostics))
270     {
271       if (m_aSchema.hasDiagnostics ())
272       {
273         final PSDiagnostics aDiagnostics = m_aSchema.getDiagnostics ();
274         for (final String sDiagnosticID : aSrcDiagnostics)
275         {
276           final PSXPathBoundDiagnostic aDiagnostic = aBoundAssertReport.getBoundDiagnosticOfID (sDiagnosticID);
277           if (aDiagnostic == null)
278             _warn (aDiagnostics, "Failed to resolve diagnostics with ID '" + sDiagnosticID + "'");
279           else
280           {
281             // Create the SVRL diagnostic-reference element
282             final DiagnosticReference aDR = new DiagnosticReference ();
283             aDR.setDiagnostic (sDiagnosticID);
284             aDR.setText (_getErrorText (aDiagnostic.getAllBoundContentElements (), aRuleMatchingNode));
285             aDstList.add (aDR);
286           }
287         }
288       }
289       else
290         _warn (m_aSchema, "Failed to resolve diagnostic because schema has no diagnostics");
291     }
292   }
293 
294   @Override
295   @Nonnull
296   public EContinue onFailedAssert (@Nonnull final PSAssertReport aAssertReport,
297                                    @Nonnull final String sTestExpression,
298                                    @Nonnull final Node aRuleMatchingNode,
299                                    final int nNodeIndex,
300                                    @Nullable final Object aContext) throws SchematronValidationException
301   {
302     if (!(aContext instanceof PSXPathBoundAssertReport))
303       throw new SchematronValidationException ("The passed context must be an XPath object but is a " + aContext);
304     final PSXPathBoundAssertReport aBoundAssertReport = (PSXPathBoundAssertReport) aContext;
305 
306     final FailedAssert aFailedAssert = new FailedAssert ();
307     aFailedAssert.setFlag (aAssertReport.getFlag ());
308     aFailedAssert.setId (aAssertReport.getID ());
309     aFailedAssert.setLocation (XMLHelper.getPathToNode2 (aRuleMatchingNode));
310     // TODO role
311     aFailedAssert.setTest (sTestExpression);
312     aFailedAssert.setText (_getErrorText (aBoundAssertReport.getAllBoundContentElements (), aRuleMatchingNode));
313     _handleDiagnosticReferences (aAssertReport.getAllDiagnostics (),
314                                  aFailedAssert.getDiagnosticReference (),
315                                  aBoundAssertReport,
316                                  aRuleMatchingNode);
317     m_aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ().add (aFailedAssert);
318     return EContinue.CONTINUE;
319   }
320 
321   @Override
322   @Nonnull
323   public EContinue onSuccessfulReport (@Nonnull final PSAssertReport aAssertReport,
324                                        @Nonnull final String sTestExpression,
325                                        @Nonnull final Node aRuleMatchingNode,
326                                        final int nNodeIndex,
327                                        @Nullable final Object aContext) throws SchematronValidationException
328   {
329     if (!(aContext instanceof PSXPathBoundAssertReport))
330       throw new SchematronValidationException ("The passed context must be an XPath object but is a " + aContext);
331     final PSXPathBoundAssertReport aBoundAssertReport = (PSXPathBoundAssertReport) aContext;
332 
333     final SuccessfulReport aSuccessfulReport = new SuccessfulReport ();
334     aSuccessfulReport.setFlag (aAssertReport.getFlag ());
335     aSuccessfulReport.setId (aAssertReport.getID ());
336     aSuccessfulReport.setLocation (XMLHelper.getPathToNode2 (aRuleMatchingNode));
337     // TODO role
338     aSuccessfulReport.setTest (sTestExpression);
339     aSuccessfulReport.setText (_getErrorText (aBoundAssertReport.getAllBoundContentElements (), aRuleMatchingNode));
340     _handleDiagnosticReferences (aAssertReport.getAllDiagnostics (),
341                                  aSuccessfulReport.getDiagnosticReference (),
342                                  aBoundAssertReport,
343                                  aRuleMatchingNode);
344     m_aSchematronOutput.getActivePatternAndFiredRuleAndFailedAssert ().add (aSuccessfulReport);
345     return EContinue.CONTINUE;
346   }
347 
348   @Nullable
349   public SchematronOutputType getSVRL ()
350   {
351     return m_aSchematronOutput;
352   }
353 }