blob: cff182d138a73454094b3ba2859d6c916c61bfee [file] [log] [blame]
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.camel.component.bean;
import java.lang.annotation.Annotation;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import org.apache.camel.AsyncCallback;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
import org.apache.camel.Expression;
import org.apache.camel.ExpressionEvaluationException;
import org.apache.camel.NoTypeConversionAvailableException;
import org.apache.camel.Pattern;
import org.apache.camel.Processor;
import org.apache.camel.RuntimeExchangeException;
import org.apache.camel.processor.DynamicRouter;
import org.apache.camel.processor.RecipientList;
import org.apache.camel.processor.RoutingSlip;
import org.apache.camel.processor.aggregate.AggregationStrategy;
import org.apache.camel.support.ExpressionAdapter;
import org.apache.camel.util.CamelContextHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.ServiceHelper;
import org.apache.camel.util.StringHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.camel.util.ObjectHelper.asString;
/**
* Information about a method to be used for invocation.
*
* @version
*/
public class MethodInfo {
private static final transient Logger LOG = LoggerFactory.getLogger(MethodInfo.class);
private CamelContext camelContext;
private Class<?> type;
private Method method;
private final List<ParameterInfo> parameters;
private final List<ParameterInfo> bodyParameters;
private final boolean hasCustomAnnotation;
private final boolean hasHandlerAnnotation;
private Expression parametersExpression;
private ExchangePattern pattern = ExchangePattern.InOut;
private RecipientList recipientList;
private RoutingSlip routingSlip;
private DynamicRouter dynamicRouter;
/**
* Adapter to invoke the method which has been annotated with the @DynamicRouter
*/
private final class DynamicRouterExpression extends ExpressionAdapter {
private final Object pojo;
private DynamicRouterExpression(Object pojo) {
this.pojo = pojo;
}
@Override
public Object evaluate(Exchange exchange) {
// evaluate arguments on each invocation as the parameters can have changed/updated since last invocation
final Object[] arguments = parametersExpression.evaluate(exchange, Object[].class);
try {
return invoke(method, pojo, arguments, exchange);
} catch (Exception e) {
throw ObjectHelper.wrapRuntimeCamelException(e);
}
}
@Override
public String toString() {
return "DynamicRouter[invoking: " + method + " on bean: " + pojo + "]";
}
}
public MethodInfo(CamelContext camelContext, Class<?> type, Method method, List<ParameterInfo> parameters, List<ParameterInfo> bodyParameters,
boolean hasCustomAnnotation, boolean hasHandlerAnnotation) {
this.camelContext = camelContext;
this.type = type;
this.method = method;
this.parameters = parameters;
this.bodyParameters = bodyParameters;
this.hasCustomAnnotation = hasCustomAnnotation;
this.hasHandlerAnnotation = hasHandlerAnnotation;
this.parametersExpression = createParametersExpression();
Pattern oneway = findOneWayAnnotation(method);
if (oneway != null) {
pattern = oneway.value();
}
if (method.getAnnotation(org.apache.camel.RoutingSlip.class) != null
&& matchContext(method.getAnnotation(org.apache.camel.RoutingSlip.class).context())) {
org.apache.camel.RoutingSlip annotation = method.getAnnotation(org.apache.camel.RoutingSlip.class);
routingSlip = new RoutingSlip(camelContext);
routingSlip.setDelimiter(annotation.delimiter());
routingSlip.setIgnoreInvalidEndpoints(annotation.ignoreInvalidEndpoints());
// add created routingSlip as a service so we have its lifecycle managed
try {
camelContext.addService(routingSlip);
} catch (Exception e) {
throw ObjectHelper.wrapRuntimeCamelException(e);
}
}
if (method.getAnnotation(org.apache.camel.DynamicRouter.class) != null
&& matchContext(method.getAnnotation(org.apache.camel.DynamicRouter.class).context())) {
org.apache.camel.DynamicRouter annotation = method.getAnnotation(org.apache.camel.DynamicRouter.class);
dynamicRouter = new DynamicRouter(camelContext);
dynamicRouter.setDelimiter(annotation.delimiter());
dynamicRouter.setIgnoreInvalidEndpoints(annotation.ignoreInvalidEndpoints());
// add created dynamicRouter as a service so we have its lifecycle managed
try {
camelContext.addService(dynamicRouter);
} catch (Exception e) {
throw ObjectHelper.wrapRuntimeCamelException(e);
}
}
if (method.getAnnotation(org.apache.camel.RecipientList.class) != null
&& matchContext(method.getAnnotation(org.apache.camel.RecipientList.class).context())) {
org.apache.camel.RecipientList annotation = method.getAnnotation(org.apache.camel.RecipientList.class);
recipientList = new RecipientList(camelContext, annotation.delimiter());
recipientList.setStopOnException(annotation.stopOnException());
recipientList.setIgnoreInvalidEndpoints(annotation.ignoreInvalidEndpoints());
recipientList.setParallelProcessing(annotation.parallelProcessing());
recipientList.setStreaming(annotation.streaming());
recipientList.setTimeout(annotation.timeout());
recipientList.setShareUnitOfWork(annotation.shareUnitOfWork());
if (ObjectHelper.isNotEmpty(annotation.executorServiceRef())) {
ExecutorService executor = camelContext.getExecutorServiceManager().newDefaultThreadPool(this, annotation.executorServiceRef());
recipientList.setExecutorService(executor);
}
if (annotation.parallelProcessing() && recipientList.getExecutorService() == null) {
// we are running in parallel so we need a thread pool
ExecutorService executor = camelContext.getExecutorServiceManager().newDefaultThreadPool(this, "@RecipientList");
recipientList.setExecutorService(executor);
}
if (ObjectHelper.isNotEmpty(annotation.strategyRef())) {
AggregationStrategy strategy = CamelContextHelper.mandatoryLookup(camelContext, annotation.strategyRef(), AggregationStrategy.class);
recipientList.setAggregationStrategy(strategy);
}
if (ObjectHelper.isNotEmpty(annotation.onPrepareRef())) {
Processor onPrepare = CamelContextHelper.mandatoryLookup(camelContext, annotation.onPrepareRef(), Processor.class);
recipientList.setOnPrepare(onPrepare);
}
// add created recipientList as a service so we have its lifecycle managed
try {
camelContext.addService(recipientList);
} catch (Exception e) {
throw ObjectHelper.wrapRuntimeCamelException(e);
}
}
}
/**
* Does the given context match this camel context
*/
private boolean matchContext(String context) {
if (ObjectHelper.isNotEmpty(context)) {
if (!camelContext.getName().equals(context)) {
return false;
}
}
return true;
}
public String toString() {
return method.toString();
}
public MethodInvocation createMethodInvocation(final Object pojo, final Exchange exchange) {
final Object[] arguments = parametersExpression.evaluate(exchange, Object[].class);
return new MethodInvocation() {
public Method getMethod() {
return method;
}
public Object[] getArguments() {
return arguments;
}
public Object proceed(AsyncCallback callback, AtomicBoolean doneSync) throws Exception {
// dynamic router should be invoked beforehand
if (dynamicRouter != null) {
if (!dynamicRouter.isStarted()) {
ServiceHelper.startService(dynamicRouter);
}
// use a expression which invokes the method to be used by dynamic router
Expression expression = new DynamicRouterExpression(pojo);
boolean sync = dynamicRouter.doRoutingSlip(exchange, expression, callback);
// must remember the done sync returned from the dynamic router
doneSync.set(sync);
return Void.TYPE;
}
// invoke pojo
if (LOG.isTraceEnabled()) {
LOG.trace(">>>> invoking: {} on bean: {} with arguments: {} for exchange: {}", new Object[]{method, pojo, asString(arguments), exchange});
}
Object result = invoke(method, pojo, arguments, exchange);
if (recipientList != null) {
// ensure its started
if (!recipientList.isStarted()) {
ServiceHelper.startService(recipientList);
}
boolean sync = recipientList.sendToRecipientList(exchange, result, callback);
// must remember the done sync returned from the recipient list
doneSync.set(sync);
// we don't want to return the list of endpoints
// return Void to indicate to BeanProcessor that there is no reply
return Void.TYPE;
}
if (routingSlip != null) {
if (!routingSlip.isStarted()) {
ServiceHelper.startService(routingSlip);
}
boolean sync = routingSlip.doRoutingSlip(exchange, result, callback);
// must remember the done sync returned from the routing slip
doneSync.set(sync);
return Void.TYPE;
}
return result;
}
public Object getThis() {
return pojo;
}
public AccessibleObject getStaticPart() {
return method;
}
};
}
public Class<?> getType() {
return type;
}
public Method getMethod() {
return method;
}
/**
* Returns the {@link org.apache.camel.ExchangePattern} that should be used when invoking this method. This value
* defaults to {@link org.apache.camel.ExchangePattern#InOut} unless some {@link org.apache.camel.Pattern} annotation is used
* to override the message exchange pattern.
*
* @return the exchange pattern to use for invoking this method.
*/
public ExchangePattern getPattern() {
return pattern;
}
public Expression getParametersExpression() {
return parametersExpression;
}
public List<ParameterInfo> getBodyParameters() {
return bodyParameters;
}
public Class<?> getBodyParameterType() {
if (bodyParameters.isEmpty()) {
return null;
}
ParameterInfo parameterInfo = bodyParameters.get(0);
return parameterInfo.getType();
}
public boolean bodyParameterMatches(Class<?> bodyType) {
Class<?> actualType = getBodyParameterType();
return actualType != null && ObjectHelper.isAssignableFrom(bodyType, actualType);
}
public List<ParameterInfo> getParameters() {
return parameters;
}
public boolean hasBodyParameter() {
return !bodyParameters.isEmpty();
}
public boolean hasCustomAnnotation() {
return hasCustomAnnotation;
}
public boolean hasHandlerAnnotation() {
return hasHandlerAnnotation;
}
public boolean isReturnTypeVoid() {
return method.getReturnType().getName().equals("void");
}
public boolean isStaticMethod() {
return Modifier.isStatic(method.getModifiers());
}
protected Object invoke(Method mth, Object pojo, Object[] arguments, Exchange exchange) throws IllegalAccessException, InvocationTargetException {
try {
return mth.invoke(pojo, arguments);
} catch (IllegalArgumentException e) {
throw new RuntimeExchangeException("IllegalArgumentException occurred invoking method: " + mth + " using arguments: " + Arrays.asList(arguments), exchange, e);
}
}
protected Expression createParametersExpression() {
final int size = parameters.size();
LOG.trace("Creating parameters expression for {} parameters", size);
final Expression[] expressions = new Expression[size];
for (int i = 0; i < size; i++) {
Expression parameterExpression = parameters.get(i).getExpression();
expressions[i] = parameterExpression;
LOG.trace("Parameter #{} has expression: {}", i, parameterExpression);
}
return new Expression() {
@SuppressWarnings("unchecked")
public <T> T evaluate(Exchange exchange, Class<T> type) {
Object[] answer = new Object[size];
Object body = exchange.getIn().getBody();
boolean multiParameterArray = false;
if (exchange.getIn().getHeader(Exchange.BEAN_MULTI_PARAMETER_ARRAY) != null) {
multiParameterArray = exchange.getIn().getHeader(Exchange.BEAN_MULTI_PARAMETER_ARRAY, Boolean.class);
}
// if there was an explicit method name to invoke, then we should support using
// any provided parameter values in the method name
String methodName = exchange.getIn().getHeader(Exchange.BEAN_METHOD_NAME, "", String.class);
// the parameter values is between the parenthesis
String methodParameters = ObjectHelper.between(methodName, "(", ")");
// use an iterator to walk the parameter values
Iterator it = null;
if (methodParameters != null) {
it = ObjectHelper.createIterator(methodParameters);
}
// remove headers as they should not be propagated
// we need to do this before the expressions gets evaluated as it may contain
// a @Bean expression which would by mistake read these headers. So the headers
// must be removed at this point of time
exchange.getIn().removeHeader(Exchange.BEAN_MULTI_PARAMETER_ARRAY);
exchange.getIn().removeHeader(Exchange.BEAN_METHOD_NAME);
for (int i = 0; i < size; i++) {
// grab the parameter value for the given index
Object parameterValue = it != null && it.hasNext() ? it.next() : null;
// and the expected parameter type
Class<?> parameterType = parameters.get(i).getType();
// the value for the parameter to use
Object value = null;
if (multiParameterArray) {
// get the value from the array
value = ((Object[])body)[i];
} else {
// prefer to use parameter value if given, as they override any bean parameter binding
// we should skip * as its a type placeholder to indicate any type
if (parameterValue != null && !parameterValue.equals("*")) {
// evaluate the parameter value binding
value = evaluateParameterValue(exchange, i, parameterValue, parameterType);
}
// use bean parameter binding, if still no value
Expression expression = expressions[i];
if (value == null && expression != null) {
value = evaluateParameterBinding(exchange, expression, i, parameterType);
}
}
// remember the value to use
if (value != Void.TYPE) {
answer[i] = value;
}
}
return (T) answer;
}
/**
* Evaluate using parameter values where the values can be provided in the method name syntax.
* <p/>
* This methods returns accordingly:
* <ul>
* <li><tt>null</tt> - if not a parameter value</li>
* <li><tt>Void.TYPE</tt> - if an explicit null, forcing Camel to pass in <tt>null</tt> for that given parameter</li>
* <li>a non <tt>null</tt> value - if the parameter was a parameter value, and to be used</li>
* </ul>
*
* @since 2.9
*/
private Object evaluateParameterValue(Exchange exchange, int index, Object parameterValue, Class<?> parameterType) {
Object answer = null;
// convert the parameter value to a String
String exp = exchange.getContext().getTypeConverter().convertTo(String.class, parameterValue);
if (exp != null) {
// must trim first as there may be spaces between parameters
exp = exp.trim();
// check if its a valid parameter value
boolean valid = BeanHelper.isValidParameterValue(exp);
if (!valid) {
// it may be a parameter type instead, and if so, then we should return null,
// as this method is only for evaluating parameter values
Boolean isClass = BeanHelper.isAssignableToExpectedType(exchange.getContext().getClassResolver(), exp, parameterType);
// the method will return a non null value if exp is a class
if (isClass != null) {
return null;
}
}
// use simple language to evaluate the expression, as it may use the simple language to refer to message body, headers etc.
Expression expression = null;
try {
expression = exchange.getContext().resolveLanguage("simple").createExpression(exp);
parameterValue = expression.evaluate(exchange, Object.class);
} catch (Exception e) {
throw new ExpressionEvaluationException(expression, "Cannot create/evaluate simple expression: " + exp
+ " to be bound to parameter at index: " + index + " on method: " + getMethod(), exchange, e);
}
if (parameterValue != null) {
// special for explicit null parameter values (as end users can explicit indicate they want null as parameter)
// see method javadoc for details
if ("null".equals(parameterValue)) {
return Void.TYPE;
}
// the parameter value was not already valid, but since the simple language have evaluated the expression
// which may change the parameterValue, so we have to check it again to see if its now valid
exp = exchange.getContext().getTypeConverter().convertTo(String.class, parameterValue);
// String values from the simple language is always valid
if (!valid) {
// re validate if the parameter was not valid the first time (String values should be accepted)
valid = parameterValue instanceof String || BeanHelper.isValidParameterValue(exp);
}
if (valid) {
// we need to unquote String parameters, as the enclosing quotes is there to denote a parameter value
if (parameterValue instanceof String) {
parameterValue = StringHelper.removeLeadingAndEndingQuotes((String) parameterValue);
}
try {
// its a valid parameter value, so convert it to the expected type of the parameter
answer = exchange.getContext().getTypeConverter().mandatoryConvertTo(parameterType, parameterValue);
if (LOG.isTraceEnabled()) {
LOG.trace("Parameter #{} evaluated as: {} type: ", new Object[]{index, answer, ObjectHelper.type(answer)});
}
} catch (NoTypeConversionAvailableException e) {
throw ObjectHelper.wrapCamelExecutionException(exchange, e);
}
}
}
}
return answer;
}
/**
* Evaluate using classic parameter binding using the pre compute expression
*/
private Object evaluateParameterBinding(Exchange exchange, Expression expression, int index, Class<?> parameterType) {
Object answer = null;
// use object first to avoid type conversion so we know if there is a value or not
Object result = expression.evaluate(exchange, Object.class);
if (result != null) {
// we got a value now try to convert it to the expected type
try {
answer = exchange.getContext().getTypeConverter().mandatoryConvertTo(parameterType, result);
if (LOG.isTraceEnabled()) {
LOG.trace("Parameter #{} evaluated as: {} type: ", new Object[]{index, answer, ObjectHelper.type(answer)});
}
} catch (NoTypeConversionAvailableException e) {
throw ObjectHelper.wrapCamelExecutionException(exchange, e);
}
} else {
LOG.trace("Parameter #{} evaluated as null", index);
}
return answer;
}
@Override
public String toString() {
return "ParametersExpression: " + Arrays.asList(expressions);
}
};
}
/**
* Finds the oneway annotation in priority order; look for method level annotations first, then the class level annotations,
* then super class annotations then interface annotations
*
* @param method the method on which to search
* @return the first matching annotation or none if it is not available
*/
protected Pattern findOneWayAnnotation(Method method) {
Pattern answer = getPatternAnnotation(method);
if (answer == null) {
Class<?> type = method.getDeclaringClass();
// lets create the search order of types to scan
List<Class<?>> typesToSearch = new ArrayList<Class<?>>();
addTypeAndSuperTypes(type, typesToSearch);
Class<?>[] interfaces = type.getInterfaces();
for (Class<?> anInterface : interfaces) {
addTypeAndSuperTypes(anInterface, typesToSearch);
}
// now lets scan for a type which the current declared class overloads
answer = findOneWayAnnotationOnMethod(typesToSearch, method);
if (answer == null) {
answer = findOneWayAnnotation(typesToSearch);
}
}
return answer;
}
/**
* Returns the pattern annotation on the given annotated element; either as a direct annotation or
* on an annotation which is also annotated
*
* @param annotatedElement the element to look for the annotation
* @return the first matching annotation or null if none could be found
*/
protected Pattern getPatternAnnotation(AnnotatedElement annotatedElement) {
return getPatternAnnotation(annotatedElement, 2);
}
/**
* Returns the pattern annotation on the given annotated element; either as a direct annotation or
* on an annotation which is also annotated
*
* @param annotatedElement the element to look for the annotation
* @param depth the current depth
* @return the first matching annotation or null if none could be found
*/
protected Pattern getPatternAnnotation(AnnotatedElement annotatedElement, int depth) {
Pattern answer = annotatedElement.getAnnotation(Pattern.class);
int nextDepth = depth - 1;
if (nextDepth > 0) {
// lets look at all the annotations to see if any of those are annotated
Annotation[] annotations = annotatedElement.getAnnotations();
for (Annotation annotation : annotations) {
Class<? extends Annotation> annotationType = annotation.annotationType();
if (annotation instanceof Pattern || annotationType.equals(annotatedElement)) {
continue;
} else {
Pattern another = getPatternAnnotation(annotationType, nextDepth);
if (pattern != null) {
if (answer == null) {
answer = another;
} else {
LOG.warn("Duplicate pattern annotation: " + another + " found on annotation: " + annotation + " which will be ignored");
}
}
}
}
}
return answer;
}
/**
* Adds the current class and all of its base classes (apart from {@link Object} to the given list
*/
protected void addTypeAndSuperTypes(Class<?> type, List<Class<?>> result) {
for (Class<?> t = type; t != null && t != Object.class; t = t.getSuperclass()) {
result.add(t);
}
}
/**
* Finds the first annotation on the base methods defined in the list of classes
*/
protected Pattern findOneWayAnnotationOnMethod(List<Class<?>> classes, Method method) {
for (Class<?> type : classes) {
try {
Method definedMethod = type.getMethod(method.getName(), method.getParameterTypes());
Pattern answer = getPatternAnnotation(definedMethod);
if (answer != null) {
return answer;
}
} catch (NoSuchMethodException e) {
// ignore
}
}
return null;
}
/**
* Finds the first annotation on the given list of classes
*/
protected Pattern findOneWayAnnotation(List<Class<?>> classes) {
for (Class<?> type : classes) {
Pattern answer = getPatternAnnotation(type);
if (answer != null) {
return answer;
}
}
return null;
}
protected boolean hasExceptionParameter() {
for (ParameterInfo parameter : parameters) {
if (Exception.class.isAssignableFrom(parameter.getType())) {
return true;
}
}
return false;
}
}