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 }