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.expression;
8   
9   import org.mule.api.MuleContext;
10  import org.mule.api.MuleMessage;
11  import org.mule.api.MuleRuntimeException;
12  import org.mule.api.context.MuleContextAware;
13  import org.mule.api.context.notification.MuleContextNotificationListener;
14  import org.mule.api.expression.ExpressionEvaluator;
15  import org.mule.api.expression.ExpressionRuntimeException;
16  import org.mule.api.lifecycle.Disposable;
17  import org.mule.api.lifecycle.Initialisable;
18  import org.mule.api.lifecycle.InitialisationException;
19  import org.mule.api.registry.RegistrationException;
20  import org.mule.config.i18n.CoreMessages;
21  import org.mule.context.notification.MuleContextNotification;
22  import org.mule.module.xml.i18n.XmlMessages;
23  import org.mule.module.xml.util.NamespaceManager;
24  
25  import java.util.ArrayList;
26  import java.util.Iterator;
27  import java.util.List;
28  import java.util.Map;
29  import java.util.WeakHashMap;
30  
31  import org.dom4j.Document;
32  import org.jaxen.JaxenException;
33  import org.jaxen.XPath;
34  import org.mule.transformer.types.DataTypeFactory;
35  
36  /**
37   * Provides a base class for XPath property extractors. The XPath engine used is jaxen (http://jaxen.org) which supports
38   * XPath queries on other object models such as JavaBeans as well as Xml
39   */
40  public abstract class AbstractXPathExpressionEvaluator implements ExpressionEvaluator, Initialisable, Disposable, MuleContextAware
41  {
42      private Map<String, XPath> cache = new WeakHashMap<String, XPath>(8);
43  
44      private MuleContext muleContext;
45      private NamespaceManager namespaceManager;
46  
47      public void setMuleContext(MuleContext context)
48      {
49          this.muleContext = context;
50  
51      }
52  
53      public void initialise() throws InitialisationException
54      {
55          try
56          {
57              /*
58                  Workaround for standalone mode, when registry bootstrap order may be non-deterministic and lead
59                  to failures on startup.
60  
61                  initialise() can't do any lookups as it will have spring create and init beans for things like
62                  global endpoints, interfering with current lifecycle and leading to failure.
63                  TODO AP/RM this will be solved by the @Inject annotation or phase, as discussed
64               */
65              this.muleContext.registerListener(new MuleContextNotificationListener<MuleContextNotification>(){
66  
67                  public void onNotification(MuleContextNotification notification)
68                  {
69                      // CONTEXT_INITIALIZED fires too soon, before registry is inited, thus using this one
70                      if (MuleContextNotification.CONTEXT_STARTING == notification.getAction())
71                      {
72                          try
73                          {
74                              namespaceManager = muleContext.getRegistry().lookupObject(NamespaceManager.class);
75                          }
76                          catch (RegistrationException e)
77                          {
78                              throw new RuntimeException(e);
79                          }
80                      }
81                  }
82              });
83          }
84          catch (Throwable t)
85          {
86              throw new InitialisationException(t, this);
87          }
88      }
89  
90      public void inject() throws Exception
91      {
92          try
93          {
94              namespaceManager = muleContext.getRegistry().lookupObject(NamespaceManager.class);
95          }
96          catch (RegistrationException e)
97          {
98              throw new ExpressionRuntimeException(CoreMessages.failedToLoad("NamespaceManager"), e);
99          }
100     }
101 
102     /** {@inheritDoc} */
103     public Object evaluate(String expression, MuleMessage message)
104     {
105         try
106         {
107             Object payload = message.getPayload();
108             //we need to convert to a Dom if its an XML string
109             if(payload instanceof String)
110             {
111                 payload = message.getPayload(DataTypeFactory.create(Document.class));
112             }
113 
114             List<?> result;
115 
116             /*  XPath context state is not thread safe so synchronization must be enforced when adding a new namespace and
117                 on evaluation when the context is read.
118              */
119             XPath xpath = getXPath(expression, payload);
120             synchronized (xpath)
121             {
122                 result = xpath.selectNodes(payload);
123             }
124 
125             result = extractResultsFromNodes(result);
126             if(result.size()==1)
127             {
128                 return result.get(0);
129             }
130             else if(result.size()==0)
131             {
132                 return null;
133             }
134             else
135             {
136                 return result;
137             }
138         }
139         catch (Exception e)
140         {
141             throw new MuleRuntimeException(XmlMessages.failedToProcessXPath(expression), e);
142         }
143     }
144 
145     protected void addNamespaces(NamespaceManager manager, XPath xpath)
146     {
147         for (Iterator<?> iterator = manager.getNamespaces().entrySet().iterator(); iterator.hasNext();)
148         {
149             Map.Entry<?, ?> entry = (Map.Entry<?, ?>)iterator.next();
150             try
151             {
152                 xpath.addNamespace(entry.getKey().toString(), entry.getValue().toString());
153             }
154             catch (JaxenException e)
155             {
156                 throw new ExpressionRuntimeException(XmlMessages.failedToRegisterNamespace(entry.getKey().toString(), entry.getValue().toString()));
157             }
158         }
159     }
160 
161     /** {@inheritDoc} */
162     public final void setName(String name)
163     {
164         throw new UnsupportedOperationException("setName");
165     }
166 
167     /*
168         The cache is not thread safe, more than one instance of the same xpath can be created, it wouldn't be an
169         issue in this case since one will eventually be selected for GC
170         A ReadWriteLock could be used to guard the cache but it would add unnecessary complexity
171      */
172     protected XPath getXPath(String expression, Object object) throws JaxenException
173     {
174         String xPathCacheKey = expression + getXPathClassName(object);
175         XPath xpath = cache.get(xPathCacheKey);
176         if(xpath==null)
177         {
178             xpath = createXPath(expression, object);
179             synchronized (xpath)
180             {
181                 if(namespaceManager!=null)
182                 {
183                     addNamespaces(namespaceManager, xpath);
184                 }
185             }
186             cache.put(xPathCacheKey, xpath);
187         }
188         return xpath;
189     }
190 
191     protected String getXPathClassName(Object object)
192     {
193         return getClass().getName();
194     }
195 
196     protected abstract XPath createXPath(String expression, Object object) throws JaxenException;
197 
198     protected List<?> extractResultsFromNodes(List<?> results)
199     {
200         if (results == null)
201         {
202             return null;
203         }
204         List<Object> newResults = new ArrayList<Object>(results.size());
205         for (Object o : results)
206         {
207             newResults.add(extractResultFromNode(o));
208         }
209         return newResults;
210     }
211 
212     /**
213      * A lifecycle method where implementor should free up any resources. If an
214      * exception is thrown it should just be logged and processing should continue.
215      * This method should not throw Runtime exceptions.
216      */
217     public void dispose()
218     {
219         cache.clear();
220     }
221 
222     public NamespaceManager getNamespaceManager()
223     {
224         return namespaceManager;
225     }
226 
227     public void setNamespaceManager(NamespaceManager namespaceManager)
228     {
229         this.namespaceManager = namespaceManager;
230     }
231 
232     public MuleContext getMuleContext()
233     {
234         return muleContext;
235     }
236 
237     protected abstract Object extractResultFromNode(Object result);
238 }