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