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 }