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