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