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