View Javadoc

1   /*
2    * $Id: FilteringXmlMessageSplitter.java 7976 2007-08-21 14:26:13Z 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 static final ThreadLocal propertiesContext = new ThreadLocal();
61      protected static 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     protected void initialise(UMOMessage message)
116     {
117         if (logger.isDebugEnabled())
118         {
119             if (splitExpression.length() == 0)
120             {
121                 logger.warn("splitExpression is not specified, no processing will take place");
122             }
123             else
124             {
125                 logger.debug("splitExpression is " + splitExpression);
126             }
127         }
128 
129         Object src = message.getPayload();
130 
131         try
132         {
133             if (src instanceof byte[])
134             {
135                 src = new String((byte[])src);
136             }
137 
138             Document dom4jDoc;
139 
140             if (src instanceof String)
141             {
142                 String xml = (String)src;
143                 SAXReader reader = new SAXReader();
144                 setDoSchemaValidation(reader, isValidateSchema());
145                 
146                 dom4jDoc = reader.read(new StringReader(xml));
147             }
148             else if (src instanceof org.dom4j.Document)
149             {
150                 dom4jDoc = (org.dom4j.Document)src;
151             }
152             else
153             {
154                 logger.error("Non-XML message payload: " + src.getClass().toString());
155                 return;
156             }
157 
158             if (dom4jDoc != null)
159             {
160                 if (splitExpression.length() > 0)
161                 {
162                     XPath xpath = dom4jDoc.createXPath(splitExpression);
163                     if (namespaces != null)
164                     {
165                         xpath.setNamespaceURIs(namespaces);
166                     }
167 
168                     List foundNodes = xpath.selectNodes(dom4jDoc);
169                     if (logger.isDebugEnabled())
170                     {
171                         logger.debug("Split into " + foundNodes.size());
172                     }
173 
174                     List parts = new LinkedList();
175                     // Rather than reparsing these when individual messages are
176                     // created, lets do it now
177                     // We can also avoid parsing the Xml again altogether
178                     for (Iterator iterator = foundNodes.iterator(); iterator.hasNext();)
179                     {
180                         Node node = (Node)iterator.next();
181                         if (node instanceof Element)
182                         {
183                             // Can't do detach here just in case the source object
184                             // was a document.
185                             node = (Node)node.clone();
186                             parts.add(DocumentHelper.createDocument((Element)node));
187                         }
188                         else
189                         {
190                             logger.warn("Dcoument node: " + node.asXML()
191                                         + " is not an element and thus is not a valid part");
192                         }
193                     }
194                     nodesContext.set(parts);
195                 }
196             }
197             else
198             {
199                 logger.warn("Unsupported message type, ignoring");
200             }
201         }
202         catch (Exception ex)
203         {
204             throw new IllegalArgumentException("Failed to initialise the payload: "
205                                                + ExceptionUtils.getStackTrace(ex));
206         }
207 
208         Map theProperties = new HashMap();
209         for (Iterator iterator = message.getPropertyNames().iterator(); iterator.hasNext();)
210         {
211             String propertyKey = (String)iterator.next();
212             theProperties.put(propertyKey, message.getProperty(propertyKey));
213         }
214         propertiesContext.set(theProperties);
215     }
216 
217     /**
218      * Retrieves a specific message part for the given endpoint. the message will
219      * then be routed via the provider.
220      * 
221      * @param message the current message being processed
222      * @param endpoint the endpoint that will be used to route the resulting message
223      *            part
224      * @return the message part to dispatch
225      */
226     protected UMOMessage getMessagePart(UMOMessage message, UMOEndpoint endpoint)
227     {
228         List nodes = (List)nodesContext.get();
229 
230         if (nodes == null)
231         {
232             logger.error("Error: nodes are null");
233             return null;
234         }
235 
236         for (Iterator i = nodes.iterator(); i.hasNext();)
237         {
238             Document doc = (Document)i.next();
239 
240             try
241             {
242                 Map theProperties = (Map)propertiesContext.get();
243                 UMOMessage result = new MuleMessage(doc, new HashMap(theProperties));
244 
245                 if (endpoint.getFilter() == null || endpoint.getFilter().accept(result))
246                 {
247                     if (logger.isDebugEnabled())
248                     {
249                         logger.debug("Endpoint filter matched for node " + i + " of " + nodes.size()
250                                      + ". Routing message over: " + endpoint.getEndpointURI().toString());
251                     }
252                     i.remove();
253                     return result;
254                 }
255                 else
256                 {
257                     if (logger.isDebugEnabled())
258                     {
259                         logger.debug("Endpoint filter did not match, returning null");
260                     }
261                 }
262             }
263             catch (Exception e)
264             {
265                 logger.error("Unable to create message for node at position " + i, e);
266                 return null;
267             }
268         }
269 
270         return null;
271     }
272 
273     protected void setDoSchemaValidation(SAXReader reader, boolean validate) throws Exception
274     {
275         reader.setValidation(validate);
276         reader.setFeature(APACHE_XML_FEATURES_VALIDATION_SCHEMA, validate);
277         reader.setFeature(APACHE_XML_FEATURES_VALIDATION_SCHEMA_FULL_CHECKING, true);
278         
279         /*
280          * By default we're not validating against an XSD. If this is the case,
281          * there's no need to continue here, so we bail.
282          */
283         if (!validate) {
284          return;
285         }
286 
287         InputStream xsdAsStream = IOUtils.getResourceAsStream(getExternalSchemaLocation(), getClass());
288         if (xsdAsStream == null)
289         {
290             throw new IllegalArgumentException("Couldn't find schema at "
291                                                + getExternalSchemaLocation());
292         }
293 
294         // Set schema language property (must be done before the schemaSource
295         // is set)
296         reader.setProperty(JAXP_PROPERTIES_SCHEMA_LANGUAGE, JAXP_PROPERTIES_SCHEMA_LANGUAGE_VALUE);
297 
298         // Need this one to map schemaLocation to a physical location
299         reader.setProperty(JAXP_PROPERTIES_SCHEMA_SOURCE, xsdAsStream);
300     }
301 }