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.lang.reflect.Proxy;
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.ExchangeException;
034 import org.apache.camel.Expression;
035 import org.apache.camel.Handler;
036 import org.apache.camel.Header;
037 import org.apache.camel.Headers;
038 import org.apache.camel.Message;
039 import org.apache.camel.OutHeaders;
040 import org.apache.camel.Properties;
041 import org.apache.camel.Property;
042 import org.apache.camel.RuntimeCamelException;
043 import org.apache.camel.builder.ExpressionBuilder;
044 import org.apache.camel.language.LanguageAnnotation;
045 import org.apache.camel.spi.Registry;
046 import org.apache.camel.util.ObjectHelper;
047 import org.apache.commons.logging.Log;
048 import org.apache.commons.logging.LogFactory;
049
050 import static org.apache.camel.util.ExchangeHelper.convertToType;
051
052
053 /**
054 * Represents the metadata about a bean type created via a combination of
055 * introspection and annotations together with some useful sensible defaults
056 *
057 * @version $Revision: 783006 $
058 */
059 public class BeanInfo {
060 private static final transient Log LOG = LogFactory.getLog(BeanInfo.class);
061 private static final List<Method> EXCLUDED_METHODS = new ArrayList<Method>();
062 private final CamelContext camelContext;
063 private final Class type;
064 private final ParameterMappingStrategy strategy;
065 private final Map<String, List<MethodInfo>> operations = new ConcurrentHashMap<String, List<MethodInfo>>();
066 private final List<MethodInfo> operationsWithBody = new ArrayList<MethodInfo>();
067 private final List<MethodInfo> operationsWithCustomAnnotation = new ArrayList<MethodInfo>();
068 private final List<MethodInfo> operationsWithHandlerAnnotation = new ArrayList<MethodInfo>();
069 private final Map<Method, MethodInfo> methodMap = new ConcurrentHashMap<Method, MethodInfo>();
070 private MethodInfo defaultMethod;
071 private BeanInfo superBeanInfo;
072
073 public BeanInfo(CamelContext camelContext, Class type) {
074 this(camelContext, type, createParameterMappingStrategy(camelContext));
075 }
076
077 public BeanInfo(CamelContext camelContext, Class type, ParameterMappingStrategy strategy) {
078 this.camelContext = camelContext;
079 this.type = type;
080 this.strategy = strategy;
081
082 // configure the default excludes methods
083 synchronized (EXCLUDED_METHODS) {
084 if (EXCLUDED_METHODS.size() == 0) {
085 // exclude all java.lang.Object methods as we dont want to invoke them
086 EXCLUDED_METHODS.addAll(Arrays.asList(Object.class.getMethods()));
087 // exclude all java.lang.reflect.Proxy methods as we dont want to invoke them
088 EXCLUDED_METHODS.addAll(Arrays.asList(Proxy.class.getMethods()));
089
090 // TODO: AOP proxies have additional methods - well known methods should be added to EXCLUDE_METHODS
091 }
092 }
093
094 introspect(getType());
095 // if there are only 1 method with 1 operation then select it as a default/fallback method
096 if (operations.size() == 1) {
097 List<MethodInfo> methods = operations.values().iterator().next();
098 if (methods.size() == 1) {
099 defaultMethod = methods.get(0);
100 }
101 }
102 }
103
104 public Class getType() {
105 return type;
106 }
107
108 public CamelContext getCamelContext() {
109 return camelContext;
110 }
111
112 public static ParameterMappingStrategy createParameterMappingStrategy(CamelContext camelContext) {
113 // lookup in registry first if there is a user define strategy
114 Registry registry = camelContext.getRegistry();
115 ParameterMappingStrategy answer = registry.lookup(BeanConstants.BEAN_PARAMETER_MAPPING_STRATEGY, ParameterMappingStrategy.class);
116 if (answer == null) {
117 // no then use the default one
118 answer = new DefaultParameterMappingStrategy();
119 }
120
121 return answer;
122 }
123
124 public MethodInvocation createInvocation(Method method, Object pojo, Exchange exchange) throws RuntimeCamelException {
125 MethodInfo methodInfo = introspect(type, method);
126 if (methodInfo != null) {
127 return methodInfo.createMethodInvocation(pojo, exchange);
128 }
129 return null;
130 }
131
132 public MethodInvocation createInvocation(Object pojo, Exchange exchange) throws RuntimeCamelException, AmbiguousMethodCallException {
133 MethodInfo methodInfo = null;
134
135 String name = exchange.getIn().getHeader(Exchange.BEAN_METHOD_NAME, String.class);
136 if (name != null) {
137 if (operations.containsKey(name)) {
138 List<MethodInfo> methods = operations.get(name);
139 if (methods != null && methods.size() == 1) {
140 methodInfo = methods.get(0);
141 }
142 }
143 }
144 if (methodInfo == null) {
145 methodInfo = chooseMethod(pojo, exchange);
146 }
147 if (methodInfo == null) {
148 methodInfo = defaultMethod;
149 }
150 if (methodInfo != null) {
151 if (LOG.isTraceEnabled()) {
152 LOG.trace("Chosen method to invoke: " + methodInfo + " on bean: " + pojo);
153 }
154 return methodInfo.createMethodInvocation(pojo, exchange);
155 }
156
157 if (LOG.isDebugEnabled()) {
158 LOG.debug("Cannot find suitable method to invoke on bean: " + pojo);
159 }
160 return null;
161 }
162
163 /**
164 * Introspects the given class
165 *
166 * @param clazz the class
167 */
168 protected void introspect(Class clazz) {
169 if (LOG.isTraceEnabled()) {
170 LOG.trace("Introspecting class: " + clazz);
171 }
172 Method[] methods = clazz.getDeclaredMethods();
173 for (Method method : methods) {
174 if (isValidMethod(clazz, method)) {
175 introspect(clazz, method);
176 }
177 }
178 Class superclass = clazz.getSuperclass();
179 if (superclass != null && !superclass.equals(Object.class)) {
180 introspect(superclass);
181 }
182 }
183
184 /**
185 * Introspects the given method
186 *
187 * @param clazz the class
188 * @param method the method
189 * @return the method info, is newer <tt>null</tt>
190 */
191 protected MethodInfo introspect(Class clazz, Method method) {
192 if (LOG.isTraceEnabled()) {
193 LOG.trace("Introspecting class: " + clazz + ", method: " + method);
194 }
195 String opName = method.getName();
196
197 MethodInfo methodInfo = createMethodInfo(clazz, method);
198
199 // methods already registered should be preferred to use instead of super classes of existing methods
200 // we want to us the method from the sub class over super classes, so if we have already registered
201 // the method then use it (we are traversing upwards: sub (child) -> super (farther) )
202 MethodInfo existingMethodInfo = overridesExistingMethod(methodInfo);
203 if (existingMethodInfo != null) {
204 if (LOG.isTraceEnabled()) {
205 LOG.trace("This method is already overriden in a subclass, so the method from the sub class is prefered: " + existingMethodInfo);
206 }
207
208 return existingMethodInfo;
209 }
210
211 if (LOG.isTraceEnabled()) {
212 LOG.trace("Adding operation: " + opName + " for method: " + methodInfo);
213 }
214
215 if (operations.containsKey(opName)) {
216 // we have an overloaded method so add the method info to the same key
217 List<MethodInfo> existing = operations.get(opName);
218 existing.add(methodInfo);
219 } else {
220 // its a new method we have not seen before so wrap it in a list and add it
221 List<MethodInfo> methods = new ArrayList<MethodInfo>();
222 methods.add(methodInfo);
223 operations.put(opName, methods);
224 }
225
226 if (methodInfo.hasCustomAnnotation()) {
227 operationsWithCustomAnnotation.add(methodInfo);
228 } else if (methodInfo.hasBodyParameter()) {
229 operationsWithBody.add(methodInfo);
230 }
231
232 if (methodInfo.hasHandlerAnnotation()) {
233 operationsWithHandlerAnnotation.add(methodInfo);
234 }
235
236 // must add to method map last otherwise we break stuff
237 methodMap.put(method, methodInfo);
238
239 return methodInfo;
240 }
241
242
243 /**
244 * Returns the {@link MethodInfo} for the given method if it exists or null
245 * if there is no metadata available for the given method
246 */
247 public MethodInfo getMethodInfo(Method method) {
248 MethodInfo answer = methodMap.get(method);
249 if (answer == null) {
250 // maybe the method is defined on a base class?
251 if (superBeanInfo == null && type != Object.class) {
252 Class superclass = type.getSuperclass();
253 if (superclass != null && superclass != Object.class) {
254 superBeanInfo = new BeanInfo(camelContext, superclass, strategy);
255 return superBeanInfo.getMethodInfo(method);
256 }
257 }
258 }
259 return answer;
260 }
261
262 @SuppressWarnings("unchecked")
263 protected MethodInfo createMethodInfo(Class clazz, Method method) {
264 Class[] parameterTypes = method.getParameterTypes();
265 Annotation[][] parametersAnnotations = method.getParameterAnnotations();
266
267 List<ParameterInfo> parameters = new ArrayList<ParameterInfo>();
268 List<ParameterInfo> bodyParameters = new ArrayList<ParameterInfo>();
269
270 boolean hasCustomAnnotation = false;
271 boolean hasHandlerAnnotation = ObjectHelper.hasAnnotation(method.getAnnotations(), Handler.class);
272
273 int size = parameterTypes.length;
274 if (LOG.isTraceEnabled()) {
275 LOG.trace("Creating MethodInfo for class: " + clazz + " method: " + method + " having " + size + " parameters");
276 }
277
278 for (int i = 0; i < size; i++) {
279 Class parameterType = parameterTypes[i];
280 Annotation[] parameterAnnotations = parametersAnnotations[i];
281 Expression expression = createParameterUnmarshalExpression(clazz, method, parameterType, parameterAnnotations);
282 hasCustomAnnotation |= expression != null;
283
284 ParameterInfo parameterInfo = new ParameterInfo(i, parameterType, parameterAnnotations, expression);
285 parameters.add(parameterInfo);
286 if (expression == null) {
287 boolean bodyAnnotation = ObjectHelper.hasAnnotation(parameterAnnotations, Body.class);
288 if (LOG.isTraceEnabled() && bodyAnnotation) {
289 LOG.trace("Parameter #" + i + " has @Body annotation");
290 }
291 hasCustomAnnotation |= bodyAnnotation;
292 if (bodyParameters.isEmpty()) {
293 // okay we have not yet set the body parameter and we have found
294 // the candidate now to use as body parameter
295 if (Exchange.class.isAssignableFrom(parameterType)) {
296 // use exchange
297 expression = ExpressionBuilder.exchangeExpression();
298 } else {
299 // lets assume its the body
300 expression = ExpressionBuilder.bodyExpression(parameterType);
301 }
302 if (LOG.isTraceEnabled()) {
303 LOG.trace("Parameter #" + i + " is the body parameter using expression " + expression);
304 }
305 parameterInfo.setExpression(expression);
306 bodyParameters.add(parameterInfo);
307 } else {
308 // will ignore the expression for parameter evaluation
309 }
310 }
311 if (LOG.isTraceEnabled()) {
312 LOG.trace("Parameter #" + i + " has parameter info: " + parameterInfo);
313 }
314 }
315
316 // now lets add the method to the repository
317 return new MethodInfo(clazz, method, parameters, bodyParameters, hasCustomAnnotation, hasHandlerAnnotation);
318 }
319
320 /**
321 * Lets try choose one of the available methods to invoke if we can match
322 * the message body to the body parameter
323 *
324 * @param pojo the bean to invoke a method on
325 * @param exchange the message exchange
326 * @return the method to invoke or null if no definitive method could be matched
327 * @throws AmbiguousMethodCallException is thrown if cannot chose method due to ambiguous
328 */
329 protected MethodInfo chooseMethod(Object pojo, Exchange exchange) throws AmbiguousMethodCallException {
330 // @Handler should be select first
331 // then any single method that has a custom @annotation
332 // or any single method that has a match parameter type that matches the Exchange payload
333 // and last then try to select the best among the rest
334
335 if (operationsWithHandlerAnnotation.size() > 1) {
336 // if we have more than 1 @Handler then its ambiguous
337 throw new AmbiguousMethodCallException(exchange, operationsWithHandlerAnnotation);
338 }
339
340 if (operationsWithHandlerAnnotation.size() == 1) {
341 // methods with handler should be preferred
342 return operationsWithHandlerAnnotation.get(0);
343 } else if (operationsWithCustomAnnotation.size() == 1) {
344 // if there is one method with an annotation then use that one
345 return operationsWithCustomAnnotation.get(0);
346 } else if (operationsWithBody.size() == 1) {
347 // if there is one method with body then use that one
348 return operationsWithBody.get(0);
349 }
350
351 Collection<MethodInfo> possibleOperations = new ArrayList<MethodInfo>();
352 possibleOperations.addAll(operationsWithBody);
353 possibleOperations.addAll(operationsWithCustomAnnotation);
354
355 if (!possibleOperations.isEmpty()) {
356 // multiple possible operations so find the best suited if possible
357 MethodInfo answer = chooseMethodWithMatchingBody(exchange, possibleOperations);
358 if (answer == null) {
359 throw new AmbiguousMethodCallException(exchange, possibleOperations);
360 } else {
361 return answer;
362 }
363 }
364
365 // not possible to determine
366 return null;
367 }
368
369 @SuppressWarnings("unchecked")
370 private MethodInfo chooseMethodWithMatchingBody(Exchange exchange, Collection<MethodInfo> operationList)
371 throws AmbiguousMethodCallException {
372 // lets see if we can find a method who's body param type matches the message body
373 Message in = exchange.getIn();
374 Object body = in.getBody();
375 if (body != null) {
376 Class bodyType = body.getClass();
377 if (LOG.isTraceEnabled()) {
378 LOG.trace("Matching for method with a single parameter that matches type: " + bodyType.getCanonicalName());
379 }
380
381 List<MethodInfo> possibles = new ArrayList<MethodInfo>();
382 List<MethodInfo> possiblesWithException = new ArrayList<MethodInfo>();
383 for (MethodInfo methodInfo : operationList) {
384 // test for MEP pattern matching
385 boolean out = exchange.getPattern().isOutCapable();
386 if (out && methodInfo.isReturnTypeVoid()) {
387 // skip this method as the MEP is Out so the method must return something
388 continue;
389 }
390
391 // try to match the arguments
392 if (methodInfo.bodyParameterMatches(bodyType)) {
393 if (LOG.isTraceEnabled()) {
394 LOG.trace("Found a possible method: " + methodInfo);
395 }
396 if (methodInfo.hasExceptionParameter()) {
397 // methods with accepts exceptions
398 possiblesWithException.add(methodInfo);
399 } else {
400 // regular methods with no exceptions
401 possibles.add(methodInfo);
402 }
403 }
404 }
405
406 // find best suited method to use
407 return chooseBestPossibleMethodInfo(exchange, operationList, body, possibles, possiblesWithException);
408 }
409
410 // no match so return null
411 return null;
412 }
413
414 @SuppressWarnings("unchecked")
415 private MethodInfo chooseBestPossibleMethodInfo(Exchange exchange, Collection<MethodInfo> operationList, Object body,
416 List<MethodInfo> possibles, List<MethodInfo> possiblesWithException)
417 throws AmbiguousMethodCallException {
418
419 Exception exception = ExpressionBuilder.exchangeExceptionExpression().evaluate(exchange, Exception.class);
420 if (exception != null && possiblesWithException.size() == 1) {
421 if (LOG.isTraceEnabled()) {
422 LOG.trace("Exchange has exception set so we prefer method that also has exception as parameter");
423 }
424 // prefer the method that accepts exception in case we have an exception also
425 return possiblesWithException.get(0);
426 } else if (possibles.size() == 1) {
427 return possibles.get(0);
428 } else if (possibles.isEmpty()) {
429 if (LOG.isTraceEnabled()) {
430 LOG.trace("No poosible methods trying to convert body to parameter types");
431 }
432
433 // lets try converting
434 Object newBody = null;
435 MethodInfo matched = null;
436 for (MethodInfo methodInfo : operationList) {
437 Object value = convertToType(exchange, methodInfo.getBodyParameterType(), body);
438 if (value != null) {
439 if (LOG.isTraceEnabled()) {
440 LOG.trace("Converted body from: " + body.getClass().getCanonicalName()
441 + "to: " + methodInfo.getBodyParameterType().getCanonicalName());
442 }
443 if (newBody != null) {
444 // we already have found one new body that could be converted so now we have 2 methods
445 // and then its ambiguous
446 throw new AmbiguousMethodCallException(exchange, Arrays.asList(matched, methodInfo));
447 } else {
448 newBody = value;
449 matched = methodInfo;
450 }
451 }
452 }
453 if (matched != null) {
454 if (LOG.isTraceEnabled()) {
455 LOG.trace("Setting converted body: " + body);
456 }
457 Message in = exchange.getIn();
458 in.setBody(newBody);
459 return matched;
460 }
461 } else {
462 // if we only have a single method with custom annotations, lets use that one
463 if (operationsWithCustomAnnotation.size() == 1) {
464 MethodInfo answer = operationsWithCustomAnnotation.get(0);
465 if (LOG.isTraceEnabled()) {
466 LOG.trace("There are only one method with annotations so we choose it: " + answer);
467 }
468 return answer;
469 }
470 // phew try to choose among multiple methods with annotations
471 return chooseMethodWithCustomAnnotations(exchange, possibles);
472 }
473
474 // cannot find a good method to use
475 return null;
476 }
477
478 /**
479 * Validates wheter the given method is a valid candidate for Camel Bean Binding.
480 *
481 * @param clazz the class
482 * @param method the method
483 * @return true if valid, false to skip the method
484 */
485 protected boolean isValidMethod(Class clazz, Method method) {
486 // must not be in the excluded list
487 for (Method excluded : EXCLUDED_METHODS) {
488 if (ObjectHelper.isOverridingMethod(excluded, method)) {
489 // the method is overriding an excluded method so its not valid
490 return false;
491 }
492 }
493
494 // must be a public method
495 if (!Modifier.isPublic(method.getModifiers())) {
496 return false;
497 }
498
499 // return type must not be an Exchange
500 if (method.getReturnType() != null && Exchange.class.isAssignableFrom(method.getReturnType())) {
501 return false;
502 }
503
504 return true;
505 }
506
507 /**
508 * Does the given method info override an existing method registered before (from a subclass)
509 *
510 * @param methodInfo the method to test
511 * @return the already registered method to use, null if not overriding any
512 */
513 private MethodInfo overridesExistingMethod(MethodInfo methodInfo) {
514 for (MethodInfo info : methodMap.values()) {
515 Method source = info.getMethod();
516 Method target = methodInfo.getMethod();
517
518 boolean override = ObjectHelper.isOverridingMethod(source, target);
519 if (override) {
520 // same name, same parameters, then its overrides an existing class
521 return info;
522 }
523 }
524
525 return null;
526 }
527
528 private MethodInfo chooseMethodWithCustomAnnotations(Exchange exchange, Collection<MethodInfo> possibles)
529 throws AmbiguousMethodCallException {
530 // if we have only one method with custom annotations lets choose that
531 MethodInfo chosen = null;
532 for (MethodInfo possible : possibles) {
533 if (possible.hasCustomAnnotation()) {
534 if (chosen != null) {
535 chosen = null;
536 break;
537 } else {
538 chosen = possible;
539 }
540 }
541 }
542 if (chosen != null) {
543 return chosen;
544 }
545 throw new AmbiguousMethodCallException(exchange, possibles);
546 }
547
548 /**
549 * Creates an expression for the given parameter type if the parameter can
550 * be mapped automatically or null if the parameter cannot be mapped due to
551 * insufficient annotations or not fitting with the default type
552 * conventions.
553 */
554 private Expression createParameterUnmarshalExpression(Class clazz, Method method, Class parameterType,
555 Annotation[] parameterAnnotation) {
556
557 // look for a parameter annotation that converts into an expression
558 for (Annotation annotation : parameterAnnotation) {
559 Expression answer = createParameterUnmarshalExpressionForAnnotation(clazz, method, parameterType, annotation);
560 if (answer != null) {
561 return answer;
562 }
563 }
564 // no annotations then try the default parameter mappings
565 return strategy.getDefaultParameterTypeExpression(parameterType);
566 }
567
568 private Expression createParameterUnmarshalExpressionForAnnotation(Class clazz, Method method, Class parameterType,
569 Annotation annotation) {
570 if (annotation instanceof Property) {
571 Property propertyAnnotation = (Property)annotation;
572 return ExpressionBuilder.propertyExpression(propertyAnnotation.value());
573 } else if (annotation instanceof Properties) {
574 return ExpressionBuilder.propertiesExpression();
575 } else if (annotation instanceof Header) {
576 Header headerAnnotation = (Header)annotation;
577 return ExpressionBuilder.headerExpression(headerAnnotation.value());
578 } else if (annotation instanceof Headers) {
579 return ExpressionBuilder.headersExpression();
580 } else if (annotation instanceof OutHeaders) {
581 return ExpressionBuilder.outHeadersExpression();
582 } else if (annotation instanceof ExchangeException) {
583 return ExpressionBuilder.exchangeExceptionExpression(parameterType);
584 } else {
585 LanguageAnnotation languageAnnotation = annotation.annotationType().getAnnotation(LanguageAnnotation.class);
586 if (languageAnnotation != null) {
587 Class<?> type = languageAnnotation.factory();
588 Object object = camelContext.getInjector().newInstance(type);
589 if (object instanceof AnnotationExpressionFactory) {
590 AnnotationExpressionFactory expressionFactory = (AnnotationExpressionFactory) object;
591 return expressionFactory.createExpression(camelContext, annotation, languageAnnotation, parameterType);
592 } else {
593 LOG.warn("Ignoring bad annotation: " + languageAnnotation + "on method: " + method
594 + " which declares a factory: " + type.getName()
595 + " which does not implement " + AnnotationExpressionFactory.class.getName());
596 }
597 }
598 }
599
600 return null;
601 }
602
603 }