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: 707928 $
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 }