org.apache.camel.component.bean.MethodInfo Maven / Gradle / Ivy
The newest version!
/*
* 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.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.CompletionStage;
import org.apache.camel.AsyncCallback;
import org.apache.camel.AsyncProcessor;
import org.apache.camel.CamelContext;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
import org.apache.camel.ExchangePropertyKey;
import org.apache.camel.Expression;
import org.apache.camel.ExpressionEvaluationException;
import org.apache.camel.InOnly;
import org.apache.camel.InOut;
import org.apache.camel.Message;
import org.apache.camel.NoTypeConversionAvailableException;
import org.apache.camel.Pattern;
import org.apache.camel.Processor;
import org.apache.camel.RuntimeCamelException;
import org.apache.camel.RuntimeExchangeException;
import org.apache.camel.StreamCache;
import org.apache.camel.spi.ErrorHandlerAware;
import org.apache.camel.support.DefaultMessage;
import org.apache.camel.support.ExchangeHelper;
import org.apache.camel.support.ExpressionAdapter;
import org.apache.camel.support.MessageHelper;
import org.apache.camel.support.ObjectHelper;
import org.apache.camel.support.PluginHelper;
import org.apache.camel.support.service.ServiceHelper;
import org.apache.camel.util.StringHelper;
import org.apache.camel.util.StringQuoteHelper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static org.apache.camel.util.ObjectHelper.asList;
import static org.apache.camel.util.ObjectHelper.asString;
/**
* Information about a method to be used for invocation.
*/
public class MethodInfo {
private static final Logger LOG = LoggerFactory.getLogger(MethodInfo.class);
private final CamelContext camelContext;
private final Class> type;
private final Method method;
private final List parameters;
private final List bodyParameters;
private final boolean hasCustomAnnotation;
private final boolean hasHandlerAnnotation;
private final Expression parametersExpression;
private ExchangePattern pattern = ExchangePattern.InOut;
private AsyncProcessor recipientList;
private AsyncProcessor routingSlip;
private AsyncProcessor 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 RuntimeCamelException.wrapRuntimeCamelException(e);
}
}
@Override
public String toString() {
return "DynamicRouter[invoking: " + method + " on bean: " + pojo + "]";
}
}
public MethodInfo(CamelContext camelContext, Class> type, Method method, List parameters,
List 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();
Map, Annotation> collectedMethodAnnotation = collectMethodAnnotations(type, method);
// configure MEP if there was an annotation with @Pattern/@InOnly/@InOut
ExchangePattern aep = findExchangePatternAnnotation(collectedMethodAnnotation);
if (aep != null) {
pattern = aep;
}
org.apache.camel.RoutingSlip routingSlipAnnotation
= (org.apache.camel.RoutingSlip) collectedMethodAnnotation.get(org.apache.camel.RoutingSlip.class);
if (routingSlipAnnotation != null) {
routingSlip = PluginHelper.getAnnotationBasedProcessorFactory(camelContext)
.createRoutingSlip(camelContext, routingSlipAnnotation);
// add created routingSlip as a service so we have its lifecycle managed
try {
camelContext.addService(routingSlip);
} catch (Exception e) {
throw RuntimeCamelException.wrapRuntimeCamelException(e);
}
}
org.apache.camel.DynamicRouter dynamicRouterAnnotation
= (org.apache.camel.DynamicRouter) collectedMethodAnnotation.get(org.apache.camel.DynamicRouter.class);
if (dynamicRouterAnnotation != null) {
dynamicRouter = PluginHelper.getAnnotationBasedProcessorFactory(camelContext)
.createDynamicRouter(camelContext, dynamicRouterAnnotation);
// add created dynamicRouter as a service so we have its lifecycle managed
try {
camelContext.addService(dynamicRouter);
} catch (Exception e) {
throw RuntimeCamelException.wrapRuntimeCamelException(e);
}
}
org.apache.camel.RecipientList recipientListAnnotation
= (org.apache.camel.RecipientList) collectedMethodAnnotation.get(org.apache.camel.RecipientList.class);
if (recipientListAnnotation != null) {
recipientList = PluginHelper.getAnnotationBasedProcessorFactory(camelContext)
.createRecipientList(camelContext, recipientListAnnotation);
// add created recipientList as a service so we have its lifecycle managed
try {
camelContext.addService(recipientList);
} catch (Exception e) {
throw RuntimeCamelException.wrapRuntimeCamelException(e);
}
}
}
/**
* 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
*/
private Map, Annotation> collectMethodAnnotations(Class> c, Method method) {
Map, Annotation> annotations = new HashMap<>();
Set> search = new LinkedHashSet<>();
// first look for the top class itself and its super classes
addTypeAndSuperTypes(c, search);
// and interfaces for all the classes as last
Set> searchInterfaces = new LinkedHashSet<>();
for (Class> aClazz : search) {
Class>[] interfaces = aClazz.getInterfaces();
for (Class> anInterface : interfaces) {
addTypeAndSuperTypes(anInterface, searchInterfaces);
}
}
search.addAll(searchInterfaces);
collectMethodAnnotations(search.iterator(), method.getName(), annotations);
return annotations;
}
/**
* Adds the current class and all of its base classes (apart from {@link Object} to the given list
*/
private static void addTypeAndSuperTypes(Class> type, Set> result) {
for (Class> t = type; t != null && t != Object.class; t = t.getSuperclass()) {
result.add(t);
}
}
private void collectMethodAnnotations(
Iterator> it, String targetMethodName, Map, Annotation> annotations) {
Class>[] paramTypes = method.getParameterTypes();
boolean aep = false;
while (it.hasNext()) {
Class> searchType = (Class>) it.next();
Method[] methods = searchType.isInterface() ? searchType.getMethods() : searchType.getDeclaredMethods();
for (Method method : methods) {
if (targetMethodName.equals(method.getName()) && Arrays.equals(paramTypes, method.getParameterTypes())) {
for (Annotation a : method.getAnnotations()) {
// favour existing annotation so only add if not exists
Class> at = a.annotationType();
annotations.putIfAbsent(at, a);
aep |= at == Pattern.class || at == InOnly.class || at == InOut.class;
}
}
}
// special for @Pattern/@InOut/@InOnly
if (!aep) {
// look for this on class level
for (Annotation a : searchType.getAnnotations()) {
// favour existing annotation so only add if not exists
Class> at = a.annotationType();
boolean valid = at == Pattern.class || at == InOnly.class || at == InOut.class;
if (valid && !annotations.containsKey(at)) {
aep = true;
annotations.put(at, a);
}
}
}
}
}
@Override
public String toString() {
return method.toString();
}
/**
* For fine grained error handling for outputs of this EIP. The base error handler is used as base and then cloned
* for each output processor.
*
* This is used internally only by Camel - not for end users.
*/
public void setErrorHandler(Processor errorHandler) {
// special for @RecipientList which needs to be injected with error handler it should use
if (recipientList instanceof ErrorHandlerAware) {
((ErrorHandlerAware) recipientList).setErrorHandler(errorHandler);
}
}
private Object[] initializeArguments(boolean hasParameters, Exchange exchange) {
if (hasParameters) {
if (parametersExpression != null) {
parametersExpression.init(camelContext);
return parametersExpression.evaluate(exchange, Object[].class);
}
}
return null;
}
public MethodInvocation createMethodInvocation(final Object pojo, boolean hasParameters, final Exchange exchange) {
final Object[] arguments = initializeArguments(hasParameters, exchange);
return new MethodInvocation() {
public Method getMethod() {
return method;
}
public Object[] getArguments() {
return arguments;
}
public boolean proceed(AsyncCallback callback) {
try {
// reset cached streams so they can be read again
MessageHelper.resetStreamCache(exchange.getIn());
return doProceed(callback);
} catch (InvocationTargetException e) {
exchange.setException(e.getTargetException());
callback.done(true);
return true;
} catch (Exception e) {
exchange.setException(e);
callback.done(true);
return true;
}
}
private boolean doProceed(AsyncCallback callback) throws Exception {
// dynamic router should be invoked beforehand
if (dynamicRouter != null) {
if (!ServiceHelper.isStarted(dynamicRouter)) {
ServiceHelper.startService(dynamicRouter);
}
// use an expression which invokes the method to be used by dynamic router
Expression expression = new DynamicRouterExpression(pojo);
expression.init(camelContext);
exchange.setProperty(ExchangePropertyKey.EVALUATE_EXPRESSION_RESULT, expression);
return dynamicRouter.process(exchange, callback);
}
// invoke pojo
if (LOG.isTraceEnabled()) {
LOG.trace(">>>> invoking: {} on bean: {} with arguments: {} for exchange: {}", method, pojo,
asString(arguments), exchange);
}
Object result = invoke(method, pojo, arguments, exchange);
// the method may be a closure or chained method returning a callable which should be called
if (result instanceof Callable) {
LOG.trace("Method returned Callback which will be called: {}", result);
Object callableResult = ((Callable) result).call();
if (callableResult != null) {
result = callableResult;
} else {
// if callable returned null we should not change the body
result = Void.TYPE;
}
}
if (recipientList != null) {
// ensure its started
if (!ServiceHelper.isStarted(recipientList)) {
ServiceHelper.startService(recipientList);
}
exchange.setProperty(ExchangePropertyKey.EVALUATE_EXPRESSION_RESULT, result);
return recipientList.process(exchange, callback);
}
if (routingSlip != null) {
if (!ServiceHelper.isStarted(routingSlip)) {
ServiceHelper.startService(routingSlip);
}
exchange.setProperty(ExchangePropertyKey.EVALUATE_EXPRESSION_RESULT, result);
return routingSlip.process(exchange, callback);
}
//If it's Java 8 async result
if (CompletionStage.class.isAssignableFrom(method.getReturnType())) {
CompletionStage> completionStage = (CompletionStage>) result;
completionStage
.whenComplete((resultObject, e) -> {
if (e != null) {
exchange.setException(e);
} else if (resultObject != null) {
fillResult(exchange, resultObject);
}
callback.done(false);
});
return false;
}
// if the method returns something then set the value returned on the Exchange
if (result != Void.TYPE && !method.getReturnType().equals(Void.TYPE)) {
fillResult(exchange, result);
}
// we did not use any of the eips, but just invoked the bean
// so notify the callback we are done synchronously
callback.done(true);
return true;
}
public Object getThis() {
return pojo;
}
public AccessibleObject getStaticPart() {
return method;
}
};
}
private void fillResult(Exchange exchange, Object result) {
LOG.trace("Setting bean invocation result : {}", result);
// the bean component forces OUT if the MEP is OUT capable
boolean out = exchange.hasOut() || ExchangeHelper.isOutCapable(exchange);
Message old;
if (out) {
old = exchange.getOut();
// propagate headers
exchange.getOut().getHeaders().putAll(exchange.getIn().getHeaders());
} else {
old = exchange.getIn();
}
// create a new message container, so we do not drag specialized message objects along
// but that is only needed if the old message is a specialized message
boolean copyNeeded = !(old.getClass().equals(DefaultMessage.class));
if (copyNeeded) {
Message msg = new DefaultMessage(exchange.getContext());
msg.copyFromWithNewBody(old, result);
// replace message on exchange
ExchangeHelper.replaceMessage(exchange, msg, false);
} else {
// no copy needed so set replace value directly
old.setBody(result);
}
}
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 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 && org.apache.camel.util.ObjectHelper.isAssignableFrom(bodyType, actualType);
}
public List getParameters() {
return parameters;
}
public boolean hasBodyParameter() {
return !bodyParameters.isEmpty();
}
public boolean hasCustomAnnotation() {
return hasCustomAnnotation;
}
public boolean hasHandlerAnnotation() {
return hasHandlerAnnotation;
}
public boolean hasParameters() {
return !parameters.isEmpty();
}
public boolean isReturnTypeVoid() {
return method.getReturnType().getName().equals("void");
}
public boolean isStaticMethod() {
return Modifier.isStatic(method.getModifiers());
}
/**
* Returns true if this method is covariant with the specified method (this method may above or below the specified
* method in the class hierarchy)
*/
public boolean isCovariantWith(MethodInfo method) {
return method.getMethod().getName().equals(this.getMethod().getName())
&& (method.getMethod().getReturnType().isAssignableFrom(this.getMethod().getReturnType())
|| this.getMethod().getReturnType().isAssignableFrom(method.getMethod().getReturnType()))
&& Arrays.deepEquals(method.getMethod().getParameterTypes(), this.getMethod().getParameterTypes());
}
protected Object invoke(Method mth, Object pojo, Object[] arguments, Exchange exchange) throws InvocationTargetException {
try {
return ObjectHelper.invokeMethodSafe(mth, pojo, arguments);
} catch (IllegalAccessException e) {
throw new RuntimeExchangeException(
"IllegalAccessException occurred invoking method: " + mth + " using arguments: " + asList(arguments),
exchange, e);
} catch (IllegalArgumentException e) {
throw new RuntimeExchangeException(
"IllegalArgumentException occurred invoking method: " + mth + " using arguments: " + asList(arguments),
exchange, e);
}
}
protected Expression[] createParameterExpressions() {
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 expressions;
}
protected Expression createParametersExpression() {
return new ParameterExpression(createParameterExpressions());
}
protected ExchangePattern findExchangePatternAnnotation(Map, Annotation> collectedMethodAnnotation) {
if (collectedMethodAnnotation.get(InOnly.class) != null) {
return ExchangePattern.InOnly;
} else if (collectedMethodAnnotation.get(InOut.class) != null) {
return ExchangePattern.InOut;
} else {
Pattern pattern = (Pattern) collectedMethodAnnotation.get(Pattern.class);
return pattern != null ? pattern.value() : null;
}
}
protected boolean hasExceptionParameter() {
for (ParameterInfo parameter : parameters) {
if (Exception.class.isAssignableFrom(parameter.getType())) {
return true;
}
}
return false;
}
/**
* Expression to evaluate the bean parameter parameters and provide the correct values when the method is invoked.
*/
private final class ParameterExpression implements Expression {
private final Expression[] expressions;
ParameterExpression(Expression[] expressions) {
this.expressions = expressions;
}
@Override
public void init(CamelContext context) {
if (expressions != null) {
for (Expression exp : expressions) {
if (exp != null) {
exp.init(context);
}
}
}
}
@Override
@SuppressWarnings("unchecked")
public T evaluate(Exchange exchange, Class type) {
Object body = exchange.getIn().getBody();
// 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(BeanConstants.BEAN_METHOD_NAME, String.class);
// the parameter values is between the parenthesis
String methodParameters = StringHelper.betweenOuterPair(methodName, '(', ')');
// use an iterator to walk the parameter values
Iterator> it = null;
if (methodParameters != null) {
// split the parameters safely separated by comma, but beware that we can have
// quoted parameters which contains comma as well, so do a safe quote split (keep quotes)
String[] parameters = StringQuoteHelper.splitSafeQuote(methodParameters, ',', true, true);
it = ObjectHelper.createIterator(parameters, ",", true);
}
// 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
if (methodName != null) {
exchange.getIn().removeHeader(Exchange.BEAN_METHOD_NAME);
}
Object[] answer = evaluateParameterExpressions(exchange, body, it);
return (T) answer;
}
/**
* Evaluates all the parameter expressions
*/
private Object[] evaluateParameterExpressions(Exchange exchange, Object body, Iterator> it) {
Object[] answer = new Object[expressions.length];
for (int i = 0; i < expressions.length; i++) {
if (body instanceof StreamCache) {
// need to reset stream cache for each expression as you may access the message body in multiple parameters
((StreamCache) body).reset();
}
// 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;
// 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 answer;
}
/**
* Evaluate using parameter values where the values can be provided in the method name syntax.
*
* This method returns accordingly:
*
* - null - if not a parameter value
* - Void.TYPE - if an explicit null, forcing Camel to pass in null for that given
* parameter
* - a non null value - if the parameter was a parameter value, and to be used
*
*/
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, exchange, parameterValue);
boolean valid;
if (exp != null) {
int pos1 = exp.indexOf(' ');
int pos2 = exp.indexOf(".class");
if (pos1 != -1 && pos2 != -1 && pos1 > pos2) {
exp = exp.substring(pos2 + 7); // clip .class
}
// check if its a valid parameter value (no type declared via .class syntax)
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);
// use "null" to indicate the expression returned a null value which is a valid response we need to honor
if (parameterValue == null) {
parameterValue = "null";
}
} 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);
}
// 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 may match the expected type, then we use it as-is
if (parameterType.isAssignableFrom(parameterValue.getClass())) {
valid = true;
} else {
// String values from the simple language is always valid
if (!valid) {
valid = parameterValue instanceof String;
if (!valid) {
// 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 it is now valid
exp = exchange.getContext().getTypeConverter().tryConvertTo(String.class, parameterValue);
// re-validate if the parameter was not valid the first time
valid = 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 {
// it is a valid parameter value, so convert it to the expected type of the parameter
answer = exchange.getContext().getTypeConverter().mandatoryConvertTo(parameterType, exchange,
parameterValue);
if (LOG.isTraceEnabled()) {
LOG.trace("Parameter #{} evaluated as: {} type: {}", index, answer,
org.apache.camel.util.ObjectHelper.type(answer));
}
} catch (Exception e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Cannot convert from type: {} to type: {} for parameter #{}",
org.apache.camel.util.ObjectHelper.type(parameterValue), parameterType, index);
}
throw new ParameterBindingException(e, method, index, parameterType, parameterValue);
}
}
}
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) {
try {
if (parameterType.isInstance(result)) {
// optimize if the value is already the same type
answer = result;
} else {
// we got a value now try to convert it to the expected type
answer = exchange.getContext().getTypeConverter().mandatoryConvertTo(parameterType, result);
}
if (LOG.isTraceEnabled()) {
LOG.trace("Parameter #{} evaluated as: {} type: {}", index, answer,
org.apache.camel.util.ObjectHelper.type(answer));
}
} catch (NoTypeConversionAvailableException e) {
if (LOG.isDebugEnabled()) {
LOG.debug("Cannot convert from type: {} to type: {} for parameter #{}",
org.apache.camel.util.ObjectHelper.type(result), parameterType, index);
}
throw new ParameterBindingException(e, method, index, parameterType, result);
}
} else {
LOG.trace("Parameter #{} evaluated as null", index);
}
return answer;
}
@Override
public String toString() {
return "ParametersExpression: " + Arrays.asList(expressions);
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy