View Javadoc

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