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