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 }