View Javadoc
1   /*
2    * Copyright (c) MuleSoft, Inc.  All rights reserved.  http://www.mulesoft.com
3    * The software in this package is published under the terms of the CPAL v1.0
4    * license, a copy of which has been included with this distribution in the
5    * LICENSE.txt file.
6    */
7   package org.mule.module.xml.transformer;
8   
9   import org.mule.api.MuleMessage;
10  import org.mule.api.lifecycle.InitialisationException;
11  import org.mule.api.transformer.Transformer;
12  import org.mule.api.transformer.TransformerException;
13  import org.mule.config.i18n.CoreMessages;
14  import org.mule.module.xml.util.LocalURIResolver;
15  import org.mule.module.xml.util.XMLUtils;
16  import org.mule.util.ClassUtils;
17  import org.mule.util.IOUtils;
18  import org.mule.util.StringUtils;
19  
20  import java.io.StringReader;
21  import java.util.HashMap;
22  import java.util.Iterator;
23  import java.util.Map;
24  import java.util.Map.Entry;
25  
26  import javax.xml.transform.ErrorListener;
27  import javax.xml.transform.OutputKeys;
28  import javax.xml.transform.Result;
29  import javax.xml.transform.Source;
30  import javax.xml.transform.TransformerFactory;
31  import javax.xml.transform.TransformerFactoryConfigurationError;
32  import javax.xml.transform.URIResolver;
33  import javax.xml.transform.stream.StreamSource;
34  
35  import org.apache.commons.pool.BasePoolableObjectFactory;
36  import org.apache.commons.pool.impl.GenericObjectPool;
37  
38  /**
39   * <code>XsltTransformer</code> performs an XSLT transform on a DOM (or other XML-ish)
40   * object.
41   * <p/>
42   * This transformer maintains a pool of {@link javax.xml.transform.Transformer} objects to speed up processing of concurrent requests.
43   * The pool can be configured using {@link #setMaxIdleTransformers(int)}.
44   * <p/>
45   * Parameter can also be set as part of the transformation context and these can be mapped to conent in the current message using
46   * property extractors or can be fixed values.
47   * <p/>
48   * <p/>
49   * For example, the current event's message has a property named "myproperty", also you want to generate a uuid as a
50   * parameter. To do this you can define context properties that can provide an expression to be evaluated on the current
51   * message.
52   * </p>
53   * <p>
54   * Example Configuration:
55   * </p>
56   * <p/>
57   * <pre>
58   *  &lt;mxml:xslt-transformer name=&quot;MyXsltTransformer&quot; xslFile=&quot;myXslFile.xsl&quot;&amp;gt
59   *      &lt;context-property name=&quot;myParameter&quot; value=&quot;#[head:myproperty]&quot;/&amp;gt
60   *      &lt;context-property name=&quot;myParameter2&quot; value=&quot;#[function:uuid]&quot;/&amp;gt
61   *  &lt;/mxml:xslt-transformer&amp;gt
62   * </pre>
63   * <p/>
64   * <p>
65   * The 'header' expression pulls a header from the current message and 'function' can execute a set of arbitrary functions.
66   * You can also pass in static values by ommitting the expression prefix '#['.
67   * </p>
68   * <p/>
69   * In addition to being able to pass in an XSLT file you can also define templates inline. For example -
70   * <p/>
71   * <pre>
72   *  &lt;mxml:xslt-transformer name=&quot;MyXsltTransformer&quot;&amp;gt
73   *      &lt;mxml:xslt-text&amp;gt
74   *          <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns="http://test.com" version="2.0">
75   * <p/>
76   *                <xsl:param name="echo"/>
77   * <p/>
78   *               <xsl:template match="/">
79   *                   <echo-value>
80   *                       <xsl:value-of select="$echo"/>
81   *                   </echo-value>
82   *               </xsl:template>
83   *           </xsl:stylesheet>
84   *  &lt;/mxml:xslt-text&amp;gt
85   * </pre>
86   */
87  
88  public class XsltTransformer extends AbstractXmlTransformer
89  {
90      // keep at least 1 XSLT Transformer ready by default
91      private static final int MIN_IDLE_TRANSFORMERS = 1;
92      // keep max. 32 XSLT Transformers around by default
93      private static final int MAX_IDLE_TRANSFORMERS = 32;
94      // MAX_IDLE is also the total limit
95      private static final int MAX_ACTIVE_TRANSFORMERS = MAX_IDLE_TRANSFORMERS;
96  
97      //Saxon shipped with Mule
98      public static final String PREFERRED_TRANSFORMER_FACTORY = "net.sf.saxon.TransformerFactoryImpl";
99  
100     protected final GenericObjectPool transformerPool;
101 
102     /**
103      * Default to Saxon
104      */
105     private volatile String xslTransformerFactoryClassName = PREFERRED_TRANSFORMER_FACTORY;
106     private volatile String xslFile;
107     private volatile String xslt;
108     private volatile Map contextProperties;
109 
110     private URIResolver uriResolver;
111 
112     public XsltTransformer()
113     {
114         super();
115         transformerPool = new GenericObjectPool(new PooledXsltTransformerFactory());
116         transformerPool.setMinIdle(MIN_IDLE_TRANSFORMERS);
117         transformerPool.setMaxIdle(MAX_IDLE_TRANSFORMERS);
118         transformerPool.setMaxActive(MAX_ACTIVE_TRANSFORMERS);
119         uriResolver = new LocalURIResolver();
120         contextProperties = new HashMap();
121     }
122 
123     public XsltTransformer(String xslFile)
124     {
125         this();
126         this.setXslFile(xslFile);
127     }
128 
129     @Override
130     public void initialise() throws InitialisationException
131     {
132         logger.debug("Initialising transformer: " + this);
133         try
134         {
135             // Only load the file once at initialize time
136             if (xslFile != null)
137             {
138                 this.xslt = IOUtils.getResourceAsString(xslFile, getClass());
139             }
140             transformerPool.addObject();
141         }
142         catch (Throwable te)
143         {
144             throw new InitialisationException(te, this);
145         }
146     }
147 
148     /**
149      * Transform, using XSLT, a XML String to another String.
150      *
151      * @return The result in the type specified by the user
152      */
153     @Override
154     public Object transformMessage(MuleMessage message, String encoding) throws TransformerException
155     {
156         Object src = message.getPayload();
157         try
158         {
159             Source sourceDoc = XMLUtils.toXmlSource(getXMLInputFactory(), isUseStaxSource(), src);
160             if (sourceDoc == null)
161             {
162                 return null;
163             }
164 
165             ResultHolder holder = getResultHolder(returnType.getType());
166 
167             // If the users hasn't specified a class, lets return the same type they gave us
168             if (holder == null)
169             {
170                 holder = getResultHolder(src.getClass());
171             }
172 
173             // If we still don't have a result type, lets fall back to using a DelayedResult
174             // as it is the most efficient.
175             if (holder == null || DelayedResult.class.equals(returnType.getType()))
176             {
177                 return getDelayedResult(message, encoding, sourceDoc);
178             }
179 
180             doTransform(message, encoding, sourceDoc, holder.getResult());
181 
182             return holder.getResultObject();
183         }
184         catch (Exception e)
185         {
186             throw new TransformerException(this, e);
187         }
188     }
189 
190     protected Object getDelayedResult(final MuleMessage message, final String encoding, final Source sourceDoc)
191     {
192         return new DelayedResult()
193         {
194             private String systemId;
195 
196             public void write(Result result) throws Exception
197             {
198                 doTransform(message, encoding, sourceDoc, result);
199             }
200 
201             public String getSystemId()
202             {
203                 return systemId;
204             }
205 
206             public void setSystemId(String systemId)
207             {
208                 this.systemId = systemId;
209             }
210         };
211     }
212 
213     protected void doTransform(MuleMessage message, String encoding, Source sourceDoc, Result result)
214             throws Exception
215     {
216         DefaultErrorListener errorListener = new DefaultErrorListener(this);
217         javax.xml.transform.Transformer transformer = null;
218 
219         try
220         {
221             transformer = (javax.xml.transform.Transformer) transformerPool.borrowObject();
222 
223             transformer.setErrorListener(errorListener);
224             transformer.setOutputProperty(OutputKeys.ENCODING, encoding);
225 
226             // set transformation parameters
227             if (contextProperties != null)
228             {
229                 for (Iterator i = contextProperties.entrySet().iterator(); i.hasNext();)
230                 {
231                     Map.Entry parameter = (Entry) i.next();
232                     String key = (String) parameter.getKey();
233                     transformer.setParameter(key, evaluateTransformParameter(key, parameter.getValue(), message));
234                 }
235             }
236 
237             transformer.transform(sourceDoc, result);
238 
239             if (errorListener.isError())
240             {
241                 throw errorListener.getException();
242             }
243         }
244         finally
245         {
246             if (transformer != null)
247             {
248                 // clear transformation parameters before returning transformer to the
249                 // pool
250                 transformer.clearParameters();
251 
252                 transformerPool.returnObject(transformer);
253             }
254         }
255     }
256 
257     /**
258      * Returns the name of the currently configured javax.xml.transform.Transformer
259      * factory class used to create XSLT Transformers.
260      *
261      * @return a TransformerFactory class name or <code>null</code> if none has been
262      *         configured
263      */
264     public String getXslTransformerFactory()
265     {
266         return xslTransformerFactoryClassName;
267     }
268 
269     /**
270      * Configures the javax.xml.transform.Transformer factory class
271      *
272      * @param xslTransformerFactory the name of the TransformerFactory class to use
273      */
274     public void setXslTransformerFactory(String xslTransformerFactory)
275     {
276         this.xslTransformerFactoryClassName = xslTransformerFactory;
277     }
278 
279     /**
280      * @return Returns the xslFile.
281      */
282     public String getXslFile()
283     {
284         return xslFile;
285     }
286 
287     /**
288      * @param xslFile The xslFile to set.
289      */
290     public void setXslFile(String xslFile)
291     {
292         this.xslFile = xslFile;
293     }
294 
295     public String getXslt()
296     {
297         return xslt;
298     }
299 
300     public void setXslt(String xslt)
301     {
302         this.xslt = xslt;
303     }
304 
305     public URIResolver getUriResolver()
306     {
307         return uriResolver;
308     }
309 
310     public void setUriResolver(URIResolver uriResolver)
311     {
312         this.uriResolver = uriResolver;
313     }
314 
315     /**
316      * Returns the StreamSource corresponding to xslt (which should have been loaded
317      * in {@link #initialise()}).
318      * 
319      * @return The StreamSource
320      */
321     protected StreamSource getStreamSource() throws InitialisationException
322     {
323         if (xslt == null)
324         {
325             throw new InitialisationException(CoreMessages.propertiesNotSet("xsl-file or xsl-text"), this);
326         }
327         else
328         {
329             return new StreamSource(new StringReader(xslt));
330         }
331     }
332 
333     protected class PooledXsltTransformerFactory extends BasePoolableObjectFactory
334     {
335         @Override
336         public Object makeObject() throws Exception
337         {
338             StreamSource source = XsltTransformer.this.getStreamSource();
339             String factoryClassName = XsltTransformer.this.getXslTransformerFactory();
340             TransformerFactory factory;
341 
342             if (PREFERRED_TRANSFORMER_FACTORY.equals(factoryClassName) && !ClassUtils.isClassOnPath(factoryClassName, getClass()))
343             {
344                 logger.warn("Preferred Transfomer Factory " + PREFERRED_TRANSFORMER_FACTORY + " not on classpath and no default is set, defaulting to JDK");
345                 factoryClassName = null;
346             }
347 
348             if (StringUtils.isNotEmpty(factoryClassName))
349             {
350 
351                 factory = (TransformerFactory) ClassUtils.instanciateClass(factoryClassName,
352                         ClassUtils.NO_ARGS, this.getClass());
353             }
354             else
355             {
356                 // fall back to JDK default
357                 try
358                 {
359                     factory = TransformerFactory.newInstance();
360                 }
361                 catch (TransformerFactoryConfigurationError e)
362                 {
363                     System.setProperty("javax.xml.transform.TransformerFactory", XMLUtils.TRANSFORMER_FACTORY_JDK5);
364                     factory = TransformerFactory.newInstance();
365                 }
366             }
367 
368             factory.setURIResolver(getUriResolver());
369             return factory.newTransformer(source);
370         }
371     }
372 
373     protected class DefaultErrorListener implements ErrorListener
374     {
375         private TransformerException e = null;
376         private final Transformer trans;
377 
378         public DefaultErrorListener(Transformer trans)
379         {
380             this.trans = trans;
381         }
382 
383         public TransformerException getException()
384         {
385             return e;
386         }
387 
388         public boolean isError()
389         {
390             return e != null;
391         }
392 
393         public void error(javax.xml.transform.TransformerException exception)
394                 throws javax.xml.transform.TransformerException
395         {
396             e = new TransformerException(trans, exception);
397         }
398 
399         public void fatalError(javax.xml.transform.TransformerException exception)
400                 throws javax.xml.transform.TransformerException
401         {
402             e = new TransformerException(trans, exception);
403         }
404 
405         public void warning(javax.xml.transform.TransformerException exception)
406                 throws javax.xml.transform.TransformerException
407         {
408             logger.warn(exception.getMessage());
409         }
410     }
411 
412     /**
413      * @return The current maximum number of allowable active transformer objects in
414      *         the pool
415      */
416     public int getMaxActiveTransformers()
417     {
418         return transformerPool.getMaxActive();
419     }
420 
421     /**
422      * Sets the the current maximum number of active transformer objects allowed in the
423      * pool
424      *
425      * @param maxActiveTransformers New maximum size to set
426      */
427     public void setMaxActiveTransformers(int maxActiveTransformers)
428     {
429         transformerPool.setMaxActive(maxActiveTransformers);
430     }
431 
432     /**
433      * @return The current maximum number of allowable idle transformer objects in the
434      *         pool
435      */
436     public int getMaxIdleTransformers()
437     {
438         return transformerPool.getMaxIdle();
439     }
440 
441     /**
442      * Sets the the current maximum number of idle transformer objects allowed in the pool
443      *
444      * @param maxIdleTransformers New maximum size to set
445      */
446     public void setMaxIdleTransformers(int maxIdleTransformers)
447     {
448         transformerPool.setMaxIdle(maxIdleTransformers);
449     }
450 
451     /**
452      * Gets the parameters to be used when applying the transformation
453      *
454      * @return a map of the parameter names and associated values
455      * @see javax.xml.transform.Transformer#setParameter(java.lang.String,
456      *      java.lang.Object)
457      */
458     public Map getContextProperties()
459     {
460         return contextProperties;
461     }
462 
463     /**
464      * Sets the parameters to be used when applying the transformation
465      *
466      * @param contextProperties a map of the parameter names and associated values
467      * @see javax.xml.transform.Transformer#setParameter(java.lang.String,
468      *      java.lang.Object)
469      */
470     public void setContextProperties(Map contextProperties)
471     {
472         this.contextProperties = contextProperties;
473     }
474 
475     /**
476      * Returns the value to be set for the parameter. This method is called for each
477      * parameter before it is set on the transformer. The purpose of this method is to
478      * allow dynamic parameters related to the event (usually message properties) to be
479      * used. Any attribute of the current MuleEvent can be accessed using Property Extractors
480      * such as JXpath, bean path or header retrieval.
481      *
482      * @param name  the name of the parameter. The name isn't used for this implementation but is exposed as a
483      *              param for classes that may need it.
484      * @param value the value of the paramter
485      * @return the object to be set as the parameter value
486      * @throws TransformerException
487      */
488     protected Object evaluateTransformParameter(String name, Object value, MuleMessage message) throws TransformerException
489     {
490         if (value instanceof String)
491         {
492             return muleContext.getExpressionManager().parse(value.toString(), message);
493         }
494 
495         return value;
496     }
497 }