View Javadoc

1   /*
2    * $Id: FilteringXmlMessageSplitter.java 11728 2008-05-13 07:31:11Z dirk.olmes $
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.routing.outbound;
12  
13  import org.mule.impl.MuleMessage;
14  import org.mule.umo.UMOMessage;
15  import org.mule.umo.endpoint.UMOEndpoint;
16  import org.mule.util.ExceptionUtils;
17  import org.mule.util.IOUtils;
18  import org.mule.util.StringUtils;
19  
20  import java.io.InputStream;
21  import java.io.StringReader;
22  import java.util.HashMap;
23  import java.util.Iterator;
24  import java.util.LinkedList;
25  import java.util.List;
26  import java.util.Map;
27  
28  import org.dom4j.Document;
29  import org.dom4j.DocumentHelper;
30  import org.dom4j.Element;
31  import org.dom4j.Node;
32  import org.dom4j.XPath;
33  import org.dom4j.io.SAXReader;
34  
35  /**
36   * <code>FilteringXmlMessageSplitter</code> will split a DOM4J document into nodes
37   * based on the "splitExpression" property. <p/> Optionally, you can specify a
38   * <code>namespaces</code> property map that contain prefix/namespace mappings.
39   * Mind if you have a default namespace declared you should map it to some namespace,
40   * and reference it in the <code>splitExpression</code> property. <p/> The splitter
41   * can optionally validate against an XML schema. By default schema validation is
42   * turned off. <p/> You may reference an external schema from the classpath by using
43   * the <code>externalSchemaLocation</code> property. <p/> Note that each part
44   * returned is actually returned as a new Document.
45   */
46  public class FilteringXmlMessageSplitter extends AbstractMessageSplitter
47  {
48      // xml parser feature names for optional XSD validation
49      public static final String APACHE_XML_FEATURES_VALIDATION_SCHEMA = "http://apache.org/xml/features/validation/schema";
50      public static final String APACHE_XML_FEATURES_VALIDATION_SCHEMA_FULL_CHECKING = "http://apache.org/xml/features/validation/schema-full-checking";
51  
52      // JAXP property for specifying external XSD location
53      public static final String JAXP_PROPERTIES_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";
54  
55      // JAXP properties for specifying external XSD language (as required by newer
56      // JAXP implementation)
57      public static final String JAXP_PROPERTIES_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";
58      public static final String JAXP_PROPERTIES_SCHEMA_LANGUAGE_VALUE = "http://www.w3.org/2001/XMLSchema";
59  
60      protected final ThreadLocal propertiesContext = new ThreadLocal();
61      protected final ThreadLocal nodesContext = new ThreadLocal();
62  
63      protected volatile String splitExpression = "";
64      protected volatile Map namespaces = null;
65      protected volatile boolean validateSchema = false;
66      protected volatile String externalSchemaLocation = "";
67  
68      public void setSplitExpression(String splitExpression)
69      {
70          this.splitExpression = StringUtils.trimToEmpty(splitExpression);
71      }
72  
73      public void setNamespaces(Map namespaces)
74      {
75          this.namespaces = namespaces;
76      }
77  
78      public String getSplitExpression()
79      {
80          return splitExpression;
81      }
82  
83      public boolean isValidateSchema()
84      {
85          return validateSchema;
86      }
87  
88      public void setValidateSchema(boolean validateSchema)
89      {
90          this.validateSchema = validateSchema;
91      }
92  
93      public String getExternalSchemaLocation()
94      {
95          return externalSchemaLocation;
96      }
97  
98      /**
99       * Set classpath location of the XSD to check against. If the resource cannot be
100      * found, an exception will be thrown at runtime.
101      * 
102      * @param externalSchemaLocation location of XSD
103      */
104     public void setExternalSchemaLocation(String externalSchemaLocation)
105     {
106         this.externalSchemaLocation = externalSchemaLocation;
107     }
108 
109     /**
110      * Template method can be used to split the message up before the getMessagePart
111      * method is called .
112      * 
113      * @param message the message being routed
114      */
115     // @Override
116     protected void initialise(UMOMessage message)
117     {
118         super.initialise(message);
119 
120         if (logger.isDebugEnabled())
121         {
122             if (splitExpression.length() == 0)
123             {
124                 logger.warn("splitExpression is not specified, no processing will take place");
125             }
126             else
127             {
128                 logger.debug("splitExpression is " + splitExpression);
129             }
130         }
131 
132         Object src = message.getPayload();
133 
134         try
135         {
136             if (src instanceof byte[])
137             {
138                 src = new String((byte[])src);
139             }
140 
141             Document dom4jDoc;
142 
143             if (src instanceof String)
144             {
145                 String xml = (String)src;
146                 SAXReader reader = new SAXReader();
147                 setDoSchemaValidation(reader, isValidateSchema());
148                 
149                 dom4jDoc = reader.read(new StringReader(xml));
150             }
151             else if (src instanceof org.dom4j.Document)
152             {
153                 dom4jDoc = (org.dom4j.Document)src;
154             }
155             else
156             {
157                 logger.error("Non-XML message payload: " + src.getClass().toString());
158                 return;
159             }
160 
161             if (dom4jDoc != null)
162             {
163                 if (splitExpression.length() > 0)
164                 {
165                     XPath xpath = dom4jDoc.createXPath(splitExpression);
166                     if (namespaces != null)
167                     {
168                         xpath.setNamespaceURIs(namespaces);
169                     }
170 
171                     List foundNodes = xpath.selectNodes(dom4jDoc);
172                     if (enableCorrelation != ENABLE_CORRELATION_NEVER)
173                     {
174                         message.setCorrelationGroupSize(foundNodes.size());
175                     }
176                     if (logger.isDebugEnabled())
177                     {
178                         logger.debug("Split into " + foundNodes.size());
179                     }
180 
181                     List parts = new LinkedList();
182                     // Rather than reparsing these when individual messages are
183                     // created, lets do it now
184                     // We can also avoid parsing the Xml again altogether
185                     for (Iterator iterator = foundNodes.iterator(); iterator.hasNext();)
186                     {
187                         Node node = (Node)iterator.next();
188                         if (node instanceof Element)
189                         {
190                             // Can't do detach here just in case the source object
191                             // was a document.
192                             node = (Node)node.clone();
193                             parts.add(DocumentHelper.createDocument((Element)node));
194                         }
195                         else
196                         {
197                             logger.warn("Dcoument node: " + node.asXML()
198                                         + " is not an element and thus is not a valid part");
199                         }
200                     }
201                     nodesContext.set(parts);
202                 }
203             }
204             else
205             {
206                 logger.warn("Unsupported message type, ignoring");
207             }
208         }
209         catch (Exception ex)
210         {
211             throw new IllegalArgumentException("Failed to initialise the payload: "
212                                                + ExceptionUtils.getStackTrace(ex));
213         }
214 
215         Map theProperties = new HashMap();
216         for (Iterator iterator = message.getPropertyNames().iterator(); iterator.hasNext();)
217         {
218             String propertyKey = (String)iterator.next();
219             theProperties.put(propertyKey, message.getProperty(propertyKey));
220         }
221         propertiesContext.set(theProperties);
222     }
223 
224     // @Override
225     protected void cleanup()
226     {
227         nodesContext.set(null);
228         propertiesContext.set(null);
229         super.cleanup();
230     }
231     
232     /**
233      * Retrieves a specific message part for the given endpoint. the message will
234      * then be routed via the provider.
235      * 
236      * @param message the current message being processed
237      * @param endpoint the endpoint that will be used to route the resulting message
238      *            part
239      * @return the message part to dispatch
240      */
241     protected UMOMessage getMessagePart(UMOMessage message, UMOEndpoint endpoint)
242     {
243         List nodes = (List)nodesContext.get();
244 
245         if (nodes == null)
246         {
247             logger.error("Error: nodes are null");
248             return null;
249         }
250 
251         for (Iterator i = nodes.iterator(); i.hasNext();)
252         {
253             Document doc = (Document)i.next();
254 
255             try
256             {
257                 Map theProperties = (Map)propertiesContext.get();
258                 UMOMessage result = new MuleMessage(doc, new HashMap(theProperties));
259 
260                 if (endpoint.getFilter() == null || endpoint.getFilter().accept(result))
261                 {
262                     if (logger.isDebugEnabled())
263                     {
264                         logger.debug("Endpoint filter matched for node " + i + " of " + nodes.size()
265                                      + ". Routing message over: " + endpoint.getEndpointURI().toString());
266                     }
267                     i.remove();
268                     return result;
269                 }
270                 else
271                 {
272                     if (logger.isDebugEnabled())
273                     {
274                         logger.debug("Endpoint filter did not match, returning null");
275                     }
276                 }
277             }
278             catch (Exception e)
279             {
280                 logger.error("Unable to create message for node at position " + i, e);
281                 return null;
282             }
283         }
284 
285         return null;
286     }
287 
288     protected void setDoSchemaValidation(SAXReader reader, boolean validate) throws Exception
289     {
290         reader.setValidation(validate);
291         reader.setFeature(APACHE_XML_FEATURES_VALIDATION_SCHEMA, validate);
292         reader.setFeature(APACHE_XML_FEATURES_VALIDATION_SCHEMA_FULL_CHECKING, true);
293         
294         /*
295          * By default we're not validating against an XSD. If this is the case,
296          * there's no need to continue here, so we bail.
297          */
298         if (!validate)
299         {
300             return;
301         }
302 
303         InputStream xsdAsStream = IOUtils.getResourceAsStream(getExternalSchemaLocation(), getClass());
304         if (xsdAsStream == null)
305         {
306             throw new IllegalArgumentException("Couldn't find schema at "
307                                                + getExternalSchemaLocation());
308         }
309 
310         // Set schema language property (must be done before the schemaSource
311         // is set)
312         reader.setProperty(JAXP_PROPERTIES_SCHEMA_LANGUAGE, JAXP_PROPERTIES_SCHEMA_LANGUAGE_VALUE);
313 
314         // Need this one to map schemaLocation to a physical location
315         reader.setProperty(JAXP_PROPERTIES_SCHEMA_SOURCE, xsdAsStream);
316     }
317 }