/* * Copyright 2016-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.amqp.outbound; import java.util.HashMap; import java.util.Map; import org.springframework.amqp.core.MessageDeliveryMode; import org.springframework.amqp.rabbit.connection.Connection; import org.springframework.amqp.rabbit.connection.ConnectionFactory; import org.springframework.amqp.rabbit.support.CorrelationData; import org.springframework.amqp.support.AmqpHeaders; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.beans.factory.BeanFactory; import org.springframework.context.Lifecycle; import org.springframework.expression.Expression; import org.springframework.integration.amqp.support.AmqpHeaderMapper; import org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper; import org.springframework.integration.channel.NullChannel; import org.springframework.integration.expression.ValueExpression; import org.springframework.integration.handler.AbstractReplyProducingMessageHandler; import org.springframework.integration.handler.ExpressionEvaluatingMessageProcessor; import org.springframework.integration.support.AbstractIntegrationMessageBuilder; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** * @author Gary Russell * @author Artem Bilan * * @since 4.3 * */ public abstract class AbstractAmqpOutboundEndpoint extends AbstractReplyProducingMessageHandler implements Lifecycle { private volatile String exchangeName; private volatile String routingKey; private volatile Expression exchangeNameExpression; private volatile Expression routingKeyExpression; private volatile ExpressionEvaluatingMessageProcessor<String> routingKeyGenerator; private volatile ExpressionEvaluatingMessageProcessor<String> exchangeNameGenerator; private volatile AmqpHeaderMapper headerMapper = DefaultAmqpHeaderMapper.outboundMapper(); private volatile Expression confirmCorrelationExpression; private volatile ExpressionEvaluatingMessageProcessor<Object> correlationDataGenerator; private volatile MessageChannel confirmAckChannel; private volatile MessageChannel confirmNackChannel; private volatile MessageChannel returnChannel; private volatile MessageDeliveryMode defaultDeliveryMode; private volatile boolean lazyConnect = true; private volatile ConnectionFactory connectionFactory; private volatile Expression delayExpression; private volatile ExpressionEvaluatingMessageProcessor<Integer> delayGenerator; private boolean headersMappedLast; private volatile boolean running; public void setHeaderMapper(AmqpHeaderMapper headerMapper) { Assert.notNull(headerMapper, "headerMapper must not be null"); this.headerMapper = headerMapper; } /** * When mapping headers for the outbound message, determine whether the headers are * mapped before the message is converted, or afterwards. This only affects headers * that might be added by the message converter. When false, the converter's headers * win; when true, any headers added by the converter will be overridden (if the * source message has a header that maps to those headers). You might wish to set this * to true, for example, when using a * {@link org.springframework.amqp.support.converter.SimpleMessageConverter} with a * String payload that contains json; the converter will set the content type to * {@code text/plain} which can be overridden to {@code application/json} by setting * the {@link AmqpHeaders#CONTENT_TYPE} message header. Default: false. * @param headersMappedLast true if headers are mapped after conversion. * @since 5.0 */ public void setHeadersMappedLast(boolean headersMappedLast) { this.headersMappedLast = headersMappedLast; } public void setExchangeName(String exchangeName) { Assert.notNull(exchangeName, "exchangeName must not be null"); this.exchangeName = exchangeName; } /** * @param exchangeNameExpression the expression to use. * @since 4.3 */ public void setExchangeNameExpression(Expression exchangeNameExpression) { this.exchangeNameExpression = exchangeNameExpression; } /** * @param exchangeNameExpression the String in SpEL syntax. * @since 4.3 */ public void setExchangeNameExpressionString(String exchangeNameExpression) { Assert.hasText(exchangeNameExpression, "'exchangeNameExpression' must not be empty"); this.exchangeNameExpression = EXPRESSION_PARSER.parseExpression(exchangeNameExpression); } public void setRoutingKey(String routingKey) { Assert.notNull(routingKey, "routingKey must not be null"); this.routingKey = routingKey; } /** * @param routingKeyExpression the expression to use. * @since 4.3 */ public void setRoutingKeyExpression(Expression routingKeyExpression) { this.routingKeyExpression = routingKeyExpression; } /** * @param routingKeyExpression the String in SpEL syntax. * @since 4.3 */ public void setRoutingKeyExpressionString(String routingKeyExpression) { Assert.hasText(routingKeyExpression, "'routingKeyExpression' must not be empty"); this.routingKeyExpression = EXPRESSION_PARSER.parseExpression(routingKeyExpression); } /** * @param confirmCorrelationExpression the expression to use. * @since 4.3 */ public void setConfirmCorrelationExpression(Expression confirmCorrelationExpression) { this.confirmCorrelationExpression = confirmCorrelationExpression; } /** * @param confirmCorrelationExpression the String in SpEL syntax. * @since 4.3 */ public void setConfirmCorrelationExpressionString(String confirmCorrelationExpression) { Assert.hasText(confirmCorrelationExpression, "'confirmCorrelationExpression' must not be empty"); this.confirmCorrelationExpression = EXPRESSION_PARSER.parseExpression(confirmCorrelationExpression); } /** * Set the channel to which acks are send (publisher confirms). * @param ackChannel the channel. */ public void setConfirmAckChannel(MessageChannel ackChannel) { this.confirmAckChannel = ackChannel; } /** * Set the channel to which nacks are send (publisher confirms). * @param nackChannel the channel. */ public void setConfirmNackChannel(MessageChannel nackChannel) { this.confirmNackChannel = nackChannel; } /** * Set the channel to which returned messages are sent. * @param returnChannel the channel. */ public void setReturnChannel(MessageChannel returnChannel) { this.returnChannel = returnChannel; } /** * Set the default delivery mode. * @param defaultDeliveryMode the delivery mode. */ public void setDefaultDeliveryMode(MessageDeliveryMode defaultDeliveryMode) { this.defaultDeliveryMode = defaultDeliveryMode; } /** * Set to {@code false} to attempt to connect during endpoint start; * default {@code true}, meaning the connection will be attempted * to be established on the arrival of the first message. * @param lazyConnect the lazyConnect to set * @since 4.1 */ public void setLazyConnect(boolean lazyConnect) { this.lazyConnect = lazyConnect; } /** * Set the value to set in the {@code x-delay} header when using the * RabbitMQ delayed message exchange plugin. By default, the {@link AmqpHeaders#DELAY} * header (if present) is mapped; setting the delay here overrides that value. * @param delay the delay. * @since 4.3.5 */ public void setDelay(int delay) { this.delayExpression = new ValueExpression<>(delay); } /** * Set the SpEL expression to calculate the {@code x-delay} header when using the * RabbitMQ delayed message exchange plugin. By default, the {@link AmqpHeaders#DELAY} * header (if present) is mapped; setting the expression here overrides that value. * @param delayExpression the expression. * @since 4.3.5 */ public void setDelayExpression(Expression delayExpression) { this.delayExpression = delayExpression; } /** * Set the SpEL expression to calculate the {@code x-delay} header when using the * RabbitMQ delayed message exchange plugin. By default, the {@link AmqpHeaders#DELAY} * header (if present) is mapped; setting the expression here overrides that value. * @param delayExpression the expression. * @since 4.3.5 */ public void setDelayExpressionString(String delayExpression) { if (delayExpression == null) { this.delayExpression = null; } else { this.delayExpression = EXPRESSION_PARSER.parseExpression(delayExpression); } } protected final void setConnectionFactory(ConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } protected String getExchangeName() { return this.exchangeName; } protected String getRoutingKey() { return this.routingKey; } protected Expression getExchangeNameExpression() { return this.exchangeNameExpression; } protected Expression getRoutingKeyExpression() { return this.routingKeyExpression; } protected ExpressionEvaluatingMessageProcessor<String> getRoutingKeyGenerator() { return this.routingKeyGenerator; } protected ExpressionEvaluatingMessageProcessor<String> getExchangeNameGenerator() { return this.exchangeNameGenerator; } protected AmqpHeaderMapper getHeaderMapper() { return this.headerMapper; } protected Expression getConfirmCorrelationExpression() { return this.confirmCorrelationExpression; } protected ExpressionEvaluatingMessageProcessor<Object> getCorrelationDataGenerator() { return this.correlationDataGenerator; } protected MessageChannel getConfirmAckChannel() { return this.confirmAckChannel; } protected MessageChannel getConfirmNackChannel() { return this.confirmNackChannel; } protected MessageChannel getReturnChannel() { return this.returnChannel; } protected MessageDeliveryMode getDefaultDeliveryMode() { return this.defaultDeliveryMode; } protected boolean isLazyConnect() { return this.lazyConnect; } protected boolean isHeadersMappedLast() { return this.headersMappedLast; } @Override protected final void doInit() { Assert.state(this.exchangeNameExpression == null || this.exchangeName == null, "Either an exchangeName or an exchangeNameExpression can be provided, but not both"); BeanFactory beanFactory = getBeanFactory(); if (this.exchangeNameExpression != null) { this.exchangeNameGenerator = new ExpressionEvaluatingMessageProcessor<String>(this.exchangeNameExpression, String.class); if (beanFactory != null) { this.exchangeNameGenerator.setBeanFactory(beanFactory); } } Assert.state(this.routingKeyExpression == null || this.routingKey == null, "Either a routingKey or a routingKeyExpression can be provided, but not both"); if (this.routingKeyExpression != null) { this.routingKeyGenerator = new ExpressionEvaluatingMessageProcessor<String>(this.routingKeyExpression, String.class); if (beanFactory != null) { this.routingKeyGenerator.setBeanFactory(beanFactory); } } if (this.confirmCorrelationExpression != null) { this.correlationDataGenerator = new ExpressionEvaluatingMessageProcessor<Object>(this.confirmCorrelationExpression, Object.class); if (beanFactory != null) { this.correlationDataGenerator.setBeanFactory(beanFactory); } } else { NullChannel nullChannel = extractTypeIfPossible(this.confirmAckChannel, NullChannel.class); Assert.state(this.confirmAckChannel == null || nullChannel != null, "A 'confirmCorrelationExpression' is required when specifying a 'confirmAckChannel'"); nullChannel = extractTypeIfPossible(this.confirmNackChannel, NullChannel.class); Assert.state(this.confirmNackChannel == null || nullChannel != null, "A 'confirmCorrelationExpression' is required when specifying a 'confirmNackChannel'"); } if (this.delayExpression != null) { this.delayGenerator = new ExpressionEvaluatingMessageProcessor<Integer>(this.delayExpression, Integer.class); if (beanFactory != null) { this.delayGenerator.setBeanFactory(beanFactory); } } endpointInit(); } /** * Subclasses can override to perform any additional initialization. * Called from afterPropertiesSet(). */ protected void endpointInit() { } @Override public synchronized void start() { if (!this.running) { if (!this.lazyConnect && this.connectionFactory != null) { try { Connection connection = this.connectionFactory.createConnection(); // NOSONAR (close) if (connection != null) { connection.close(); } } catch (RuntimeException e) { logger.error("Failed to eagerly establish the connection.", e); } } doStart(); this.running = true; } } @Override public synchronized void stop() { if (this.running) { doStop(); } this.running = false; } protected void doStart() { } protected void doStop() { } @Override public boolean isRunning() { return this.running; } protected CorrelationData generateCorrelationData(Message<?> requestMessage) { CorrelationData correlationData = null; if (this.correlationDataGenerator != null) { Object userCorrelationData = this.correlationDataGenerator.processMessage(requestMessage); if (userCorrelationData != null) { if (userCorrelationData instanceof CorrelationData) { correlationData = (CorrelationData) userCorrelationData; } else { correlationData = new CorrelationDataWrapper(requestMessage .getHeaders().getId().toString(), userCorrelationData); } } } return correlationData; } protected String generateExchangeName(Message<?> requestMessage) { String exchangeName = this.exchangeName; if (this.exchangeNameGenerator != null) { exchangeName = this.exchangeNameGenerator.processMessage(requestMessage); } return exchangeName; } protected String generateRoutingKey(Message<?> requestMessage) { String routingKey = this.routingKey; if (this.routingKeyGenerator != null) { routingKey = this.routingKeyGenerator.processMessage(requestMessage); } return routingKey; } protected void addDelayProperty(Message<?> message, org.springframework.amqp.core.Message amqpMessage) { if (this.delayGenerator != null) { amqpMessage.getMessageProperties().setDelay(this.delayGenerator.processMessage(message)); } } protected AbstractIntegrationMessageBuilder<?> buildReply(MessageConverter converter, org.springframework.amqp.core.Message amqpReplyMessage) { Object replyObject = converter.fromMessage(amqpReplyMessage); AbstractIntegrationMessageBuilder<?> builder = (replyObject instanceof Message) ? this.getMessageBuilderFactory().fromMessage((Message<?>) replyObject) : this.getMessageBuilderFactory().withPayload(replyObject); Map<String, ?> headers = getHeaderMapper().toHeadersFromReply(amqpReplyMessage.getMessageProperties()); builder.copyHeadersIfAbsent(headers); return builder; } protected Message<?> buildReturnedMessage(org.springframework.amqp.core.Message message, int replyCode, String replyText, String exchange, String routingKey, MessageConverter converter) { Object returnedObject = converter.fromMessage(message); AbstractIntegrationMessageBuilder<?> builder = (returnedObject instanceof Message) ? this.getMessageBuilderFactory().fromMessage((Message<?>) returnedObject) : this.getMessageBuilderFactory().withPayload(returnedObject); Map<String, ?> headers = getHeaderMapper().toHeadersFromReply(message.getMessageProperties()); builder.copyHeadersIfAbsent(headers) .setHeader(AmqpHeaders.RETURN_REPLY_CODE, replyCode) .setHeader(AmqpHeaders.RETURN_REPLY_TEXT, replyText) .setHeader(AmqpHeaders.RETURN_EXCHANGE, exchange) .setHeader(AmqpHeaders.RETURN_ROUTING_KEY, routingKey); return builder.build(); } protected void handleConfirm(CorrelationData correlationData, boolean ack, String cause) { Object userCorrelationData = correlationData; if (correlationData == null) { if (logger.isDebugEnabled()) { logger.debug("No correlation data provided for ack: " + ack + " cause:" + cause); } return; } if (correlationData instanceof CorrelationDataWrapper) { userCorrelationData = ((CorrelationDataWrapper) correlationData).getUserData(); } Map<String, Object> headers = new HashMap<String, Object>(); headers.put(AmqpHeaders.PUBLISH_CONFIRM, ack); if (!ack && StringUtils.hasText(cause)) { headers.put(AmqpHeaders.PUBLISH_CONFIRM_NACK_CAUSE, cause); } AbstractIntegrationMessageBuilder<?> builder = userCorrelationData instanceof Message ? this.getMessageBuilderFactory().fromMessage((Message<?>) userCorrelationData) : this.getMessageBuilderFactory().withPayload(userCorrelationData); Message<?> confirmMessage = builder .copyHeaders(headers) .build(); if (ack && this.confirmAckChannel != null) { sendOutput(confirmMessage, this.confirmAckChannel, true); } else if (!ack && this.confirmNackChannel != null) { sendOutput(confirmMessage, this.confirmNackChannel, true); } else { if (logger.isInfoEnabled()) { logger.info("Nowhere to send publisher confirm " + (ack ? "ack" : "nack") + " for " + userCorrelationData); } } } protected static final class CorrelationDataWrapper extends CorrelationData { private final Object userData; private CorrelationDataWrapper(String id, Object userData) { super(id); this.userData = userData; } public Object getUserData() { return this.userData; } } }