/* * Copyright 2002-2017 the original author or authors. * * Licensed 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.springframework.integration.gateway; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.SynthesizingMethodParameter; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.integration.expression.ExpressionUtils; import org.springframework.integration.mapping.InboundMessageMapper; import org.springframework.integration.mapping.MessageMappingException; import org.springframework.integration.support.AbstractIntegrationMessageBuilder; import org.springframework.integration.support.DefaultMessageBuilderFactory; import org.springframework.integration.support.MessageBuilderFactory; import org.springframework.integration.util.MessagingAnnotationUtils; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Headers; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; /** * A Message Mapper implementation that supports mapping <i>to</i> a * Message from an argument array when invoking gateway methods. * <p> * Some examples of legal method signatures:<br> * <tt>public void dealWith(Object payload);</tt><br> * <tt>public void dealWith(Message message);</tt><br> * <tt>public void dealWith(@Header String myHeader, Object payload);</tt><br> * <br> * <tt>public void dealWith(@Headers Map headers, Object payload);</tt><br> * <tt>public void dealWith(@Headers Properties headers, Map payload);</tt><br> * <tt>public void dealWith(Properties headers, Object payload);</tt><br> * <p> * Some examples of illegal method signatures: <br> * <tt>public void dealWith(Object payload, String payload);</tt><br> * <tt>public void dealWith(Message message, Object payload);</tt><br> * <tt>public void dealWith(Properties headers, Map payload);</tt><br> * * @author Mark Fisher * @author Iwein Fuld * @author Oleg Zhurakousky * @author Gary Russell * @author Artem Bilan * @since 2.0 */ class GatewayMethodInboundMessageMapper implements InboundMessageMapper<Object[]>, BeanFactoryAware { private final Log logger = LogFactory.getLog(this.getClass()); private static final SpelExpressionParser PARSER = new SpelExpressionParser(); private final Method method; private final Map<String, Expression> headerExpressions; private final Map<String, Expression> globalHeaderExpressions; private final Map<String, Object> headers; private final List<MethodParameter> parameterList; private final MethodArgsMessageMapper argsMapper; private volatile Expression payloadExpression; private final Map<String, Expression> parameterPayloadExpressions = new HashMap<String, Expression>(); private volatile StandardEvaluationContext payloadExpressionEvaluationContext; private volatile BeanFactory beanFactory; private final MessageBuilderFactory messageBuilderFactory; GatewayMethodInboundMessageMapper(Method method) { this(method, null); } GatewayMethodInboundMessageMapper(Method method, Map<String, Expression> headerExpressions) { this(method, headerExpressions, null, null, null); } GatewayMethodInboundMessageMapper(Method method, Map<String, Expression> headerExpressions, Map<String, Expression> globalHeaderExpressions, MethodArgsMessageMapper mapper, MessageBuilderFactory messageBuilderFactory) { this(method, headerExpressions, globalHeaderExpressions, null, mapper, messageBuilderFactory); } GatewayMethodInboundMessageMapper(Method method, Map<String, Expression> headerExpressions, Map<String, Expression> globalHeaderExpressions, Map<String, Object> headers, MethodArgsMessageMapper mapper, MessageBuilderFactory messageBuilderFactory) { Assert.notNull(method, "method must not be null"); this.method = method; this.headerExpressions = headerExpressions; this.headers = headers; this.globalHeaderExpressions = globalHeaderExpressions; this.parameterList = getMethodParameterList(method); this.payloadExpression = parsePayloadExpression(method); if (mapper == null) { this.argsMapper = new DefaultMethodArgsMessageMapper(); } else { this.argsMapper = mapper; } if (messageBuilderFactory == null) { this.messageBuilderFactory = new DefaultMessageBuilderFactory(); } else { this.messageBuilderFactory = messageBuilderFactory; } } public void setPayloadExpression(String expressionString) { this.payloadExpression = PARSER.parseExpression(expressionString); } @Override public void setBeanFactory(final BeanFactory beanFactory) { if (beanFactory != null) { this.beanFactory = beanFactory; this.payloadExpressionEvaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory); } } @Override public Message<?> toMessage(Object[] arguments) { Assert.notNull(arguments, "cannot map null arguments to Message"); if (arguments.length != this.parameterList.size()) { String prefix = (arguments.length < this.parameterList.size()) ? "Not enough" : "Too many"; throw new IllegalArgumentException(prefix + " parameters provided for method [" + this.method + "], expected " + this.parameterList.size() + " but received " + arguments.length + "."); } return this.mapArgumentsToMessage(arguments); } private Message<?> mapArgumentsToMessage(Object[] arguments) { try { return this.argsMapper.toMessage(new MethodArgsHolder(this.method, arguments)); } catch (Exception e) { if (e instanceof MessagingException) { throw (MessagingException) e; } else { throw new MessageMappingException("Failed to map arguments", e); } } } private Map<String, Object> evaluateHeaders(EvaluationContext methodInvocationEvaluationContext, Map<String, Expression> headerExpressions) { Map<String, Object> evaluatedHeaders = new HashMap<String, Object>(); for (Map.Entry<String, Expression> entry : headerExpressions.entrySet()) { Object value = entry.getValue().getValue(methodInvocationEvaluationContext); if (value != null) { evaluatedHeaders.put(entry.getKey(), value); } } return evaluatedHeaders; } private StandardEvaluationContext createMethodInvocationEvaluationContext(Object[] arguments) { StandardEvaluationContext context = ExpressionUtils.createStandardEvaluationContext(this.beanFactory); context.setVariable("args", arguments); context.setVariable("gatewayMethod", this.method); return context; } private Object evaluatePayloadExpression(String expressionString, Object argumentValue) { Expression expression = this.parameterPayloadExpressions.get(expressionString); if (expression == null) { expression = PARSER.parseExpression(expressionString); this.parameterPayloadExpressions.put(expressionString, expression); } return expression.getValue(this.payloadExpressionEvaluationContext, argumentValue); } private void copyHeaders(Map<?, ?> argumentValue, Map<String, Object> headers) { for (Entry<?, ?> entry : argumentValue.entrySet()) { Object key = entry.getKey(); if (!(key instanceof String)) { if (this.logger.isWarnEnabled()) { this.logger.warn("Invalid header name [" + key + "], name type must be String. Skipping mapping of this header to MessageHeaders."); } } else { headers.put((String) key, entry.getValue()); } } } private void throwExceptionForMultipleMessageOrPayloadParameters(MethodParameter methodParameter) { throw new MessagingException( "At most one parameter (or expression via method-level @Payload) may be mapped to the " + "payload or Message. Found more than one on method [" + methodParameter.getMethod() + "]"); } private String determineHeaderName(Annotation headerAnnotation, MethodParameter methodParameter) { String valueAttribute = (String) AnnotationUtils.getValue(headerAnnotation); String headerName = StringUtils.hasText(valueAttribute) ? valueAttribute : methodParameter.getParameterName(); Assert.notNull(headerName, "Cannot determine header name. Possible reasons: -debug is " + "disabled or header name is not explicitly provided via @Header annotation."); return headerName; } private static List<MethodParameter> getMethodParameterList(Method method) { List<MethodParameter> parameterList = new LinkedList<MethodParameter>(); ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer(); int parameterCount = method.getParameterTypes().length; for (int i = 0; i < parameterCount; i++) { MethodParameter methodParameter = new SynthesizingMethodParameter(method, i); methodParameter.initParameterNameDiscovery(parameterNameDiscoverer); parameterList.add(methodParameter); } return parameterList; } private static Expression parsePayloadExpression(Method method) { Expression expression = null; Annotation payload = method.getAnnotation(Payload.class); if (payload != null) { String expressionString = (String) AnnotationUtils.getValue(payload); Assert.hasText(expressionString, "@Payload at method-level on a Gateway must provide a non-empty Expression."); expression = PARSER.parseExpression(expressionString); } return expression; } public class DefaultMethodArgsMessageMapper implements MethodArgsMessageMapper { @Override public Message<?> toMessage(MethodArgsHolder holder) throws Exception { Object messageOrPayload = null; boolean foundPayloadAnnotation = false; Object[] arguments = holder.getArgs(); EvaluationContext methodInvocationEvaluationContext = createMethodInvocationEvaluationContext(arguments); Map<String, Object> headers = new HashMap<String, Object>(); if (GatewayMethodInboundMessageMapper.this.payloadExpression != null) { messageOrPayload = GatewayMethodInboundMessageMapper.this.payloadExpression.getValue(methodInvocationEvaluationContext); } for (int i = 0; i < GatewayMethodInboundMessageMapper.this.parameterList.size(); i++) { Object argumentValue = arguments[i]; MethodParameter methodParameter = GatewayMethodInboundMessageMapper.this.parameterList.get(i); Annotation annotation = MessagingAnnotationUtils.findMessagePartAnnotation(methodParameter.getParameterAnnotations(), false); if (annotation != null) { if (annotation.annotationType().equals(Payload.class)) { if (messageOrPayload != null) { throwExceptionForMultipleMessageOrPayloadParameters(methodParameter); } String expression = (String) AnnotationUtils.getValue(annotation); if (!StringUtils.hasText(expression)) { messageOrPayload = argumentValue; } else { messageOrPayload = evaluatePayloadExpression(expression, argumentValue); } foundPayloadAnnotation = true; } else if (annotation.annotationType().equals(Header.class)) { String headerName = GatewayMethodInboundMessageMapper.this.determineHeaderName(annotation, methodParameter); if ((Boolean) AnnotationUtils.getValue(annotation, "required") && argumentValue == null) { throw new IllegalArgumentException("Received null argument value for required header: '" + headerName + "'"); } headers.put(headerName, argumentValue); } else if (annotation.annotationType().equals(Headers.class)) { if (argumentValue != null) { if (!(argumentValue instanceof Map)) { throw new IllegalArgumentException("@Headers annotation is only valid for Map-typed parameters"); } for (Object key : ((Map<?, ?>) argumentValue).keySet()) { Assert.isInstanceOf(String.class, key, "Invalid header name [" + key + "], name type must be String."); Object value = ((Map<?, ?>) argumentValue).get(key); headers.put((String) key, value); } } } } else if (messageOrPayload == null) { messageOrPayload = argumentValue; } else if (Map.class.isAssignableFrom(methodParameter.getParameterType())) { if (messageOrPayload instanceof Map && !foundPayloadAnnotation) { if (GatewayMethodInboundMessageMapper.this.payloadExpression == null) { throw new MessagingException("Ambiguous method parameters; found more than one " + "Map-typed parameter and neither one contains a @Payload annotation"); } } GatewayMethodInboundMessageMapper.this.copyHeaders((Map<?, ?>) argumentValue, headers); } else if (GatewayMethodInboundMessageMapper.this.payloadExpression == null) { GatewayMethodInboundMessageMapper.this.throwExceptionForMultipleMessageOrPayloadParameters(methodParameter); } } Assert.isTrue(messageOrPayload != null, "unable to determine a Message or payload parameter on method [" + GatewayMethodInboundMessageMapper.this.method + "]"); AbstractIntegrationMessageBuilder<?> builder = (messageOrPayload instanceof Message) ? GatewayMethodInboundMessageMapper.this.messageBuilderFactory.fromMessage((Message<?>) messageOrPayload) : GatewayMethodInboundMessageMapper.this.messageBuilderFactory.withPayload(messageOrPayload); builder.copyHeadersIfAbsent(headers); // Explicit headers in XML override any @Header annotations... if (!CollectionUtils.isEmpty(GatewayMethodInboundMessageMapper.this.headerExpressions)) { Map<String, Object> evaluatedHeaders = evaluateHeaders(methodInvocationEvaluationContext, GatewayMethodInboundMessageMapper.this.headerExpressions); builder.copyHeaders(evaluatedHeaders); } // ...whereas global (default) headers do not... if (!CollectionUtils.isEmpty(GatewayMethodInboundMessageMapper.this.globalHeaderExpressions)) { Map<String, Object> evaluatedHeaders = evaluateHeaders(methodInvocationEvaluationContext, GatewayMethodInboundMessageMapper.this.globalHeaderExpressions); builder.copyHeadersIfAbsent(evaluatedHeaders); } if (GatewayMethodInboundMessageMapper.this.headers != null) { builder.copyHeadersIfAbsent(GatewayMethodInboundMessageMapper.this.headers); } return builder.build(); } } }