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