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