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     */
017    package org.apache.camel.component.bean;
018    
019    import java.lang.annotation.Annotation;
020    import java.lang.reflect.Method;
021    import java.lang.reflect.Modifier;
022    import java.lang.reflect.Proxy;
023    import java.util.ArrayList;
024    import java.util.Arrays;
025    import java.util.Collection;
026    import java.util.List;
027    import java.util.Map;
028    import java.util.concurrent.ConcurrentHashMap;
029    
030    import org.apache.camel.Body;
031    import org.apache.camel.CamelContext;
032    import org.apache.camel.Exchange;
033    import org.apache.camel.ExchangeException;
034    import org.apache.camel.Expression;
035    import org.apache.camel.Handler;
036    import org.apache.camel.Header;
037    import org.apache.camel.Headers;
038    import org.apache.camel.Message;
039    import org.apache.camel.OutHeaders;
040    import org.apache.camel.Properties;
041    import org.apache.camel.Property;
042    import org.apache.camel.RuntimeCamelException;
043    import org.apache.camel.builder.ExpressionBuilder;
044    import org.apache.camel.language.LanguageAnnotation;
045    import org.apache.camel.spi.Registry;
046    import org.apache.camel.util.ObjectHelper;
047    import org.apache.commons.logging.Log;
048    import org.apache.commons.logging.LogFactory;
049    
050    import static org.apache.camel.util.ExchangeHelper.convertToType;
051    
052    
053    /**
054     * Represents the metadata about a bean type created via a combination of
055     * introspection and annotations together with some useful sensible defaults
056     *
057     * @version $Revision: 783006 $
058     */
059    public class BeanInfo {
060        private static final transient Log LOG = LogFactory.getLog(BeanInfo.class);
061        private static final List<Method> EXCLUDED_METHODS = new ArrayList<Method>();
062        private final CamelContext camelContext;
063        private final Class type;
064        private final ParameterMappingStrategy strategy;
065        private final Map<String, List<MethodInfo>> operations = new ConcurrentHashMap<String, List<MethodInfo>>();
066        private final List<MethodInfo> operationsWithBody = new ArrayList<MethodInfo>();
067        private final List<MethodInfo> operationsWithCustomAnnotation = new ArrayList<MethodInfo>();
068        private final List<MethodInfo> operationsWithHandlerAnnotation = new ArrayList<MethodInfo>();
069        private final Map<Method, MethodInfo> methodMap = new ConcurrentHashMap<Method, MethodInfo>();
070        private MethodInfo defaultMethod;
071        private BeanInfo superBeanInfo;
072    
073        public BeanInfo(CamelContext camelContext, Class type) {
074            this(camelContext, type, createParameterMappingStrategy(camelContext));
075        }
076    
077        public BeanInfo(CamelContext camelContext, Class type, ParameterMappingStrategy strategy) {
078            this.camelContext = camelContext;
079            this.type = type;
080            this.strategy = strategy;
081    
082            // configure the default excludes methods
083            synchronized (EXCLUDED_METHODS) {
084                if (EXCLUDED_METHODS.size() == 0) {
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    
090                    // TODO: AOP proxies have additional methods - well known methods should be added to EXCLUDE_METHODS
091                }
092            }
093    
094            introspect(getType());
095            // if there are only 1 method with 1 operation then select it as a default/fallback method
096            if (operations.size() == 1) {
097                List<MethodInfo> methods = operations.values().iterator().next();
098                if (methods.size() == 1) {
099                    defaultMethod = methods.get(0);
100                }
101            }
102        }
103    
104        public Class getType() {
105            return type;
106        }
107    
108        public CamelContext getCamelContext() {
109            return camelContext;
110        }
111    
112        public static ParameterMappingStrategy createParameterMappingStrategy(CamelContext camelContext) {
113            // lookup in registry first if there is a user define strategy
114            Registry registry = camelContext.getRegistry();
115            ParameterMappingStrategy answer = registry.lookup(BeanConstants.BEAN_PARAMETER_MAPPING_STRATEGY, ParameterMappingStrategy.class);
116            if (answer == null) {
117                // no then use the default one
118                answer = new DefaultParameterMappingStrategy();
119            }
120    
121            return answer;
122        }
123    
124        public MethodInvocation createInvocation(Method method, Object pojo, Exchange exchange) throws RuntimeCamelException {
125            MethodInfo methodInfo = introspect(type, method);
126            if (methodInfo != null) {
127                return methodInfo.createMethodInvocation(pojo, exchange);
128            }
129            return null;
130        }
131    
132        public MethodInvocation createInvocation(Object pojo, Exchange exchange) throws RuntimeCamelException, AmbiguousMethodCallException {
133            MethodInfo methodInfo = null;
134    
135            String name = exchange.getIn().getHeader(Exchange.BEAN_METHOD_NAME, String.class);
136            if (name != null) {
137                if (operations.containsKey(name)) {
138                    List<MethodInfo> methods = operations.get(name);
139                    if (methods != null && methods.size() == 1) {
140                        methodInfo = methods.get(0);
141                    }
142                }
143            }
144            if (methodInfo == null) {
145                methodInfo = chooseMethod(pojo, exchange);
146            }
147            if (methodInfo == null) {
148                methodInfo = defaultMethod;
149            }
150            if (methodInfo != null) {
151                if (LOG.isTraceEnabled()) {
152                    LOG.trace("Chosen method to invoke: " + methodInfo + " on bean: " + pojo);
153                }
154                return methodInfo.createMethodInvocation(pojo, exchange);
155            }
156    
157            if (LOG.isDebugEnabled()) {
158                LOG.debug("Cannot find suitable method to invoke on bean: " + pojo);
159            }
160            return null;
161        }
162    
163        /**
164         * Introspects the given class
165         *
166         * @param clazz the class
167         */
168        protected void introspect(Class clazz) {
169            if (LOG.isTraceEnabled()) {
170                LOG.trace("Introspecting class: " + clazz);
171            }
172            Method[] methods = clazz.getDeclaredMethods();
173            for (Method method : methods) {
174                if (isValidMethod(clazz, method)) {
175                    introspect(clazz, method);
176                }
177            }
178            Class superclass = clazz.getSuperclass();
179            if (superclass != null && !superclass.equals(Object.class)) {
180                introspect(superclass);
181            }
182        }
183    
184        /**
185         * Introspects the given method
186         *
187         * @param clazz the class
188         * @param method the method
189         * @return the method info, is newer <tt>null</tt>
190         */
191        protected MethodInfo introspect(Class clazz, Method method) {
192            if (LOG.isTraceEnabled()) {
193                LOG.trace("Introspecting class: " + clazz + ", method: " + method);
194            }
195            String opName = method.getName();
196    
197            MethodInfo methodInfo = createMethodInfo(clazz, method);
198    
199            // methods already registered should be preferred to use instead of super classes of existing methods
200            // we want to us the method from the sub class over super classes, so if we have already registered
201            // the method then use it (we are traversing upwards: sub (child) -> super (farther) )
202            MethodInfo existingMethodInfo = overridesExistingMethod(methodInfo);
203            if (existingMethodInfo != null) {
204                if (LOG.isTraceEnabled()) {
205                    LOG.trace("This method is already overriden in a subclass, so the method from the sub class is prefered: " + existingMethodInfo);
206                }
207    
208                return existingMethodInfo;
209            }
210    
211            if (LOG.isTraceEnabled()) {
212                LOG.trace("Adding operation: " + opName + " for method: " + methodInfo);
213            }
214    
215            if (operations.containsKey(opName)) {
216                // we have an overloaded method so add the method info to the same key
217                List<MethodInfo> existing = operations.get(opName);
218                existing.add(methodInfo);
219            } else {
220                // its a new method we have not seen before so wrap it in a list and add it
221                List<MethodInfo> methods = new ArrayList<MethodInfo>();
222                methods.add(methodInfo);
223                operations.put(opName, methods);
224            }
225    
226            if (methodInfo.hasCustomAnnotation()) {
227                operationsWithCustomAnnotation.add(methodInfo);
228            } else if (methodInfo.hasBodyParameter()) {
229                operationsWithBody.add(methodInfo);
230            }
231    
232            if (methodInfo.hasHandlerAnnotation()) {
233                operationsWithHandlerAnnotation.add(methodInfo);
234            }
235    
236            // must add to method map last otherwise we break stuff
237            methodMap.put(method, methodInfo);
238    
239            return methodInfo;
240        }
241    
242    
243        /**
244         * Returns the {@link MethodInfo} for the given method if it exists or null
245         * if there is no metadata available for the given method
246         */
247        public MethodInfo getMethodInfo(Method method) {
248            MethodInfo answer = methodMap.get(method);
249            if (answer == null) {
250                // maybe the method is defined on a base class?
251                if (superBeanInfo == null && type != Object.class) {
252                    Class superclass = type.getSuperclass();
253                    if (superclass != null && superclass != Object.class) {
254                        superBeanInfo = new BeanInfo(camelContext, superclass, strategy);
255                        return superBeanInfo.getMethodInfo(method);
256                    }
257                }
258            }
259            return answer;
260        }
261    
262        @SuppressWarnings("unchecked")
263        protected MethodInfo createMethodInfo(Class clazz, Method method) {
264            Class[] parameterTypes = method.getParameterTypes();
265            Annotation[][] parametersAnnotations = method.getParameterAnnotations();
266    
267            List<ParameterInfo> parameters = new ArrayList<ParameterInfo>();
268            List<ParameterInfo> bodyParameters = new ArrayList<ParameterInfo>();
269    
270            boolean hasCustomAnnotation = false;
271            boolean hasHandlerAnnotation = ObjectHelper.hasAnnotation(method.getAnnotations(), Handler.class);
272    
273            int size = parameterTypes.length;
274            if (LOG.isTraceEnabled()) {
275                LOG.trace("Creating MethodInfo for class: " + clazz + " method: " + method + " having " + size + " parameters");
276            }
277    
278            for (int i = 0; i < size; i++) {
279                Class parameterType = parameterTypes[i];
280                Annotation[] parameterAnnotations = parametersAnnotations[i];
281                Expression expression = createParameterUnmarshalExpression(clazz, method, parameterType, parameterAnnotations);
282                hasCustomAnnotation |= expression != null;
283    
284                ParameterInfo parameterInfo = new ParameterInfo(i, parameterType, parameterAnnotations, expression);
285                parameters.add(parameterInfo);
286                if (expression == null) {
287                    boolean bodyAnnotation = ObjectHelper.hasAnnotation(parameterAnnotations, Body.class);
288                    if (LOG.isTraceEnabled() && bodyAnnotation) {
289                        LOG.trace("Parameter #" + i + " has @Body annotation");
290                    }
291                    hasCustomAnnotation |= bodyAnnotation;
292                    if (bodyParameters.isEmpty()) {
293                        // okay we have not yet set the body parameter and we have found
294                        // the candidate now to use as body parameter
295                        if (Exchange.class.isAssignableFrom(parameterType)) {
296                            // use exchange
297                            expression = ExpressionBuilder.exchangeExpression();
298                        } else {
299                            // lets assume its the body
300                            expression = ExpressionBuilder.bodyExpression(parameterType);
301                        }
302                        if (LOG.isTraceEnabled()) {
303                            LOG.trace("Parameter #" + i + " is the body parameter using expression " + expression);
304                        }
305                        parameterInfo.setExpression(expression);
306                        bodyParameters.add(parameterInfo);
307                    } else {
308                        // will ignore the expression for parameter evaluation
309                    }
310                }
311                if (LOG.isTraceEnabled()) {
312                    LOG.trace("Parameter #" + i + " has parameter info: " + parameterInfo);
313                }
314            }
315    
316            // now lets add the method to the repository
317            return new MethodInfo(clazz, method, parameters, bodyParameters, hasCustomAnnotation, hasHandlerAnnotation);
318        }
319    
320        /**
321         * Lets try choose one of the available methods to invoke if we can match
322         * the message body to the body parameter
323         *
324         * @param pojo the bean to invoke a method on
325         * @param exchange the message exchange
326         * @return the method to invoke or null if no definitive method could be matched
327         * @throws AmbiguousMethodCallException is thrown if cannot chose method due to ambiguous
328         */
329        protected MethodInfo chooseMethod(Object pojo, Exchange exchange) throws AmbiguousMethodCallException {
330            // @Handler should be select first
331            // then any single method that has a custom @annotation
332            // or any single method that has a match parameter type that matches the Exchange payload
333            // and last then try to select the best among the rest
334    
335            if (operationsWithHandlerAnnotation.size() > 1) {
336                // if we have more than 1 @Handler then its ambiguous
337                throw new AmbiguousMethodCallException(exchange, operationsWithHandlerAnnotation);
338            }
339    
340            if (operationsWithHandlerAnnotation.size() == 1) {
341                // methods with handler should be preferred
342                return operationsWithHandlerAnnotation.get(0);
343            } else if (operationsWithCustomAnnotation.size() == 1) {
344                // if there is one method with an annotation then use that one
345                return operationsWithCustomAnnotation.get(0);
346            } else if (operationsWithBody.size() == 1) {
347                // if there is one method with body then use that one
348                return operationsWithBody.get(0);
349            }
350    
351            Collection<MethodInfo> possibleOperations = new ArrayList<MethodInfo>();
352            possibleOperations.addAll(operationsWithBody);
353            possibleOperations.addAll(operationsWithCustomAnnotation);
354    
355            if (!possibleOperations.isEmpty()) {
356                 // multiple possible operations so find the best suited if possible
357                MethodInfo answer = chooseMethodWithMatchingBody(exchange, possibleOperations);
358                if (answer == null) {
359                    throw new AmbiguousMethodCallException(exchange, possibleOperations);
360                } else {
361                    return answer;
362                }
363            }
364    
365            // not possible to determine
366            return null;
367        }
368    
369        @SuppressWarnings("unchecked")
370        private MethodInfo chooseMethodWithMatchingBody(Exchange exchange, Collection<MethodInfo> operationList)
371            throws AmbiguousMethodCallException {
372            // lets see if we can find a method who's body param type matches the message body
373            Message in = exchange.getIn();
374            Object body = in.getBody();
375            if (body != null) {
376                Class bodyType = body.getClass();
377                if (LOG.isTraceEnabled()) {
378                    LOG.trace("Matching for method with a single parameter that matches type: " + bodyType.getCanonicalName());
379                }
380    
381                List<MethodInfo> possibles = new ArrayList<MethodInfo>();
382                List<MethodInfo> possiblesWithException = new ArrayList<MethodInfo>();
383                for (MethodInfo methodInfo : operationList) {
384                    // test for MEP pattern matching
385                    boolean out = exchange.getPattern().isOutCapable();
386                    if (out && methodInfo.isReturnTypeVoid()) {
387                        // skip this method as the MEP is Out so the method must return something
388                        continue;
389                    }
390    
391                    // try to match the arguments
392                    if (methodInfo.bodyParameterMatches(bodyType)) {
393                        if (LOG.isTraceEnabled()) {
394                            LOG.trace("Found a possible method: " + methodInfo);
395                        }
396                        if (methodInfo.hasExceptionParameter()) {
397                            // methods with accepts exceptions
398                            possiblesWithException.add(methodInfo);
399                        } else {
400                            // regular methods with no exceptions
401                            possibles.add(methodInfo);
402                        }
403                    }
404                }
405    
406                // find best suited method to use
407                return chooseBestPossibleMethodInfo(exchange, operationList, body, possibles, possiblesWithException);
408            }
409    
410            // no match so return null
411            return null;
412        }
413    
414        @SuppressWarnings("unchecked")
415        private MethodInfo chooseBestPossibleMethodInfo(Exchange exchange, Collection<MethodInfo> operationList, Object body,
416                                                        List<MethodInfo> possibles, List<MethodInfo> possiblesWithException)
417            throws AmbiguousMethodCallException {
418    
419            Exception exception = ExpressionBuilder.exchangeExceptionExpression().evaluate(exchange, Exception.class);
420            if (exception != null && possiblesWithException.size() == 1) {
421                if (LOG.isTraceEnabled()) {
422                    LOG.trace("Exchange has exception set so we prefer method that also has exception as parameter");
423                }
424                // prefer the method that accepts exception in case we have an exception also
425                return possiblesWithException.get(0);
426            } else if (possibles.size() == 1) {
427                return possibles.get(0);
428            } else if (possibles.isEmpty()) {
429                if (LOG.isTraceEnabled()) {
430                    LOG.trace("No poosible methods trying to convert body to parameter types");
431                }
432    
433                // lets try converting
434                Object newBody = null;
435                MethodInfo matched = null;
436                for (MethodInfo methodInfo : operationList) {
437                    Object value = convertToType(exchange, methodInfo.getBodyParameterType(), body);
438                    if (value != null) {
439                        if (LOG.isTraceEnabled()) {
440                            LOG.trace("Converted body from: " + body.getClass().getCanonicalName()
441                                    + "to: " + methodInfo.getBodyParameterType().getCanonicalName());
442                        }
443                        if (newBody != null) {
444                            // we already have found one new body that could be converted so now we have 2 methods
445                            // and then its ambiguous
446                            throw new AmbiguousMethodCallException(exchange, Arrays.asList(matched, methodInfo));
447                        } else {
448                            newBody = value;
449                            matched = methodInfo;
450                        }
451                    }
452                }
453                if (matched != null) {
454                    if (LOG.isTraceEnabled()) {
455                        LOG.trace("Setting converted body: " + body);
456                    }
457                    Message in = exchange.getIn();
458                    in.setBody(newBody);
459                    return matched;
460                }
461            } else {
462                // if we only have a single method with custom annotations, lets use that one
463                if (operationsWithCustomAnnotation.size() == 1) {
464                    MethodInfo answer = operationsWithCustomAnnotation.get(0);
465                    if (LOG.isTraceEnabled()) {
466                        LOG.trace("There are only one method with annotations so we choose it: " + answer);
467                    }
468                    return answer;
469                }
470                // phew try to choose among multiple methods with annotations
471                return chooseMethodWithCustomAnnotations(exchange, possibles);
472            }
473    
474            // cannot find a good method to use
475            return null;
476        }
477    
478        /**
479         * Validates wheter the given method is a valid candidate for Camel Bean Binding.
480         *
481         * @param clazz   the class
482         * @param method  the method
483         * @return true if valid, false to skip the method
484         */
485        protected boolean isValidMethod(Class clazz, Method method) {
486            // must not be in the excluded list
487            for (Method excluded : EXCLUDED_METHODS) {
488                if (ObjectHelper.isOverridingMethod(excluded, method)) {
489                    // the method is overriding an excluded method so its not valid
490                    return false;
491                }
492            }
493    
494            // must be a public method
495            if (!Modifier.isPublic(method.getModifiers())) {
496                return false;
497            }
498    
499            // return type must not be an Exchange
500            if (method.getReturnType() != null && Exchange.class.isAssignableFrom(method.getReturnType())) {
501                return false;
502            }
503    
504            return true;
505        }
506    
507        /**
508         * Does the given method info override an existing method registered before (from a subclass)
509         *
510         * @param methodInfo  the method to test
511         * @return the already registered method to use, null if not overriding any
512         */
513        private MethodInfo overridesExistingMethod(MethodInfo methodInfo) {
514            for (MethodInfo info : methodMap.values()) {
515                Method source = info.getMethod();
516                Method target = methodInfo.getMethod();
517    
518                boolean override = ObjectHelper.isOverridingMethod(source, target);
519                if (override) {
520                    // same name, same parameters, then its overrides an existing class
521                    return info;
522                }
523            }
524    
525            return null;
526        }
527    
528        private MethodInfo chooseMethodWithCustomAnnotations(Exchange exchange, Collection<MethodInfo> possibles)
529            throws AmbiguousMethodCallException {
530            // if we have only one method with custom annotations lets choose that
531            MethodInfo chosen = null;
532            for (MethodInfo possible : possibles) {
533                if (possible.hasCustomAnnotation()) {
534                    if (chosen != null) {
535                        chosen = null;
536                        break;
537                    } else {
538                        chosen = possible;
539                    }
540                }
541            }
542            if (chosen != null) {
543                return chosen;
544            }
545            throw new AmbiguousMethodCallException(exchange, possibles);
546        }
547    
548        /**
549         * Creates an expression for the given parameter type if the parameter can
550         * be mapped automatically or null if the parameter cannot be mapped due to
551         * insufficient annotations or not fitting with the default type
552         * conventions.
553         */
554        private Expression createParameterUnmarshalExpression(Class clazz, Method method, Class parameterType,
555                                                              Annotation[] parameterAnnotation) {
556    
557            // look for a parameter annotation that converts into an expression
558            for (Annotation annotation : parameterAnnotation) {
559                Expression answer = createParameterUnmarshalExpressionForAnnotation(clazz, method, parameterType, annotation);
560                if (answer != null) {
561                    return answer;
562                }
563            }
564            // no annotations then try the default parameter mappings
565            return strategy.getDefaultParameterTypeExpression(parameterType);
566        }
567    
568        private Expression createParameterUnmarshalExpressionForAnnotation(Class clazz, Method method, Class parameterType,
569                                                                           Annotation annotation) {
570            if (annotation instanceof Property) {
571                Property propertyAnnotation = (Property)annotation;
572                return ExpressionBuilder.propertyExpression(propertyAnnotation.value());
573            } else if (annotation instanceof Properties) {
574                return ExpressionBuilder.propertiesExpression();
575            } else if (annotation instanceof Header) {
576                Header headerAnnotation = (Header)annotation;
577                return ExpressionBuilder.headerExpression(headerAnnotation.value());
578            } else if (annotation instanceof Headers) {
579                return ExpressionBuilder.headersExpression();
580            } else if (annotation instanceof OutHeaders) {
581                return ExpressionBuilder.outHeadersExpression();
582            } else if (annotation instanceof ExchangeException) {
583                return ExpressionBuilder.exchangeExceptionExpression(parameterType);
584            } else {
585                LanguageAnnotation languageAnnotation = annotation.annotationType().getAnnotation(LanguageAnnotation.class);
586                if (languageAnnotation != null) {
587                    Class<?> type = languageAnnotation.factory();
588                    Object object = camelContext.getInjector().newInstance(type);
589                    if (object instanceof AnnotationExpressionFactory) {
590                        AnnotationExpressionFactory expressionFactory = (AnnotationExpressionFactory) object;
591                        return expressionFactory.createExpression(camelContext, annotation, languageAnnotation, parameterType);
592                    } else {
593                        LOG.warn("Ignoring bad annotation: " + languageAnnotation + "on method: " + method
594                                + " which declares a factory: " + type.getName()
595                                + " which does not implement " + AnnotationExpressionFactory.class.getName());
596                    }
597                }
598            }
599    
600            return null;
601        }
602    
603    }