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.MuleRuntimeException;
11  import org.mule.api.lifecycle.Disposable;
12  import org.mule.api.lifecycle.Initialisable;
13  import org.mule.api.lifecycle.InitialisationException;
14  import org.mule.api.transformer.TransformerException;
15  import org.mule.config.i18n.CoreMessages;
16  import org.mule.module.xml.i18n.XmlMessages;
17  import org.mule.transformer.types.DataTypeFactory;
18  import org.mule.util.IOUtils;
19  
20  import java.io.ByteArrayInputStream;
21  import java.io.InputStream;
22  import java.io.StringReader;
23  import java.util.ArrayList;
24  import java.util.Iterator;
25  import java.util.List;
26  import java.util.Map;
27  
28  import javax.xml.namespace.QName;
29  import javax.xml.stream.XMLStreamReader;
30  
31  import net.sf.saxon.Configuration;
32  import net.sf.saxon.javax.xml.xquery.XQCommonHandler;
33  import net.sf.saxon.javax.xml.xquery.XQConnection;
34  import net.sf.saxon.javax.xml.xquery.XQDataSource;
35  import net.sf.saxon.javax.xml.xquery.XQException;
36  import net.sf.saxon.javax.xml.xquery.XQItem;
37  import net.sf.saxon.javax.xml.xquery.XQItemType;
38  import net.sf.saxon.javax.xml.xquery.XQPreparedExpression;
39  import net.sf.saxon.javax.xml.xquery.XQResultSequence;
40  import net.sf.saxon.xqj.SaxonXQDataSource;
41  import org.apache.commons.pool.BasePoolableObjectFactory;
42  import org.apache.commons.pool.impl.GenericObjectPool;
43  import org.dom4j.io.DOMWriter;
44  import org.dom4j.io.DocumentSource;
45  import org.w3c.dom.Document;
46  import org.w3c.dom.Element;
47  import org.w3c.dom.Node;
48  import org.xml.sax.InputSource;
49  
50  /**
51   * The XQuery Module gives users the ability to perform XQuery transformations on XML messages in Mule
52   */
53  public class XQueryTransformer extends AbstractXmlTransformer implements Disposable
54  {
55      public static final String SOURCE_DOCUMENT_NAMESPACE = "document";
56  
57      // keep at least 1 XSLT Transformer ready by default
58      private static final int MIN_IDLE_TRANSFORMERS = 1;
59      // keep max. 32 XSLT Transformers around by default
60      private static final int MAX_IDLE_TRANSFORMERS = 32;
61      // MAX_IDLE is also the total limit
62      private static final int MAX_ACTIVE_TRANSFORMERS = MAX_IDLE_TRANSFORMERS;
63  
64      protected final GenericObjectPool transformerPool;
65  
66      private volatile String xqueryFile;
67      private volatile String xquery;
68      private volatile Map contextProperties;
69      private volatile XQCommonHandler commonHandler;
70      private volatile XQConnection connection;
71      protected Configuration configuration;
72  
73      public XQueryTransformer()
74      {
75          super();
76          transformerPool = new GenericObjectPool(new PooledXQueryTransformerFactory());
77          transformerPool.setMinIdle(MIN_IDLE_TRANSFORMERS);
78          transformerPool.setMaxIdle(MAX_IDLE_TRANSFORMERS);
79          transformerPool.setMaxActive(MAX_ACTIVE_TRANSFORMERS);
80  
81          registerSourceType(DataTypeFactory.STRING);
82          registerSourceType(DataTypeFactory.BYTE_ARRAY);
83          registerSourceType(DataTypeFactory.create(DocumentSource.class));
84          registerSourceType(DataTypeFactory.create(org.dom4j.Document.class));
85          registerSourceType(DataTypeFactory.create(Document.class));
86          registerSourceType(DataTypeFactory.create(Element.class));
87          registerSourceType(DataTypeFactory.INPUT_STREAM);
88          setReturnDataType(DataTypeFactory.create(Element.class));
89      }
90  
91      public XQueryTransformer(String xqueryFile)
92      {
93          this();
94          this.xqueryFile = xqueryFile;
95      }
96  
97      /**
98       *
99       */
100     @Override
101     public void initialise() throws InitialisationException
102     {
103 
104         if (xquery != null && xqueryFile != null)
105         {
106             throw new InitialisationException(XmlMessages.canOnlySetFileOrXQuery(), this);
107         }
108 
109         try
110         {
111             if (xqueryFile != null)
112             {
113                 xquery = IOUtils.getResourceAsString(xqueryFile, getClass());
114             }
115             if (configuration == null)
116             {
117                 configuration = new Configuration();
118             }
119 
120             XQDataSource ds = new SaxonXQDataSource(configuration);
121             if (commonHandler != null)
122             {
123                 ds.setCommonHandler(commonHandler);
124             }
125             connection = ds.getConnection();
126 
127             transformerPool.addObject();
128 
129         }
130         catch (Throwable te)
131         {
132             throw new InitialisationException(te, this);
133         }
134     }
135 
136     @Override
137     public void dispose()
138     {
139         try
140         {
141             connection.close();
142         }
143         catch (XQException e)
144         {
145             logger.warn(e.getMessage());
146         }
147     }
148 
149     @Override
150     public Object transformMessage(MuleMessage message, String outputEncoding) throws TransformerException
151     {
152         try
153         {
154             XQPreparedExpression transformer = null;
155             try
156             {
157                 transformer = (XQPreparedExpression) transformerPool.borrowObject();
158 
159                 bindParameters(transformer, message);
160 
161                 bindDocument(message.getPayload(), transformer);
162 
163                 XQResultSequence result = transformer.executeQuery();
164                 //No support for return Arrays yet
165                 List results = new ArrayList();
166                 while (result.next())
167                 {
168                     XQItem item = result.getItem();
169 
170                     Class type = returnType.getType();
171                     if (Node.class.isAssignableFrom(type) || Node[].class.isAssignableFrom(type))
172                     {
173                         results.add(item.getNode());
174                     }
175                     else if (String.class.isAssignableFrom(type) || String[].class.isAssignableFrom(type))
176                     {
177                         results.add(item.getItemAsString());
178                     }
179                     else if (XMLStreamReader.class.isAssignableFrom(type) || XMLStreamReader[].class.isAssignableFrom(type))
180                     {
181                         try
182                         {
183                             results.add(item.getItemAsStream());
184                         }
185                         catch (XQException e)
186                         {
187                             throw new TransformerException(XmlMessages.streamNotAvailble(getName()));
188                         }
189                     }
190                     else
191                     {
192                         //This can be a JAXB bound  object instance depending on whether the CommonHandler has been set
193                         try
194                         {
195                             results.add(item.getObject());
196                         }
197                         catch (XQException e)
198                         {
199                             throw new TransformerException(XmlMessages.objectNotAvailble(getName()));
200 
201                         }
202                     }
203                     if (!type.isArray())
204                     {
205                         break;
206                     }
207                 }
208                 if (returnType.getType().isArray())
209                 {
210                     return results.toArray();
211                 }
212                 if (results.size() == 1)
213                 {
214                     return results.get(0);
215                 }
216                 else if (results.size() == 0)
217                 {
218                     return null;
219                 }
220                 else
221                 {
222                     return results.toArray();
223                 }
224 
225             }
226             finally
227             {
228                 if (transformer != null)
229                 {
230                     if (transformer.getWarnings() != null)
231                     {
232                         logger.warn(transformer.getWarnings().getMessage(), transformer.getWarnings().fillInStackTrace());
233                     }
234                     // clear transformation parameters before returning transformer to the
235                     // pool
236                     //TODO find out what the scope is for bound variables, there doesn't seem to be a way to unbind them
237                     unbindParameters(transformer);
238                     transformerPool.returnObject(transformer);
239                 }
240             }
241 
242         }
243         catch (Exception e)
244         {
245             throw new TransformerException(this, e);
246         }
247     }
248 
249     protected void bindParameters(XQPreparedExpression transformer, MuleMessage message) throws XQException, TransformerException
250     {
251         // set transformation parameters
252         if (contextProperties != null)
253         {
254             for (Iterator i = contextProperties.entrySet().iterator(); i.hasNext();)
255             {
256                 Map.Entry parameter = (Map.Entry) i.next();
257                 String key = (String) parameter.getKey();
258                 Object o = evaluateTransformParameter(key, parameter.getValue(), message);
259 
260                 if (o instanceof String)
261                 {
262                     transformer.bindAtomicValue(new QName(key), o.toString(), connection.createAtomicItemType(XQItemType.XQBASETYPE_STRING));
263                 }
264                 else if (o instanceof Boolean)
265                 {
266                     transformer.bindBoolean(new QName(key), ((Boolean) o).booleanValue(), connection.createAtomicItemType(XQItemType.XQBASETYPE_BOOLEAN));
267                 }
268                 else if (o instanceof Byte)
269                 {
270                     transformer.bindByte(new QName(key), ((Byte) o).byteValue(), connection.createAtomicItemType(XQItemType.XQBASETYPE_BYTE));
271                 }
272                 else if (o instanceof Short)
273                 {
274                     transformer.bindShort(new QName(key), ((Short) o).shortValue(), connection.createAtomicItemType(XQItemType.XQBASETYPE_SHORT));
275                 }
276                 else if (o instanceof Integer)
277                 {
278                     transformer.bindInt(new QName(key), ((Integer) o).intValue(), connection.createAtomicItemType(XQItemType.XQBASETYPE_INT));
279                 }
280                 else if (o instanceof Long)
281                 {
282                     transformer.bindLong(new QName(key), ((Long) o).longValue(), connection.createAtomicItemType(XQItemType.XQBASETYPE_LONG));
283                 }
284                 else if (o instanceof Float)
285                 {
286                     transformer.bindFloat(new QName(key), ((Float) o).floatValue(), connection.createAtomicItemType(XQItemType.XQBASETYPE_FLOAT));
287                 }
288                 else if (o instanceof Double)
289                 {
290                     transformer.bindDouble(new QName(key), ((Double) o).doubleValue(), connection.createAtomicItemType(XQItemType.XQBASETYPE_DOUBLE));
291                 }
292                 else
293                 {
294                     logger.error("Cannot bind value: " + o + " cannot be bound to the Xquery context. Not of supported type");
295                 }
296             }
297         }
298     }
299 
300     /**
301      * Removes any parameter bindings from the transformer, replacing them with empty strings
302      *
303      * @param transformer the transformer to remove properties from
304      */
305     protected void unbindParameters(XQPreparedExpression transformer) throws XQException
306     {
307         // Replace transformation parameters with null values
308         if (contextProperties != null)
309         {
310             for (Iterator i = contextProperties.entrySet().iterator(); i.hasNext(); )
311             {
312                 Map.Entry parameter = (Map.Entry) i.next();
313                 String key = (String) parameter.getKey();
314 
315                 transformer.bindAtomicValue(new QName(key), "", connection.createAtomicItemType(XQItemType.XQBASETYPE_STRING));
316             }
317         }
318     }
319 
320     /**
321      * Returns the InputSource corresponding to xqueryFile or xquery
322      *
323      * @param src
324      * @param transformer
325      * @throws net.sf.saxon.javax.xml.xquery.XQException
326      *
327      * @throws Exception
328      *
329      */
330     protected void bindDocument(Object src, XQPreparedExpression transformer) throws Exception
331     {
332         if (src instanceof byte[])
333         {
334             transformer.bindDocument(new QName(SOURCE_DOCUMENT_NAMESPACE), new InputSource(new ByteArrayInputStream((byte[]) src)));
335 
336         }
337         else if (src instanceof InputStream)
338         {
339             transformer.bindDocument(new QName(SOURCE_DOCUMENT_NAMESPACE), new InputSource((InputStream) src));
340 
341         }
342         else if (src instanceof String)
343         {
344             transformer.bindDocument(new QName(SOURCE_DOCUMENT_NAMESPACE), new InputSource(new StringReader((String) src)));
345 
346         }
347         else if (src instanceof Document)
348         {
349             transformer.bindNode(new QName(SOURCE_DOCUMENT_NAMESPACE), (Document) src, null);
350 
351         }
352         else if (src instanceof Element)
353         {
354             transformer.bindNode(new QName(SOURCE_DOCUMENT_NAMESPACE), (Element) src, null);
355 
356         }
357         else if (src instanceof org.dom4j.Document)
358         {
359             DOMWriter domWriter = new DOMWriter();
360             Document dom = domWriter.write((org.dom4j.Document) src);
361             transformer.bindNode(new QName(SOURCE_DOCUMENT_NAMESPACE), dom, null);
362 
363         }
364         else if (src instanceof DocumentSource)
365         {
366             transformer.bindDocument(new QName(SOURCE_DOCUMENT_NAMESPACE), ((DocumentSource) src).getInputSource());
367 
368         }
369         else
370         {
371             throw new IllegalArgumentException(CoreMessages.transformUnexpectedType(src.getClass(), null).getMessage());
372         }
373     }
374 
375     public Configuration getConfiguration()
376     {
377         return configuration;
378     }
379 
380     public void setConfiguration(Configuration configuration)
381     {
382         this.configuration = configuration;
383     }
384 
385     /**
386      * @return Returns the xqueryFile.
387      */
388     public String getXqueryFile()
389     {
390         return xqueryFile;
391     }
392 
393     /**
394      * @param xqueryFile The xqueryFile to set.
395      */
396     public void setXqueryFile(String xqueryFile)
397     {
398         this.xqueryFile = xqueryFile;
399     }
400 
401     public String getXquery()
402     {
403         return xquery;
404     }
405 
406     public void setXquery(String xquery)
407     {
408         this.xquery = xquery;
409     }
410 
411     public XQCommonHandler getCommonHandler()
412     {
413         return commonHandler;
414     }
415 
416     public void setCommonHandler(XQCommonHandler commonHandler)
417     {
418         this.commonHandler = commonHandler;
419     }
420 
421 
422     protected class PooledXQueryTransformerFactory extends BasePoolableObjectFactory
423     {
424         @Override
425         public Object makeObject() throws Exception
426         {
427             return connection.prepareExpression(xquery);
428         }
429 
430         @Override
431         public void destroyObject(Object o) throws Exception
432         {
433             ((XQPreparedExpression) o).close();
434             super.destroyObject(o);
435         }
436     }
437 
438 
439     /**
440      * @return The current maximum number of allowable active transformer objects in
441      *         the pool
442      */
443     public int getMaxActiveTransformers()
444     {
445         return transformerPool.getMaxActive();
446     }
447 
448     /**
449      * Sets the the current maximum number of active transformer objects allowed in the
450      * pool
451      *
452      * @param maxActiveTransformers New maximum size to set
453      */
454     public void setMaxActiveTransformers(int maxActiveTransformers)
455     {
456         transformerPool.setMaxActive(maxActiveTransformers);
457     }
458 
459     /**
460      * @return The current maximum number of allowable idle transformer objects in the
461      *         pool
462      */
463     public int getMaxIdleTransformers()
464     {
465         return transformerPool.getMaxIdle();
466     }
467 
468     /**
469      * Sets the the current maximum number of idle transformer objects allowed in the pool
470      *
471      * @param maxIdleTransformers New maximum size to set
472      */
473     public void setMaxIdleTransformers(int maxIdleTransformers)
474     {
475         transformerPool.setMaxIdle(maxIdleTransformers);
476     }
477 
478     /**
479      * Gets the parameters to be used when applying the transformation
480      *
481      * @return a map of the parameter names and associated values
482      * @see javax.xml.transform.Transformer#setParameter(java.lang.String,
483      *      java.lang.Object)
484      */
485     public Map getContextProperties()
486     {
487         return contextProperties;
488     }
489 
490     /**
491      * Sets the parameters to be used when applying the transformation
492      *
493      * @param contextProperties a map of the parameter names and associated values
494      * @see javax.xml.transform.Transformer#setParameter(java.lang.String,
495      *      java.lang.Object)
496      */
497     public void setContextProperties(Map contextProperties)
498     {
499         this.contextProperties = contextProperties;
500     }
501 
502     /**
503      * <p>
504      * Returns the value to be set for the parameter. This method is called for each
505      * parameter before it is set on the transformer. The purpose of this method is to
506      * allow dynamic parameters related to the event (usually message properties) to be
507      * used. Any expression using the Mule expression syntax can be used.
508      * </p>
509      * <p>
510      * For example: If the current event's message has a property named "myproperty", to
511      * pass this in you would set the transform parameter's value to be
512      * "#[mule.message:header(myproperty)]".
513      * <p/>
514      * <p>
515      * This method may be overloaded by a sub class to provide a different dynamic
516      * parameter implementation.
517      * </p>
518      *
519      * @param name  the name of the parameter
520      * @param value the value of the paramter
521      * @return the object to be set as the parameter value
522      * @throws TransformerException
523      */
524     protected Object evaluateTransformParameter(String name, Object value, MuleMessage message) throws TransformerException
525     {
526         if (value instanceof String)
527         {
528             return muleContext.getExpressionManager().parse(value.toString(), message);
529         }
530         return value;
531     }
532 
533     @Override
534     public Object clone() throws CloneNotSupportedException
535     {
536         Object clone = super.clone();
537         try
538         {
539             ((Initialisable) clone).initialise();
540             return clone;
541         }
542         catch (InitialisationException e)
543         {
544             throw new MuleRuntimeException(CoreMessages.failedToClone(getClass().getName()), e);
545         }
546     }
547 }