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.xslt;
18  
19  import java.net.URL;
20  import java.util.Locale;
21  import java.util.Map;
22  
23  import javax.annotation.Nonnull;
24  import javax.annotation.Nullable;
25  import javax.annotation.concurrent.NotThreadSafe;
26  import javax.xml.transform.ErrorListener;
27  import javax.xml.transform.Transformer;
28  import javax.xml.transform.TransformerException;
29  import javax.xml.transform.URIResolver;
30  import javax.xml.transform.dom.DOMResult;
31  import javax.xml.transform.dom.DOMSource;
32  
33  import org.oclc.purl.dsdl.svrl.SchematronOutputType;
34  import org.slf4j.Logger;
35  import org.slf4j.LoggerFactory;
36  import org.w3c.dom.Document;
37  import org.w3c.dom.Node;
38  import org.xml.sax.EntityResolver;
39  
40  import com.helger.commons.ValueEnforcer;
41  import com.helger.commons.annotation.ReturnsMutableCopy;
42  import com.helger.commons.annotation.ReturnsMutableObject;
43  import com.helger.commons.collection.impl.CommonsLinkedHashMap;
44  import com.helger.commons.collection.impl.ICommonsOrderedMap;
45  import com.helger.commons.io.resource.IReadableResource;
46  import com.helger.commons.state.EValidity;
47  import com.helger.commons.string.ToStringGenerator;
48  import com.helger.commons.traits.IGenericImplTrait;
49  import com.helger.schematron.AbstractSchematronResource;
50  import com.helger.schematron.SchematronDebug;
51  import com.helger.schematron.svrl.SVRLMarshaller;
52  import com.helger.schematron.xslt.validator.ISchematronXSLTValidator;
53  import com.helger.schematron.xslt.validator.SchematronXSLTValidatorDefault;
54  import com.helger.xml.XMLFactory;
55  import com.helger.xml.serialize.write.XMLWriter;
56  import com.helger.xml.transform.DefaultTransformURIResolver;
57  import com.helger.xml.transform.LoggingTransformErrorListener;
58  
59  import net.sf.saxon.jaxp.TransformerImpl;
60  import net.sf.saxon.lib.StandardLogger;
61  import net.sf.saxon.s9api.XsltTransformer;
62  import net.sf.saxon.trace.TraceEventMulticaster;
63  import net.sf.saxon.trace.XSLTTraceListener;
64  
65  /**
66   * Abstract implementation of a Schematron resource that is based on XSLT
67   * transformations.
68   *
69   * @author Philip Helger
70   * @param <IMPLTYPE>
71   *        Implementation type
72   */
73  @NotThreadSafe
74  public abstract class AbstractSchematronXSLTBasedResourceml#AbstractSchematronXSLTBasedResource">AbstractSchematronXSLTBasedResource <IMPLTYPE extends AbstractSchematronXSLTBasedResource <IMPLTYPE>>
75                                                            extends
76                                                            AbstractSchematronResource implements
77                                                            ISchematronXSLTBasedResource,
78                                                            IGenericImplTrait <IMPLTYPE>
79  {
80    private static final Logger LOGGER = LoggerFactory.getLogger (AbstractSchematronXSLTBasedResource.class);
81  
82    protected ErrorListener m_aCustomErrorListener;
83    protected URIResolver m_aCustomURIResolver = new DefaultTransformURIResolver ();
84    protected final ICommonsOrderedMap <String, Object> m_aCustomParameters = new CommonsLinkedHashMap <> ();
85    private ISchematronXSLTValidator m_aXSLTValidator = new SchematronXSLTValidatorDefault ();
86  
87    public AbstractSchematronXSLTBasedResource (@Nonnull final IReadableResource aSCHResource)
88    {
89      super (aSCHResource);
90      // The URI resolver is necessary for the XSLT to resolve URLs relative to
91      // the SCH
92      final URL aBaseURL = aSCHResource.getAsURL ();
93      setURIResolver (new DefaultTransformURIResolver ().setDefaultBase (aBaseURL != null ? aBaseURL.toExternalForm ()
94                                                                                          : null));
95    }
96  
97    @Nullable
98    public ErrorListener getErrorListener ()
99    {
100     return m_aCustomErrorListener;
101   }
102 
103   @Nonnull
104   public final IMPLTYPE setErrorListener (@Nullable final ErrorListener aCustomErrorListener)
105   {
106     m_aCustomErrorListener = aCustomErrorListener;
107     return thisAsT ();
108   }
109 
110   @Nullable
111   public URIResolver getURIResolver ()
112   {
113     return m_aCustomURIResolver;
114   }
115 
116   @Nonnull
117   public final IMPLTYPE setURIResolver (@Nullable final URIResolver aCustomURIResolver)
118   {
119     m_aCustomURIResolver = aCustomURIResolver;
120     return thisAsT ();
121   }
122 
123   @Nonnull
124   @ReturnsMutableObject
125   public final ICommonsOrderedMap <String, Object> parameters ()
126   {
127     return m_aCustomParameters;
128   }
129 
130   @Deprecated
131   public boolean hasParameters ()
132   {
133     return m_aCustomParameters.isNotEmpty ();
134   }
135 
136   @Nonnull
137   @ReturnsMutableCopy
138   @Deprecated
139   public ICommonsOrderedMap <String, Object> getParameters ()
140   {
141     return m_aCustomParameters.getClone ();
142   }
143 
144   @Nonnull
145   @Deprecated
146   public IMPLTYPE setParameters (@Nullable final Map <String, ?> aCustomParameters)
147   {
148     m_aCustomParameters.setAll (aCustomParameters);
149     return thisAsT ();
150   }
151 
152   /**
153    * Set the XML entity resolver to be used when reading the XML to be
154    * validated.
155    *
156    * @param aEntityResolver
157    *        The entity resolver to set. May be <code>null</code>.
158    * @return this
159    * @since 4.2.3
160    */
161   @Nonnull
162   public final IMPLTYPE setEntityResolver (@Nullable final EntityResolver aEntityResolver)
163   {
164     internalSetEntityResolver (aEntityResolver);
165     return thisAsT ();
166   }
167 
168   /**
169    * @return The XSLT provider passed in the constructor. May be
170    *         <code>null</code>.
171    */
172   @Nullable
173   public abstract ISchematronXSLTBasedProvider getXSLTProvider ();
174 
175   /**
176    * @return The XSLT validator to be used. Never <code>null</code>.
177    */
178   @Nonnull
179   public ISchematronXSLTValidator getXSLTValidator ()
180   {
181     return m_aXSLTValidator;
182   }
183 
184   @Nonnull
185   public final IMPLTYPE setXSLTValidator (@Nonnull final ISchematronXSLTValidator aXSLTValidator)
186   {
187     ValueEnforcer.notNull (aXSLTValidator, "XSLTValidator");
188     m_aXSLTValidator = aXSLTValidator;
189     return thisAsT ();
190   }
191 
192   public final boolean isValidSchematron ()
193   {
194     final ISchematronXSLTBasedProvider aXSLTProvider = getXSLTProvider ();
195     return aXSLTProvider != null && aXSLTProvider.isValidSchematron ();
196   }
197 
198   @Nonnull
199   public EValidity getSchematronValidity (@Nonnull final Node aXMLNode,
200                                           @Nullable final String sBaseURI) throws Exception
201   {
202     ValueEnforcer.notNull (aXMLNode, "XMLNode");
203 
204     // We don't have a short circuit here - apply the full validation
205     final SchematronOutputType aSO = applySchematronValidationToSVRL (aXMLNode, sBaseURI);
206     if (aSO == null)
207       return EValidity.INVALID;
208 
209     // And now filter all elements that make the passed source invalid
210     return m_aXSLTValidator.getSchematronValidity (aSO);
211   }
212 
213   @Nullable
214   public final Document applySchematronValidation (@Nonnull final Node aXMLNode,
215                                                    @Nullable final String sBaseURI) throws TransformerException
216   {
217     ValueEnforcer.notNull (aXMLNode, "XMLNode");
218 
219     final ISchematronXSLTBasedProvider aXSLTProvider = getXSLTProvider ();
220     if (aXSLTProvider == null || !aXSLTProvider.isValidSchematron ())
221     {
222       // We cannot progress because of invalid Schematron
223       return null;
224     }
225 
226     // Debug print the created XSLT document
227     if (SchematronDebug.isShowCreatedXSLT ())
228       LOGGER.info ("Created XSLT document: " + XMLWriter.getNodeAsString (aXSLTProvider.getXSLTDocument ()));
229 
230     // Create result document
231     final Document ret = XMLFactory.newDocument ();
232 
233     // Create the transformer object from the templates specified in the
234     // constructor
235     final Transformer aTransformer = aXSLTProvider.getXSLTTransformer ();
236 
237     // Apply customizations
238     // Ensure an error listener is present
239     if (m_aCustomErrorListener != null)
240       aTransformer.setErrorListener (m_aCustomErrorListener);
241     else
242       aTransformer.setErrorListener (new LoggingTransformErrorListener (Locale.US));
243 
244     // Set the optional URI Resolver
245     if (m_aCustomURIResolver != null)
246       aTransformer.setURIResolver (m_aCustomURIResolver);
247 
248     // Set all custom parameters
249     if (m_aCustomParameters != null)
250       for (final Map.Entry <String, ?> aEntry : m_aCustomParameters.entrySet ())
251         aTransformer.setParameter (aEntry.getKey (), aEntry.getValue ());
252 
253     if (LOGGER.isDebugEnabled ())
254       LOGGER.debug ("Applying Schematron XSLT on XML [start]");
255 
256     // Enable this for hardcore Saxon debugging only
257     if (false)
258       if (aTransformer.getClass ().getName ().equals ("net.sf.saxon.jaxp.TransformerImpl"))
259       {
260         final XsltTransformer aXT = ((TransformerImpl) aTransformer).getUnderlyingXsltTransformer ();
261 
262         aXT.setMessageListener ( (a, b, c) -> LOGGER.info ("MessageListener: " + a + ", " + b + ", " + c));
263         aXT.setTraceFunctionDestination (new StandardLogger (System.err));
264         if (false)
265           aXT.getUnderlyingController ().setTraceListener (new XSLTTraceListener ());
266         if (false)
267         {
268           final XSLTTraceListener aTL = new XSLTTraceListener ();
269           aTL.setOutputDestination (new StandardLogger (System.err));
270           aXT.getUnderlyingController ().setTraceListener (TraceEventMulticaster.add (aTL, null));
271         }
272 
273         if (false)
274           System.out.println ("mode=" + aXT.getInitialMode ());
275         if (false)
276           System.out.println ("temp=" + aXT.getInitialTemplate ());
277         if (false)
278           System.out.println (aTransformer.getOutputProperties ());
279       }
280 
281     // Do the main transformation
282     {
283       final DOMSource aSource = new DOMSource (aXMLNode);
284       aSource.setSystemId (sBaseURI);
285       aTransformer.transform (aSource, new DOMResult (ret));
286     }
287 
288     if (LOGGER.isDebugEnabled ())
289       LOGGER.debug ("Applying Schematron XSLT on XML [end]");
290 
291     // Debug print the created SVRL document
292     if (SchematronDebug.isShowCreatedSVRL ())
293       LOGGER.info ("Created SVRL:\n" + XMLWriter.getNodeAsString (ret));
294 
295     return ret;
296   }
297 
298   @Nullable
299   public SchematronOutputType applySchematronValidationToSVRL (@Nonnull final Node aXMLSource,
300                                                                @Nullable final String sBaseURI) throws Exception
301   {
302     final Document aDoc = applySchematronValidation (aXMLSource, sBaseURI);
303     if (aDoc == null)
304       return null;
305 
306     // Avoid NPE later on
307     if (aDoc.getDocumentElement () == null)
308       throw new IllegalStateException ("Internal error: created SVRL DOM Document has no document node!");
309     return new SVRLMarshaller ().read (aDoc);
310   }
311 
312   @Override
313   public String toString ()
314   {
315     return ToStringGenerator.getDerived (super.toString ()).append ("XSLTValidator", m_aXSLTValidator).getToString ();
316   }
317 }