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 }