/*
* Copyright 2013-2015 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.xd.dirt.integration.bus;
import static org.springframework.util.MimeTypeUtils.ALL;
import static org.springframework.util.MimeTypeUtils.APPLICATION_OCTET_STREAM;
import static org.springframework.util.MimeTypeUtils.APPLICATION_OCTET_STREAM_VALUE;
import static org.springframework.util.MimeTypeUtils.TEXT_PLAIN;
import static org.springframework.util.MimeTypeUtils.TEXT_PLAIN_VALUE;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.Lifecycle;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.codec.Codec;
import org.springframework.integration.context.IntegrationContextUtils;
import org.springframework.integration.endpoint.EventDrivenConsumer;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHandler;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.MessagingException;
import org.springframework.messaging.SubscribableChannel;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.AlternativeJdkIdGenerator;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.IdGenerator;
import org.springframework.util.MimeType;
import org.springframework.util.StringUtils;
/**
* @author David Turanski
* @author Gary Russell
* @author Ilayaperumal Gopinathan
*/
public abstract class MessageBusSupport
implements MessageBus, ApplicationContextAware, InitializingBean {
protected static final String P2P_NAMED_CHANNEL_TYPE_PREFIX = "queue:";
protected static final String TAP_TYPE_PREFIX = "tap:";
protected static final String PUBSUB_NAMED_CHANNEL_TYPE_PREFIX = "topic:";
protected static final String JOB_CHANNEL_TYPE_PREFIX = "job:";
protected static final String PARTITION_HEADER = "partition";
protected final Logger logger = LoggerFactory.getLogger(getClass());
private volatile AbstractApplicationContext applicationContext;
private volatile Codec codec;
private final StringConvertingContentTypeResolver contentTypeResolver = new StringConvertingContentTypeResolver();
private final ThreadLocal<Boolean> revertingDirectBinding = new ThreadLocal<Boolean>();
protected static final List<MimeType> MEDIATYPES_MEDIATYPE_ALL = Collections.singletonList(ALL);
private static final int DEFAULT_BACKOFF_INITIAL_INTERVAL = 1000;
private static final int DEFAULT_BACKOFF_MAX_INTERVAL = 10000;
private static final double DEFAULT_BACKOFF_MULTIPLIER = 2.0;
private static final int DEFAULT_CONCURRENCY = 1;
private static final int DEFAULT_MAX_ATTEMPTS = 3;
private static final int DEFAULT_BATCH_SIZE = 50;
private static final int DEFAULT_BATCH_BUFFER_LIMIT = 10000;
private static final int DEFAULT_BATCH_TIMEOUT = 0;
/**
* The set of properties every bus implementation must support (or at least tolerate).
*/
protected static final Set<Object> CONSUMER_STANDARD_PROPERTIES = new SetBuilder()
.add(BusProperties.COUNT)
.add(BusProperties.SEQUENCE)
.build();
protected static final Set<Object> PRODUCER_STANDARD_PROPERTIES = new HashSet<Object>(Arrays.asList(
BusProperties.NEXT_MODULE_COUNT,
BusProperties.NEXT_MODULE_CONCURRENCY
));
protected static final Set<Object> CONSUMER_RETRY_PROPERTIES = new HashSet<Object>(Arrays.asList(new String[] {
BusProperties.BACK_OFF_INITIAL_INTERVAL,
BusProperties.BACK_OFF_MAX_INTERVAL,
BusProperties.BACK_OFF_MULTIPLIER,
BusProperties.MAX_ATTEMPTS
}));
protected static final Set<Object> PRODUCER_PARTITIONING_PROPERTIES = new HashSet<Object>(
Arrays.asList(new String[] {
BusProperties.PARTITION_KEY_EXPRESSION,
BusProperties.PARTITION_KEY_EXTRACTOR_CLASS,
BusProperties.PARTITION_SELECTOR_CLASS,
BusProperties.PARTITION_SELECTOR_EXPRESSION,
}));
protected static final Set<Object> PRODUCER_BATCHING_BASIC_PROPERTIES = new HashSet<Object>(
Arrays.asList(new String[] {
BusProperties.BATCHING_ENABLED,
BusProperties.BATCH_SIZE,
BusProperties.BATCH_TIMEOUT,
}));
protected static final Set<Object> PRODUCER_BATCHING_ADVANCED_PROPERTIES = new HashSet<Object>(
Arrays.asList(new String[] {
BusProperties.BATCH_BUFFER_LIMIT,
}));
private final List<Binding> bindings = Collections.synchronizedList(new ArrayList<Binding>());
private final IdGenerator idGenerator = new AlternativeJdkIdGenerator();
protected volatile EvaluationContext evaluationContext;
private volatile PartitionSelectorStrategy partitionSelector = new DefaultPartitionSelector();
/**
* Used in the canonical case, when the binding does not involve an alias name.
*/
protected final SharedChannelProvider<DirectChannel> directChannelProvider = new
SharedChannelProvider<DirectChannel>(
DirectChannel.class) {
@Override
protected DirectChannel createSharedChannel(String name) {
return new DirectChannel();
}
};
protected volatile long defaultBackOffInitialInterval = DEFAULT_BACKOFF_INITIAL_INTERVAL;
protected volatile long defaultBackOffMaxInterval = DEFAULT_BACKOFF_MAX_INTERVAL;
protected volatile double defaultBackOffMultiplier = DEFAULT_BACKOFF_MULTIPLIER;
protected volatile int defaultConcurrency = DEFAULT_CONCURRENCY;
protected volatile int defaultMaxAttempts = DEFAULT_MAX_ATTEMPTS;
// properties for bus implementations that support batching
protected volatile boolean defaultBatchingEnabled = false;
protected volatile int defaultBatchSize = DEFAULT_BATCH_SIZE;
protected volatile int defaultBatchBufferLimit = DEFAULT_BATCH_BUFFER_LIMIT;
protected volatile long defaultBatchTimeout = DEFAULT_BATCH_TIMEOUT;
// compression
protected volatile boolean defaultCompress = false;
protected volatile boolean defaultDurableSubscription = false;
// Payload type cache
private volatile Map<String, Class<?>> payloadTypeCache = new ConcurrentHashMap<>();
/**
* For bus implementations that support a prefix, apply the prefix to the name.
* @param prefix the prefix.
* @param name the name.
*/
public static String applyPrefix(String prefix, String name) {
return prefix + name;
}
/**
* For bus implementations that include a pub/sub component in identifiers, construct the name.
* @param name the name.
*/
public static String applyPubSub(String name) {
return "topic." + name;
}
/**
* Build the requests entity name.
* @param name the name.
* @return the request entity name.
*/
public static String applyRequests(String name) {
return name + ".requests";
}
/**
* For bus implementations that support dead lettering, construct the name of the dead letter entity for the
* underlying pipe name.
* @param name the name.
*/
public static String constructDLQName(String name) {
return name + ".dlq";
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
this.applicationContext = (AbstractApplicationContext) applicationContext;
}
protected AbstractApplicationContext getApplicationContext() {
return this.applicationContext;
}
protected ConfigurableListableBeanFactory getBeanFactory() {
return this.applicationContext.getBeanFactory();
}
public void setCodec(Codec codec) {
this.codec = codec;
}
protected IdGenerator getIdGenerator() {
return idGenerator;
}
public void setIntegrationEvaluationContext(EvaluationContext evaluationContext) {
this.evaluationContext = evaluationContext;
}
/**
* Set the partition strategy to be used by this bus if no partitionExpression is provided for a module.
* @param partitionSelector The selector.
*/
public void setPartitionSelector(PartitionSelectorStrategy partitionSelector) {
this.partitionSelector = partitionSelector;
}
/**
* Set the default retry back off initial interval for this bus; can be overridden with consumer
* 'backOffInitialInterval' property.
* @param defaultBackOffInitialInterval
*/
public void setDefaultBackOffInitialInterval(long defaultBackOffInitialInterval) {
this.defaultBackOffInitialInterval = defaultBackOffInitialInterval;
}
/**
* Set the default retry back off multiplier for this bus; can be overridden with consumer 'backOffMultiplier'
* property.
* @param defaultBackOffMultiplier
*/
public void setDefaultBackOffMultiplier(double defaultBackOffMultiplier) {
this.defaultBackOffMultiplier = defaultBackOffMultiplier;
}
/**
* Set the default retry back off max interval for this bus; can be overridden with consumer 'backOffMaxInterval'
* property.
* @param defaultBackOffMaxInterval
*/
public void setDefaultBackOffMaxInterval(long defaultBackOffMaxInterval) {
this.defaultBackOffMaxInterval = defaultBackOffMaxInterval;
}
/**
* Set the default concurrency for this bus; can be overridden with consumer 'concurrency' property.
* @param defaultConcurrency
*/
public void setDefaultConcurrency(int defaultConcurrency) {
this.defaultConcurrency = defaultConcurrency;
}
/**
* The default maximum delivery attempts for this bus. Can be overridden by consumer property 'maxAttempts' if
* supported. Values less than 2 disable retry and one delivery attempt is made.
* @param defaultMaxAttempts The default maximum attempts.
*/
public void setDefaultMaxAttempts(int defaultMaxAttempts) {
this.defaultMaxAttempts = defaultMaxAttempts;
}
/**
* Set whether this bus batches message sends by default. Only applies to bus implementations that support
* batching.
* @param defaultBatchingEnabled the defaultBatchingEnabled to set.
*/
public void setDefaultBatchingEnabled(boolean defaultBatchingEnabled) {
this.defaultBatchingEnabled = defaultBatchingEnabled;
}
/**
* Set the default batch size; only applies when batching is enabled and the bus supports batching.
* @param defaultBatchSize the defaultBatchSize to set.
*/
public void setDefaultBatchSize(int defaultBatchSize) {
this.defaultBatchSize = defaultBatchSize;
}
/**
* Set the default batch buffer limit - used to send a batch early if its size exceeds this. Only applies if
* batching is enabled and the bus supports this property.
* @param defaultBatchBufferLimit the defaultBatchBufferLimit to set.
*/
public void setDefaultBatchBufferLimit(int defaultBatchBufferLimit) {
this.defaultBatchBufferLimit = defaultBatchBufferLimit;
}
/**
* Set the default batch timeout - used to send a batch if no messages arrive during this time. Only applies if
* batching is enabled and the bus supports this property.
* @param defaultBatchTimeout the defaultBatchTimeout to set.
*/
public void setDefaultBatchTimeout(long defaultBatchTimeout) {
this.defaultBatchTimeout = defaultBatchTimeout;
}
/**
* Set whether compression will be used by producers, by default.
* @param defaultCompress 'true' to use compression.
*/
public void setDefaultCompress(boolean defaultCompress) {
this.defaultCompress = defaultCompress;
}
/**
* Set whether subscriptions to taps/topics are durable.
* @param defaultDurableSubscription true for durable (default false).
*/
public void setDefaultDurableSubscription(boolean defaultDurableSubscription) {
this.defaultDurableSubscription = defaultDurableSubscription;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(applicationContext, "The 'applicationContext' property cannot be null");
if (this.evaluationContext == null) {
this.evaluationContext = IntegrationContextUtils.getEvaluationContext(getBeanFactory());
}
onInit();
}
protected void onInit() {
}
/**
* Dynamically create a producer for the named channel.
* @param name The name.
* @param properties The properties.
* @return The channel.
*/
@Override
public MessageChannel bindDynamicProducer(String name, Properties properties) {
return doBindDynamicProducer(name, name, properties);
}
/**
* Create a producer for the named channel and bind it to the bus. Synchronized to avoid creating multiple
* instances.
* @param name The name.
* @param channelName The name of the channel to be created, and registered as bean.
* @param properties The properties.
* @return The channel.
*/
protected synchronized MessageChannel doBindDynamicProducer(String name, String channelName,
Properties properties) {
MessageChannel channel = this.directChannelProvider.lookupSharedChannel(channelName);
if (channel == null) {
try {
channel = this.directChannelProvider.createAndRegisterChannel(channelName);
bindProducer(name, channel, properties);
}
catch (RuntimeException e) {
destroyCreatedChannel(channelName, channel);
throw new MessageBusException(
"Failed to bind dynamic channel '" + name + "' with properties " + properties, e);
}
}
return channel;
}
/**
* Dynamically create a producer for the named channel. Note: even though it's pub/sub, we still use a direct
* channel. It will be bridged to a pub/sub channel in the local bus and bound to an appropriate element for other
* buses.
* @param name The name.
* @param properties The properties.
* @return The channel.
*/
@Override
public MessageChannel bindDynamicPubSubProducer(String name, Properties properties) {
return doBindDynamicPubSubProducer(name, name, properties);
}
/**
* Create a producer for the named channel and bind it to the bus. Synchronized to avoid creating multiple
* instances.
* @param name The name.
* @param channelName The name of the channel to be created, and registered as bean.
* @param properties The properties.
* @return The channel.
*/
protected synchronized MessageChannel doBindDynamicPubSubProducer(String name, String channelName,
Properties properties) {
MessageChannel channel = this.directChannelProvider.lookupSharedChannel(channelName);
if (channel == null) {
try {
channel = this.directChannelProvider.createAndRegisterChannel(channelName);
bindPubSubProducer(name, channel, properties);
}
catch (RuntimeException e) {
destroyCreatedChannel(channelName, channel);
throw new MessageBusException(
"Failed to bind dynamic channel '" + name + "' with properties " + properties, e);
}
}
return channel;
}
private void destroyCreatedChannel(String name, MessageChannel channel) {
BeanFactory beanFactory = this.applicationContext.getBeanFactory();
if (beanFactory.containsBean(name)) {
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory).destroySingleton(name);
}
}
}
@Override
public void unbindConsumers(String name) {
deleteBindings("inbound." + name);
}
@Override
public void unbindProducers(String name) {
deleteBindings("outbound." + name);
}
@Override
public void unbindConsumer(String name, MessageChannel channel) {
deleteBinding("inbound." + name, channel);
}
@Override
public void unbindProducer(String name, MessageChannel channel) {
deleteBinding("outbound." + name, channel);
}
@Override
public boolean isCapable(Capability capability) {
return false;
}
protected void addBinding(Binding binding) {
this.bindings.add(binding);
}
protected void deleteBindings(String name) {
Assert.hasText(name, "a valid name is required to remove bindings");
List<Binding> bindingsToRemove = new ArrayList<Binding>();
synchronized (this.bindings) {
Iterator<Binding> iterator = this.bindings.iterator();
while (iterator.hasNext()) {
Binding binding = iterator.next();
if (binding.getEndpoint().getComponentName().equals(name)) {
bindingsToRemove.add(binding);
}
}
for (Binding binding : bindingsToRemove) {
doDeleteBinding(binding);
}
}
}
protected void deleteBinding(String name, MessageChannel channel) {
Assert.hasText(name, "a valid name is required to remove a binding");
Assert.notNull(channel, "a valid channel is required to remove a binding");
Binding bindingToRemove = null;
synchronized (this.bindings) {
Iterator<Binding> iterator = this.bindings.iterator();
while (iterator.hasNext()) {
Binding binding = iterator.next();
if (binding.getChannel().equals(channel) &&
binding.getEndpoint().getComponentName().equals(name)) {
bindingToRemove = binding;
break;
}
}
if (bindingToRemove != null) {
doDeleteBinding(bindingToRemove);
}
}
}
private void doDeleteBinding(Binding binding) {
if (Binding.CONSUMER.equals(binding.getType())) {
/*
* Revert the direct binding before stopping the consumer; the module
* outputChannel will temporarily have 2 subscribers.
*/
revertDirectBindingIfNecessary(binding);
}
binding.stop();
this.bindings.remove(binding);
}
protected void stopBindings() {
for (Lifecycle bean : this.bindings) {
try {
bean.stop();
}
catch (Exception e) {
if (logger.isWarnEnabled()) {
logger.warn("failed to stop adapter", e);
}
}
}
}
protected final MessageValues serializePayloadIfNecessary(Message<?> message) {
Object originalPayload = message.getPayload();
Object originalContentType = message.getHeaders().get(MessageHeaders.CONTENT_TYPE);
//Pass content type as String since some transport adapters will exclude CONTENT_TYPE Header otherwise
Object contentType = JavaClassMimeTypeConversion.mimeTypeFromObject(originalPayload).toString();
Object payload = serializePayloadIfNecessary(originalPayload);
MessageValues messageValues = new MessageValues(message);
messageValues.setPayload(payload);
messageValues.put(MessageHeaders.CONTENT_TYPE, contentType);
if (originalContentType != null) {
messageValues.put(XdHeaders.XD_ORIGINAL_CONTENT_TYPE, originalContentType);
}
return messageValues;
}
private byte[] serializePayloadIfNecessary(Object originalPayload) {
if (originalPayload instanceof byte[]) {
return (byte[]) originalPayload;
}
else {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
try {
if (originalPayload instanceof String) {
return ((String) originalPayload).getBytes("UTF-8");
}
this.codec.encode(originalPayload, bos);
return bos.toByteArray();
}
catch (IOException e) {
throw new SerializationException("unable to serialize payload ["
+ originalPayload.getClass().getName() + "]", e);
}
}
}
protected final MessageValues deserializePayloadIfNecessary(Message<?> message) {
return deserializePayloadIfNecessary(new MessageValues(message));
}
protected final MessageValues deserializePayloadIfNecessary(MessageValues message) {
MessageValues messageToSend = message;
Object originalPayload = message.getPayload();
MimeType contentType = contentTypeResolver.resolve(messageToSend);
Object payload = deserializePayload(originalPayload, contentType);
if (payload != null) {
messageToSend.setPayload(payload);
Object originalContentType = messageToSend.get(XdHeaders.XD_ORIGINAL_CONTENT_TYPE);
messageToSend.put(MessageHeaders.CONTENT_TYPE, originalContentType);
messageToSend.put(XdHeaders.XD_ORIGINAL_CONTENT_TYPE, null);
}
return messageToSend;
}
private Object deserializePayload(Object payload, MimeType contentType) {
if (payload instanceof byte[]) {
if (contentType == null || APPLICATION_OCTET_STREAM.equals(contentType)) {
return payload;
}
else {
return deserializePayload((byte[]) payload, contentType);
}
}
return payload;
}
private Object deserializePayload(byte[] bytes, MimeType contentType) {
if (TEXT_PLAIN.equals(contentType)) {
try {
return new String(bytes, "UTF-8");
}
catch (UnsupportedEncodingException e) {
throw new SerializationException("unable to deserialize [java.lang.String]. Encoding not supported.", e);
}
}
else {
String className = JavaClassMimeTypeConversion.classNameFromMimeType(contentType);
try {
// Cache types to avoid unnecessary ClassUtils.forName calls.
Class<?> targetType = payloadTypeCache.get(className);
if (targetType == null) {
targetType = ClassUtils.forName(className, null);
payloadTypeCache.put(className, targetType);
}
return codec.decode(bytes, targetType);
} catch (ClassNotFoundException e) {
throw new SerializationException("unable to deserialize [" + className + "]. Class not found.", e);//NOSONAR
} catch (IOException e) {
throw new SerializationException("unable to deserialize [" + className + "]", e);
}
}
}
/**
* Determine the partition to which to send this message. If a partition key extractor class is provided, it is
* invoked to determine the key. Otherwise, the partition key expression is evaluated to obtain the key value. If a
* partition selector class is provided, it will be invoked to determine the partition. Otherwise, if the partition
* expression is not null, it is evaluated against the key and is expected to return an integer to which the modulo
* function will be applied, using the partitionCount as the divisor. If no partition expression is provided, the
* key will be passed to the bus partition strategy along with the partitionCount. The default partition strategy
* uses {@code key.hashCode()}, and the result will be the mod of that value.
* @param message the message.
* @param meta the partitioning metadata.
* @return the partition.
*/
protected int determinePartition(Message<?> message, PartitioningMetadata meta) {
Object key = null;
if (StringUtils.hasText(meta.partitionKeyExtractorClass)) {
key = invokeExtractor(meta.partitionKeyExtractorClass, message);
}
else if (meta.partitionKeyExpression != null) {
key = meta.partitionKeyExpression.getValue(this.evaluationContext, message);
}
Assert.notNull(key, "Partition key cannot be null");
int partition;
if (StringUtils.hasText(meta.partitionSelectorClass)) {
partition = invokePartitionSelector(meta.partitionSelectorClass, key, meta.partitionCount);
}
else if (meta.partitionSelectorExpression != null) {
partition = meta.partitionSelectorExpression.getValue(this.evaluationContext, key, Integer.class);
}
else {
partition = this.partitionSelector.selectPartition(key, meta.partitionCount);
}
partition = partition % meta.partitionCount;
if (partition < 0) { // protection in case a user selector returns a negative.
partition = Math.abs(partition);
}
return partition;
}
private Object invokeExtractor(String partitionKeyExtractorClassName, Message<?> message) {
if (this.applicationContext.containsBean(partitionKeyExtractorClassName)) {
return this.applicationContext.getBean(partitionKeyExtractorClassName, PartitionKeyExtractorStrategy.class)
.extractKey(message);
}
Class<?> clazz;
try {
clazz = ClassUtils.forName(partitionKeyExtractorClassName, this.applicationContext.getClassLoader());
}
catch (Exception e) {
logger.error("Failed to load key extractor", e);
throw new MessageBusException("Failed to load key extractor: " + partitionKeyExtractorClassName, e);
}
try {
Object extractor = clazz.newInstance();
Assert.isInstanceOf(PartitionKeyExtractorStrategy.class, extractor);
this.applicationContext.getBeanFactory().registerSingleton(partitionKeyExtractorClassName, extractor);
this.applicationContext.getBeanFactory().initializeBean(extractor, partitionKeyExtractorClassName);
return ((PartitionKeyExtractorStrategy) extractor).extractKey(message);
}
catch (Exception e) {
logger.error("Failed to instantiate key extractor", e);
throw new MessageBusException("Failed to instantiate key extractor: " + partitionKeyExtractorClassName, e);
}
}
private int invokePartitionSelector(String partitionSelectorClassName, Object key, int partitionCount) {
if (this.applicationContext.containsBean(partitionSelectorClassName)) {
return this.applicationContext.getBean(partitionSelectorClassName, PartitionSelectorStrategy.class)
.selectPartition(key, partitionCount);
}
Class<?> clazz;
try {
clazz = ClassUtils.forName(partitionSelectorClassName, this.applicationContext.getClassLoader());
}
catch (Exception e) {
logger.error("Failed to load partition selector", e);
throw new MessageBusException("Failed to load partition selector: " + partitionSelectorClassName, e);
}
try {
Object extractor = clazz.newInstance();
Assert.isInstanceOf(PartitionKeyExtractorStrategy.class, extractor);
this.applicationContext.getBeanFactory().registerSingleton(partitionSelectorClassName, extractor);
this.applicationContext.getBeanFactory().initializeBean(extractor, partitionSelectorClassName);
return ((PartitionSelectorStrategy) extractor).selectPartition(key, partitionCount);
}
catch (Exception e) {
logger.error("Failed to instantiate partition selector", e);
throw new MessageBusException("Failed to instantiate partition selector: " + partitionSelectorClassName,
e);
}
}
/**
* Validate the provided deployment properties for the consumer against those supported by this bus implementation.
* The consumer is that part of the bus that consumes messages from the underlying infrastructure and sends them to
* the next module. Consumer properties are used to configure the consumer.
* @param name The name.
* @param properties The properties.
* @param supported The supported properties.
*/
protected void validateConsumerProperties(String name, Properties properties, Set<Object> supported) {
if (properties != null) {
validateProperties(name, properties, supported, "consumer");
}
}
/**
* Validate the provided deployment properties for the producer against those supported by this bus implementation.
* When a module sends a message to the bus, the producer uses these properties while sending it to the underlying
* infrastructure.
* @param name The name.
* @param properties The properties.
* @param supported The supported properties.
*/
protected void validateProducerProperties(String name, Properties properties, Set<Object> supported) {
if (properties != null) {
validateProperties(name, properties, supported, "producer");
validatePartitioning(name, properties);
}
}
private void validateProperties(String name, Properties properties, Set<Object> supported, String type) {
StringBuilder builder = new StringBuilder();
int errors = 0;
for (Entry<Object, Object> entry : properties.entrySet()) {
if (!supported.contains(entry.getKey())) {
builder.append(entry.getKey()).append(",");
errors++;
}
}
if (errors > 0) {
throw new IllegalArgumentException(getClass().getSimpleName() + " does not support "
+ type
+ " propert"
+ (errors == 1 ? "y: " : "ies: ")
+ builder.substring(0, builder.length() - 1)
+ " for " + name + ".");
}
}
private void validatePartitioning(String name, Properties properties) {
if (!isCapable(Capability.NATIVE_PARTITIONING)
&& (StringUtils.hasText(properties.getProperty(BusProperties.PARTITION_KEY_EXPRESSION))
|| StringUtils.hasText(properties.getProperty(BusProperties.PARTITION_KEY_EXTRACTOR_CLASS)))) {
String nextModuleCount = properties.getProperty(BusProperties.NEXT_MODULE_COUNT);
Assert.hasText(nextModuleCount,
String.format(getClass().getSimpleName() + " requires partitioned data to be sent to a module "
+ "having 'count' > 1 for '%s'", name));
try {
Assert.isTrue(Integer.parseInt(nextModuleCount) > 1,
String.format(getClass().getSimpleName() + " requires that module '%s' sends partitioned data to a"
+ " module having 'count' > 1", name));
}
catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format("Property '%s' for " +
"module '%s' does not contain a valid integer, current value is '%s'",
BusProperties.NEXT_MODULE_COUNT, name, nextModuleCount));
}
}
}
protected String buildPartitionRoutingExpression(String expressionRoot) {
return "'" + expressionRoot + "-' + headers['" + PARTITION_HEADER + "']";
}
/**
* Create and configure a retry template if the consumer 'maxAttempts' property is set.
* @param properties The properties.
* @return The retry template, or null if retry is not enabled.
*/
protected RetryTemplate buildRetryTemplateIfRetryEnabled(AbstractBusPropertiesAccessor properties) {
int maxAttempts = properties.getMaxAttempts(this.defaultMaxAttempts);
if (maxAttempts > 1) {
RetryTemplate template = new RetryTemplate();
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(maxAttempts);
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(properties.getBackOffInitialInterval(this.defaultBackOffInitialInterval));
backOffPolicy.setMultiplier(properties.getBackOffMultiplier(this.defaultBackOffMultiplier));
backOffPolicy.setMaxInterval(properties.getBackOffMaxInterval(this.defaultBackOffMaxInterval));
template.setRetryPolicy(retryPolicy);
template.setBackOffPolicy(backOffPolicy);
return template;
}
else {
return null;
}
}
protected boolean isNamedChannel(String name) {
return name.startsWith(PUBSUB_NAMED_CHANNEL_TYPE_PREFIX) || name.startsWith(P2P_NAMED_CHANNEL_TYPE_PREFIX)
|| name.startsWith(JOB_CHANNEL_TYPE_PREFIX);
}
/**
* Attempt to create a direct binding (avoiding the bus) if the consumer is local. Named channel producers are not
* bound directly.
* @param name The name.
* @param moduleOutputChannel The channel to bind.
* @param properties The producer properties.
* @return true if the producer is bound.
*/
protected boolean bindNewProducerDirectlyIfPossible(String name, SubscribableChannel moduleOutputChannel,
AbstractBusPropertiesAccessor properties) {
if (!properties.isDirectBindingAllowed()) {
return false;
}
else if (isNamedChannel(name)) {
return false;
}
else if (this.revertingDirectBinding.get() != null) {
// we're in the process of unbinding a direct binding
this.revertingDirectBinding.remove();
return false;
}
else {
Binding consumerBinding = null;
synchronized (this.bindings) {
for (Binding binding : this.bindings) {
if (binding.getName().equals(name) && Binding.CONSUMER.equals(binding.getType())) {
consumerBinding = binding;
break;
}
}
}
if (consumerBinding == null) {
return false;
}
else {
bindProducerDirectly(name, moduleOutputChannel, consumerBinding.getChannel(), properties);
return true;
}
}
}
private void bindProducerDirectly(String name, SubscribableChannel producerChannel,
MessageChannel consumerChannel, AbstractBusPropertiesAccessor properties) {
DirectHandler handler = new DirectHandler(consumerChannel);
EventDrivenConsumer consumer = new EventDrivenConsumer(producerChannel, handler);
consumer.setBeanFactory(getBeanFactory());
consumer.setBeanName("outbound." + name);
consumer.afterPropertiesSet();
Binding binding = Binding.forDirectProducer(name, producerChannel, consumer, properties);
addBinding(binding);
binding.start();
if (logger.isInfoEnabled()) {
logger.info("Producer bound directly: " + binding);
}
}
/**
* Attempt to bind a producer directly (avoiding the bus) if there is already a local producer. PubSub producers
* cannot be bound directly. Create the direct binding, then unbind the existing bus producer.
* @param name The name.
* @param consumerChannel The channel to bind the producer to.
*/
protected void bindExistingProducerDirectlyIfPossible(String name, MessageChannel consumerChannel) {
if (!isNamedChannel(name)) {
Binding producerBinding = null;
synchronized (this.bindings) {
for (Binding binding : this.bindings) {
if (binding.getName().equals(name) && Binding.PRODUCER.equals(binding.getType())) {
producerBinding = binding;
break;
}
}
if (producerBinding != null && producerBinding.getChannel() instanceof SubscribableChannel) {
AbstractBusPropertiesAccessor properties = producerBinding.getPropertiesAccessor();
if (properties.isDirectBindingAllowed()) {
bindProducerDirectly(name, (SubscribableChannel) producerBinding.getChannel(), consumerChannel,
properties);
producerBinding.stop();
this.bindings.remove(producerBinding);
}
}
}
}
}
private void revertDirectBindingIfNecessary(Binding binding) {
try {
synchronized (this.bindings) { // Not necessary, called while synchronized, but just in case...
Binding directBinding = null;
Iterator<Binding> iterator = this.bindings.iterator();
while (iterator.hasNext()) {
Binding producer = iterator.next();
if (Binding.DIRECT.equals(producer.getType()) && binding.getName().equals(producer.getName())) {
this.revertingDirectBinding.set(Boolean.TRUE);
bindProducer(producer.getName(), producer.getChannel(),
producer.getPropertiesAccessor().getProperties());
directBinding = producer;
break;
}
}
if (directBinding != null) {
directBinding.stop();
this.bindings.remove(directBinding);
if (logger.isInfoEnabled()) {
logger.info("direct binding reverted: " + directBinding);
}
}
}
}
catch (Exception e) {
logger.error("Could not revert direct binding: " + binding, e);
}
}
/**
* Default partition strategy; only works on keys with "real" hash codes, such as String. Caller now always applies
* modulo so no need to do so here.
*/
private class DefaultPartitionSelector implements PartitionSelectorStrategy {
@Override
public int selectPartition(Object key, int partitionCount) {
int hashCode = key.hashCode();
if (hashCode == Integer.MIN_VALUE) {
hashCode = 0;
}
return Math.abs(hashCode);
}
}
protected static class PartitioningMetadata {
private final String partitionKeyExtractorClass;
private final Expression partitionKeyExpression;
private final String partitionSelectorClass;
private final Expression partitionSelectorExpression;
private final int partitionCount;
public PartitioningMetadata(AbstractBusPropertiesAccessor properties, int partitionCount) {
this.partitionCount = partitionCount;
this.partitionKeyExtractorClass = properties.getPartitionKeyExtractorClass();
this.partitionKeyExpression = properties.getPartitionKeyExpression();
this.partitionSelectorClass = properties.getPartitionSelectorClass();
this.partitionSelectorExpression = properties.getPartitionSelectorExpression();
}
public boolean isPartitionedModule() {
return StringUtils.hasText(this.partitionKeyExtractorClass) || this.partitionKeyExpression != null;
}
public int getPartitionCount() {
return partitionCount;
}
}
/**
* Looks up or optionally creates a new channel to use.
* @author Eric Bottard
*/
protected abstract class SharedChannelProvider<T extends MessageChannel> {
private final Class<T> requiredType;
protected SharedChannelProvider(Class<T> clazz) {
this.requiredType = clazz;
}
public synchronized final T lookupOrCreateSharedChannel(String name) {
T channel = lookupSharedChannel(name);
if (channel == null) {
channel = createAndRegisterChannel(name);
}
return channel;
}
@SuppressWarnings("unchecked")
public T createAndRegisterChannel(String name) {
T channel = createSharedChannel(name);
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
beanFactory.registerSingleton(name, channel);
channel = (T) beanFactory.initializeBean(channel, name);
if (logger.isDebugEnabled()) {
logger.debug("Registered channel:" + name);
}
return channel;
}
protected abstract T createSharedChannel(String name);
public T lookupSharedChannel(String name) {
T channel = null;
if (applicationContext.containsBean(name)) {
try {
channel = applicationContext.getBean(name, requiredType);
}
catch (Exception e) {
throw new IllegalArgumentException("bean '" + name
+ "' is already registered but does not match the required type");
}
}
return channel;
}
}
/**
* Handles representing any java class as a {@link MimeType}.
* @author David Turanski
* @see <a href="http://docs.oracle.com/javase/7/docs/api/java/lang/Class.html#getName"/>
*/
abstract static class JavaClassMimeTypeConversion {
public static final MimeType APPLICATION_OCTET_STREAM_MIME_TYPE = MimeType.valueOf(APPLICATION_OCTET_STREAM_VALUE);
public static final MimeType TEXT_PLAIN_MIME_TYPE = MimeType.valueOf(TEXT_PLAIN_VALUE);
private static ConcurrentMap<String, MimeType> mimeTypesCache = new ConcurrentHashMap<>();
static MimeType mimeTypeFromObject(Object obj) {
Assert.notNull(obj, "object cannot be null.");
if (obj instanceof byte[]) {
return APPLICATION_OCTET_STREAM_MIME_TYPE;
}
if (obj instanceof String) {
return TEXT_PLAIN_MIME_TYPE;
}
String className = obj.getClass().getName();
MimeType mimeType = mimeTypesCache.get(className);
if (mimeType == null) {
String modifiedClassName = className;
if (obj.getClass().isArray()) {
// Need to remove trailing ';' for an object array, e.g. "[Ljava.lang.String;" or multi-dimensional
// "[[[Ljava.lang.String;"
if (modifiedClassName.endsWith(";")) {
modifiedClassName = modifiedClassName.substring(0, modifiedClassName.length() - 1);
}
// Wrap in quotes to handle the illegal '[' character
modifiedClassName = "\"" + modifiedClassName + "\"";
}
mimeType = MimeType.valueOf("application/x-java-object;type=" + modifiedClassName);
mimeTypesCache.put(className, mimeType);
}
return mimeType;
}
static String classNameFromMimeType(MimeType mimeType) {
Assert.notNull(mimeType, "mimeType cannot be null.");
String className = mimeType.getParameter("type");
if (className == null) {
return null;
}
//unwrap quotes if any
className = className.replace("\"", "");
// restore trailing ';'
if (className.contains("[L")) {
className += ";";
}
return className;
}
}
public static class SetBuilder {
private final Set<Object> set = new HashSet<Object>();
public SetBuilder add(Object o) {
this.set.add(o);
return this;
}
public SetBuilder addAll(Set<Object> set) {
this.set.addAll(set);
return this;
}
public Set<Object> build() {
return this.set;
}
}
public static class DirectHandler implements MessageHandler {
private final MessageChannel outputChannel;
public DirectHandler(MessageChannel outputChannel) {
this.outputChannel = outputChannel;
}
@Override
public void handleMessage(Message<?> message) throws MessagingException {
this.outputChannel.send(message);
}
}
/**
* Perform manual acknowledgement based on the metadata stored in message bus.
*/
public void doManualAck(LinkedList<MessageHeaders> messageHeaders) {
}
}