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