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 }