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