View Javadoc

1   /*
2    * $Id: DynamicEntryPoint.java 7963 2007-08-21 08:53:15Z 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.impl.model.resolvers;
12  
13  import org.mule.config.MuleProperties;
14  import org.mule.config.i18n.CoreMessages;
15  import org.mule.impl.MuleMessage;
16  import org.mule.impl.NoSatisfiableMethodsException;
17  import org.mule.impl.OptimizedRequestContext;
18  import org.mule.impl.TooManySatisfiableMethodsException;
19  import org.mule.impl.VoidResult;
20  import org.mule.providers.NullPayload;
21  import org.mule.umo.UMOEventContext;
22  import org.mule.umo.lifecycle.Callable;
23  import org.mule.umo.model.UMOEntryPoint;
24  import org.mule.util.ClassUtils;
25  
26  import java.lang.reflect.InvocationTargetException;
27  import java.lang.reflect.Method;
28  import java.util.Arrays;
29  import java.util.HashSet;
30  import java.util.Iterator;
31  import java.util.List;
32  import java.util.Set;
33  
34  import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentHashMap;
35  import edu.emory.mathcs.backport.java.util.concurrent.ConcurrentMap;
36  import org.apache.commons.lang.BooleanUtils;
37  import org.apache.commons.logging.Log;
38  import org.apache.commons.logging.LogFactory;
39  
40  /**
41   * <code>DynamicEntryPoint</code> is used to detemine the entry point on a bean
42   * after an event has been received for it. The entrypoint is then discovered using
43   * the event payload type as the argument. An entry point will try and match for
44   * different argument types, so it's possible to have multiple entry points on a
45   * single component.
46   */
47  
48  public class DynamicEntryPoint implements UMOEntryPoint
49  {
50      /**
51       * logger used by this class
52       */
53      protected static final Log logger = LogFactory.getLog(DynamicEntryPoint.class);
54  
55      // we don't want to match these methods when looking for a service method
56      protected static final Set IgnoredMethodNames = new HashSet(Arrays.asList(new String[]{"equals",
57          "getInvocationHandler"}));
58  
59      // @GuardedBy(itself)
60      private final ConcurrentMap entryPoints = new ConcurrentHashMap();
61  
62      public DynamicEntryPoint()
63      {
64          super();
65      }
66  
67      protected Method addMethodByArgumentType(Method method, String payloadClass)
68      {
69          Method previousMethod = (Method) entryPoints.putIfAbsent(payloadClass, method);
70          return (previousMethod != null ? previousMethod : method);
71      }
72  
73      protected Method addMethodByName(Method method, String payloadClass)
74      {
75          String methodName = method.getName();
76  
77          ConcurrentMap argumentTypes = (ConcurrentMap) entryPoints.get(methodName);
78          if (argumentTypes == null)
79          {
80              argumentTypes = new ConcurrentHashMap();
81              ConcurrentMap previousTypes = (ConcurrentMap) entryPoints.putIfAbsent(methodName, argumentTypes);
82              if (previousTypes != null)
83              {
84                  argumentTypes = previousTypes;
85              }
86          }
87  
88          Method previousMethod = (Method) argumentTypes.putIfAbsent(payloadClass, method);
89          return (previousMethod != null ? previousMethod : method);
90      }
91  
92      protected Method getMethodByArgumentType(String argumentType)
93      {
94          return (Method) entryPoints.get(argumentType);
95      }
96  
97      protected Method getMethodByName(String methodName, String argumentType)
98      {
99          ConcurrentMap argumentTypes = (ConcurrentMap) entryPoints.get(methodName);
100         return (argumentTypes != null ? (Method) argumentTypes.get(argumentType) : null);
101     }
102 
103     public Object invoke(Object component, UMOEventContext context) throws Exception
104     {
105         Method method = null;
106         Object payload = null;
107 
108         // Transports such as SOAP need to ignore the method property
109         boolean ignoreMethod = BooleanUtils.toBoolean((Boolean) context.getMessage().removeProperty(
110             MuleProperties.MULE_IGNORE_METHOD_PROPERTY));
111 
112         if (!ignoreMethod)
113         {
114             // Check for method override and remove it from the event
115             Object methodOverride = context.getMessage().removeProperty(MuleProperties.MULE_METHOD_PROPERTY);
116 
117             if (methodOverride instanceof Method)
118             {
119                 // Methods are (hopefully) directly useable
120                 method = (Method) methodOverride;
121             }
122             else if (methodOverride != null)
123             {
124                 payload = context.getTransformedMessage();
125                 String payloadClassName = payload.getClass().getName();
126 
127                 // try lookup first
128                 String methodOverrideName = methodOverride.toString();
129                 method = this.getMethodByName(methodOverrideName, payloadClassName);
130 
131                 // method is not yet in the cache, so find it by name
132                 if (method == null)
133                 {
134                     // get all methods that match the current argument types
135                     List matchingMethods = ClassUtils.getSatisfiableMethods(component.getClass(), ClassUtils
136                         .getClassTypes(payload), true, true, IgnoredMethodNames);
137 
138                     // try to find the method matching the methodOverride
139                     for (Iterator i = matchingMethods.iterator(); i.hasNext();)
140                     {
141                         Method candidate = (Method) i.next();
142                         if (candidate.getName().equals(methodOverride))
143                         {
144                             method = candidate;
145                             break;
146                         }
147                     }
148 
149                     // this will throw up unless the component is a Callable
150                     this.validateMethod(component, method, methodOverrideName);
151 
152                     // if validateMethod didn't complain AND we have a valid method
153                     // reference, add it to the cache
154                     if (method != null)
155                     {
156                         method = this.addMethodByName(method, payloadClassName);
157                     }
158                 }
159             }
160         }
161 
162         // do we need to lookup the method?
163         if (method == null)
164         {
165             // prefer Callable
166             if (component instanceof Callable)
167             {
168                 method = Callable.class.getMethods()[0];
169                 payload = context;
170             }
171             else
172             {
173                 // no Callable: try to find the method dynamically
174                 // first we try to find a method that accepts UMOEventContext
175                 method = this.getMethodByArgumentType(context.getClass().getName());
176                 if (method == null)
177                 {
178                     // if that failed we try to find the method by payload
179                     payload = context.getTransformedMessage();
180                     method = this.getMethodByArgumentType(payload.getClass().getName());
181                     if (method != null)
182                     {
183                         OptimizedRequestContext.unsafeRewriteEvent(new MuleMessage(payload, context.getMessage()));
184                     }
185                 }
186                 else
187                 {
188                     payload = context;
189                 }
190             }
191         }
192 
193         // method is not in cache, so find it
194         if (method == null)
195         {
196             // do any methods on the component accept a context?
197             List methods = ClassUtils.getSatisfiableMethods(component.getClass(), ClassUtils
198                 .getClassTypes(context), true, false, IgnoredMethodNames);
199 
200             int numMethods = methods.size();
201             if (numMethods > 1)
202             {
203                 // too many methods match the context argument
204                 TooManySatisfiableMethodsException tmsmex = new TooManySatisfiableMethodsException(component
205                     .getClass(), methods);
206                 throw new InvocationTargetException(tmsmex, "There must be only one method accepting "
207                                 + context.getClass().getName() + " in component "
208                                 + component.getClass().getName());
209             }
210             else if (numMethods == 1)
211             {
212                 // found exact match for method with context argument
213                 payload = context;
214                 method = this.addMethodByArgumentType((Method) methods.get(0), payload.getClass().getName());
215             }
216             else
217             {
218                 // no method for context: try payload
219                 payload = context.getTransformedMessage();
220                 OptimizedRequestContext.unsafeRewriteEvent(new MuleMessage(payload, context.getMessage()));
221 
222                 methods = ClassUtils.getSatisfiableMethods(component.getClass(), ClassUtils
223                     .getClassTypes(payload), true, true, IgnoredMethodNames);
224 
225                 numMethods = methods.size();
226 
227                 if (numMethods > 1)
228                 {
229                     // too many methods match the payload argument
230                     throw new TooManySatisfiableMethodsException(component.getClass(), methods);
231                 }
232                 else if (numMethods == 1)
233                 {
234                     // found exact match for payload argument
235                     method = this.addMethodByArgumentType((Method) methods.get(0), payload.getClass()
236                         .getName());
237                 }
238                 else
239                 {
240                     // no method for payload argument either - bail out
241                     throw new NoSatisfiableMethodsException(component.getClass(), ClassUtils
242                         .getClassTypes(payload));
243                 }
244             }
245         }
246 
247         if (payload == null)
248         {
249             payload = context.getTransformedMessage();
250             OptimizedRequestContext.unsafeRewriteEvent(new MuleMessage(payload, context.getMessage()));
251         }
252 
253         if (logger.isDebugEnabled())
254         {
255             logger.debug("Dynamic Entrypoint using method: " + component.getClass().getName() + "."
256                             + method.getName() + "(" + payload.getClass().getName() + ")");
257         }
258 
259         return this.invokeMethod(component, method, payload);
260     }
261 
262     /**
263      * This method will actually invoke the given method on the given component.
264      */
265     protected Object invokeMethod(Object component, Method method, Object argument)
266         throws InvocationTargetException, IllegalAccessException
267     {
268         String methodCall = null;
269 
270         if (logger.isDebugEnabled())
271         {
272             methodCall = component.getClass().getName() + "." + method.getName() + "("
273                             + argument.getClass().getName() + ")";
274             logger.debug("Invoking " + methodCall);
275         }
276 
277         // TODO MULE-1088: in order to properly support an array as argument for a
278         // component method, the block below would need to be removed. Unfortunately
279         // this would break the LoanBroker, which passes a BankQuoteRequest in an
280         // array even though the correct method only takes a single non-Array
281         // argument. This is most likely SOAP behaviour; see the JIRA for more.
282         // It is not entirely clear to me whether this is a bug in the LoanBroker or
283         // intended behaviour, and what the check for Object[] assignment
284         // compatibility is supposed to do/prevent in the first place?
285         // Any kind of interpretation/rewriting of the array argument is pretty
286         // much futile at this point, because we have already found a matching method
287         // - otherwise we wouldn't be here!
288 
289         // this will wrap the given argument for the invocation
290         Object[] invocationArgs;
291 
292         if (argument.getClass().isArray())
293         {
294             if (Object[].class.isAssignableFrom(argument.getClass()))
295             {
296                 invocationArgs = (Object[]) argument;
297             }
298             else
299             {
300                 invocationArgs = new Object[]{argument};
301             }
302         }
303         else if (argument instanceof NullPayload)
304         {
305             invocationArgs = null;
306         }
307         else
308         {
309             invocationArgs = new Object[]{argument};
310         }
311 
312         Object result = method.invoke(component, invocationArgs);
313         if (method.getReturnType().equals(Void.TYPE))
314         {
315             result = VoidResult.getInstance();
316         }
317 
318         if (logger.isDebugEnabled())
319         {
320             logger.debug("Result of call " + methodCall + " is " + (result == null ? "null" : "not null"));
321         }
322 
323         return result;
324     }
325 
326     /**
327      * This method can be used to validate that the method exists and is allowed to
328      * be executed.
329      */
330     protected void validateMethod(Object component, Method method, String methodName)
331         throws NoSuchMethodException
332     {
333         boolean fallback = component instanceof Callable;
334 
335         if (method != null)
336         {
337             // This will throw NoSuchMethodException if it doesn't exist
338             try
339             {
340                 component.getClass().getMethod(method.getName(), method.getParameterTypes());
341             }
342             catch (NoSuchMethodException e)
343             {
344                 if (!fallback)
345                 {
346                     throw e;
347                 }
348             }
349         }
350         else
351         {
352             if (!fallback)
353             {
354                 throw new NoSuchMethodException(
355                     CoreMessages.methodWithParamsNotFoundOnObject(methodName, "unknown", 
356                         component.getClass()).toString());
357             }
358         }
359     }
360 
361 }