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    
020    import java.lang.annotation.Annotation;
021    import java.lang.reflect.Method;
022    import java.lang.reflect.Modifier;
023    import java.util.ArrayList;
024    import java.util.Arrays;
025    import java.util.Collection;
026    import java.util.HashMap;
027    import java.util.List;
028    import java.util.Map;
029    import java.util.concurrent.ConcurrentHashMap;
030    
031    import org.apache.camel.Body;
032    import org.apache.camel.CamelContext;
033    import org.apache.camel.Exchange;
034    import org.apache.camel.ExchangeException;
035    import org.apache.camel.Expression;
036    import org.apache.camel.Header;
037    import org.apache.camel.Headers;
038    import org.apache.camel.Message;
039    import org.apache.camel.NoTypeConversionAvailableException;
040    import org.apache.camel.OutHeaders;
041    import org.apache.camel.Properties;
042    import org.apache.camel.Property;
043    import org.apache.camel.RuntimeCamelException;
044    import org.apache.camel.builder.ExpressionBuilder;
045    import org.apache.camel.language.LanguageAnnotation;
046    import org.apache.camel.spi.Registry;
047    import org.apache.camel.util.ObjectHelper;
048    import org.apache.commons.logging.Log;
049    import org.apache.commons.logging.LogFactory;
050    
051    import static org.apache.camel.util.ExchangeHelper.convertToType;
052    
053    
054    /**
055     * Represents the metadata about a bean type created via a combination of
056     * introspection and annotations together with some useful sensible defaults
057     *
058     * @version $Revision: 747428 $
059     */
060    public class BeanInfo {
061        private static final transient Log LOG = LogFactory.getLog(BeanInfo.class);
062        private final CamelContext camelContext;
063        private Class type;
064        private ParameterMappingStrategy strategy;
065        private Map<String, MethodInfo> operations = new ConcurrentHashMap<String, MethodInfo>();
066        private MethodInfo defaultMethod;
067        private List<MethodInfo> operationsWithBody = new ArrayList<MethodInfo>();
068        private List<MethodInfo> operationsWithCustomAnnotation = new ArrayList<MethodInfo>();
069        private Map<Method, MethodInfo> methodMap = new HashMap<Method, MethodInfo>();
070        private BeanInfo superBeanInfo;
071    
072        public BeanInfo(CamelContext camelContext, Class type) {
073            this(camelContext, type, createParameterMappingStrategy(camelContext));
074        }
075    
076        public BeanInfo(CamelContext camelContext, Class type, ParameterMappingStrategy strategy) {
077            this.camelContext = camelContext;
078            this.type = type;
079            this.strategy = strategy;
080            introspect(getType());
081            if (operations.size() == 1) {
082                Collection<MethodInfo> methodInfos = operations.values();
083                for (MethodInfo methodInfo : methodInfos) {
084                    defaultMethod = methodInfo;
085                }
086            }
087        }
088    
089        public Class getType() {
090            return type;
091        }
092    
093        public CamelContext getCamelContext() {
094            return camelContext;
095        }
096    
097        public MethodInvocation createInvocation(Method method, Object pojo, Exchange exchange)
098            throws RuntimeCamelException {
099            MethodInfo methodInfo = introspect(type, method);
100            if (methodInfo != null) {
101                return methodInfo.createMethodInvocation(pojo, exchange);
102            }
103            return null;
104        }
105    
106        public MethodInvocation createInvocation(Object pojo, Exchange exchange) throws RuntimeCamelException,
107            AmbiguousMethodCallException {
108            MethodInfo methodInfo = null;
109    
110            // TODO use some other mechanism?
111            String name = exchange.getIn().getHeader(Exchange.BEAN_METHOD_NAME, String.class);
112            if (name != null) {
113                methodInfo = operations.get(name);
114            }
115            if (methodInfo == null) {
116                methodInfo = chooseMethod(pojo, exchange);
117            }
118            if (methodInfo == null) {
119                methodInfo = defaultMethod;
120            }
121            if (methodInfo != null) {
122                return methodInfo.createMethodInvocation(pojo, exchange);
123            }
124            return null;
125        }
126    
127        protected void introspect(Class clazz) {
128            if (LOG.isTraceEnabled()) {
129                LOG.trace("Introspecting class: " + clazz);
130            }
131            Method[] methods = clazz.getDeclaredMethods();
132            for (Method method : methods) {
133                if (isValidMethod(clazz, method)) {
134                    introspect(clazz, method);
135                }
136            }
137            Class superclass = clazz.getSuperclass();
138            if (superclass != null && !superclass.equals(Object.class)) {
139                introspect(superclass);
140            }
141        }
142    
143        protected MethodInfo introspect(Class clazz, Method method) {
144            if (LOG.isTraceEnabled()) {
145                LOG.trace("Introspecting class: " + clazz + ", method: " + method);
146            }
147            String opName = method.getName();
148    
149            MethodInfo methodInfo = createMethodInfo(clazz, method);
150    
151            // methods already registered should be preferred to use instead of super classes of existing methods
152            // we want to us the method from the sub class over super classes, so if we have already registered
153            // the method then use it (we are traversing upwards: sub (child) -> super (farther) )
154            MethodInfo existingMethodInfo = overridesExistingMethod(methodInfo);
155            if (existingMethodInfo != null) {
156                if (LOG.isTraceEnabled()) {
157                    LOG.trace("This method is already overriden in a subclass, so the method from the sub class is prefered: " + existingMethodInfo);
158                }
159    
160                return existingMethodInfo;
161            }
162    
163            if (LOG.isTraceEnabled()) {
164                LOG.trace("Adding operation: " + opName + " for method: " + methodInfo);
165            }
166            operations.put(opName, methodInfo);
167    
168            if (methodInfo.hasBodyParameter()) {
169                operationsWithBody.add(methodInfo);
170            }
171            if (methodInfo.isHasCustomAnnotation() && !methodInfo.hasBodyParameter()) {
172                operationsWithCustomAnnotation.add(methodInfo);
173            }
174    
175            // must add to method map last otherwise we break stuff
176            methodMap.put(method, methodInfo);
177    
178            return methodInfo;
179        }
180    
181        /**
182         * Does the given method info override an existing method registered before (from a subclass)
183         *
184         * @param methodInfo  the method to test
185         * @return the already registered method to use, null if not overriding any
186         */
187        private MethodInfo overridesExistingMethod(MethodInfo methodInfo) {
188            for (MethodInfo info : methodMap.values()) {
189    
190                // name test
191                if (!info.getMethod().getName().equals(methodInfo.getMethod().getName())) {
192                    continue;
193                }
194    
195                // parameter types
196                if (info.getMethod().getParameterTypes().length != methodInfo.getMethod().getParameterTypes().length) {
197                    continue;
198                }
199    
200                for (int i = 0; i < info.getMethod().getParameterTypes().length; i++) {
201                    Class type1 = info.getMethod().getParameterTypes()[i];
202                    Class type2 = methodInfo.getMethod().getParameterTypes()[i];
203                    if (!type1.equals(type2)) {
204                        continue;
205                    }
206                }
207    
208                // same name, same parameters, then its overrides an existing class
209                return info;
210            }
211    
212            return null;
213        }
214    
215        /**
216         * Returns the {@link MethodInfo} for the given method if it exists or null
217         * if there is no metadata available for the given method
218         */
219        public MethodInfo getMethodInfo(Method method) {
220            MethodInfo answer = methodMap.get(method);
221            if (answer == null) {
222                // maybe the method is defined on a base class?
223                if (superBeanInfo == null && type != Object.class) {
224                    Class superclass = type.getSuperclass();
225                    if (superclass != null && superclass != Object.class) {
226                        superBeanInfo = new BeanInfo(camelContext, superclass, strategy);
227                        return superBeanInfo.getMethodInfo(method);
228                    }
229                }
230            }
231            return answer;
232        }
233    
234        @SuppressWarnings("unchecked")
235        protected MethodInfo createMethodInfo(Class clazz, Method method) {
236            Class[] parameterTypes = method.getParameterTypes();
237            Annotation[][] parametersAnnotations = method.getParameterAnnotations();
238    
239            List<ParameterInfo> parameters = new ArrayList<ParameterInfo>();
240            List<ParameterInfo> bodyParameters = new ArrayList<ParameterInfo>();
241    
242            boolean hasCustomAnnotation = false;
243            for (int i = 0; i < parameterTypes.length; i++) {
244                Class parameterType = parameterTypes[i];
245                Annotation[] parameterAnnotations = parametersAnnotations[i];
246                Expression expression = createParameterUnmarshalExpression(clazz, method, parameterType,
247                                                                           parameterAnnotations);
248                hasCustomAnnotation |= expression != null;
249    
250                ParameterInfo parameterInfo = new ParameterInfo(i, parameterType, parameterAnnotations,
251                                                                expression);
252                parameters.add(parameterInfo);
253    
254                if (expression == null) {
255                    hasCustomAnnotation |= ObjectHelper.hasAnnotation(parameterAnnotations, Body.class);
256                    if (bodyParameters.isEmpty()) {
257                        // lets assume its the body
258                        if (Exchange.class.isAssignableFrom(parameterType)) {
259                            expression = ExpressionBuilder.exchangeExpression();
260                        } else {
261                            expression = ExpressionBuilder.bodyExpression(parameterType);
262                        }
263                        parameterInfo.setExpression(expression);
264                        bodyParameters.add(parameterInfo);
265                    } else {
266                        // will ignore the expression for parameter evaluation
267                    }
268                }
269    
270            }
271    
272            // now lets add the method to the repository
273    
274            // TODO allow an annotation to expose the operation name to use
275            /* if (method.getAnnotation(Operation.class) != null) { String name =
276             * method.getAnnotation(Operation.class).name(); if (name != null &&
277             * name.length() > 0) { opName = name; } }
278             */
279            MethodInfo methodInfo = new MethodInfo(clazz, method, parameters, bodyParameters, hasCustomAnnotation);
280            return methodInfo;
281        }
282    
283        /**
284         * Lets try choose one of the available methods to invoke if we can match
285         * the message body to the body parameter
286         *
287         * @param pojo the bean to invoke a method on
288         * @param exchange the message exchange
289         * @return the method to invoke or null if no definitive method could be
290         *         matched
291         */
292        protected MethodInfo chooseMethod(Object pojo, Exchange exchange) throws AmbiguousMethodCallException {
293            if (operationsWithBody.size() == 1) {
294                return operationsWithBody.get(0);
295            } else if (!operationsWithBody.isEmpty()) {
296                return chooseMethodWithMatchingBody(exchange, operationsWithBody);
297            } else if (operationsWithCustomAnnotation.size() == 1) {
298                return operationsWithCustomAnnotation.get(0);
299            }
300            return null;
301        }
302    
303        @SuppressWarnings("unchecked")
304        protected MethodInfo chooseMethodWithMatchingBody(Exchange exchange, Collection<MethodInfo> operationList) throws AmbiguousMethodCallException {
305            // lets see if we can find a method who's body param type matches the message body
306            Message in = exchange.getIn();
307            Object body = in.getBody();
308            if (body != null) {
309                Class bodyType = body.getClass();
310                if (LOG.isTraceEnabled()) {
311                    LOG.trace("Matching for method with a single parameter that matches type: " + bodyType.getCanonicalName());
312                }
313    
314                List<MethodInfo> possibles = new ArrayList<MethodInfo>();
315                for (MethodInfo methodInfo : operationList) {
316                    // TODO: AOP proxies have additional methods - consider having a static
317                    // method exclude list to skip all known AOP proxy methods
318                    // TODO: This class could use some TRACE logging
319    
320                    // test for MEP pattern matching
321                    boolean out = exchange.getPattern().isOutCapable();
322                    if (out && methodInfo.isReturnTypeVoid()) {
323                        // skip this method as the MEP is Out so the method must return something
324                        continue;
325                    }
326    
327                    // try to match the arguments
328                    if (methodInfo.bodyParameterMatches(bodyType)) {
329                        possibles.add(methodInfo);
330                    }
331                }
332                if (possibles.size() == 1) {
333                    return possibles.get(0);
334                } else if (possibles.isEmpty()) {
335                    // lets try converting
336                    Object newBody = null;
337                    MethodInfo matched = null;
338                    for (MethodInfo methodInfo : operationList) {
339                        Object value = null;
340                        try {
341                            value = convertToType(exchange, methodInfo.getBodyParameterType(), body);
342                            if (value != null) {
343                                if (newBody != null) {
344                                    throw new AmbiguousMethodCallException(exchange, Arrays.asList(matched, methodInfo));
345                                } else {
346                                    newBody = value;
347                                    matched = methodInfo;
348                                }
349                            }
350                        } catch (NoTypeConversionAvailableException e) {
351                            // we can safely ignore this exception as we want a behaviour similar to
352                            // that if convertToType return null
353                        }
354                    }
355                    if (matched != null) {
356                        in.setBody(newBody);
357                        return matched;
358                    }
359                } else {
360                    // if we only have a single method with custom annotations, lets use that one
361                    if (operationsWithCustomAnnotation.size() == 1) {
362                        return operationsWithCustomAnnotation.get(0);
363                    }
364                    return chooseMethodWithCustomAnnotations(exchange, possibles);
365                }
366            }
367            // no match so return null
368            return null;
369        }
370    
371        protected MethodInfo chooseMethodWithCustomAnnotations(Exchange exchange, Collection<MethodInfo> possibles) throws AmbiguousMethodCallException {
372            // if we have only one method with custom annotations lets choose that
373            MethodInfo chosen = null;
374            for (MethodInfo possible : possibles) {
375                if (possible.isHasCustomAnnotation()) {
376                    if (chosen != null) {
377                        chosen = null;
378                        break;
379                    } else {
380                        chosen = possible;
381                    }
382                }
383            }
384            if (chosen != null) {
385                return chosen;
386            }
387            throw new AmbiguousMethodCallException(exchange, possibles);
388        }
389    
390        /**
391         * Creates an expression for the given parameter type if the parameter can
392         * be mapped automatically or null if the parameter cannot be mapped due to
393         * insufficient annotations or not fitting with the default type
394         * conventions.
395         */
396        protected Expression createParameterUnmarshalExpression(Class clazz, Method method, Class parameterType,
397                                                                Annotation[] parameterAnnotation) {
398    
399            // TODO look for a parameter annotation that converts into an expression
400            for (Annotation annotation : parameterAnnotation) {
401                Expression answer = createParameterUnmarshalExpressionForAnnotation(clazz, method, parameterType, annotation);
402                if (answer != null) {
403                    return answer;
404                }
405            }
406            return strategy.getDefaultParameterTypeExpression(parameterType);
407        }
408    
409        protected boolean isPossibleBodyParameter(Annotation[] annotations) {
410            if (annotations != null) {
411                for (Annotation annotation : annotations) {
412                    if ((annotation instanceof Property)
413                            || (annotation instanceof Header)
414                            || (annotation instanceof Headers)
415                            || (annotation instanceof OutHeaders)
416                            || (annotation instanceof Properties)) {
417                        return false;
418                    }
419                    LanguageAnnotation languageAnnotation = annotation.annotationType().getAnnotation(LanguageAnnotation.class);
420                    if (languageAnnotation != null) {
421                        return false;
422                    }
423                }
424            }
425            return true;
426        }
427    
428        protected Expression createParameterUnmarshalExpressionForAnnotation(Class clazz, Method method,
429                                                                             Class parameterType,
430                                                                             Annotation annotation) {
431            if (annotation instanceof Property) {
432                Property propertyAnnotation = (Property)annotation;
433                return ExpressionBuilder.propertyExpression(propertyAnnotation.value());
434            } else if (annotation instanceof Properties) {
435                return ExpressionBuilder.propertiesExpression();
436            } else if (annotation instanceof Header) {
437                Header headerAnnotation = (Header)annotation;
438                return ExpressionBuilder.headerExpression(headerAnnotation.value());
439            } else if (annotation instanceof Headers) {
440                return ExpressionBuilder.headersExpression();
441            } else if (annotation instanceof OutHeaders) {
442                return ExpressionBuilder.outHeadersExpression();
443            } else if (annotation instanceof ExchangeException) {
444                return ExpressionBuilder.exchangeExceptionExpression();
445            } else {
446                LanguageAnnotation languageAnnotation = annotation.annotationType().getAnnotation(LanguageAnnotation.class);
447                if (languageAnnotation != null) {
448                    Class<?> type = languageAnnotation.factory();
449                    Object object = camelContext.getInjector().newInstance(type);
450                    if (object instanceof AnnotationExpressionFactory) {
451                        AnnotationExpressionFactory expressionFactory = (AnnotationExpressionFactory) object;
452                        return expressionFactory.createExpression(camelContext, annotation, languageAnnotation, parameterType);
453                    } else {
454                        LOG.error("Ignoring bad annotation: " + languageAnnotation + "on method: " + method
455                                + " which declares a factory: " + type.getName()
456                                + " which does not implement " + AnnotationExpressionFactory.class.getName());
457                    }
458                }
459            }
460    
461            return null;
462        }
463    
464        protected boolean isValidMethod(Class clazz, Method method) {
465            // must be a public method
466            if (!Modifier.isPublic(method.getModifiers())) {
467                return false;
468            }
469    
470            // return type must not be an Exchange
471            if (method.getReturnType() != null && Exchange.class.isAssignableFrom(method.getReturnType())) {
472                return false;
473            }
474    
475            return true;
476        }
477    
478        public static ParameterMappingStrategy createParameterMappingStrategy(CamelContext camelContext) {
479            Registry registry = camelContext.getRegistry();
480            ParameterMappingStrategy answer = registry.lookup(ParameterMappingStrategy.class.getName(),
481                                                              ParameterMappingStrategy.class);
482            if (answer == null) {
483                answer = new DefaultParameterMappingStrategy();
484            }
485            return answer;
486        }
487    }