001/**
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.camel.component.bean;
018
019import java.lang.annotation.Annotation;
020import java.lang.reflect.Method;
021import java.lang.reflect.Modifier;
022import java.lang.reflect.Proxy;
023import java.util.ArrayList;
024import java.util.Arrays;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.Comparator;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.Iterator;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034
035import org.apache.camel.AttachmentObjects;
036import org.apache.camel.Attachments;
037import org.apache.camel.Body;
038import org.apache.camel.CamelContext;
039import org.apache.camel.Exchange;
040import org.apache.camel.ExchangeException;
041import org.apache.camel.ExchangeProperty;
042import org.apache.camel.Expression;
043import org.apache.camel.Handler;
044import org.apache.camel.Header;
045import org.apache.camel.Headers;
046import org.apache.camel.Message;
047import org.apache.camel.OutHeaders;
048import org.apache.camel.Properties;
049import org.apache.camel.Property;
050import org.apache.camel.PropertyInject;
051import org.apache.camel.builder.ExpressionBuilder;
052import org.apache.camel.language.LanguageAnnotation;
053import org.apache.camel.spi.Registry;
054import org.apache.camel.util.CastUtils;
055import org.apache.camel.util.IntrospectionSupport;
056import org.apache.camel.util.ObjectHelper;
057import org.apache.camel.util.StringHelper;
058import org.apache.camel.util.StringQuoteHelper;
059import org.slf4j.Logger;
060import org.slf4j.LoggerFactory;
061
062/**
063 * Represents the metadata about a bean type created via a combination of
064 * introspection and annotations together with some useful sensible defaults
065 */
066public class BeanInfo {
067    private static final Logger LOG = LoggerFactory.getLogger(BeanInfo.class);
068    private static final String CGLIB_CLASS_SEPARATOR = "$$";
069    private static final List<Method> EXCLUDED_METHODS = new ArrayList<Method>();
070    private final CamelContext camelContext;
071    private final BeanComponent component;
072    private final Class<?> type;
073    private final ParameterMappingStrategy strategy;
074    private final MethodInfo defaultMethod;
075    // shared state with details of operations introspected from the bean, created during the constructor
076    private Map<String, List<MethodInfo>> operations = new HashMap<String, List<MethodInfo>>();
077    private List<MethodInfo> operationsWithBody = new ArrayList<MethodInfo>();
078    private List<MethodInfo> operationsWithNoBody = new ArrayList<MethodInfo>();
079    private List<MethodInfo> operationsWithCustomAnnotation = new ArrayList<MethodInfo>();
080    private List<MethodInfo> operationsWithHandlerAnnotation = new ArrayList<MethodInfo>();
081    private Map<Method, MethodInfo> methodMap = new HashMap<Method, MethodInfo>();
082    private boolean publicConstructors;
083
084    static {
085        // exclude all java.lang.Object methods as we dont want to invoke them
086        EXCLUDED_METHODS.addAll(Arrays.asList(Object.class.getMethods()));
087        // exclude all java.lang.reflect.Proxy methods as we dont want to invoke them
088        EXCLUDED_METHODS.addAll(Arrays.asList(Proxy.class.getMethods()));
089        try {
090            // but keep toString as this method is okay
091            EXCLUDED_METHODS.remove(Object.class.getMethod("toString"));
092            EXCLUDED_METHODS.remove(Proxy.class.getMethod("toString"));
093        } catch (Throwable e) {
094            // ignore
095        }
096    }
097
098    public BeanInfo(CamelContext camelContext, Class<?> type) {
099        this(camelContext, type, createParameterMappingStrategy(camelContext));
100    }
101
102    public BeanInfo(CamelContext camelContext, Method explicitMethod) {
103        this(camelContext, explicitMethod.getDeclaringClass(), explicitMethod, createParameterMappingStrategy(camelContext));
104    }
105
106    public BeanInfo(CamelContext camelContext, Class<?> type, ParameterMappingStrategy strategy) {
107        this(camelContext, type, null, strategy);
108    }
109
110    public BeanInfo(CamelContext camelContext, Class<?> type, Method explicitMethod, ParameterMappingStrategy strategy) {
111        this.camelContext = camelContext;
112        this.type = type;
113        this.strategy = strategy;
114        this.component = camelContext.getComponent("bean", BeanComponent.class);
115
116        final BeanInfoCacheKey key = new BeanInfoCacheKey(type, explicitMethod);
117
118        // lookup if we have a bean info cache
119        BeanInfo beanInfo = component.getBeanInfoFromCache(key);
120        if (beanInfo != null) {
121            // copy the values from the cache we need
122            defaultMethod = beanInfo.defaultMethod;
123            operations = beanInfo.operations;
124            operationsWithBody = beanInfo.operationsWithBody;
125            operationsWithNoBody = beanInfo.operationsWithNoBody;
126            operationsWithCustomAnnotation = beanInfo.operationsWithCustomAnnotation;
127            operationsWithHandlerAnnotation = beanInfo.operationsWithHandlerAnnotation;
128            methodMap = beanInfo.methodMap;
129            publicConstructors = beanInfo.publicConstructors;
130            return;
131        }
132
133        if (explicitMethod != null) {
134            // must be a valid method
135            if (!isValidMethod(type, explicitMethod)) {
136                throw new IllegalArgumentException("The method " + explicitMethod + " is not valid (for example the method must be public)");
137            }
138            introspect(getType(), explicitMethod);
139        } else {
140            introspect(getType());
141        }
142
143        // if there are only 1 method with 1 operation then select it as a default/fallback method
144        MethodInfo method = null;
145        if (operations.size() == 1) {
146            List<MethodInfo> methods = operations.values().iterator().next();
147            if (methods.size() == 1) {
148                method = methods.get(0);
149            }
150        }
151        defaultMethod = method;
152
153        // mark the operations lists as unmodifiable, as they should not change during runtime
154        // to keep this code thread safe
155        operations = Collections.unmodifiableMap(operations);
156        operationsWithBody = Collections.unmodifiableList(operationsWithBody);
157        operationsWithNoBody = Collections.unmodifiableList(operationsWithNoBody);
158        operationsWithCustomAnnotation = Collections.unmodifiableList(operationsWithCustomAnnotation);
159        operationsWithHandlerAnnotation = Collections.unmodifiableList(operationsWithHandlerAnnotation);
160        methodMap = Collections.unmodifiableMap(methodMap);
161
162        // add new bean info to cache
163        component.addBeanInfoToCache(key, this);
164    }
165
166    public Class<?> getType() {
167        return type;
168    }
169
170    public CamelContext getCamelContext() {
171        return camelContext;
172    }
173
174    public static ParameterMappingStrategy createParameterMappingStrategy(CamelContext camelContext) {
175        // lookup in registry first if there is a user define strategy
176        Registry registry = camelContext.getRegistry();
177        ParameterMappingStrategy answer = registry.lookupByNameAndType(BeanConstants.BEAN_PARAMETER_MAPPING_STRATEGY, ParameterMappingStrategy.class);
178        if (answer == null) {
179            // no then use the default one
180            answer = new DefaultParameterMappingStrategy();
181        }
182
183        return answer;
184    }
185
186    public MethodInvocation createInvocation(Object pojo, Exchange exchange)
187        throws AmbiguousMethodCallException, MethodNotFoundException {
188        return createInvocation(pojo, exchange, null);
189    }
190
191    private MethodInvocation createInvocation(Object pojo, Exchange exchange, Method explicitMethod)
192        throws AmbiguousMethodCallException, MethodNotFoundException {
193        MethodInfo methodInfo = null;
194        
195        // find the explicit method to invoke
196        if (explicitMethod != null) {
197            for (List<MethodInfo> infos : operations.values()) {
198                for (MethodInfo info : infos) {
199                    if (explicitMethod.equals(info.getMethod())) {
200                        return info.createMethodInvocation(pojo, exchange);
201                    }
202                }
203            }
204            throw new MethodNotFoundException(exchange, pojo, explicitMethod.getName());
205        }
206
207        String methodName = exchange.getIn().getHeader(Exchange.BEAN_METHOD_NAME, String.class);
208        if (methodName != null) {
209
210            // do not use qualifier for name
211            String name = methodName;
212            if (methodName.contains("(")) {
213                name = ObjectHelper.before(methodName, "(");
214                // the must be a ending parenthesis
215                if (!methodName.endsWith(")")) {
216                    throw new IllegalArgumentException("Method should end with parenthesis, was " + methodName);
217                }
218                // and there must be an even number of parenthesis in the syntax
219                // (we can use betweenOuterPair as it return null if the syntax is invalid)
220                if (ObjectHelper.betweenOuterPair(methodName, '(', ')') == null) {
221                    throw new IllegalArgumentException("Method should have even pair of parenthesis, was " + methodName);
222                }
223            }
224            boolean emptyParameters = methodName.endsWith("()");
225
226            // special for getClass, as we want the user to be able to invoke this method
227            // for example to log the class type or the likes
228            if ("class".equals(name) || "getClass".equals(name)) {
229                try {
230                    Method method = pojo.getClass().getMethod("getClass");
231                    methodInfo = new MethodInfo(exchange.getContext(), pojo.getClass(), method, Collections.<ParameterInfo>emptyList(), Collections.<ParameterInfo>emptyList(), false, false);
232                } catch (NoSuchMethodException e) {
233                    throw new MethodNotFoundException(exchange, pojo, "getClass");
234                }
235            // special for length on an array type
236            } else if ("length".equals(name) && pojo.getClass().isArray()) {
237                try {
238                    // need to use arrayLength method from ObjectHelper as Camel's bean OGNL support is method invocation based
239                    // and not for accessing fields. And hence we need to create a MethodInfo instance with a method to call
240                    // and therefore use arrayLength from ObjectHelper to return the array length field.
241                    Method method = ObjectHelper.class.getMethod("arrayLength", Object[].class);
242                    ParameterInfo pi = new ParameterInfo(0, Object[].class, null, ExpressionBuilder.mandatoryBodyExpression(Object[].class, true));
243                    List<ParameterInfo> lpi = new ArrayList<ParameterInfo>(1);
244                    lpi.add(pi);
245                    methodInfo = new MethodInfo(exchange.getContext(), pojo.getClass(), method, lpi, lpi, false, false);
246                    // Need to update the message body to be pojo for the invocation
247                    exchange.getIn().setBody(pojo);
248                } catch (NoSuchMethodException e) {
249                    throw new MethodNotFoundException(exchange, pojo, "getClass");
250                }
251            } else {
252                List<MethodInfo> methods = getOperations(name);
253                if (methods != null && methods.size() == 1) {
254                    // only one method then choose it
255                    methodInfo = methods.get(0);
256
257                    // validate that if we want an explicit no-arg method, then that's what we get
258                    if (emptyParameters && methodInfo.hasParameters()) {
259                        throw new MethodNotFoundException(exchange, pojo, methodName, "(with no parameters)");
260                    }
261                } else if (methods != null) {
262                    // there are more methods with that name so we cannot decide which to use
263
264                    // but first let's try to choose a method and see if that complies with the name
265                    // must use the method name which may have qualifiers
266                    methodInfo = chooseMethod(pojo, exchange, methodName);
267
268                    // validate that if we want an explicit no-arg method, then that's what we get
269                    if (emptyParameters) {
270                        if (methodInfo == null || methodInfo.hasParameters()) {
271                            // we could not find a no-arg method with that name
272                            throw new MethodNotFoundException(exchange, pojo, methodName, "(with no parameters)");
273                        }
274                    }
275
276                    if (methodInfo == null || (name != null && !name.equals(methodInfo.getMethod().getName()))) {
277                        throw new AmbiguousMethodCallException(exchange, methods);
278                    }
279                } else {
280                    // a specific method was given to invoke but not found
281                    throw new MethodNotFoundException(exchange, pojo, methodName);
282                }
283            }
284        }
285
286        if (methodInfo == null) {
287            // no name or type
288            methodInfo = chooseMethod(pojo, exchange, null);
289        }
290        if (methodInfo == null) {
291            methodInfo = defaultMethod;
292        }
293        if (methodInfo != null) {
294            LOG.trace("Chosen method to invoke: {} on bean: {}", methodInfo, pojo);
295            return methodInfo.createMethodInvocation(pojo, exchange);
296        }
297
298        LOG.debug("Cannot find suitable method to invoke on bean: {}", pojo);
299        return null;
300    }
301
302    /**
303     * Introspects the given class
304     *
305     * @param clazz the class
306     */
307    private void introspect(Class<?> clazz) {
308        // get the target clazz as it could potentially have been enhanced by CGLIB etc.
309        clazz = getTargetClass(clazz);
310        ObjectHelper.notNull(clazz, "clazz", this);
311
312        LOG.trace("Introspecting class: {}", clazz);
313
314        // does the class have any public constructors?
315        publicConstructors = clazz.getConstructors().length > 0;
316
317        // favor declared methods, and then filter out duplicate interface methods
318        List<Method> methods;
319        if (Modifier.isPublic(clazz.getModifiers())) {
320            LOG.trace("Preferring class methods as class: {} is public accessible", clazz);
321            methods = new ArrayList<Method>(Arrays.asList(clazz.getDeclaredMethods()));
322        } else {
323            LOG.trace("Preferring interface methods as class: {} is not public accessible", clazz);
324            methods = getInterfaceMethods(clazz);
325            // and then we must add its declared methods as well
326            List<Method> extraMethods = Arrays.asList(clazz.getDeclaredMethods());
327            methods.addAll(extraMethods);
328        }
329
330        Set<Method> overrides = new HashSet<Method>();
331
332        // do not remove duplicates form class from the Java itself as they have some "duplicates" we need
333        boolean javaClass = clazz.getName().startsWith("java.") || clazz.getName().startsWith("javax.");
334        if (!javaClass) {
335            // it may have duplicate methods already, even from declared or from interfaces + declared
336            for (Method source : methods) {
337
338                // skip bridge methods in duplicate checks (as the bridge method is inserted by the compiler due to type erasure)
339                if (source.isBridge()) {
340                    continue;
341                }
342
343                for (Method target : methods) {
344                    // skip ourselves
345                    if (ObjectHelper.isOverridingMethod(source, target, true)) {
346                        continue;
347                    }
348                    // skip duplicates which may be assign compatible (favor keep first added method when duplicate)
349                    if (ObjectHelper.isOverridingMethod(source, target, false)) {
350                        overrides.add(target);
351                    }
352                }
353            }
354            methods.removeAll(overrides);
355            overrides.clear();
356        }
357
358        // if we are a public class, then add non duplicate interface classes also
359        if (Modifier.isPublic(clazz.getModifiers())) {
360            // add additional interface methods
361            List<Method> extraMethods = getInterfaceMethods(clazz);
362            for (Method source : extraMethods) {
363                for (Method target : methods) {
364                    if (ObjectHelper.isOverridingMethod(source, target, false)) {
365                        overrides.add(source);
366                    }
367                }
368                for (Method target : methodMap.keySet()) {
369                    if (ObjectHelper.isOverridingMethod(source, target, false)) {
370                        overrides.add(source);
371                    }
372                }
373            }
374            // remove all the overrides methods
375            extraMethods.removeAll(overrides);
376            methods.addAll(extraMethods);
377        }
378
379        // now introspect the methods and filter non valid methods
380        for (Method method : methods) {
381            boolean valid = isValidMethod(clazz, method);
382            LOG.trace("Method: {} is valid: {}", method, valid);
383            if (valid) {
384                introspect(clazz, method);
385            }
386        }
387
388        Class<?> superclass = clazz.getSuperclass();
389        if (superclass != null && !superclass.equals(Object.class)) {
390            introspect(superclass);
391        }
392    }
393
394    /**
395     * Introspects the given method
396     *
397     * @param clazz the class
398     * @param method the method
399     * @return the method info, is newer <tt>null</tt>
400     */
401    private MethodInfo introspect(Class<?> clazz, Method method) {
402        LOG.trace("Introspecting class: {}, method: {}", clazz, method);
403        String opName = method.getName();
404
405        MethodInfo methodInfo = createMethodInfo(clazz, method);
406
407        // methods already registered should be preferred to use instead of super classes of existing methods
408        // we want to us the method from the sub class over super classes, so if we have already registered
409        // the method then use it (we are traversing upwards: sub (child) -> super (farther) )
410        MethodInfo existingMethodInfo = overridesExistingMethod(methodInfo);
411        if (existingMethodInfo != null) {
412            LOG.trace("This method is already overridden in a subclass, so the method from the sub class is preferred: {}", existingMethodInfo);
413            return existingMethodInfo;
414        }
415
416        LOG.trace("Adding operation: {} for method: {}", opName, methodInfo);
417
418        List<MethodInfo> existing = getOperations(opName);
419        if (existing != null) {
420            // we have an overloaded method so add the method info to the same key
421            existing.add(methodInfo);
422        } else {
423            // its a new method we have not seen before so wrap it in a list and add it
424            List<MethodInfo> methods = new ArrayList<MethodInfo>();
425            methods.add(methodInfo);
426            operations.put(opName, methods);
427        }
428
429        if (methodInfo.hasCustomAnnotation()) {
430            operationsWithCustomAnnotation.add(methodInfo);
431        } else if (methodInfo.hasBodyParameter()) {
432            operationsWithBody.add(methodInfo);
433        } else {
434            operationsWithNoBody.add(methodInfo);
435        }
436
437        if (methodInfo.hasHandlerAnnotation()) {
438            operationsWithHandlerAnnotation.add(methodInfo);
439        }
440
441        // must add to method map last otherwise we break stuff
442        methodMap.put(method, methodInfo);
443
444        return methodInfo;
445    }
446
447    /**
448     * Returns the {@link MethodInfo} for the given method if it exists or null
449     * if there is no metadata available for the given method
450     */
451    public MethodInfo getMethodInfo(Method method) {
452        MethodInfo answer = methodMap.get(method);
453        if (answer == null) {
454            // maybe the method overrides, and the method map keeps info of the source override we can use
455            for (Method source : methodMap.keySet()) {
456                if (ObjectHelper.isOverridingMethod(source, method, false)) {
457                    answer = methodMap.get(source);
458                    break;
459                }
460            }
461        }
462
463        if (answer == null) {
464            // maybe the method is defined on a base class?
465            if (type != Object.class) {
466                Class<?> superclass = type.getSuperclass();
467                if (superclass != null && superclass != Object.class) {
468                    BeanInfo superBeanInfo = new BeanInfo(camelContext, superclass, strategy);
469                    return superBeanInfo.getMethodInfo(method);
470                }
471            }
472        }
473        return answer;
474    }
475
476    protected MethodInfo createMethodInfo(Class<?> clazz, Method method) {
477        Class<?>[] parameterTypes = method.getParameterTypes();
478        List<Annotation>[] parametersAnnotations = collectParameterAnnotations(clazz, method);
479
480        List<ParameterInfo> parameters = new ArrayList<ParameterInfo>();
481        List<ParameterInfo> bodyParameters = new ArrayList<ParameterInfo>();
482
483        boolean hasCustomAnnotation = false;
484        boolean hasHandlerAnnotation = ObjectHelper.hasAnnotation(method.getAnnotations(), Handler.class);
485
486        int size = parameterTypes.length;
487        if (LOG.isTraceEnabled()) {
488            LOG.trace("Creating MethodInfo for class: {} method: {} having {} parameters", new Object[]{clazz, method, size});
489        }
490
491        for (int i = 0; i < size; i++) {
492            Class<?> parameterType = parameterTypes[i];
493            Annotation[] parameterAnnotations = parametersAnnotations[i].toArray(new Annotation[parametersAnnotations[i].size()]);
494            Expression expression = createParameterUnmarshalExpression(clazz, method, parameterType, parameterAnnotations);
495            hasCustomAnnotation |= expression != null;
496
497            ParameterInfo parameterInfo = new ParameterInfo(i, parameterType, parameterAnnotations, expression);
498            LOG.trace("Parameter #{}: {}", i, parameterInfo);
499            parameters.add(parameterInfo);
500            if (expression == null) {
501                boolean bodyAnnotation = ObjectHelper.hasAnnotation(parameterAnnotations, Body.class);
502                LOG.trace("Parameter #{} has @Body annotation", i);
503                hasCustomAnnotation |= bodyAnnotation;
504                if (bodyParameters.isEmpty()) {
505                    // okay we have not yet set the body parameter and we have found
506                    // the candidate now to use as body parameter
507                    if (Exchange.class.isAssignableFrom(parameterType)) {
508                        // use exchange
509                        expression = ExpressionBuilder.exchangeExpression();
510                    } else {
511                        // assume it's the body and it must be mandatory convertible to the parameter type
512                        // but we allow null bodies in case the message really contains a null body
513                        expression = ExpressionBuilder.mandatoryBodyExpression(parameterType, true);
514                    }
515                    LOG.trace("Parameter #{} is the body parameter using expression {}", i, expression);
516                    parameterInfo.setExpression(expression);
517                    bodyParameters.add(parameterInfo);
518                } else {
519                    // will ignore the expression for parameter evaluation
520                }
521            }
522            LOG.trace("Parameter #{} has parameter info: ", i, parameterInfo);
523        }
524
525        // now let's add the method to the repository
526        return new MethodInfo(camelContext, clazz, method, parameters, bodyParameters, hasCustomAnnotation, hasHandlerAnnotation);
527    }
528
529    protected List<Annotation>[] collectParameterAnnotations(Class<?> c, Method m) {
530        @SuppressWarnings("unchecked")
531        List<Annotation>[] annotations = new List[m.getParameterTypes().length];
532        for (int i = 0; i < annotations.length; i++) {
533            annotations[i] = new ArrayList<Annotation>();
534        }
535        collectParameterAnnotations(c, m, annotations);
536        return annotations;
537    }
538
539    protected void collectParameterAnnotations(Class<?> c, Method m, List<Annotation>[] a) {
540        try {
541            Annotation[][] pa = c.getDeclaredMethod(m.getName(), m.getParameterTypes()).getParameterAnnotations();
542            for (int i = 0; i < pa.length; i++) {
543                a[i].addAll(Arrays.asList(pa[i]));
544            }
545        } catch (NoSuchMethodException e) {
546            // no method with signature of m declared on c
547        }
548        for (Class<?> i : c.getInterfaces()) {
549            collectParameterAnnotations(i, m, a);
550        }
551        if (!c.isInterface() && c.getSuperclass() != null) {
552            collectParameterAnnotations(c.getSuperclass(), m, a);
553        }
554    }
555
556    /**
557     * Choose one of the available methods to invoke if we can match
558     * the message body to the body parameter
559     *
560     * @param pojo the bean to invoke a method on
561     * @param exchange the message exchange
562     * @param name an optional name of the method that must match, use <tt>null</tt> to indicate all methods
563     * @return the method to invoke or null if no definitive method could be matched
564     * @throws AmbiguousMethodCallException is thrown if cannot choose method due to ambiguity
565     */
566    protected MethodInfo chooseMethod(Object pojo, Exchange exchange, String name) throws AmbiguousMethodCallException {
567        // @Handler should be select first
568        // then any single method that has a custom @annotation
569        // or any single method that has a match parameter type that matches the Exchange payload
570        // and last then try to select the best among the rest
571
572        // must use defensive copy, to avoid altering the shared lists
573        // and we want to remove unwanted operations from these local lists
574        final List<MethodInfo> localOperationsWithBody = new ArrayList<MethodInfo>(operationsWithBody);
575        final List<MethodInfo> localOperationsWithNoBody = new ArrayList<MethodInfo>(operationsWithNoBody);
576        final List<MethodInfo> localOperationsWithCustomAnnotation = new ArrayList<MethodInfo>(operationsWithCustomAnnotation);
577        final List<MethodInfo> localOperationsWithHandlerAnnotation = new ArrayList<MethodInfo>(operationsWithHandlerAnnotation);
578
579        // remove all abstract methods
580        removeAllAbstractMethods(localOperationsWithBody);
581        removeAllAbstractMethods(localOperationsWithNoBody);
582        removeAllAbstractMethods(localOperationsWithCustomAnnotation);
583        removeAllAbstractMethods(localOperationsWithHandlerAnnotation);
584
585        if (name != null) {
586            // filter all lists to only include methods with this name
587            removeNonMatchingMethods(localOperationsWithHandlerAnnotation, name);
588            removeNonMatchingMethods(localOperationsWithCustomAnnotation, name);
589            removeNonMatchingMethods(localOperationsWithBody, name);
590            removeNonMatchingMethods(localOperationsWithNoBody, name);
591        } else {
592            // remove all getter/setter as we do not want to consider these methods
593            removeAllSetterOrGetterMethods(localOperationsWithHandlerAnnotation);
594            removeAllSetterOrGetterMethods(localOperationsWithCustomAnnotation);
595            removeAllSetterOrGetterMethods(localOperationsWithBody);
596            removeAllSetterOrGetterMethods(localOperationsWithNoBody);
597        }
598
599        if (localOperationsWithHandlerAnnotation.size() > 1) {
600            // if we have more than 1 @Handler then its ambiguous
601            throw new AmbiguousMethodCallException(exchange, localOperationsWithHandlerAnnotation);
602        }
603
604        if (localOperationsWithHandlerAnnotation.size() == 1) {
605            // methods with handler should be preferred
606            return localOperationsWithHandlerAnnotation.get(0);
607        } else if (localOperationsWithCustomAnnotation.size() == 1) {
608            // if there is one method with an annotation then use that one
609            return localOperationsWithCustomAnnotation.get(0);
610        }
611
612        // named method and with no parameters
613        boolean noParameters = name != null && name.endsWith("()");
614        if (noParameters && localOperationsWithNoBody.size() == 1) {
615            // if there was a method name configured and it has no parameters, then use the method with no body (eg no parameters)
616            return localOperationsWithNoBody.get(0);
617        } else if (!noParameters && localOperationsWithBody.size() == 1 && localOperationsWithCustomAnnotation.isEmpty()) {
618            // if there is one method with body then use that one
619            return localOperationsWithBody.get(0);
620        }
621
622        Collection<MethodInfo> possibleOperations = new ArrayList<MethodInfo>();
623        possibleOperations.addAll(localOperationsWithBody);
624        possibleOperations.addAll(localOperationsWithCustomAnnotation);
625
626        if (!possibleOperations.isEmpty()) {
627
628            MethodInfo answer = null;
629
630            if (name != null) {
631                // do we have hardcoded parameters values provided from the method name then use that for matching
632                String parameters = ObjectHelper.between(name, "(", ")");
633                if (parameters != null) {
634                    // special as we have hardcoded parameters, so we need to choose method that matches those parameters the best
635                    LOG.trace("Choosing best matching method matching parameters: {}", parameters);
636                    answer = chooseMethodWithMatchingParameters(exchange, parameters, possibleOperations);
637                }
638            }
639            if (answer == null) {
640                // multiple possible operations so find the best suited if possible
641                answer = chooseMethodWithMatchingBody(exchange, possibleOperations, localOperationsWithCustomAnnotation);
642            }
643            if (answer == null && possibleOperations.size() > 1) {
644                answer = getSingleCovariantMethod(possibleOperations);
645            }
646            
647            if (answer == null) {
648                throw new AmbiguousMethodCallException(exchange, possibleOperations);
649            } else {
650                return answer;
651            }
652        }
653
654        // not possible to determine
655        return null;
656    }
657
658    private MethodInfo chooseMethodWithMatchingParameters(Exchange exchange, String parameters, Collection<MethodInfo> operationList)
659        throws AmbiguousMethodCallException {
660        // we have hardcoded parameters so need to match that with the given operations
661        Iterator<?> it = ObjectHelper.createIterator(parameters);
662        int count = 0;
663        while (it.hasNext()) {
664            it.next();
665            count++;
666        }
667
668        List<MethodInfo> operations = new ArrayList<MethodInfo>();
669        for (MethodInfo info : operationList) {
670            if (info.getParameters().size() == count) {
671                operations.add(info);
672            }
673        }
674
675        if (operations.isEmpty()) {
676            return null;
677        } else if (operations.size() == 1) {
678            return operations.get(0);
679        }
680
681        // okay we still got multiple operations, so need to match the best one
682        List<MethodInfo> candidates = new ArrayList<MethodInfo>();
683        MethodInfo fallbackCandidate = null;
684        for (MethodInfo info : operations) {
685            it = ObjectHelper.createIterator(parameters, ",", false);
686            int index = 0;
687            boolean matches = true;
688            while (it.hasNext()) {
689                String parameter = (String) it.next();
690                if (parameter != null) {
691                    // must trim
692                    parameter = parameter.trim();
693                }
694
695                Class<?> parameterType = BeanHelper.getValidParameterType(parameter);
696                Class<?> expectedType = info.getParameters().get(index).getType();
697
698                if (parameterType != null && expectedType != null) {
699
700                    // if its a simple language then we need to evaluate the expression
701                    // so we have the result and can find out what type the parameter actually is
702                    if (StringHelper.hasStartToken(parameter, "simple")) {
703                        LOG.trace("Evaluating simple expression for parameter #{}: {} to determine the class type of the parameter", index, parameter);
704                        Object out = getCamelContext().resolveLanguage("simple").createExpression(parameter).evaluate(exchange, Object.class);
705                        if (out != null) {
706                            parameterType = out.getClass();
707                        }
708                    }
709
710                    // skip java.lang.Object type, when we have multiple possible methods we want to avoid it if possible
711                    if (Object.class.equals(expectedType)) {
712                        fallbackCandidate = info;
713                        matches = false;
714                        break;
715                    }
716
717                    boolean matchingTypes = isParameterMatchingType(parameterType, expectedType);
718                    if (!matchingTypes) {
719                        matches = false;
720                        break;
721                    }
722                }
723
724                index++;
725            }
726
727            if (matches) {
728                candidates.add(info);
729            }
730        }
731
732        if (candidates.size() > 1) {
733            MethodInfo answer = getSingleCovariantMethod(candidates);
734            if (answer != null) {
735                return answer;
736            }
737        }
738        return candidates.size() == 1 ? candidates.get(0) : fallbackCandidate;
739    }
740
741    private boolean isParameterMatchingType(Class<?> parameterType, Class<?> expectedType) {
742        if (Number.class.equals(parameterType)) {
743            // number should match long/int/etc.
744            if (Integer.class.isAssignableFrom(expectedType) || Long.class.isAssignableFrom(expectedType)
745                    || int.class.isAssignableFrom(expectedType) || long.class.isAssignableFrom(expectedType)) {
746                return true;
747            }
748        }
749        return parameterType.isAssignableFrom(expectedType);
750    }
751
752    private MethodInfo getSingleCovariantMethod(Collection<MethodInfo> candidates) {
753        // if all the candidates are actually covariant, it doesn't matter which one we call
754        MethodInfo firstCandidate = candidates.iterator().next();
755        for (MethodInfo candidate : candidates) {
756            if (!firstCandidate.isCovariantWith(candidate)) {
757                return null;
758            }
759        }
760        return firstCandidate;
761    }
762
763    private MethodInfo chooseMethodWithMatchingBody(Exchange exchange, Collection<MethodInfo> operationList,
764                                                    List<MethodInfo> operationsWithCustomAnnotation)
765        throws AmbiguousMethodCallException {
766        // see if we can find a method whose body param type matches the message body
767        Message in = exchange.getIn();
768        Object body = in.getBody();
769        if (body != null) {
770            Class<?> bodyType = body.getClass();
771            if (LOG.isTraceEnabled()) {
772                LOG.trace("Matching for method with a single parameter that matches type: {}", bodyType.getCanonicalName());
773            }
774
775            List<MethodInfo> possibles = new ArrayList<MethodInfo>();
776            List<MethodInfo> possiblesWithException = new ArrayList<MethodInfo>();
777            for (MethodInfo methodInfo : operationList) {
778                // test for MEP pattern matching
779                boolean out = exchange.getPattern().isOutCapable();
780                if (out && methodInfo.isReturnTypeVoid()) {
781                    // skip this method as the MEP is Out so the method must return something
782                    continue;
783                }
784
785                // try to match the arguments
786                if (methodInfo.bodyParameterMatches(bodyType)) {
787                    LOG.trace("Found a possible method: {}", methodInfo);
788                    if (methodInfo.hasExceptionParameter()) {
789                        // methods with accepts exceptions
790                        possiblesWithException.add(methodInfo);
791                    } else {
792                        // regular methods with no exceptions
793                        possibles.add(methodInfo);
794                    }
795                }
796            }
797
798            // find best suited method to use
799            return chooseBestPossibleMethodInfo(exchange, operationList, body, possibles, possiblesWithException, operationsWithCustomAnnotation);
800        }
801
802        // no match so return null
803        return null;
804    }
805
806    private MethodInfo chooseBestPossibleMethodInfo(Exchange exchange, Collection<MethodInfo> operationList, Object body,
807                                                    List<MethodInfo> possibles, List<MethodInfo> possiblesWithException,
808                                                    List<MethodInfo> possibleWithCustomAnnotation)
809        throws AmbiguousMethodCallException {
810
811        Exception exception = ExpressionBuilder.exchangeExceptionExpression().evaluate(exchange, Exception.class);
812        if (exception != null && possiblesWithException.size() == 1) {
813            LOG.trace("Exchange has exception set so we prefer method that also has exception as parameter");
814            // prefer the method that accepts exception in case we have an exception also
815            return possiblesWithException.get(0);
816        } else if (possibles.size() == 1) {
817            return possibles.get(0);
818        } else if (possibles.isEmpty()) {
819            LOG.trace("No possible methods so now trying to convert body to parameter types");
820
821            // let's try converting
822            Object newBody = null;
823            MethodInfo matched = null;
824            int matchCounter = 0;
825            for (MethodInfo methodInfo : operationList) {
826                if (methodInfo.getBodyParameterType() != null) {
827                    if (methodInfo.getBodyParameterType().isInstance(body)) {
828                        return methodInfo;
829                    }
830
831                    // we should only try to convert, as we are looking for best match
832                    Object value = exchange.getContext().getTypeConverter().tryConvertTo(methodInfo.getBodyParameterType(), exchange, body);
833                    if (value != null) {
834                        if (LOG.isTraceEnabled()) {
835                            LOG.trace("Converted body from: {} to: {}",
836                                    body.getClass().getCanonicalName(), methodInfo.getBodyParameterType().getCanonicalName());
837                        }
838                        matchCounter++;
839                        newBody = value;
840                        matched = methodInfo;
841                    }
842                }
843            }
844            if (matchCounter > 1) {
845                throw new AmbiguousMethodCallException(exchange, Arrays.asList(matched, matched));
846            }
847            if (matched != null) {
848                LOG.trace("Setting converted body: {}", body);
849                Message in = exchange.getIn();
850                in.setBody(newBody);
851                return matched;
852            }
853        } else {
854            // if we only have a single method with custom annotations, let's use that one
855            if (possibleWithCustomAnnotation.size() == 1) {
856                MethodInfo answer = possibleWithCustomAnnotation.get(0);
857                LOG.trace("There are only one method with annotations so we choose it: {}", answer);
858                return answer;
859            }
860            // try to choose among multiple methods with annotations
861            MethodInfo chosen = chooseMethodWithCustomAnnotations(exchange, possibles);
862            if (chosen != null) {
863                return chosen;
864            }
865            // just make sure the methods aren't all actually the same
866            chosen = getSingleCovariantMethod(possibles);
867            if (chosen != null) {
868                return chosen;
869            }
870            throw new AmbiguousMethodCallException(exchange, possibles);
871        }
872
873        // cannot find a good method to use
874        return null;
875    }
876
877    /**
878     * Validates whether the given method is a valid candidate for Camel Bean Binding.
879     *
880     * @param clazz   the class
881     * @param method  the method
882     * @return true if valid, false to skip the method
883     */
884    protected boolean isValidMethod(Class<?> clazz, Method method) {
885        // must not be in the excluded list
886        for (Method excluded : EXCLUDED_METHODS) {
887            if (ObjectHelper.isOverridingMethod(excluded, method)) {
888                // the method is overriding an excluded method so its not valid
889                return false;
890            }
891        }
892
893        // must be a public method
894        if (!Modifier.isPublic(method.getModifiers())) {
895            return false;
896        }
897
898        // return type must not be an Exchange and it should not be a bridge method
899        if ((method.getReturnType() != null && Exchange.class.isAssignableFrom(method.getReturnType())) || method.isBridge()) {
900            return false;
901        }
902
903        return true;
904    }
905
906    /**
907     * Does the given method info override an existing method registered before (from a subclass)
908     *
909     * @param methodInfo  the method to test
910     * @return the already registered method to use, null if not overriding any
911     */
912    private MethodInfo overridesExistingMethod(MethodInfo methodInfo) {
913        for (MethodInfo info : methodMap.values()) {
914            Method source = info.getMethod();
915            Method target = methodInfo.getMethod();
916
917            boolean override = ObjectHelper.isOverridingMethod(source, target);
918            if (override) {
919                // same name, same parameters, then its overrides an existing class
920                return info;
921            }
922        }
923
924        return null;
925    }
926
927    private MethodInfo chooseMethodWithCustomAnnotations(Exchange exchange, Collection<MethodInfo> possibles)
928        throws AmbiguousMethodCallException {
929        // if we have only one method with custom annotations let's choose that
930        MethodInfo chosen = null;
931        for (MethodInfo possible : possibles) {
932            if (possible.hasCustomAnnotation()) {
933                if (chosen != null) {
934                    chosen = null;
935                    break;
936                } else {
937                    chosen = possible;
938                }
939            }
940        }
941        return chosen;
942    }
943
944    /**
945     * Creates an expression for the given parameter type if the parameter can
946     * be mapped automatically or null if the parameter cannot be mapped due to
947     * insufficient annotations or not fitting with the default type
948     * conventions.
949     */
950    private Expression createParameterUnmarshalExpression(Class<?> clazz, Method method, 
951            Class<?> parameterType, Annotation[] parameterAnnotation) {
952
953        // look for a parameter annotation that converts into an expression
954        for (Annotation annotation : parameterAnnotation) {
955            Expression answer = createParameterUnmarshalExpressionForAnnotation(clazz, method, parameterType, annotation);
956            if (answer != null) {
957                return answer;
958            }
959        }
960        // no annotations then try the default parameter mappings
961        return strategy.getDefaultParameterTypeExpression(parameterType);
962    }
963
964    private Expression createParameterUnmarshalExpressionForAnnotation(Class<?> clazz, Method method, 
965            Class<?> parameterType, Annotation annotation) {
966        if (annotation instanceof AttachmentObjects) {
967            return ExpressionBuilder.attachmentObjectsExpression();
968        } else if (annotation instanceof Attachments) {
969            return ExpressionBuilder.attachmentsExpression();
970        } else if (annotation instanceof Property) {
971            Property propertyAnnotation = (Property)annotation;
972            return ExpressionBuilder.exchangePropertyExpression(propertyAnnotation.value());
973        } else if (annotation instanceof ExchangeProperty) {
974            ExchangeProperty propertyAnnotation = (ExchangeProperty)annotation;
975            return ExpressionBuilder.exchangePropertyExpression(propertyAnnotation.value());
976        } else if (annotation instanceof Properties) {
977            return ExpressionBuilder.propertiesExpression();
978        } else if (annotation instanceof Header) {
979            Header headerAnnotation = (Header)annotation;
980            return ExpressionBuilder.headerExpression(headerAnnotation.value());
981        } else if (annotation instanceof Headers) {
982            return ExpressionBuilder.headersExpression();
983        } else if (annotation instanceof OutHeaders) {
984            return ExpressionBuilder.outHeadersExpression();
985        } else if (annotation instanceof ExchangeException) {
986            return ExpressionBuilder.exchangeExceptionExpression(CastUtils.cast(parameterType, Exception.class));
987        } else if (annotation instanceof PropertyInject) {
988            PropertyInject propertyAnnotation = (PropertyInject) annotation;
989            Expression inject = ExpressionBuilder.propertiesComponentExpression(propertyAnnotation.value(), null, propertyAnnotation.defaultValue());
990            return ExpressionBuilder.convertToExpression(inject, parameterType);
991        } else {
992            LanguageAnnotation languageAnnotation = annotation.annotationType().getAnnotation(LanguageAnnotation.class);
993            if (languageAnnotation != null) {
994                Class<?> type = languageAnnotation.factory();
995                Object object = camelContext.getInjector().newInstance(type);
996                if (object instanceof AnnotationExpressionFactory) {
997                    AnnotationExpressionFactory expressionFactory = (AnnotationExpressionFactory) object;
998                    return expressionFactory.createExpression(camelContext, annotation, languageAnnotation, parameterType);
999                } else {
1000                    LOG.warn("Ignoring bad annotation: " + languageAnnotation + "on method: " + method
1001                            + " which declares a factory: " + type.getName()
1002                            + " which does not implement " + AnnotationExpressionFactory.class.getName());
1003                }
1004            }
1005        }
1006
1007        return null;
1008    }
1009    
1010    private static List<Method> getInterfaceMethods(Class<?> clazz) {
1011        final List<Method> answer = new ArrayList<Method>();
1012
1013        while (clazz != null && !clazz.equals(Object.class)) {
1014            for (Class<?> interfaceClazz : clazz.getInterfaces()) {
1015                for (Method interfaceMethod : interfaceClazz.getDeclaredMethods()) {
1016                    answer.add(interfaceMethod);
1017                }
1018            }
1019            clazz = clazz.getSuperclass();
1020        }
1021
1022        return answer;
1023    }
1024
1025    private static void removeAllSetterOrGetterMethods(List<MethodInfo> methods) {
1026        Iterator<MethodInfo> it = methods.iterator();
1027        while (it.hasNext()) {
1028            MethodInfo info = it.next();
1029            if (IntrospectionSupport.isGetter(info.getMethod())) {
1030                // skip getters
1031                it.remove();
1032            } else if (IntrospectionSupport.isSetter(info.getMethod())) {
1033                // skip setters
1034                it.remove();
1035            }
1036        }
1037    }
1038
1039    private void removeNonMatchingMethods(List<MethodInfo> methods, String name) {
1040        Iterator<MethodInfo> it = methods.iterator();
1041        while (it.hasNext()) {
1042            MethodInfo info = it.next();
1043            if (!matchMethod(info.getMethod(), name)) {
1044                // method does not match so remove it
1045                it.remove();
1046            }
1047        }
1048    }
1049
1050    private void removeAllAbstractMethods(List<MethodInfo> methods) {
1051        Iterator<MethodInfo> it = methods.iterator();
1052        while (it.hasNext()) {
1053            MethodInfo info = it.next();
1054            // if the class is an interface then keep the method
1055            boolean isFromInterface = Modifier.isInterface(info.getMethod().getDeclaringClass().getModifiers());
1056            if (!isFromInterface && Modifier.isAbstract(info.getMethod().getModifiers())) {
1057                // we cannot invoke an abstract method
1058                it.remove();
1059            }
1060        }
1061    }
1062
1063    private boolean matchMethod(Method method, String methodName) {
1064        if (methodName == null) {
1065            return true;
1066        }
1067
1068        if (methodName.contains("(") && !methodName.endsWith(")")) {
1069            throw new IllegalArgumentException("Name must have both starting and ending parenthesis, was: " + methodName);
1070        }
1071
1072        // do not use qualifier for name matching
1073        String name = methodName;
1074        if (name.contains("(")) {
1075            name = ObjectHelper.before(name, "(");
1076        }
1077
1078        // must match name
1079        if (name != null && !name.equals(method.getName())) {
1080            return false;
1081        }
1082
1083        // is it a method with no parameters
1084        boolean noParameters = methodName.endsWith("()");
1085        if (noParameters) {
1086            return method.getParameterTypes().length == 0;
1087        }
1088
1089        // match qualifier types which is used to select among overloaded methods
1090        String types = ObjectHelper.between(methodName, "(", ")");
1091        if (ObjectHelper.isNotEmpty(types)) {
1092            // we must qualify based on types to match method
1093            String[] parameters = StringQuoteHelper.splitSafeQuote(types, ',');
1094            Iterator<?> it = ObjectHelper.createIterator(parameters);
1095            for (int i = 0; i < method.getParameterTypes().length; i++) {
1096                if (it.hasNext()) {
1097                    Class<?> parameterType = method.getParameterTypes()[i];
1098
1099                    String qualifyType = (String) it.next();
1100                    if (ObjectHelper.isEmpty(qualifyType)) {
1101                        continue;
1102                    }
1103                    // trim the type
1104                    qualifyType = qualifyType.trim();
1105
1106                    if ("*".equals(qualifyType)) {
1107                        // * is a wildcard so we accept and match that parameter type
1108                        continue;
1109                    }
1110
1111                    if (BeanHelper.isValidParameterValue(qualifyType)) {
1112                        // its a parameter value, so continue to next parameter
1113                        // as we should only check for FQN/type parameters
1114                        continue;
1115                    }
1116
1117                    // if qualify type indeed is a class, then it must be assignable with the parameter type
1118                    Boolean assignable = BeanHelper.isAssignableToExpectedType(getCamelContext().getClassResolver(), qualifyType, parameterType);
1119                    // the method will return null if the qualifyType is not a class
1120                    if (assignable != null && !assignable) {
1121                        return false;
1122                    }
1123
1124                } else {
1125                    // there method has more parameters than was specified in the method name qualifiers
1126                    return false;
1127                }
1128            }
1129
1130            // if the method has no more types then we can only regard it as matched
1131            // if there are no more qualifiers
1132            if (it.hasNext()) {
1133                return false;
1134            }
1135        }
1136
1137        // the method matched
1138        return true;
1139    }
1140
1141    private static Class<?> getTargetClass(Class<?> clazz) {
1142        if (clazz != null && clazz.getName().contains(CGLIB_CLASS_SEPARATOR)) {
1143            Class<?> superClass = clazz.getSuperclass();
1144            if (superClass != null && !Object.class.equals(superClass)) {
1145                return superClass;
1146            }
1147        }
1148        return clazz;
1149    }
1150
1151    /**
1152     * Do we have a method with the given name.
1153     * <p/>
1154     * Shorthand method names for getters is supported, so you can pass in eg 'name' and Camel
1155     * will can find the real 'getName' method instead.
1156     *
1157     * @param methodName the method name
1158     * @return <tt>true</tt> if we have such a method.
1159     */
1160    public boolean hasMethod(String methodName) {
1161        return getOperations(methodName) != null;
1162    }
1163
1164    /**
1165     * Do we have a static method with the given name.
1166     * <p/>
1167     * Shorthand method names for getters is supported, so you can pass in eg 'name' and Camel
1168     * will can find the real 'getName' method instead.
1169     *
1170     * @param methodName the method name
1171     * @return <tt>true</tt> if we have such a static method.
1172     */
1173    public boolean hasStaticMethod(String methodName) {
1174        List<MethodInfo> methods = getOperations(methodName);
1175        if (methods == null || methods.isEmpty()) {
1176            return false;
1177        }
1178        for (MethodInfo method : methods) {
1179            if (method.isStaticMethod()) {
1180                return true;
1181            }
1182        }
1183        return false;
1184    }
1185
1186    /**
1187     * Returns whether the bean class has any public constructors.
1188     */
1189    public boolean hasPublicConstructors() {
1190        return publicConstructors;
1191    }
1192
1193    /**
1194     * Gets the list of methods sorted by A..Z method name.
1195     *
1196     * @return the methods.
1197     */
1198    public List<MethodInfo> getMethods() {
1199        if (operations.isEmpty()) {
1200            return Collections.emptyList();
1201        }
1202
1203        List<MethodInfo> methods = new ArrayList<MethodInfo>();
1204        for (Collection<MethodInfo> col : operations.values()) {
1205            methods.addAll(col);
1206        }
1207
1208        // sort the methods by name A..Z
1209        methods.sort(new Comparator<MethodInfo>() {
1210            public int compare(MethodInfo o1, MethodInfo o2) {
1211                return o1.getMethod().getName().compareTo(o2.getMethod().getName());
1212            }
1213        });
1214        return methods;
1215    }
1216
1217    /**
1218     * Get the operation(s) with the given name. We can have multiple when methods is overloaded.
1219     * <p/>
1220     * Shorthand method names for getters is supported, so you can pass in eg 'name' and Camel
1221     * will can find the real 'getName' method instead.
1222     *
1223     * @param methodName the method name
1224     * @return the found method, or <tt>null</tt> if not found
1225     */
1226    private List<MethodInfo> getOperations(String methodName) {
1227        // do not use qualifier for name
1228        if (methodName.contains("(")) {
1229            methodName = ObjectHelper.before(methodName, "(");
1230        }
1231
1232        List<MethodInfo> answer = operations.get(methodName);
1233        if (answer != null) {
1234            return answer;
1235        }
1236
1237        // now try all getters to see if any of those matched the methodName
1238        for (Method method : methodMap.keySet()) {
1239            if (IntrospectionSupport.isGetter(method)) {
1240                String shorthandMethodName = IntrospectionSupport.getGetterShorthandName(method);
1241                // if the two names matches then see if we can find it using that name
1242                if (methodName != null && methodName.equals(shorthandMethodName)) {
1243                    return operations.get(method.getName());
1244                }
1245            }
1246        }
1247
1248        return null;
1249    }
1250
1251}