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 }