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