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.util.*; 023 import java.util.concurrent.ConcurrentHashMap; 024 025 import org.apache.camel.Body; 026 import org.apache.camel.CamelContext; 027 import org.apache.camel.Exchange; 028 import org.apache.camel.Expression; 029 import org.apache.camel.Header; 030 import org.apache.camel.Headers; 031 import org.apache.camel.Message; 032 import org.apache.camel.OutHeaders; 033 import org.apache.camel.Properties; 034 import org.apache.camel.Property; 035 import org.apache.camel.RuntimeCamelException; 036 import org.apache.camel.builder.ExpressionBuilder; 037 import org.apache.camel.language.LanguageAnnotation; 038 import org.apache.camel.spi.Registry; 039 import org.apache.camel.util.ObjectHelper; 040 import org.apache.commons.logging.Log; 041 import org.apache.commons.logging.LogFactory; 042 043 import static org.apache.camel.util.ExchangeHelper.convertToType; 044 045 /** 046 * Represents the metadata about a bean type created via a combination of 047 * introspection and annotations together with some useful sensible defaults 048 * 049 * @version $Revision: 45733 $ 050 */ 051 public class BeanInfo { 052 private static final transient Log LOG = LogFactory.getLog(BeanInfo.class); 053 private final CamelContext camelContext; 054 private Class type; 055 private ParameterMappingStrategy strategy; 056 private Map<String, MethodInfo> operations = new ConcurrentHashMap<String, MethodInfo>(); 057 private MethodInfo defaultMethod; 058 private List<MethodInfo> operationsWithBody = new ArrayList<MethodInfo>(); 059 private List<MethodInfo> operationsWithCustomAnnotation = new ArrayList<MethodInfo>(); 060 private Map<Method, MethodInfo> methodMap = new HashMap<Method, MethodInfo>(); 061 private BeanInfo superBeanInfo; 062 063 public BeanInfo(CamelContext camelContext, Class type) { 064 this(camelContext, type, createParameterMappingStrategy(camelContext)); 065 } 066 067 public BeanInfo(CamelContext camelContext, Class type, ParameterMappingStrategy strategy) { 068 this.camelContext = camelContext; 069 this.type = type; 070 this.strategy = strategy; 071 introspect(getType()); 072 if (operations.size() == 1) { 073 Collection<MethodInfo> methodInfos = operations.values(); 074 for (MethodInfo methodInfo : methodInfos) { 075 defaultMethod = methodInfo; 076 } 077 } 078 } 079 080 public Class getType() { 081 return type; 082 } 083 084 public CamelContext getCamelContext() { 085 return camelContext; 086 } 087 088 public MethodInvocation createInvocation(Method method, Object pojo, Exchange exchange) 089 throws RuntimeCamelException { 090 MethodInfo methodInfo = introspect(type, method); 091 if (methodInfo != null) { 092 return methodInfo.createMethodInvocation(pojo, exchange); 093 } 094 return null; 095 } 096 097 public MethodInvocation createInvocation(Object pojo, Exchange exchange) throws RuntimeCamelException, 098 AmbiguousMethodCallException { 099 MethodInfo methodInfo = null; 100 101 // TODO use some other mechanism? 102 String name = exchange.getIn().getHeader(BeanProcessor.METHOD_NAME, String.class); 103 if (name != null) { 104 methodInfo = operations.get(name); 105 } 106 if (methodInfo == null) { 107 methodInfo = chooseMethod(pojo, exchange); 108 } 109 if (methodInfo == null) { 110 methodInfo = defaultMethod; 111 } 112 if (methodInfo != null) { 113 return methodInfo.createMethodInvocation(pojo, exchange); 114 } 115 return null; 116 } 117 118 protected void introspect(Class clazz) { 119 Method[] methods = clazz.getDeclaredMethods(); 120 for (Method method : methods) { 121 if (isValidMethod(clazz, method)) { 122 introspect(clazz, method); 123 } 124 } 125 Class superclass = clazz.getSuperclass(); 126 if (superclass != null && !superclass.equals(Object.class)) { 127 introspect(superclass); 128 } 129 } 130 131 protected MethodInfo introspect(Class clazz, Method method) { 132 String opName = method.getName(); 133 134 MethodInfo methodInfo = createMethodInfo(clazz, method); 135 136 operations.put(opName, methodInfo); 137 if (methodInfo.hasBodyParameter()) { 138 operationsWithBody.add(methodInfo); 139 } 140 if (methodInfo.isHasCustomAnnotation() && !methodInfo.hasBodyParameter()) { 141 operationsWithCustomAnnotation.add(methodInfo); 142 } 143 methodMap.put(method, methodInfo); 144 return methodInfo; 145 } 146 147 /** 148 * Returns the {@link MethodInfo} for the given method if it exists or null 149 * if there is no metadata available for the given method 150 * 151 * @param method 152 */ 153 public MethodInfo getMethodInfo(Method method) { 154 MethodInfo answer = methodMap.get(method); 155 if (answer == null) { 156 // maybe the method is defined on a base class? 157 if (superBeanInfo == null && type != Object.class) { 158 Class superclass = type.getSuperclass(); 159 if (superclass != null && superclass != Object.class) { 160 superBeanInfo = new BeanInfo(camelContext, superclass, strategy); 161 return superBeanInfo.getMethodInfo(method); 162 } 163 } 164 } 165 return answer; 166 } 167 168 169 protected MethodInfo createMethodInfo(Class clazz, Method method) { 170 Class[] parameterTypes = method.getParameterTypes(); 171 Annotation[][] parametersAnnotations = method.getParameterAnnotations(); 172 173 List<ParameterInfo> parameters = new ArrayList<ParameterInfo>(); 174 List<ParameterInfo> bodyParameters = new ArrayList<ParameterInfo>(); 175 176 boolean hasCustomAnnotation = false; 177 for (int i = 0; i < parameterTypes.length; i++) { 178 Class parameterType = parameterTypes[i]; 179 Annotation[] parameterAnnotations = parametersAnnotations[i]; 180 Expression expression = createParameterUnmarshalExpression(clazz, method, parameterType, 181 parameterAnnotations); 182 hasCustomAnnotation |= expression != null; 183 184 ParameterInfo parameterInfo = new ParameterInfo(i, parameterType, parameterAnnotations, 185 expression); 186 parameters.add(parameterInfo); 187 188 if (expression == null) { 189 hasCustomAnnotation |= ObjectHelper.hasAnnotation(parameterAnnotations, Body.class); 190 if (bodyParameters.isEmpty()) { 191 // lets assume its the body 192 if (Exchange.class.isAssignableFrom(parameterType)) { 193 expression = ExpressionBuilder.exchangeExpression(); 194 } else { 195 expression = ExpressionBuilder.bodyExpression(parameterType); 196 } 197 parameterInfo.setExpression(expression); 198 bodyParameters.add(parameterInfo); 199 } else { 200 // will ignore the expression for parameter evaluation 201 } 202 } 203 204 } 205 206 // now lets add the method to the repository 207 208 // TODO allow an annotation to expose the operation name to use 209 /* if (method.getAnnotation(Operation.class) != null) { String name = 210 * method.getAnnotation(Operation.class).name(); if (name != null && 211 * name.length() > 0) { opName = name; } } 212 */ 213 MethodInfo methodInfo = new MethodInfo(clazz, method, parameters, bodyParameters, hasCustomAnnotation); 214 return methodInfo; 215 } 216 217 /** 218 * Lets try choose one of the available methods to invoke if we can match 219 * the message body to the body parameter 220 * 221 * @param pojo the bean to invoke a method on 222 * @param exchange the message exchange 223 * @return the method to invoke or null if no definitive method could be 224 * matched 225 */ 226 protected MethodInfo chooseMethod(Object pojo, Exchange exchange) throws AmbiguousMethodCallException { 227 if (operationsWithBody.size() == 1) { 228 return operationsWithBody.get(0); 229 } else if (!operationsWithBody.isEmpty()) { 230 return chooseMethodWithMatchingBody(exchange, operationsWithBody); 231 } else if (operationsWithCustomAnnotation.size() == 1) { 232 return operationsWithCustomAnnotation.get(0); 233 } 234 return null; 235 } 236 237 protected MethodInfo chooseMethodWithMatchingBody(Exchange exchange, Collection<MethodInfo> operationList) throws AmbiguousMethodCallException { 238 // lets see if we can find a method who's body param type matches the message body 239 Message in = exchange.getIn(); 240 Object body = in.getBody(); 241 if (body != null) { 242 Class bodyType = body.getClass(); 243 244 List<MethodInfo> possibles = new ArrayList<MethodInfo>(); 245 for (MethodInfo methodInfo : operationList) { 246 // TODO: AOP proxies have additioan methods - consider having a static 247 // method exclude list to skip all known AOP proxy methods 248 // TODO: This class could use some TRACE logging 249 250 // test for MEP pattern matching 251 boolean out = exchange.getPattern().isOutCapable(); 252 if (out && methodInfo.isReturnTypeVoid()) { 253 // skip this method as the MEP is Out so the method must return someting 254 continue; 255 } 256 257 // try to match the arguments 258 if (methodInfo.bodyParameterMatches(bodyType)) { 259 possibles.add(methodInfo); 260 } 261 } 262 if (possibles.size() == 1) { 263 return possibles.get(0); 264 } else if (possibles.isEmpty()) { 265 // lets try converting 266 Object newBody = null; 267 MethodInfo matched = null; 268 for (MethodInfo methodInfo : operationList) { 269 Object value = convertToType(exchange, methodInfo.getBodyParameterType(), body); 270 if (value != null) { 271 if (newBody != null) { 272 throw new AmbiguousMethodCallException(exchange, Arrays.asList(matched, 273 methodInfo)); 274 } else { 275 newBody = value; 276 matched = methodInfo; 277 } 278 } 279 } 280 if (matched != null) { 281 in.setBody(newBody); 282 return matched; 283 } 284 } else { 285 // if we only have a single method with custom annotations, lets use that one 286 if (operationsWithCustomAnnotation.size() == 1) { 287 return operationsWithCustomAnnotation.get(0); 288 } 289 return chooseMethodWithCustomAnnotations(exchange, possibles); 290 } 291 } 292 // no match so return null 293 return null; 294 } 295 296 protected MethodInfo chooseMethodWithCustomAnnotations(Exchange exchange, Collection<MethodInfo> possibles) throws AmbiguousMethodCallException { 297 // if we have only one method with custom annotations lets choose that 298 MethodInfo chosen = null; 299 for (MethodInfo possible : possibles) { 300 if (possible.isHasCustomAnnotation()) { 301 if (chosen != null) { 302 chosen = null; 303 break; 304 } else { 305 chosen = possible; 306 } 307 } 308 } 309 if (chosen != null) { 310 return chosen; 311 } 312 throw new AmbiguousMethodCallException(exchange, possibles); 313 } 314 315 /** 316 * Creates an expression for the given parameter type if the parameter can 317 * be mapped automatically or null if the parameter cannot be mapped due to 318 * unsufficient annotations or not fitting with the default type 319 * conventions. 320 */ 321 protected Expression createParameterUnmarshalExpression(Class clazz, Method method, Class parameterType, 322 Annotation[] parameterAnnotation) { 323 324 // TODO look for a parameter annotation that converts into an expression 325 for (Annotation annotation : parameterAnnotation) { 326 Expression answer = createParameterUnmarshalExpressionForAnnotation(clazz, method, parameterType, 327 annotation); 328 if (answer != null) { 329 return answer; 330 } 331 } 332 return strategy.getDefaultParameterTypeExpression(parameterType); 333 } 334 335 protected boolean isPossibleBodyParameter(Annotation[] annotations) { 336 if (annotations != null) { 337 for (Annotation annotation : annotations) { 338 if ((annotation instanceof Property) 339 || (annotation instanceof Header) 340 || (annotation instanceof Headers) 341 || (annotation instanceof OutHeaders) 342 || (annotation instanceof Properties)) { 343 return false; 344 } 345 LanguageAnnotation languageAnnotation = annotation.annotationType().getAnnotation(LanguageAnnotation.class); 346 if (languageAnnotation != null) { 347 return false; 348 } 349 } 350 } 351 return true; 352 } 353 354 protected Expression createParameterUnmarshalExpressionForAnnotation(Class clazz, Method method, 355 Class parameterType, 356 Annotation annotation) { 357 if (annotation instanceof Property) { 358 Property propertyAnnotation = (Property)annotation; 359 return ExpressionBuilder.propertyExpression(propertyAnnotation.name()); 360 } else if (annotation instanceof Properties) { 361 return ExpressionBuilder.propertiesExpression(); 362 } else if (annotation instanceof Header) { 363 Header headerAnnotation = (Header)annotation; 364 return ExpressionBuilder.headerExpression(headerAnnotation.name()); 365 } else if (annotation instanceof Headers) { 366 return ExpressionBuilder.headersExpression(); 367 } else if (annotation instanceof OutHeaders) { 368 return ExpressionBuilder.outHeadersExpression(); 369 } else { 370 LanguageAnnotation languageAnnotation = annotation.annotationType().getAnnotation(LanguageAnnotation.class); 371 if (languageAnnotation != null) { 372 Class<?> type = languageAnnotation.factory(); 373 Object object = camelContext.getInjector().newInstance(type); 374 if (object instanceof AnnotationExpressionFactory) { 375 AnnotationExpressionFactory expressionFactory = (AnnotationExpressionFactory) object; 376 return expressionFactory.createExpression(camelContext, annotation, languageAnnotation, parameterType); 377 } else { 378 LOG.error("Ignoring bad annotation: " + languageAnnotation + "on method: " + method 379 + " which declares a factory: " + type.getName() 380 + " which does not implement " + AnnotationExpressionFactory.class.getName()); 381 } 382 } 383 } 384 385 return null; 386 } 387 388 protected boolean isValidMethod(Class clazz, Method method) { 389 // must be a public method 390 if (!Modifier.isPublic(method.getModifiers())) { 391 return false; 392 } 393 394 // return type must not be an Exchange 395 if (method.getReturnType() != null && Exchange.class.isAssignableFrom(method.getReturnType())) { 396 return false; 397 } 398 399 return true; 400 } 401 402 public static ParameterMappingStrategy createParameterMappingStrategy(CamelContext camelContext) { 403 Registry registry = camelContext.getRegistry(); 404 ParameterMappingStrategy answer = registry.lookup(ParameterMappingStrategy.class.getName(), 405 ParameterMappingStrategy.class); 406 if (answer == null) { 407 answer = new DefaultParameterMappingStrategy(); 408 } 409 return answer; 410 } 411 }