View Javadoc

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