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