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;
18  
19  import java.io.File;
20  import java.io.InputStream;
21  import java.net.MalformedURLException;
22  import java.net.URL;
23  import java.nio.charset.Charset;
24  
25  import javax.annotation.Nonnull;
26  import javax.annotation.Nullable;
27  import javax.annotation.concurrent.NotThreadSafe;
28  import javax.xml.transform.Source;
29  import javax.xml.xpath.XPathFunctionResolver;
30  import javax.xml.xpath.XPathVariableResolver;
31  
32  import org.oclc.purl.dsdl.svrl.SchematronOutputType;
33  import org.w3c.dom.Document;
34  import org.w3c.dom.Node;
35  
36  import com.helger.commons.ValueEnforcer;
37  import com.helger.commons.annotations.Nonempty;
38  import com.helger.commons.charset.CharsetManager;
39  import com.helger.commons.io.IInputStreamProvider;
40  import com.helger.commons.io.IReadableResource;
41  import com.helger.commons.io.resource.ClassPathResource;
42  import com.helger.commons.io.resource.FileSystemResource;
43  import com.helger.commons.io.resource.URLResource;
44  import com.helger.commons.io.resource.inmemory.AbstractMemoryReadableResource;
45  import com.helger.commons.io.resource.inmemory.ReadableResourceByteArray;
46  import com.helger.commons.io.resource.inmemory.ReadableResourceInputStream;
47  import com.helger.commons.state.EValidity;
48  import com.helger.commons.xml.serialize.DOMReader;
49  import com.helger.commons.xml.serialize.XMLWriterSettings;
50  import com.helger.schematron.AbstractSchematronResource;
51  import com.helger.schematron.SchematronException;
52  import com.helger.schematron.SchematronUtils;
53  import com.helger.schematron.pure.bound.IPSBoundSchema;
54  import com.helger.schematron.pure.bound.PSBoundSchemaCache;
55  import com.helger.schematron.pure.bound.PSBoundSchemaCacheKey;
56  import com.helger.schematron.pure.errorhandler.DoNothingPSErrorHandler;
57  import com.helger.schematron.pure.errorhandler.IPSErrorHandler;
58  import com.helger.schematron.pure.exchange.PSWriter;
59  import com.helger.schematron.pure.model.PSSchema;
60  import com.helger.schematron.svrl.SVRLWriter;
61  
62  /**
63   * A Schematron resource that is not XSLT based but using the pure (native Java)
64   * implementation. This class itself is not thread safe, but the underlying
65   * cache is thread safe. So once you configured this object fully (with all the
66   * setter), it can be considered thread safe.<br>
67   * <b>Important:</b> This class can <u>only</u> handle XPath expressions but no
68   * XSLT functions in Schematron asserts and reports!
69   *
70   * @author Philip Helger
71   */
72  @NotThreadSafe
73  public class SchematronResourcePure extends AbstractSchematronResource
74  {
75    private String m_sPhase;
76    private IPSErrorHandler m_aErrorHandler;
77    private XPathVariableResolver m_aVariableResolver;
78    private XPathFunctionResolver m_aFunctionResolver;
79    // Status var
80    private IPSBoundSchema m_aBoundSchema;
81  
82    public SchematronResourcePure (@Nonnull final IReadableResource aResource)
83    {
84      this (aResource, (String) null, (IPSErrorHandler) null);
85    }
86  
87    public SchematronResourcePure (@Nonnull final IReadableResource aResource,
88                                   @Nullable final String sPhase,
89                                   @Nullable final IPSErrorHandler aErrorHandler)
90    {
91      super (aResource);
92      setPhase (sPhase);
93      setErrorHandler (aErrorHandler);
94    }
95  
96    /**
97     * @return The phase to be used. May be <code>null</code>.
98     */
99    @Nullable
100   public String getPhase ()
101   {
102     return m_sPhase;
103   }
104 
105   /**
106    * Set the Schematron phase to be evaluated. Changing the phase will result in
107    * a newly bound schema!
108    *
109    * @param sPhase
110    *        The name of the phase to use. May be <code>null</code> which means
111    *        all phases.
112    * @return this
113    */
114   @Nonnull
115   public SchematronResourcePure setPhase (@Nullable final String sPhase)
116   {
117     if (m_aBoundSchema != null)
118       throw new IllegalStateException ("Schematron was already bound and can therefore not be altered!");
119     m_sPhase = sPhase;
120     return this;
121   }
122 
123   /**
124    * @return The error handler to be used to bind the schema. May be
125    *         <code>null</code>.
126    */
127   @Nullable
128   public IPSErrorHandler getErrorHandler ()
129   {
130     return m_aErrorHandler;
131   }
132 
133   /**
134    * Set the error handler to be used during binding.
135    *
136    * @param aErrorHandler
137    *        The error handler. May be <code>null</code>.
138    * @return this
139    */
140   @Nonnull
141   public SchematronResourcePure setErrorHandler (@Nullable final IPSErrorHandler aErrorHandler)
142   {
143     if (m_aBoundSchema != null)
144       throw new IllegalStateException ("Schematron was already bound and can therefore not be altered!");
145     m_aErrorHandler = aErrorHandler;
146     return this;
147   }
148 
149   /**
150    * @return The variable resolver to be used. May be <code>null</code>.
151    */
152   @Nullable
153   public XPathVariableResolver getVariableResolver ()
154   {
155     return m_aVariableResolver;
156   }
157 
158   /**
159    * Set the variable resolver to be used in the XPath statements. This can only
160    * be set before the Schematron is bound. If it is already bound an exception
161    * is thrown to indicate the unnecessity of the call.
162    *
163    * @param aVariableResolver
164    *        The variable resolver to set. May be <code>null</code>.
165    * @return this
166    */
167   @Nonnull
168   public SchematronResourcePure setVariableResolver (@Nullable final XPathVariableResolver aVariableResolver)
169   {
170     if (m_aBoundSchema != null)
171       throw new IllegalStateException ("Schematron was already bound and can therefore not be altered!");
172     m_aVariableResolver = aVariableResolver;
173     return this;
174   }
175 
176   /**
177    * @return The function resolver to be used. May be <code>null</code>.
178    */
179   @Nullable
180   public XPathFunctionResolver getFunctionResolver ()
181   {
182     return m_aFunctionResolver;
183   }
184 
185   /**
186    * Set the function resolver to be used in the XPath statements. This can only
187    * be set before the Schematron is bound. If it is already bound an exception
188    * is thrown to indicate the unnecessity of the call.
189    *
190    * @param aFunctionResolver
191    *        The function resolver to set. May be <code>null</code>.
192    * @return this
193    */
194   @Nonnull
195   public SchematronResourcePure setFunctionResolver (@Nullable final XPathFunctionResolver aFunctionResolver)
196   {
197     if (m_aBoundSchema != null)
198       throw new IllegalStateException ("Schematron was already bound and can therefore not be altered!");
199     m_aFunctionResolver = aFunctionResolver;
200     return this;
201   }
202 
203   @Nonnull
204   protected IPSBoundSchema createBoundSchema ()
205   {
206     final IReadableResource aResource = getResource ();
207     final IPSErrorHandler aErrorHandler = getErrorHandler ();
208     final PSBoundSchemaCacheKey aCacheKey = new PSBoundSchemaCacheKey (aResource,
209                                                                        getPhase (),
210                                                                        aErrorHandler,
211                                                                        getVariableResolver (),
212                                                                        getFunctionResolver ());
213     if (aResource instanceof AbstractMemoryReadableResource)
214     {
215       // No need to cache anything for memory resources
216       try
217       {
218         return aCacheKey.createBoundSchema ();
219       }
220       catch (final SchematronException ex)
221       {
222         // Convert to runtime exception
223         throw new IllegalStateException ("Failed to bind Schematron", ex);
224       }
225     }
226 
227     // Resolve from cache - inside the cacheKey the reading and binding
228     // happens
229     return PSBoundSchemaCache.getInstance ().getFromCache (aCacheKey);
230   }
231 
232   @Nonnull
233   protected IPSBoundSchema getOrCreateBoundSchema ()
234   {
235     if (m_aBoundSchema == null)
236       m_aBoundSchema = createBoundSchema ();
237     return m_aBoundSchema;
238   }
239 
240   public boolean isValidSchematron ()
241   {
242     try
243     {
244       // Use the provided error handler (if any)
245       final IPSErrorHandler aErrorHandler = m_aErrorHandler != null ? m_aErrorHandler
246                                                                    : DoNothingPSErrorHandler.getInstance ();
247       return getOrCreateBoundSchema ().getOriginalSchema ().isValid (aErrorHandler);
248     }
249     catch (final RuntimeException ex)
250     {
251       // May happen when XPath errors are contained
252       return false;
253     }
254   }
255 
256   /**
257    * Use the internal error handler to validate all elements in the schematron.
258    * It tries to catch as many errors as possible.
259    */
260   public void validateCompletely ()
261   {
262     // Use the provided error handler (if any)
263     final IPSErrorHandler aErrorHandler = m_aErrorHandler != null ? m_aErrorHandler
264                                                                  : DoNothingPSErrorHandler.getInstance ();
265     validateCompletely (aErrorHandler);
266   }
267 
268   /**
269    * Use the provided error handler to validate all elements in the schematron.
270    * It tries to catch as many errors as possible.
271    *
272    * @param aErrorHandler
273    *        The error handler to use. May not be <code>null</code>.
274    */
275   public void validateCompletely (@Nonnull final IPSErrorHandler aErrorHandler)
276   {
277     ValueEnforcer.notNull (aErrorHandler, "ErrorHandler");
278 
279     try
280     {
281       getOrCreateBoundSchema ().getOriginalSchema ().validateCompletely (aErrorHandler);
282     }
283     catch (final RuntimeException ex)
284     {
285       // May happen when XPath errors are contained
286     }
287   }
288 
289   /**
290    * The main method to convert a node to an SVRL document.
291    *
292    * @param aXMLNode
293    *        The source node to be validated. May not be <code>null</code>.
294    * @return The SVRL document. Never <code>null</code>.
295    * @throws SchematronException
296    *         in case of a sever error validating the schema
297    * @deprecated Use {@link #applySchematronValidationToSVRL(Node)} instead
298    */
299   @Deprecated
300   @Nonnull
301   public SchematronOutputType applySchematronValidation (@Nonnull final Node aXMLNode) throws SchematronException
302   {
303     return applySchematronValidationToSVRL (aXMLNode);
304   }
305 
306   /**
307    * The main method to convert a node to an SVRL document.
308    *
309    * @param aXMLNode
310    *        The source node to be validated. May not be <code>null</code>.
311    * @return The SVRL document. Never <code>null</code>.
312    * @throws SchematronException
313    *         in case of a sever error validating the schema
314    */
315   @Nonnull
316   public SchematronOutputType applySchematronValidationToSVRL (@Nonnull final Node aXMLNode) throws SchematronException
317   {
318     return getOrCreateBoundSchema ().validateComplete (aXMLNode);
319   }
320 
321   @Nonnull
322   public EValidity getSchematronValidity (@Nonnull final IInputStreamProvider aXMLResource) throws Exception
323   {
324     if (!isValidSchematron ())
325       return EValidity.INVALID;
326 
327     final Document aDoc = DOMReader.readXMLDOM (aXMLResource.getInputStream ());
328     if (aDoc == null)
329       throw new IllegalArgumentException ("Failed to read resource " + aXMLResource + " as XML");
330 
331     return getOrCreateBoundSchema ().validatePartially (aDoc);
332   }
333 
334   @Nonnull
335   public EValidity getSchematronValidity (@Nonnull final Source aXMLSource) throws Exception
336   {
337     if (!isValidSchematron ())
338       return EValidity.INVALID;
339 
340     // Convert Source to Node
341     final Node aNode = SchematronUtils.getNodeOfSource (aXMLSource);
342     if (aNode == null)
343       return EValidity.INVALID;
344 
345     return getOrCreateBoundSchema ().validatePartially (aNode);
346   }
347 
348   @Nullable
349   public Document applySchematronValidation (@Nonnull final IInputStreamProvider aXMLResource) throws Exception
350   {
351     final SchematronOutputType aSO = applySchematronValidationToSVRL (aXMLResource);
352     return aSO == null ? null : SVRLWriter.createXML (aSO);
353   }
354 
355   @Nullable
356   public Document applySchematronValidation (@Nonnull final Source aXMLSource) throws Exception
357   {
358     final SchematronOutputType aSO = applySchematronValidationToSVRL (aXMLSource);
359     return aSO == null ? null : SVRLWriter.createXML (aSO);
360   }
361 
362   @Nullable
363   public SchematronOutputType applySchematronValidationToSVRL (@Nonnull final IInputStreamProvider aXMLResource) throws Exception
364   {
365     ValueEnforcer.notNull (aXMLResource, "XMLResource");
366 
367     if (!isValidSchematron ())
368       return null;
369 
370     final InputStream aIS = aXMLResource.getInputStream ();
371     if (aIS == null)
372       return null;
373 
374     final Document aDoc = DOMReader.readXMLDOM (aIS);
375     if (aDoc == null)
376       throw new IllegalArgumentException ("Failed to read resource " + aXMLResource + " as XML");
377 
378     return applySchematronValidationToSVRL (aDoc);
379   }
380 
381   @Nullable
382   public SchematronOutputType applySchematronValidationToSVRL (@Nonnull final Source aXMLSource) throws Exception
383   {
384     ValueEnforcer.notNull (aXMLSource, "XMLSource");
385 
386     if (!isValidSchematron ())
387       return null;
388 
389     // Convert to Node
390     final Node aNode = SchematronUtils.getNodeOfSource (aXMLSource);
391     if (aNode == null)
392       return null;
393 
394     return applySchematronValidationToSVRL (aNode);
395   }
396 
397   /**
398    * Create a new {@link SchematronResourcePure} from a Classpath Schematron
399    * rules
400    *
401    * @param sSCHPath
402    *        The classpath relative path to the Schematron rules.
403    * @return Never <code>null</code>.
404    */
405   @Nonnull
406   public static SchematronResourcePure fromClassPath (@Nonnull @Nonempty final String sSCHPath)
407   {
408     return new SchematronResourcePure (new ClassPathResource (sSCHPath));
409   }
410 
411   /**
412    * Create a new {@link SchematronResourcePure} from file system Schematron
413    * rules
414    *
415    * @param sSCHPath
416    *        The file system path to the Schematron rules.
417    * @return Never <code>null</code>.
418    */
419   @Nonnull
420   public static SchematronResourcePure fromFile (@Nonnull @Nonempty final String sSCHPath)
421   {
422     return new SchematronResourcePure (new FileSystemResource (sSCHPath));
423   }
424 
425   /**
426    * Create a new {@link SchematronResourcePure} from file system Schematron
427    * rules
428    *
429    * @param aSCHFile
430    *        The file system path to the Schematron rules.
431    * @return Never <code>null</code>.
432    */
433   @Nonnull
434   public static SchematronResourcePure fromFile (@Nonnull final File aSCHFile)
435   {
436     return new SchematronResourcePure (new FileSystemResource (aSCHFile));
437   }
438 
439   /**
440    * Create a new {@link SchematronResourcePure} from Schematron rules provided
441    * at a URL
442    *
443    * @param sSCHURL
444    *        The URL to the Schematron rules. May neither be <code>null</code>
445    *        nor empty.
446    * @return Never <code>null</code>.
447    * @throws MalformedURLException
448    *         In case an invalid URL is provided
449    */
450   @Nonnull
451   public static SchematronResourcePure fromURL (@Nonnull @Nonempty final String sSCHURL) throws MalformedURLException
452   {
453     return new SchematronResourcePure (new URLResource (sSCHURL));
454   }
455 
456   /**
457    * Create a new {@link SchematronResourcePure} from Schematron rules provided
458    * at a URL
459    *
460    * @param aSCHURL
461    *        The URL to the Schematron rules. May not be <code>null</code>.
462    * @return Never <code>null</code>.
463    */
464   @Nonnull
465   public static SchematronResourcePure fromURL (@Nonnull final URL aSCHURL)
466   {
467     return new SchematronResourcePure (new URLResource (aSCHURL));
468   }
469 
470   /**
471    * Create a new {@link SchematronResourcePure} from Schematron rules provided
472    * by an arbitrary {@link InputStream}.<br>
473    * <b>Important:</b> in this case, no include resolution will be performed!!
474    *
475    * @param aSchematronIS
476    *        The {@link InputStream} to read the Schematron rules from. May not
477    *        be <code>null</code>.
478    * @return Never <code>null</code>.
479    */
480   @Nonnull
481   public static SchematronResourcePure fromInputStream (@Nonnull final InputStream aSchematronIS)
482   {
483     return new SchematronResourcePure (new ReadableResourceInputStream (aSchematronIS));
484   }
485 
486   /**
487    * Create a new {@link SchematronResourcePure} from Schematron rules provided
488    * by an arbitrary byte array.<br>
489    * <b>Important:</b> in this case, no include resolution will be performed!!
490    *
491    * @param aSchematron
492    *        The byte array representing the Schematron. May not be
493    *        <code>null</code>.
494    * @return Never <code>null</code>.
495    */
496   @Nonnull
497   public static SchematronResourcePure fromByteArray (@Nonnull final byte [] aSchematron)
498   {
499     return new SchematronResourcePure (new ReadableResourceByteArray (aSchematron));
500   }
501 
502   /**
503    * Create a new {@link SchematronResourcePure} from Schematron rules provided
504    * by an arbitrary String.<br>
505    * <b>Important:</b> in this case, no include resolution will be performed!!
506    *
507    * @param sSchematron
508    *        The String representing the Schematron. May not be <code>null</code>
509    *        .
510    * @param aCharset
511    *        The charset to be used to convert the String to a byte array.
512    * @return Never <code>null</code>.
513    */
514   @Nonnull
515   public static SchematronResourcePure fromString (@Nonnull final String sSchematron, @Nonnull final Charset aCharset)
516   {
517     return fromByteArray (CharsetManager.getAsBytes (sSchematron, aCharset));
518   }
519 
520   /**
521    * Create a new {@link SchematronResourcePure} from Schematron rules provided
522    * by a domain model.<br>
523    * <b>Important:</b> in this case, no include resolution will be performed!!
524    *
525    * @param aSchematron
526    *        The Schematron model to be used. May not be <code>null</code> .
527    * @return Never <code>null</code>.
528    */
529   @Nonnull
530   public static SchematronResourcePure fromSchema (@Nonnull final PSSchema aSchematron)
531   {
532     return fromString (new PSWriter ().getXMLString (aSchematron), XMLWriterSettings.DEFAULT_XML_CHARSET_OBJ);
533   }
534 }