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