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.Exchange;
031    import org.apache.camel.Expression;
032    import org.apache.camel.Header;
033    import org.apache.camel.Message;
034    import org.apache.camel.Property;
035    import org.apache.camel.RuntimeCamelException;
036    import org.apache.camel.builder.ExpressionBuilder;
037    import org.apache.commons.logging.Log;
038    import org.apache.commons.logging.LogFactory;
039    
040    import static org.apache.camel.util.ExchangeHelper.convertToType;
041    
042    /**
043     * Represents the metadata about a bean type created via a combination of
044     * introspection and annotations together with some useful sensible defaults
045     * 
046     * @version $Revision: $
047     */
048    public class BeanInfo {
049        private static final transient Log LOG = LogFactory.getLog(BeanInfo.class);
050        private Class type;
051        private ParameterMappingStrategy strategy;
052        private Map<String, MethodInfo> operations = new ConcurrentHashMap<String, MethodInfo>();
053        private MethodInfo defaultMethod;
054        private List<MethodInfo> operationsWithBody = new ArrayList<MethodInfo>();
055    
056        public BeanInfo(Class type, ParameterMappingStrategy strategy) {
057            this.type = type;
058            this.strategy = strategy;
059            introspect(getType());
060            if (operations.size() == 1) {
061                Collection<MethodInfo> methodInfos = operations.values();
062                for (MethodInfo methodInfo : methodInfos) {
063                    defaultMethod = methodInfo;
064                }
065            }
066        }
067    
068        public Class getType() {
069            return type;
070        }
071    
072        public MethodInvocation createInvocation(Method method, Object pojo, Exchange exchange)
073            throws RuntimeCamelException {
074            MethodInfo methodInfo = introspect(type, method);
075            if (methodInfo != null) {
076                return methodInfo.createMethodInvocation(pojo, exchange);
077            }
078            return null;
079        }
080    
081        public MethodInvocation createInvocation(Object pojo, Exchange exchange) throws RuntimeCamelException,
082            AmbiguousMethodCallException {
083            MethodInfo methodInfo = null;
084    
085            // TODO use some other mechanism?
086            String name = exchange.getIn().getHeader(BeanProcessor.METHOD_NAME, String.class);
087            if (name != null) {
088                methodInfo = operations.get(name);
089            }
090            if (methodInfo == null) {
091                methodInfo = chooseMethod(pojo, exchange);
092            }
093            if (methodInfo == null) {
094                methodInfo = defaultMethod;
095            }
096            if (methodInfo != null) {
097                return methodInfo.createMethodInvocation(pojo, exchange);
098            }
099            return null;
100        }
101    
102        protected void introspect(Class clazz) {
103            Method[] methods = clazz.getDeclaredMethods();
104            for (Method method : methods) {
105                if (isValidMethod(clazz, method)) {
106                    introspect(clazz, method);
107                }
108            }
109            Class superclass = clazz.getSuperclass();
110            if (superclass != null && !superclass.equals(Object.class)) {
111                introspect(superclass);
112            }
113        }
114    
115        protected MethodInfo introspect(Class clazz, Method method) {
116            Class[] parameterTypes = method.getParameterTypes();
117            Annotation[][] parametersAnnotations = method.getParameterAnnotations();
118            final Expression[] parameterExpressions = new Expression[parameterTypes.length];
119    
120            List<ParameterInfo> parameters = new ArrayList<ParameterInfo>();
121            List<ParameterInfo> bodyParameters = new ArrayList<ParameterInfo>();
122    
123            for (int i = 0; i < parameterTypes.length; i++) {
124                Class parameterType = parameterTypes[i];
125                Annotation[] parameterAnnotations = parametersAnnotations[i];
126                Expression expression = createParameterUnmarshalExpression(clazz, method, parameterType,
127                                                                           parameterAnnotations);
128                if (expression == null) {
129                    if (parameterTypes.length == 1 && bodyParameters.isEmpty()) {
130                        // lets assume its the body
131                        expression = ExpressionBuilder.bodyExpression(parameterType);
132                    } else {
133                        if (LOG.isDebugEnabled()) {
134                            LOG.debug("No expression available for method: " + method.toString()
135                                      + " which already has a body so ignoring parameter: " + i
136                                      + " so ignoring method");
137                        }
138                        return null;
139                    }
140                }
141    
142                ParameterInfo parameterInfo = new ParameterInfo(i, parameterType, parameterAnnotations,
143                                                                expression);
144                parameters.add(parameterInfo);
145                if (isPossibleBodyParameter(parameterAnnotations)) {
146                    bodyParameters.add(parameterInfo);
147                }
148            }
149    
150            // now lets add the method to the repository
151            String opName = method.getName();
152    
153            /*
154             * 
155             * TODO allow an annotation to expose the operation name to use
156             * 
157             * if (method.getAnnotation(Operation.class) != null) { String name =
158             * method.getAnnotation(Operation.class).name(); if (name != null &&
159             * name.length() > 0) { opName = name; } }
160             */
161            MethodInfo methodInfo = new MethodInfo(clazz, method, parameters, bodyParameters);
162            operations.put(opName, methodInfo);
163            if (methodInfo.hasBodyParameter()) {
164                operationsWithBody.add(methodInfo);
165            }
166            return methodInfo;
167        }
168    
169        /**
170         * Lets try choose one of the available methods to invoke if we can match
171         * the message body to the body parameter
172         * 
173         * @param pojo the bean to invoke a method on
174         * @param exchange the message exchange
175         * @return the method to invoke or null if no definitive method could be
176         *         matched
177         */
178        protected MethodInfo chooseMethod(Object pojo, Exchange exchange) throws AmbiguousMethodCallException {
179            if (operationsWithBody.size() == 1) {
180                return operationsWithBody.get(0);
181            } else if (!operationsWithBody.isEmpty()) {
182                // lets see if we can find a method who's body param type matches
183                // the message body
184                Message in = exchange.getIn();
185                Object body = in.getBody();
186                if (body != null) {
187                    Class bodyType = body.getClass();
188    
189                    List<MethodInfo> possibles = new ArrayList<MethodInfo>();
190                    for (MethodInfo methodInfo : operationsWithBody) {
191                        if (methodInfo.bodyParameterMatches(bodyType)) {
192                            possibles.add(methodInfo);
193                        }
194                    }
195                    if (possibles.size() == 1) {
196                        return possibles.get(0);
197                    } else if (possibles.isEmpty()) {
198                        // lets try converting
199                        Object newBody = null;
200                        MethodInfo matched = null;
201                        for (MethodInfo methodInfo : operationsWithBody) {
202                            Object value = convertToType(exchange, methodInfo.getBodyParameterType(), body);
203                            if (value != null) {
204                                if (newBody != null) {
205                                    throw new AmbiguousMethodCallException(exchange, Arrays.asList(matched,
206                                                                                                   methodInfo));
207                                } else {
208                                    newBody = value;
209                                    matched = methodInfo;
210                                }
211                            }
212                        }
213                        if (matched != null) {
214                            in.setBody(newBody);
215                            return matched;
216                        }
217                    } else {
218                        throw new AmbiguousMethodCallException(exchange, possibles);
219                    }
220                }
221                return null;
222            }
223            return null;
224        }
225    
226        /**
227         * Creates an expression for the given parameter type if the parameter can
228         * be mapped automatically or null if the parameter cannot be mapped due to
229         * unsufficient annotations or not fitting with the default type
230         * conventions.
231         */
232        protected Expression createParameterUnmarshalExpression(Class clazz, Method method, Class parameterType,
233                                                                Annotation[] parameterAnnotation) {
234    
235            // TODO look for a parameter annotation that converts into an expression
236            for (Annotation annotation : parameterAnnotation) {
237                Expression answer = createParameterUnmarshalExpressionForAnnotation(clazz, method, parameterType,
238                                                                                    annotation);
239                if (answer != null) {
240                    return answer;
241                }
242            }
243            return strategy.getDefaultParameterTypeExpression(parameterType);
244        }
245    
246        protected boolean isPossibleBodyParameter(Annotation[] annotations) {
247            if (annotations != null) {
248                for (Annotation annotation : annotations) {
249                    if ((annotation instanceof Property) || (annotation instanceof Header)) {
250                        return false;
251                    }
252                }
253            }
254            return true;
255        }
256    
257        protected Expression createParameterUnmarshalExpressionForAnnotation(Class clazz, Method method,
258                                                                             Class parameterType,
259                                                                             Annotation annotation) {
260            if (annotation instanceof Property) {
261                Property propertyAnnotation = (Property)annotation;
262                return ExpressionBuilder.propertyExpression(propertyAnnotation.name());
263            } else if (annotation instanceof Header) {
264                Header headerAnnotation = (Header)annotation;
265                return ExpressionBuilder.headerExpression(headerAnnotation.name());
266            } else if (annotation instanceof Body) {
267                Body content = (Body)annotation;
268                return ExpressionBuilder.bodyExpression(parameterType);
269    
270                // TODO allow annotations to be used to create expressions?
271                /*
272                 * } else if (annotation instanceof XPath) { XPath xpathAnnotation =
273                 * (XPath) annotation; return new
274                 * JAXPStringXPathExpression(xpathAnnotation.xpath()); }
275                 */
276            }
277            return null;
278        }
279    
280        protected boolean isValidMethod(Class clazz, Method method) {
281            return Modifier.isPublic(method.getModifiers());
282        }
283    }