/*
* Copyright 2015-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.cloud.stream.binding;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.cloud.stream.binder.BinderException;
import org.springframework.cloud.stream.binder.BinderHeaders;
import org.springframework.cloud.stream.binder.PartitionHandler;
import org.springframework.cloud.stream.binder.PartitionKeyExtractorStrategy;
import org.springframework.cloud.stream.binder.PartitionSelectorStrategy;
import org.springframework.cloud.stream.binder.ProducerProperties;
import org.springframework.cloud.stream.config.BindingProperties;
import org.springframework.cloud.stream.config.BindingServiceProperties;
import org.springframework.cloud.stream.converter.CompositeMessageConverterFactory;
import org.springframework.cloud.stream.converter.MessageConverterUtils;
import org.springframework.integration.channel.AbstractMessageChannel;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.integration.support.MessageBuilderFactory;
import org.springframework.integration.support.MutableMessageBuilderFactory;
import org.springframework.integration.support.MutableMessageHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageChannel;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.converter.AbstractMessageConverter;
import org.springframework.messaging.converter.MessageConversionException;
import org.springframework.messaging.converter.MessageConverter;
import org.springframework.messaging.support.ChannelInterceptorAdapter;
import org.springframework.tuple.Tuple;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
import org.springframework.util.StringUtils;
/**
* A {@link MessageChannelConfigurer} that sets data types and message converters based on {@link
* org.springframework.cloud.stream.config.BindingProperties#contentType}. Also adds a
* {@link org.springframework.messaging.support.ChannelInterceptor} to
* the message channel to set the `ContentType` header for the message (if not already set) based on the `ContentType`
* binding property of the channel.
*
* @author Ilayaperumal Gopinathan
* @author Marius Bogoevici
* @author Maxim Kirilov
* @author Gary Russell
*/
public class MessageConverterConfigurer implements MessageChannelConfigurer, BeanFactoryAware, InitializingBean {
private final MessageBuilderFactory messageBuilderFactory = new MutableMessageBuilderFactory();
private ConfigurableListableBeanFactory beanFactory;
private final CompositeMessageConverterFactory compositeMessageConverterFactory;
private final BindingServiceProperties bindingServiceProperties;
public MessageConverterConfigurer(BindingServiceProperties bindingServiceProperties,
CompositeMessageConverterFactory compositeMessageConverterFactory) {
Assert.notNull(compositeMessageConverterFactory, "The message converter factory cannot be null");
this.bindingServiceProperties = bindingServiceProperties;
this.compositeMessageConverterFactory = compositeMessageConverterFactory;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}
@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(this.beanFactory, "Bean factory cannot be empty");
}
@Override
public void configureInputChannel(MessageChannel messageChannel, String channelName) {
configureMessageChannel(messageChannel, channelName, true);
}
@Override
public void configureOutputChannel(MessageChannel messageChannel, String channelName) {
configureMessageChannel(messageChannel, channelName, false);
}
/**
* Setup data-type and message converters for the given message channel.
*
* @param channel message channel to set the data-type and message converters
* @param channelName the channel name
*/
private void configureMessageChannel(MessageChannel channel, String channelName, boolean input) {
Assert.isAssignable(AbstractMessageChannel.class, channel.getClass());
AbstractMessageChannel messageChannel = (AbstractMessageChannel) channel;
final BindingProperties bindingProperties = this.bindingServiceProperties.getBindingProperties(
channelName);
final String contentType = bindingProperties.getContentType();
ProducerProperties producerProperties = bindingProperties.getProducer();
if (!input && producerProperties != null && producerProperties.isPartitioned()) {
messageChannel.addInterceptor(new PartitioningInterceptor(bindingProperties,
getPartitionKeyExtractorStrategy(producerProperties), getPartitionSelectorStrategy(producerProperties)));
}
if (StringUtils.hasText(contentType)) {
messageChannel.addInterceptor(new ContentTypeConvertingInterceptor(contentType, input));
}
}
private PartitionKeyExtractorStrategy getPartitionKeyExtractorStrategy(ProducerProperties producerProperties) {
if (producerProperties.getPartitionKeyExtractorClass() != null) {
return getBean(
producerProperties.getPartitionKeyExtractorClass().getName(),
PartitionKeyExtractorStrategy.class);
}
return null;
}
private PartitionSelectorStrategy getPartitionSelectorStrategy(ProducerProperties producerProperties) {
if (producerProperties.getPartitionSelectorClass() != null) {
return getBean(
producerProperties.getPartitionSelectorClass().getName(),
PartitionSelectorStrategy.class);
}
return new DefaultPartitionSelector();
}
@SuppressWarnings("unchecked")
private <T> T getBean(String className, Class<T> type) {
if (this.beanFactory.containsBean(className)) {
return this.beanFactory.getBean(className, type);
}
else {
synchronized (this) {
T bean;
Class<?> clazz;
try {
clazz = ClassUtils.forName(className, this.beanFactory.getBeanClassLoader());
}
catch (Exception e) {
throw new BinderException("Failed to load class: " + className, e);
}
try {
bean = (T) clazz.newInstance();
Assert.isInstanceOf(type, bean);
this.beanFactory.registerSingleton(className, bean);
this.beanFactory.initializeBean(bean, className);
}
catch (Exception e) {
throw new BinderException("Failed to instantiate class: " + className, e);
}
return bean;
}
}
}
private final class ContentTypeConvertingInterceptor extends ChannelInterceptorAdapter {
private final String contentType;
private final MimeType mimeType;
private final boolean input;
private final Class<?> klazz;
private final MessageConverter messageConverter;
private final boolean provideHint;
private ContentTypeConvertingInterceptor(String contentType, boolean input) {
this.contentType = contentType;
this.mimeType = MessageConverterUtils.getMimeType(contentType);
this.input = input;
if (MessageConverterUtils.X_JAVA_OBJECT.includes(this.mimeType)) {
this.klazz =
MessageConverterUtils
.getJavaTypeForJavaObjectContentType(this.mimeType);
}
else if (this.mimeType.equals(MessageConverterUtils.X_SPRING_TUPLE)) {
this.klazz = Tuple.class;
}
else if (this.mimeType.getType().equals("text") || this.mimeType.getSubtype().equals(
"json") || this.mimeType.getSubtype().equals("xml")) {
this.klazz = String.class;
}
else {
this.klazz = byte[].class;
}
this.messageConverter =
MessageConverterConfigurer.this.compositeMessageConverterFactory
.getMessageConverterForType(this.mimeType);
this.provideHint = this.messageConverter instanceof AbstractMessageConverter;
}
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
Message<?> sentMessage = null;
if (this.klazz.isAssignableFrom(message.getPayload().getClass())) {
Object contentTypeFromMessage = message.getHeaders().get(MessageHeaders.CONTENT_TYPE);
if (contentTypeFromMessage == null) {
sentMessage = MessageConverterConfigurer.this.messageBuilderFactory
.fromMessage(message)
.setHeader(MessageHeaders.CONTENT_TYPE, this.contentType)
.build();
}
else {
sentMessage = message;
}
}
else {
Object converted;
if (this.input) {
if (this.provideHint) {
converted = ((AbstractMessageConverter) this.messageConverter).fromMessage(message, this.klazz,
this.mimeType);
}
else {
converted = this.messageConverter.fromMessage(message, this.klazz);
}
}
else {
if (this.provideHint) {
converted = ((AbstractMessageConverter) this.messageConverter).toMessage(message.getPayload(),
new MutableMessageHeaders(message.getHeaders()), this.mimeType);
}
else {
converted = this.messageConverter.toMessage(message.getPayload(),
new MutableMessageHeaders(message.getHeaders()));
}
}
if (converted instanceof Message) {
sentMessage = (Message<?>) converted;
}
else {
sentMessage = MessageConverterConfigurer.this.messageBuilderFactory.withPayload(converted)
.copyHeaders(message.getHeaders()).setHeaderIfAbsent(MessageHeaders.CONTENT_TYPE,
this.mimeType).build();
}
}
if (sentMessage == null) {
throw new MessageConversionException("Cannot convert " + message + " to " + this.contentType);
}
return sentMessage;
}
}
protected final class PartitioningInterceptor extends ChannelInterceptorAdapter {
private final BindingProperties bindingProperties;
private final PartitionHandler partitionHandler;
PartitioningInterceptor(BindingProperties bindingProperties,
PartitionKeyExtractorStrategy partitionKeyExtractorStrategy,
PartitionSelectorStrategy partitionSelectorStrategy) {
this.bindingProperties = bindingProperties;
this.partitionHandler = new PartitionHandler(ExpressionUtils.createStandardEvaluationContext(MessageConverterConfigurer.this.beanFactory),
this.bindingProperties.getProducer(), partitionKeyExtractorStrategy, partitionSelectorStrategy);
}
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
if (!message.getHeaders().containsKey(BinderHeaders.PARTITION_OVERRIDE)) {
int partition = this.partitionHandler.determinePartition(message);
return MessageConverterConfigurer.this.messageBuilderFactory
.fromMessage(message)
.setHeader(BinderHeaders.PARTITION_HEADER, partition)
.build();
}
else {
return MessageConverterConfigurer.this.messageBuilderFactory
.fromMessage(message)
.setHeader(BinderHeaders.PARTITION_HEADER,
message.getHeaders().get(BinderHeaders.PARTITION_OVERRIDE))
.removeHeader(BinderHeaders.PARTITION_OVERRIDE)
.build();
}
}
}
/**
* 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 static 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);
}
}
}