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 020 import java.lang.annotation.Annotation; 021 import java.lang.reflect.Method; 022 import java.lang.reflect.Modifier; 023 import java.util.ArrayList; 024 import java.util.Arrays; 025 import java.util.Collection; 026 import java.util.HashMap; 027 import java.util.List; 028 import java.util.Map; 029 import java.util.concurrent.ConcurrentHashMap; 030 031 import org.apache.camel.Body; 032 import org.apache.camel.CamelContext; 033 import org.apache.camel.Exchange; 034 import org.apache.camel.Expression; 035 import org.apache.camel.Header; 036 import org.apache.camel.Headers; 037 import org.apache.camel.Message; 038 import org.apache.camel.NoTypeConversionAvailableException; 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: 758700 $ 058 */ 059 public class BeanInfo { 060 private static final transient Log LOG = LogFactory.getLog(BeanInfo.class); 061 private final CamelContext camelContext; 062 private Class type; 063 private ParameterMappingStrategy strategy; 064 private final Map<String, List<MethodInfo>> operations = new ConcurrentHashMap<String, List<MethodInfo>>(); 065 private MethodInfo defaultMethod; 066 private List<MethodInfo> operationsWithBody = new ArrayList<MethodInfo>(); 067 private List<MethodInfo> operationsWithCustomAnnotation = new ArrayList<MethodInfo>(); 068 private Map<Method, MethodInfo> methodMap = new HashMap<Method, MethodInfo>(); 069 private BeanInfo superBeanInfo; 070 071 public BeanInfo(CamelContext camelContext, Class type) { 072 this(camelContext, type, createParameterMappingStrategy(camelContext)); 073 } 074 075 public BeanInfo(CamelContext camelContext, Class type, ParameterMappingStrategy strategy) { 076 this.camelContext = camelContext; 077 this.type = type; 078 this.strategy = strategy; 079 introspect(getType()); 080 // if there are only 1 method with 1 operation then select it as a default/fallback method 081 if (operations.size() == 1) { 082 List<MethodInfo> methods = operations.values().iterator().next(); 083 if (methods.size() == 1) { 084 defaultMethod = methods.get(0); 085 } 086 } 087 } 088 089 public Class getType() { 090 return type; 091 } 092 093 public CamelContext getCamelContext() { 094 return camelContext; 095 } 096 097 public MethodInvocation createInvocation(Method method, Object pojo, Exchange exchange) 098 throws RuntimeCamelException { 099 MethodInfo methodInfo = introspect(type, method); 100 if (methodInfo != null) { 101 return methodInfo.createMethodInvocation(pojo, exchange); 102 } 103 return null; 104 } 105 106 public MethodInvocation createInvocation(Object pojo, Exchange exchange) throws RuntimeCamelException, 107 AmbiguousMethodCallException { 108 MethodInfo methodInfo = null; 109 110 String name = exchange.getIn().getHeader(BeanProcessor.METHOD_NAME, String.class); 111 if (name != null) { 112 if (operations.containsKey(name)) { 113 List<MethodInfo> methods = operations.get(name); 114 if (methods != null && methods.size() == 1) { 115 methodInfo = methods.get(0); 116 } 117 } 118 } 119 if (methodInfo == null) { 120 methodInfo = chooseMethod(pojo, exchange); 121 } 122 if (methodInfo == null) { 123 methodInfo = defaultMethod; 124 } 125 if (methodInfo != null) { 126 return methodInfo.createMethodInvocation(pojo, exchange); 127 } 128 return null; 129 } 130 131 protected void introspect(Class clazz) { 132 if (LOG.isTraceEnabled()) { 133 LOG.trace("Introspecting class: " + clazz); 134 } 135 Method[] methods = clazz.getDeclaredMethods(); 136 for (Method method : methods) { 137 if (isValidMethod(clazz, method)) { 138 introspect(clazz, method); 139 } 140 } 141 Class superclass = clazz.getSuperclass(); 142 if (superclass != null && !superclass.equals(Object.class)) { 143 introspect(superclass); 144 } 145 } 146 147 protected MethodInfo introspect(Class clazz, Method method) { 148 if (LOG.isTraceEnabled()) { 149 LOG.trace("Introspecting class: " + clazz + ", method: " + method); 150 } 151 String opName = method.getName(); 152 153 MethodInfo methodInfo = createMethodInfo(clazz, method); 154 155 // methods already registered should be prefered to use instead of super classes of existing methods 156 // we want to us the method from the sub class over super classes, so if we have already registered 157 // the method then use it (we are traversing upwards: sub (child) -> super (farther) ) 158 MethodInfo existingMethodInfo = overridesExistingMethod(methodInfo); 159 if (existingMethodInfo != null) { 160 if (LOG.isTraceEnabled()) { 161 LOG.trace("This method is already overriden in a subclass, so the method from the sub class is prefered: " + existingMethodInfo); 162 } 163 164 return existingMethodInfo; 165 } 166 167 if (LOG.isTraceEnabled()) { 168 LOG.trace("Adding operation: " + opName + " for method: " + methodInfo); 169 } 170 if (operations.containsKey(opName)) { 171 // we have an overloaded method so add the method info to the same key 172 List<MethodInfo> existing = operations.get(opName); 173 existing.add(methodInfo); 174 } else { 175 // its a new method we have not seen before so wrap it in a list and add it 176 List<MethodInfo> methods = new ArrayList<MethodInfo>(); 177 methods.add(methodInfo); 178 operations.put(opName, methods); 179 } 180 181 if (methodInfo.hasBodyParameter()) { 182 operationsWithBody.add(methodInfo); 183 } 184 if (methodInfo.isHasCustomAnnotation() && !methodInfo.hasBodyParameter()) { 185 operationsWithCustomAnnotation.add(methodInfo); 186 } 187 188 // must add to method map last otherwise we break stuff 189 methodMap.put(method, methodInfo); 190 191 return methodInfo; 192 } 193 194 /** 195 * Does the given method info override an existing method registered before (from a subclass) 196 * 197 * @param methodInfo the method to test 198 * @return the already registered method to use, null if not overriding any 199 */ 200 private MethodInfo overridesExistingMethod(MethodInfo methodInfo) { 201 for (MethodInfo info : methodMap.values()) { 202 203 // name test 204 if (!info.getMethod().getName().equals(methodInfo.getMethod().getName())) { 205 continue; 206 } 207 208 // parameter types 209 if (info.getMethod().getParameterTypes().length != methodInfo.getMethod().getParameterTypes().length) { 210 continue; 211 } 212 213 boolean found = false; 214 for (int i = 0; i < info.getMethod().getParameterTypes().length; i++) { 215 Class type1 = info.getMethod().getParameterTypes()[i]; 216 Class type2 = methodInfo.getMethod().getParameterTypes()[i]; 217 if (type1.equals(type2)) { 218 found = true; 219 break; 220 } 221 } 222 223 if (found) { 224 // same name, same parameters, then its overrides an existing class 225 return info; 226 } 227 } 228 229 return null; 230 } 231 232 /** 233 * Returns the {@link MethodInfo} for the given method if it exists or null 234 * if there is no metadata available for the given method 235 */ 236 public MethodInfo getMethodInfo(Method method) { 237 MethodInfo answer = methodMap.get(method); 238 if (answer == null) { 239 // maybe the method is defined on a base class? 240 if (superBeanInfo == null && type != Object.class) { 241 Class superclass = type.getSuperclass(); 242 if (superclass != null && superclass != Object.class) { 243 superBeanInfo = new BeanInfo(camelContext, superclass, strategy); 244 return superBeanInfo.getMethodInfo(method); 245 } 246 } 247 } 248 return answer; 249 } 250 251 protected MethodInfo createMethodInfo(Class clazz, Method method) { 252 Class[] parameterTypes = method.getParameterTypes(); 253 Annotation[][] parametersAnnotations = method.getParameterAnnotations(); 254 255 List<ParameterInfo> parameters = new ArrayList<ParameterInfo>(); 256 List<ParameterInfo> bodyParameters = new ArrayList<ParameterInfo>(); 257 258 boolean hasCustomAnnotation = false; 259 for (int i = 0; i < parameterTypes.length; i++) { 260 Class parameterType = parameterTypes[i]; 261 Annotation[] parameterAnnotations = parametersAnnotations[i]; 262 Expression expression = createParameterUnmarshalExpression(clazz, method, parameterType, 263 parameterAnnotations); 264 hasCustomAnnotation |= expression != null; 265 266 ParameterInfo parameterInfo = new ParameterInfo(i, parameterType, parameterAnnotations, 267 expression); 268 parameters.add(parameterInfo); 269 270 if (expression == null) { 271 hasCustomAnnotation |= ObjectHelper.hasAnnotation(parameterAnnotations, Body.class); 272 if (bodyParameters.isEmpty()) { 273 // lets assume its the body 274 if (Exchange.class.isAssignableFrom(parameterType)) { 275 expression = ExpressionBuilder.exchangeExpression(); 276 } else { 277 expression = ExpressionBuilder.bodyExpression(parameterType); 278 } 279 parameterInfo.setExpression(expression); 280 bodyParameters.add(parameterInfo); 281 } else { 282 // will ignore the expression for parameter evaluation 283 } 284 } 285 286 } 287 288 // now lets add the method to the repository 289 290 // TODO allow an annotation to expose the operation name to use 291 /* if (method.getAnnotation(Operation.class) != null) { String name = 292 * method.getAnnotation(Operation.class).name(); if (name != null && 293 * name.length() > 0) { opName = name; } } 294 */ 295 MethodInfo methodInfo = new MethodInfo(clazz, method, parameters, bodyParameters, hasCustomAnnotation); 296 return methodInfo; 297 } 298 299 /** 300 * Lets try choose one of the available methods to invoke if we can match 301 * the message body to the body parameter 302 * 303 * @param pojo the bean to invoke a method on 304 * @param exchange the message exchange 305 * @return the method to invoke or null if no definitive method could be 306 * matched 307 */ 308 protected MethodInfo chooseMethod(Object pojo, Exchange exchange) throws AmbiguousMethodCallException { 309 if (operationsWithBody.size() == 1) { 310 return operationsWithBody.get(0); 311 } else if (!operationsWithBody.isEmpty()) { 312 return chooseMethodWithMatchingBody(exchange, operationsWithBody); 313 } else if (operationsWithCustomAnnotation.size() == 1) { 314 return operationsWithCustomAnnotation.get(0); 315 } 316 return null; 317 } 318 319 protected MethodInfo chooseMethodWithMatchingBody(Exchange exchange, Collection<MethodInfo> operationList) throws AmbiguousMethodCallException { 320 // lets see if we can find a method who's body param type matches the message body 321 Message in = exchange.getIn(); 322 Object body = in.getBody(); 323 if (body != null) { 324 Class bodyType = body.getClass(); 325 if (LOG.isTraceEnabled()) { 326 LOG.trace("Matching for method with a single parameter that matches type: " + bodyType.getCanonicalName()); 327 } 328 329 List<MethodInfo> possibles = new ArrayList<MethodInfo>(); 330 for (MethodInfo methodInfo : operationList) { 331 // TODO: AOP proxies have additioan methods - consider having a static 332 // method exclude list to skip all known AOP proxy methods 333 // TODO: This class could use some TRACE logging 334 335 // test for MEP pattern matching 336 boolean out = exchange.getPattern().isOutCapable(); 337 if (out && methodInfo.isReturnTypeVoid()) { 338 // skip this method as the MEP is Out so the method must return someting 339 continue; 340 } 341 342 // try to match the arguments 343 if (methodInfo.bodyParameterMatches(bodyType)) { 344 possibles.add(methodInfo); 345 } 346 } 347 if (possibles.size() == 1) { 348 return possibles.get(0); 349 } else if (possibles.isEmpty()) { 350 // lets try converting 351 Object newBody = null; 352 MethodInfo matched = null; 353 for (MethodInfo methodInfo : operationList) { 354 Object value = null; 355 try { 356 value = convertToType(exchange, methodInfo.getBodyParameterType(), body); 357 if (value != null) { 358 if (newBody != null) { 359 throw new AmbiguousMethodCallException(exchange, Arrays.asList(matched, 360 methodInfo)); 361 } else { 362 newBody = value; 363 matched = methodInfo; 364 } 365 } 366 } catch (NoTypeConversionAvailableException e) { 367 // we can safely ignore this exception as we want a behaviour similar to 368 // that if convertToType return null 369 } 370 } 371 if (matched != null) { 372 in.setBody(newBody); 373 return matched; 374 } 375 } else { 376 // if we only have a single method with custom annotations, lets use that one 377 if (operationsWithCustomAnnotation.size() == 1) { 378 return operationsWithCustomAnnotation.get(0); 379 } 380 return chooseMethodWithCustomAnnotations(exchange, possibles); 381 } 382 } 383 // no match so return null 384 return null; 385 } 386 387 protected MethodInfo chooseMethodWithCustomAnnotations(Exchange exchange, Collection<MethodInfo> possibles) throws AmbiguousMethodCallException { 388 // if we have only one method with custom annotations lets choose that 389 MethodInfo chosen = null; 390 for (MethodInfo possible : possibles) { 391 if (possible.isHasCustomAnnotation()) { 392 if (chosen != null) { 393 chosen = null; 394 break; 395 } else { 396 chosen = possible; 397 } 398 } 399 } 400 if (chosen != null) { 401 return chosen; 402 } 403 throw new AmbiguousMethodCallException(exchange, possibles); 404 } 405 406 /** 407 * Creates an expression for the given parameter type if the parameter can 408 * be mapped automatically or null if the parameter cannot be mapped due to 409 * unsufficient annotations or not fitting with the default type 410 * conventions. 411 */ 412 protected Expression createParameterUnmarshalExpression(Class clazz, Method method, Class parameterType, 413 Annotation[] parameterAnnotation) { 414 415 // TODO look for a parameter annotation that converts into an expression 416 for (Annotation annotation : parameterAnnotation) { 417 Expression answer = createParameterUnmarshalExpressionForAnnotation(clazz, method, parameterType, 418 annotation); 419 if (answer != null) { 420 return answer; 421 } 422 } 423 return strategy.getDefaultParameterTypeExpression(parameterType); 424 } 425 426 protected boolean isPossibleBodyParameter(Annotation[] annotations) { 427 if (annotations != null) { 428 for (Annotation annotation : annotations) { 429 if ((annotation instanceof Property) 430 || (annotation instanceof Header) 431 || (annotation instanceof Headers) 432 || (annotation instanceof OutHeaders) 433 || (annotation instanceof Properties)) { 434 return false; 435 } 436 LanguageAnnotation languageAnnotation = annotation.annotationType().getAnnotation(LanguageAnnotation.class); 437 if (languageAnnotation != null) { 438 return false; 439 } 440 } 441 } 442 return true; 443 } 444 445 protected Expression createParameterUnmarshalExpressionForAnnotation(Class clazz, Method method, 446 Class parameterType, 447 Annotation annotation) { 448 if (annotation instanceof Property) { 449 Property propertyAnnotation = (Property)annotation; 450 return ExpressionBuilder.propertyExpression(propertyAnnotation.name()); 451 } else if (annotation instanceof Properties) { 452 return ExpressionBuilder.propertiesExpression(); 453 } else if (annotation instanceof Header) { 454 Header headerAnnotation = (Header)annotation; 455 return ExpressionBuilder.headerExpression(headerAnnotation.name()); 456 } else if (annotation instanceof Headers) { 457 return ExpressionBuilder.headersExpression(); 458 } else if (annotation instanceof OutHeaders) { 459 return ExpressionBuilder.outHeadersExpression(); 460 } else { 461 LanguageAnnotation languageAnnotation = annotation.annotationType().getAnnotation(LanguageAnnotation.class); 462 if (languageAnnotation != null) { 463 Class<?> type = languageAnnotation.factory(); 464 Object object = camelContext.getInjector().newInstance(type); 465 if (object instanceof AnnotationExpressionFactory) { 466 AnnotationExpressionFactory expressionFactory = (AnnotationExpressionFactory) object; 467 return expressionFactory.createExpression(camelContext, annotation, languageAnnotation, parameterType); 468 } else { 469 LOG.error("Ignoring bad annotation: " + languageAnnotation + "on method: " + method 470 + " which declares a factory: " + type.getName() 471 + " which does not implement " + AnnotationExpressionFactory.class.getName()); 472 } 473 } 474 } 475 476 return null; 477 } 478 479 protected boolean isValidMethod(Class clazz, Method method) { 480 // must be a public method 481 if (!Modifier.isPublic(method.getModifiers())) { 482 return false; 483 } 484 485 // return type must not be an Exchange 486 if (method.getReturnType() != null && Exchange.class.isAssignableFrom(method.getReturnType())) { 487 return false; 488 } 489 490 return true; 491 } 492 493 public static ParameterMappingStrategy createParameterMappingStrategy(CamelContext camelContext) { 494 Registry registry = camelContext.getRegistry(); 495 ParameterMappingStrategy answer = registry.lookup(ParameterMappingStrategy.class.getName(), 496 ParameterMappingStrategy.class); 497 if (answer == null) { 498 answer = new DefaultParameterMappingStrategy(); 499 } 500 return answer; 501 } 502 }