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