/*
 * Copyright 2013-2019 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
 *
 *      https://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.cloud.stream.binder.rabbit;

import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Pattern;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Envelope;

import org.springframework.amqp.AmqpRejectAndDontRequeueException;
import org.springframework.amqp.ImmediateAcknowledgeAmqpException;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.batch.BatchingStrategy;
import org.springframework.amqp.rabbit.batch.SimpleBatchingStrategy;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.LocalizedQueueConnectionFactory;
import org.springframework.amqp.rabbit.connection.RabbitUtils;
import org.springframework.amqp.rabbit.core.BatchingRabbitTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.DirectMessageListenerContainer;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.amqp.rabbit.retry.RejectAndDontRequeueRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.amqp.rabbit.support.DefaultMessagePropertiesConverter;
import org.springframework.amqp.rabbit.support.ListenerExecutionFailedException;
import org.springframework.amqp.rabbit.support.MessagePropertiesConverter;
import org.springframework.amqp.support.AmqpHeaders;
import org.springframework.amqp.support.converter.AbstractMessageConverter;
import org.springframework.amqp.support.converter.MessageConversionException;
import org.springframework.amqp.support.converter.SimpleMessageConverter;
import org.springframework.amqp.support.postprocessor.DelegatingDecompressingPostProcessor;
import org.springframework.amqp.support.postprocessor.GZipPostProcessor;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties.ContainerType;
import org.springframework.boot.autoconfigure.amqp.RabbitProperties.Retry;
import org.springframework.cloud.stream.binder.AbstractMessageChannelBinder;
import org.springframework.cloud.stream.binder.BinderHeaders;
import org.springframework.cloud.stream.binder.BinderSpecificPropertiesProvider;
import org.springframework.cloud.stream.binder.DefaultPollableMessageSource;
import org.springframework.cloud.stream.binder.ExtendedConsumerProperties;
import org.springframework.cloud.stream.binder.ExtendedProducerProperties;
import org.springframework.cloud.stream.binder.ExtendedPropertiesBinder;
import org.springframework.cloud.stream.binder.HeaderMode;
import org.springframework.cloud.stream.binder.rabbit.properties.RabbitCommonProperties;
import org.springframework.cloud.stream.binder.rabbit.properties.RabbitConsumerProperties;
import org.springframework.cloud.stream.binder.rabbit.properties.RabbitExtendedBindingProperties;
import org.springframework.cloud.stream.binder.rabbit.properties.RabbitProducerProperties;
import org.springframework.cloud.stream.binder.rabbit.provisioning.RabbitExchangeQueueProvisioner;
import org.springframework.cloud.stream.config.ListenerContainerCustomizer;
import org.springframework.cloud.stream.config.MessageSourceCustomizer;
import org.springframework.cloud.stream.provisioning.ConsumerDestination;
import org.springframework.cloud.stream.provisioning.ProducerDestination;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.core.task.SimpleAsyncTaskExecutor;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.integration.IntegrationMessageHeaderAccessor;
import org.springframework.integration.StaticMessageHeaderAccessor;
import org.springframework.integration.acks.AcknowledgmentCallback;
import org.springframework.integration.acks.AcknowledgmentCallback.Status;
import org.springframework.integration.amqp.inbound.AmqpInboundChannelAdapter;
import org.springframework.integration.amqp.inbound.AmqpMessageSource;
import org.springframework.integration.amqp.outbound.AmqpOutboundEndpoint;
import org.springframework.integration.amqp.support.AmqpMessageHeaderErrorMessageStrategy;
import org.springframework.integration.amqp.support.DefaultAmqpHeaderMapper;
import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.core.MessageProducer;
import org.springframework.integration.support.DefaultErrorMessageStrategy;
import org.springframework.integration.support.ErrorMessageStrategy;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.retry.RetryPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * A {@link org.springframework.cloud.stream.binder.Binder} implementation backed by
 * RabbitMQ.
 *
 * @author Mark Fisher
 * @author Gary Russell
 * @author Jennifer Hickey
 * @author Gunnar Hillert
 * @author Ilayaperumal Gopinathan
 * @author David Turanski
 * @author Marius Bogoevici
 * @author Artem Bilan
 * @author Soby Chacko
 * @author Oleg Zhurakousky
 */
// @checkstyle:off
public class RabbitMessageChannelBinder extends
		AbstractMessageChannelBinder<ExtendedConsumerProperties<RabbitConsumerProperties>, ExtendedProducerProperties<RabbitProducerProperties>, RabbitExchangeQueueProvisioner>
		implements
		ExtendedPropertiesBinder<MessageChannel, RabbitConsumerProperties, RabbitProducerProperties>,
		DisposableBean {

	private static final SimplePassthroughMessageConverter passThoughConverter = new SimplePassthroughMessageConverter();

	private static final AmqpMessageHeaderErrorMessageStrategy errorMessageStrategy = new AmqpMessageHeaderErrorMessageStrategy();

	private static final MessagePropertiesConverter inboundMessagePropertiesConverter = new DefaultMessagePropertiesConverter() {
		@Override
		public MessageProperties toMessageProperties(AMQP.BasicProperties source,
				Envelope envelope, String charset) {
			MessageProperties properties = super.toMessageProperties(source, envelope,
					charset);
			properties.setDeliveryMode(null);
			return properties;
		}
	};

	private static final Pattern interceptorNeededPattern = Pattern.compile("(payload|#root|#this)");

	// @checkstyle:on

	private final RabbitProperties rabbitProperties;

	private boolean destroyConnectionFactory;

	private ConnectionFactory connectionFactory;

	private MessagePostProcessor decompressingPostProcessor = new DelegatingDecompressingPostProcessor();

	private MessagePostProcessor compressingPostProcessor = new GZipPostProcessor();

	private volatile String[] adminAddresses;

	private volatile String[] nodes;

	private volatile boolean clustered;

	private RabbitExtendedBindingProperties extendedBindingProperties = new RabbitExtendedBindingProperties();

	public RabbitMessageChannelBinder(ConnectionFactory connectionFactory,
			RabbitProperties rabbitProperties,
			RabbitExchangeQueueProvisioner provisioningProvider) {

		this(connectionFactory, rabbitProperties, provisioningProvider, null, null);
	}

	public RabbitMessageChannelBinder(ConnectionFactory connectionFactory,
			RabbitProperties rabbitProperties,
			RabbitExchangeQueueProvisioner provisioningProvider,
			ListenerContainerCustomizer<AbstractMessageListenerContainer> containerCustomizer) {

		this(connectionFactory, rabbitProperties, provisioningProvider, containerCustomizer, null);
	}

	public RabbitMessageChannelBinder(ConnectionFactory connectionFactory,
			RabbitProperties rabbitProperties,
			RabbitExchangeQueueProvisioner provisioningProvider,
			ListenerContainerCustomizer<AbstractMessageListenerContainer> containerCustomizer,
			MessageSourceCustomizer<AmqpMessageSource> sourceCustomizer) {

		super(new String[0], provisioningProvider, containerCustomizer, sourceCustomizer);
		Assert.notNull(connectionFactory, "connectionFactory must not be null");
		Assert.notNull(rabbitProperties, "rabbitProperties must not be null");
		this.connectionFactory = connectionFactory;
		this.rabbitProperties = rabbitProperties;
	}

	/**
	 * Set a {@link MessagePostProcessor} to decompress messages. Defaults to a
	 * {@link DelegatingDecompressingPostProcessor} with its default delegates.
	 * @param decompressingPostProcessor the post processor.
	 */
	public void setDecompressingPostProcessor(
			MessagePostProcessor decompressingPostProcessor) {
		this.decompressingPostProcessor = decompressingPostProcessor;
	}

	/**
	 * Set a {@link org.springframework.amqp.core.MessagePostProcessor} to compress
	 * messages. Defaults to a
	 * {@link org.springframework.amqp.support.postprocessor.GZipPostProcessor}.
	 * @param compressingPostProcessor the post processor.
	 */
	public void setCompressingPostProcessor(
			MessagePostProcessor compressingPostProcessor) {
		this.compressingPostProcessor = compressingPostProcessor;
	}

	public void setAdminAddresses(String[] adminAddresses) {
		this.adminAddresses = Arrays.copyOf(adminAddresses, adminAddresses.length);
	}

	public void setNodes(String[] nodes) {
		this.nodes = Arrays.copyOf(nodes, nodes.length);
		this.clustered = nodes.length > 1;
	}

	public void setExtendedBindingProperties(
			RabbitExtendedBindingProperties extendedBindingProperties) {
		this.extendedBindingProperties = extendedBindingProperties;
	}

	@Override
	public void onInit() throws Exception {
		super.onInit();
		if (this.clustered) {
			String[] addresses = StringUtils.commaDelimitedListToStringArray(
					this.rabbitProperties.getAddresses());

			Assert.state(
					addresses.length == this.adminAddresses.length
							&& addresses.length == this.nodes.length,
					"'addresses', 'adminAddresses', and 'nodes' properties must have equal length");

			this.connectionFactory = new LocalizedQueueConnectionFactory(
					this.connectionFactory, addresses, this.adminAddresses, this.nodes,
					this.rabbitProperties.getVirtualHost(),
					this.rabbitProperties.getUsername(),
					this.rabbitProperties.getPassword(),
					this.rabbitProperties.getSsl().getEnabled(),
					this.rabbitProperties.getSsl().getKeyStore(),
					this.rabbitProperties.getSsl().getTrustStore(),
					this.rabbitProperties.getSsl().getKeyStorePassword(),
					this.rabbitProperties.getSsl().getTrustStorePassword());
			this.destroyConnectionFactory = true;
		}
	}

	@Override
	public void destroy() throws Exception {
		if (this.connectionFactory instanceof DisposableBean) {
			if (this.destroyConnectionFactory) {
				((DisposableBean) this.connectionFactory).destroy();
			}
		}
	}

	@Override
	public RabbitConsumerProperties getExtendedConsumerProperties(String channelName) {
		return this.extendedBindingProperties.getExtendedConsumerProperties(channelName);
	}

	@Override
	public RabbitProducerProperties getExtendedProducerProperties(String channelName) {
		return this.extendedBindingProperties.getExtendedProducerProperties(channelName);
	}

	@Override
	public String getDefaultsPrefix() {
		return this.extendedBindingProperties.getDefaultsPrefix();
	}

	@Override
	public Class<? extends BinderSpecificPropertiesProvider> getExtendedPropertiesEntryClass() {
		return this.extendedBindingProperties.getExtendedPropertiesEntryClass();
	}

	@Override
	protected MessageHandler createProducerMessageHandler(
			final ProducerDestination producerDestination,
			ExtendedProducerProperties<RabbitProducerProperties> producerProperties,
			MessageChannel errorChannel) {
		Assert.state(
				!HeaderMode.embeddedHeaders.equals(producerProperties.getHeaderMode()),
				"the RabbitMQ binder does not support embedded headers since RabbitMQ supports headers natively");
		String prefix = producerProperties.getExtension().getPrefix();
		String exchangeName = producerDestination.getName();
		String destination = StringUtils.isEmpty(prefix) ? exchangeName
				: exchangeName.substring(prefix.length());
		final AmqpOutboundEndpoint endpoint = new AmqpOutboundEndpoint(
				buildRabbitTemplate(producerProperties.getExtension(),
						errorChannel != null));
		endpoint.setExchangeName(producerDestination.getName());
		RabbitProducerProperties extendedProperties = producerProperties.getExtension();
		boolean expressionInterceptorNeeded = expressionInterceptorNeeded(
				extendedProperties);
		Expression routingKeyExpression = extendedProperties.getRoutingKeyExpression();
		if (!producerProperties.isPartitioned()) {
			if (routingKeyExpression == null) {
				endpoint.setRoutingKey(destination);
			}
			else {
				if (expressionInterceptorNeeded) {
					endpoint.setRoutingKeyExpressionString("headers['"
							+ RabbitExpressionEvaluatingInterceptor.ROUTING_KEY_HEADER
							+ "']");
				}
				else {
					endpoint.setRoutingKeyExpression(routingKeyExpression);
				}
			}
		}
		else {
			if (routingKeyExpression == null) {
				endpoint.setRoutingKeyExpression(
						buildPartitionRoutingExpression(destination, false));
			}
			else {
				if (expressionInterceptorNeeded) {
					endpoint.setRoutingKeyExpression(
							buildPartitionRoutingExpression("headers['"
									+ RabbitExpressionEvaluatingInterceptor.ROUTING_KEY_HEADER
									+ "']", true));
				}
				else {
					endpoint.setRoutingKeyExpression(buildPartitionRoutingExpression(
							routingKeyExpression.getExpressionString(), true));
				}
			}
		}
		if (extendedProperties.getDelayExpression() != null) {
			if (expressionInterceptorNeeded) {
				endpoint.setDelayExpressionString("headers['"
						+ RabbitExpressionEvaluatingInterceptor.DELAY_HEADER + "']");
			}
			else {
				endpoint.setDelayExpression(extendedProperties.getDelayExpression());
			}
		}
		DefaultAmqpHeaderMapper mapper = DefaultAmqpHeaderMapper.outboundMapper();
		List<String> headerPatterns = new ArrayList<>(extendedProperties.getHeaderPatterns().length + 3);
		headerPatterns.add("!" + BinderHeaders.PARTITION_HEADER);
		headerPatterns.add("!" + IntegrationMessageHeaderAccessor.SOURCE_DATA);
		headerPatterns.add("!" + IntegrationMessageHeaderAccessor.DELIVERY_ATTEMPT);
		headerPatterns.addAll(Arrays.asList(extendedProperties.getHeaderPatterns()));
		mapper.setRequestHeaderNames(
				headerPatterns.toArray(new String[headerPatterns.size()]));
		endpoint.setHeaderMapper(mapper);
		endpoint.setDefaultDeliveryMode(extendedProperties.getDeliveryMode());
		endpoint.setBeanFactory(this.getBeanFactory());
		if (errorChannel != null) {
			checkConnectionFactoryIsErrorCapable();
			endpoint.setReturnChannel(errorChannel);
			endpoint.setConfirmNackChannel(errorChannel);
			String ackChannelBeanName = StringUtils
					.hasText(extendedProperties.getConfirmAckChannel())
							? extendedProperties.getConfirmAckChannel()
							: IntegrationContextUtils.NULL_CHANNEL_BEAN_NAME;
			if (!ackChannelBeanName.equals(IntegrationContextUtils.NULL_CHANNEL_BEAN_NAME)
					&& !getApplicationContext().containsBean(ackChannelBeanName)) {
				GenericApplicationContext context = (GenericApplicationContext) getApplicationContext();
				context.registerBean(ackChannelBeanName, DirectChannel.class,
						() -> new DirectChannel());
			}
			endpoint.setConfirmAckChannelName(ackChannelBeanName);
			endpoint.setConfirmCorrelationExpressionString("#root");
			endpoint.setErrorMessageStrategy(new DefaultErrorMessageStrategy());
		}
		endpoint.setHeadersMappedLast(true);
		return endpoint;
	}

	@Override
	protected void postProcessOutputChannel(MessageChannel outputChannel,
			ExtendedProducerProperties<RabbitProducerProperties> producerProperties) {
		RabbitProducerProperties extendedProperties = producerProperties.getExtension();
		if (expressionInterceptorNeeded(extendedProperties)) {
			((AbstractMessageChannel) outputChannel).addInterceptor(0,
					new RabbitExpressionEvaluatingInterceptor(
							extendedProperties.getRoutingKeyExpression(),
							extendedProperties.getDelayExpression(),
							getEvaluationContext()));
		}
	}

	private boolean expressionInterceptorNeeded(RabbitProducerProperties extendedProperties) {
		Expression rkExpression = extendedProperties.getRoutingKeyExpression();
		Expression delayExpression = extendedProperties.getDelayExpression();
		return (rkExpression != null && interceptorNeededPattern.matcher(rkExpression.getExpressionString()).find())
				|| (delayExpression != null
						&& interceptorNeededPattern.matcher(delayExpression.getExpressionString()).find());
	}

	private void checkConnectionFactoryIsErrorCapable() {
		if (!(this.connectionFactory instanceof CachingConnectionFactory)) {
			logger.warn(
					"Unknown connection factory type, cannot determine error capabilities: "
							+ this.connectionFactory.getClass());
		}
		else {
			CachingConnectionFactory ccf = (CachingConnectionFactory) this.connectionFactory;
			if (!ccf.isPublisherConfirms() && !ccf.isPublisherReturns()) {
				logger.warn(
						"Producer error channel is enabled, but the connection factory is not configured for "
								+ "returns or confirms; the error channel will receive no messages");
			}
			else if (!ccf.isPublisherConfirms()) {
				logger.info(
						"Producer error channel is enabled, but the connection factory is only configured to "
								+ "handle returned messages; negative acks will not be reported");
			}
			else if (!ccf.isPublisherReturns()) {
				logger.info(
						"Producer error channel is enabled, but the connection factory is only configured to "
								+ "handle negatively acked messages; returned messages will not be reported");
			}
		}
	}

	private Expression buildPartitionRoutingExpression(String expressionRoot,
			boolean rootIsExpression) {
		String partitionRoutingExpression = rootIsExpression
				? expressionRoot + " + '-' + headers['" + BinderHeaders.PARTITION_HEADER
						+ "']"
				: "'" + expressionRoot + "-' + headers['" + BinderHeaders.PARTITION_HEADER
						+ "']";
		return new SpelExpressionParser().parseExpression(partitionRoutingExpression);
	}

	@Override
	protected MessageProducer createConsumerEndpoint(
			ConsumerDestination consumerDestination, String group,
			ExtendedConsumerProperties<RabbitConsumerProperties> properties) {
		Assert.state(!HeaderMode.embeddedHeaders.equals(properties.getHeaderMode()),
				"the RabbitMQ binder does not support embedded headers since RabbitMQ supports headers natively");
		String destination = consumerDestination.getName();
		boolean directContainer = properties.getExtension().getContainerType()
				.equals(ContainerType.DIRECT);
		AbstractMessageListenerContainer listenerContainer = directContainer
				? new DirectMessageListenerContainer(this.connectionFactory)
				: new SimpleMessageListenerContainer(this.connectionFactory);
		listenerContainer
				.setAcknowledgeMode(properties.getExtension().getAcknowledgeMode());
		listenerContainer.setChannelTransacted(properties.getExtension().isTransacted());
		listenerContainer
				.setDefaultRequeueRejected(properties.getExtension().isRequeueRejected());
		int concurrency = properties.getConcurrency();
		concurrency = concurrency > 0 ? concurrency : 1;
		if (directContainer) {
			setDMLCProperties(properties,
					(DirectMessageListenerContainer) listenerContainer, concurrency);
		}
		else {
			setSMLCProperties(properties,
					(SimpleMessageListenerContainer) listenerContainer, concurrency);
		}
		listenerContainer.setPrefetchCount(properties.getExtension().getPrefetch());
		listenerContainer
				.setRecoveryInterval(properties.getExtension().getRecoveryInterval());
		listenerContainer.setTaskExecutor(
				new SimpleAsyncTaskExecutor(consumerDestination.getName() + "-"));
		String[] queues = StringUtils.tokenizeToStringArray(destination, ",", true, true);
		listenerContainer.setQueueNames(queues);
		listenerContainer.setAfterReceivePostProcessors(this.decompressingPostProcessor);
		listenerContainer.setMessagePropertiesConverter(
				RabbitMessageChannelBinder.inboundMessagePropertiesConverter);
		listenerContainer.setExclusive(properties.getExtension().isExclusive());
		listenerContainer
				.setMissingQueuesFatal(properties.getExtension().getMissingQueuesFatal());
		if (properties.getExtension().getFailedDeclarationRetryInterval() != null) {
			listenerContainer.setFailedDeclarationRetryInterval(
					properties.getExtension().getFailedDeclarationRetryInterval());
		}
		if (getApplicationEventPublisher() != null) {
			listenerContainer
					.setApplicationEventPublisher(getApplicationEventPublisher());
		}
		else if (getApplicationContext() != null) {
			listenerContainer.setApplicationEventPublisher(getApplicationContext());
		}
		getContainerCustomizer().configure(listenerContainer,
				consumerDestination.getName(), group);
		if (StringUtils.hasText(properties.getExtension().getConsumerTagPrefix())) {
			final AtomicInteger index = new AtomicInteger();
			listenerContainer.setConsumerTagStrategy(
					q -> properties.getExtension().getConsumerTagPrefix() + "#"
							+ index.getAndIncrement());
		}
		listenerContainer.afterPropertiesSet();

		AmqpInboundChannelAdapter adapter = new AmqpInboundChannelAdapter(
				listenerContainer);
		adapter.setBindSourceMessage(true);
		adapter.setBeanFactory(this.getBeanFactory());
		adapter.setBeanName("inbound." + destination);
		DefaultAmqpHeaderMapper mapper = DefaultAmqpHeaderMapper.inboundMapper();
		mapper.setRequestHeaderNames(properties.getExtension().getHeaderPatterns());
		adapter.setHeaderMapper(mapper);
		ErrorInfrastructure errorInfrastructure = registerErrorInfrastructure(
				consumerDestination, group, properties);
		if (properties.getMaxAttempts() > 1) {
			adapter.setRetryTemplate(buildRetryTemplate(properties));
			adapter.setRecoveryCallback(errorInfrastructure.getRecoverer());
		}
		else {
			adapter.setErrorMessageStrategy(errorMessageStrategy);
			adapter.setErrorChannel(errorInfrastructure.getErrorChannel());
		}
		adapter.setMessageConverter(passThoughConverter);
		return adapter;
	}

	private void setSMLCProperties(
			ExtendedConsumerProperties<RabbitConsumerProperties> properties,
			SimpleMessageListenerContainer listenerContainer, int concurrency) {

		listenerContainer.setConcurrentConsumers(concurrency);
		int maxConcurrency = properties.getExtension().getMaxConcurrency();
		if (maxConcurrency > concurrency) {
			listenerContainer.setMaxConcurrentConsumers(maxConcurrency);
		}
		listenerContainer.setDeBatchingEnabled(!properties.isBatchMode());
		listenerContainer.setBatchSize(properties.getExtension().getBatchSize());
		if (properties.getExtension().getQueueDeclarationRetries() != null) {
			listenerContainer.setDeclarationRetries(
					properties.getExtension().getQueueDeclarationRetries());
		}
	}

	private void setDMLCProperties(
			ExtendedConsumerProperties<RabbitConsumerProperties> properties,
			DirectMessageListenerContainer listenerContainer, int concurrency) {

		listenerContainer.setConsumersPerQueue(concurrency);
		if (properties.getExtension().getMaxConcurrency() > concurrency) {
			this.logger
					.warn("maxConcurrency is not supported by the direct container type");
		}
		if (properties.getExtension().getBatchSize() > 1) {
			this.logger.warn("batchSize is not supported by the direct container type");
		}
		if (properties.getExtension().getQueueDeclarationRetries() != null) {
			this.logger.warn(
					"queueDeclarationRetries is not supported by the direct container type");
		}
	}

	@Override
	protected PolledConsumerResources createPolledConsumerResources(String name,
			String group, ConsumerDestination destination,
			ExtendedConsumerProperties<RabbitConsumerProperties> consumerProperties) {

		Assert.isTrue(!consumerProperties.isMultiplex(),
				"The Spring Integration polled MessageSource does not currently support muiltiple queues");
		AmqpMessageSource source = new AmqpMessageSource(this.connectionFactory,
				destination.getName());
		source.setRawMessageHeader(true);
		getMessageSourceCustomizer().configure(source, destination.getName(), group);
		return new PolledConsumerResources(source, registerErrorInfrastructure(
				destination, group, consumerProperties, true));
	}

	@Override
	protected void postProcessPollableSource(DefaultPollableMessageSource bindingTarget) {
		bindingTarget.setAttributesProvider((accessor, message) -> {
			Object rawMessage = message.getHeaders()
					.get(AmqpMessageHeaderErrorMessageStrategy.AMQP_RAW_MESSAGE);
			if (rawMessage != null) {
				accessor.setAttribute(
						AmqpMessageHeaderErrorMessageStrategy.AMQP_RAW_MESSAGE,
						rawMessage);
			}
		});
	}

	@Override
	protected ErrorMessageStrategy getErrorMessageStrategy() {
		return errorMessageStrategy;
	}

	@Override
	protected MessageHandler getErrorMessageHandler(ConsumerDestination destination,
			String group,
			final ExtendedConsumerProperties<RabbitConsumerProperties> properties) {

		if (properties.getExtension().isRepublishToDlq()) {
			return new MessageHandler() {

				private final RabbitTemplate template = new RabbitTemplate(
						RabbitMessageChannelBinder.this.connectionFactory);

				{
					this.template.setUsePublisherConnection(true);
				}

				private final String exchange = deadLetterExchangeName(properties.getExtension());

				private final String routingKey = properties.getExtension()
						.getDeadLetterRoutingKey();

				private final int frameMaxHeadroom = properties.getExtension()
						.getFrameMaxHeadroom();

				private int maxStackTraceLength = -1;

				private Boolean dlxPresent;

				@Override
				public void handleMessage(org.springframework.messaging.Message<?> message) throws MessagingException {
					Message amqpMessage = StaticMessageHeaderAccessor.getSourceData(message);

					if (!(message instanceof ErrorMessage)) {
						logger.error("Expected an ErrorMessage, not a "
								+ message.getClass().toString() + " for: " + message);
					}
					else if (amqpMessage == null) {
						logger.error("No raw message header in " + message);
					}
					else {
						if (!checkDlx()) {
							return;
						}
						Throwable cause = (Throwable) message.getPayload();
						if (!shouldRepublish(cause)) {
							if (logger.isDebugEnabled()) {
								logger.debug("Skipping republish of: " + message);
							}
							return;
						}
						MessageProperties messageProperties = amqpMessage
								.getMessageProperties();
						Map<String, Object> headers = messageProperties.getHeaders();
						String stackTraceAsString = getStackTraceAsString(cause);
						if (this.maxStackTraceLength < 0) {
							int rabbitMaxStackTraceLength = RabbitUtils
									.getMaxFrame(this.template.getConnectionFactory());
							if (rabbitMaxStackTraceLength > 0) {
								// maxStackTraceLength -= this.frameMaxHeadroom;
								this.maxStackTraceLength = rabbitMaxStackTraceLength
										- this.frameMaxHeadroom;
							}
						}
						if (this.maxStackTraceLength > 0 && stackTraceAsString
								.length() > this.maxStackTraceLength) {
							stackTraceAsString = stackTraceAsString.substring(0,
									this.maxStackTraceLength);
							logger.warn(
									"Stack trace in republished message header truncated due to frame_max limitations; "
											+ "consider increasing frame_max on the broker or reduce the stack trace depth",
									cause);
						}
						headers.put(RepublishMessageRecoverer.X_EXCEPTION_STACKTRACE,
								stackTraceAsString);
						headers.put(RepublishMessageRecoverer.X_EXCEPTION_MESSAGE,
								cause.getCause() != null ? cause.getCause().getMessage()
										: cause.getMessage());
						headers.put(RepublishMessageRecoverer.X_ORIGINAL_EXCHANGE,
								messageProperties.getReceivedExchange());
						headers.put(RepublishMessageRecoverer.X_ORIGINAL_ROUTING_KEY,
								messageProperties.getReceivedRoutingKey());
						if (properties.getExtension().getRepublishDeliveyMode() != null) {
							messageProperties.setDeliveryMode(
									properties.getExtension().getRepublishDeliveyMode());
						}
						this.template.send(this.exchange,
								this.routingKey != null ? this.routingKey
										: messageProperties.getConsumerQueue(),
								amqpMessage);
						if (properties.getExtension().getAcknowledgeMode().equals(AcknowledgeMode.MANUAL)) {
							org.springframework.messaging.Message<?> original =
									((ErrorMessage) message).getOriginalMessage();
							if (original != null) {
								// If we are using manual acks, ack the original message.
								try {
									original.getHeaders().get(AmqpHeaders.CHANNEL, Channel.class)
										.basicAck(original.getHeaders()
												.get(AmqpHeaders.DELIVERY_TAG, Long.class), false);
								}
								catch (IOException e) {
									logger.debug("Failed to ack original message", e);
								}
							}
						}
					}
				}

				private boolean checkDlx() {
					if (this.dlxPresent == null) {
						if (properties.getExtension().isAutoBindDlq()) {
							this.dlxPresent = Boolean.TRUE;
						}
						else {
							this.dlxPresent = this.template.execute(channel -> {
								String dlx = deadLetterExchangeName(properties.getExtension());
								try {
									channel.exchangeDeclarePassive(dlx);
									return Boolean.TRUE;
								}
								catch (IOException e) {
									logger.warn("'republishToDlq' is true, but the '"
											+ dlx
											+ "' dead letter exchange is not present; disabling 'republishToDlq'");
									return Boolean.FALSE;
								}
							});
						}
					}
					return this.dlxPresent;
				}

				/**
				 * Traverse the cause tree, stopping at AmqpRejectAndDontRequeueException
				 * or ImmediateAcknowledgeAmqpException.
				 * @param throwable the throwable.
				 * @return true if neither found or AmqpRejectAndDontRequeueException is
				 * found first.
				 */
				private boolean shouldRepublish(Throwable throwable) {
					Throwable cause = throwable;
					while (cause != null
							&& !(cause instanceof AmqpRejectAndDontRequeueException)
							&& !(cause instanceof ImmediateAcknowledgeAmqpException)) {
						cause = cause.getCause();
					}
					return !(cause instanceof ImmediateAcknowledgeAmqpException);
				}

			};
		}
		else if (properties.getMaxAttempts() > 1) {
			return new MessageHandler() {
				private final RejectAndDontRequeueRecoverer recoverer = new RejectAndDontRequeueRecoverer();

				@Override
				public void handleMessage(
						org.springframework.messaging.Message<?> message)
						throws MessagingException {
					Message amqpMessage = StaticMessageHeaderAccessor.getSourceData(message);
					/*
					 * NOTE: The following IF and subsequent ELSE IF should never happen
					 * under normal interaction and it should always go to the last ELSE
					 * However, given that this is a handler subscribing to the public
					 * channel and that we can't control what type of Message may be sent
					 * to that channel (user decides to send a Message manually) the
					 * 'IF/ELSE IF' provides a safety net to handle any message properly.
					 */
					if (!(message instanceof ErrorMessage)) {
						logger.error("Expected an ErrorMessage, not a "
								+ message.getClass().toString() + " for: " + message);
						throw new ListenerExecutionFailedException(
								"Unexpected error message " + message,
								new AmqpRejectAndDontRequeueException(""), (Message[]) null);
					}
					else if (amqpMessage == null) {
						logger.error("No raw message header in " + message);
						throw new ListenerExecutionFailedException(
								"Unexpected error message " + message,
								new AmqpRejectAndDontRequeueException(""), amqpMessage);
					}
					else {
						this.recoverer.recover(amqpMessage,
								(Throwable) message.getPayload());
					}
				}

			};
		}
		else {
			return super.getErrorMessageHandler(destination, group, properties);
		}
	}

	@Override
	protected MessageHandler getPolledConsumerErrorMessageHandler(
			ConsumerDestination destination, String group,
			ExtendedConsumerProperties<RabbitConsumerProperties> properties) {
		MessageHandler handler = getErrorMessageHandler(destination, group, properties);
		if (handler != null) {
			return handler;
		}
		final MessageHandler superHandler = super.getErrorMessageHandler(destination,
				group, properties);
		return message -> {
			Message amqpMessage = (Message) message.getHeaders()
					.get(AmqpMessageHeaderErrorMessageStrategy.AMQP_RAW_MESSAGE);
			if (!(message instanceof ErrorMessage)) {
				logger.error("Expected an ErrorMessage, not a "
						+ message.getClass().toString() + " for: " + message);
			}
			else if (amqpMessage == null) {
				if (superHandler != null) {
					superHandler.handleMessage(message);
				}
			}
			else {
				if (message.getPayload() instanceof MessagingException) {
					AcknowledgmentCallback ack = StaticMessageHeaderAccessor
							.getAcknowledgmentCallback(
									((MessagingException) message.getPayload())
											.getFailedMessage());
					if (ack != null) {
						if (properties.getExtension().isRequeueRejected()) {
							ack.acknowledge(Status.REQUEUE);
						}
						else {
							ack.acknowledge(Status.REJECT);
						}
					}
				}
			}
		};
	}

	@Override
	protected String errorsBaseName(ConsumerDestination destination, String group,
			ExtendedConsumerProperties<RabbitConsumerProperties> consumerProperties) {
		return destination.getName() + ".errors";
	}

	private String deadLetterExchangeName(RabbitCommonProperties properties) {
		if (properties.getDeadLetterExchange() == null) {
			return applyPrefix(properties.getPrefix(),
					RabbitCommonProperties.DEAD_LETTER_EXCHANGE);
		}
		else {
			return properties.getDeadLetterExchange();
		}
	}

	@Override
	protected void afterUnbindConsumer(ConsumerDestination consumerDestination,
			String group,
			ExtendedConsumerProperties<RabbitConsumerProperties> consumerProperties) {
		provisioningProvider.cleanAutoDeclareContext(consumerDestination,
				consumerProperties);
	}

	private RabbitTemplate buildRabbitTemplate(RabbitProducerProperties properties,
			boolean mandatory) {
		RabbitTemplate rabbitTemplate;
		if (properties.isBatchingEnabled()) {
			BatchingStrategy batchingStrategy = getBatchingStrategy(properties);
			TaskScheduler taskScheduler = getApplicationContext()
					.getBean(IntegrationContextUtils.TASK_SCHEDULER_BEAN_NAME, TaskScheduler.class);
			rabbitTemplate = new BatchingRabbitTemplate(batchingStrategy, taskScheduler);
		}
		else {
			rabbitTemplate = new RabbitTemplate();
		}
		rabbitTemplate.setMessageConverter(passThoughConverter);
		rabbitTemplate.setChannelTransacted(properties.isTransacted());
		rabbitTemplate.setConnectionFactory(this.connectionFactory);
		rabbitTemplate.setUsePublisherConnection(true);
		if (properties.isCompress()) {
			rabbitTemplate.setBeforePublishPostProcessors(this.compressingPostProcessor);
		}
		rabbitTemplate.setMandatory(mandatory); // returned messages
		if (rabbitProperties != null
				&& rabbitProperties.getTemplate().getRetry().isEnabled()) {
			Retry retry = rabbitProperties.getTemplate().getRetry();
			RetryPolicy retryPolicy = new SimpleRetryPolicy(retry.getMaxAttempts());
			ExponentialBackOffPolicy backOff = new ExponentialBackOffPolicy();
			backOff.setInitialInterval(retry.getInitialInterval().toMillis());
			backOff.setMultiplier(retry.getMultiplier());
			backOff.setMaxInterval(retry.getMaxInterval().toMillis());
			RetryTemplate retryTemplate = new RetryTemplate();
			retryTemplate.setRetryPolicy(retryPolicy);
			retryTemplate.setBackOffPolicy(backOff);
			rabbitTemplate.setRetryTemplate(retryTemplate);
		}
		rabbitTemplate.afterPropertiesSet();
		return rabbitTemplate;
	}

	private BatchingStrategy getBatchingStrategy(RabbitProducerProperties properties) {
		BatchingStrategy batchingStrategy;
		if (properties.getBatchingStrategyBeanName() != null) {
			batchingStrategy = getApplicationContext()
					.getBean(properties.getBatchingStrategyBeanName(), BatchingStrategy.class);
		}
		else {
			batchingStrategy = new SimpleBatchingStrategy(
					properties.getBatchSize(),
					properties.getBatchBufferLimit(),
					properties.getBatchTimeout()
			);
		}
		return batchingStrategy;
	}

	private String getStackTraceAsString(Throwable cause) {
		StringWriter stringWriter = new StringWriter();
		PrintWriter printWriter = new PrintWriter(stringWriter, true);
		cause.printStackTrace(printWriter);
		return stringWriter.getBuffer().toString();
	}

	private static final class SimplePassthroughMessageConverter
			extends AbstractMessageConverter {

		private static final SimpleMessageConverter converter = new SimpleMessageConverter();

		SimplePassthroughMessageConverter() {
			super();
		}

		@Override
		protected Message createMessage(Object object,
				MessageProperties messageProperties) {
			if (object instanceof byte[]) {
				return new Message((byte[]) object, messageProperties);
			}
			else {
				// just for safety (backwards compatibility)
				return converter.toMessage(object, messageProperties);
			}
		}

		@Override
		public Object fromMessage(Message message) throws MessageConversionException {
			return message.getBody();
		}

	}

}