org.springframework.cloud.stream.binder.AbstractBinder.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.cloud.stream.binder.AbstractBinder.java

Source

/*
 * Copyright 2013-2016 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.binder;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.core.serializer.support.SerializationFailedException;
import org.springframework.expression.EvaluationContext;
import org.springframework.integration.codec.Codec;
import org.springframework.integration.expression.ExpressionUtils;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

/**
 * Base class for {@link Binder} implementations.
 *
 * @author David Turanski
 * @author Gary Russell
 * @author Ilayaperumal Gopinathan
 * @author Mark Fisher
 * @author Marius Bogoevici
 */
public abstract class AbstractBinder<T, C extends ConsumerProperties, P extends ProducerProperties>
        implements ApplicationContextAware, InitializingBean, Binder<T, C, P> {

    /**
     * The delimiter between a group and index when constructing a binder
     * consumer/producer.
     */
    private static final String GROUP_INDEX_DELIMITER = ".";

    protected final Log logger = LogFactory.getLog(getClass());

    private volatile AbstractApplicationContext applicationContext;

    private volatile Codec codec;

    private final StringConvertingContentTypeResolver contentTypeResolver = new StringConvertingContentTypeResolver();

    private volatile EvaluationContext evaluationContext;

    private volatile Map<String, Class<?>> payloadTypeCache = new ConcurrentHashMap<>();

    /**
     * For binder 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 binder 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;
    }

    public void setIntegrationEvaluationContext(EvaluationContext evaluationContext) {
        this.evaluationContext = evaluationContext;
    }

    @Override
    public final void afterPropertiesSet() throws Exception {
        Assert.notNull(this.applicationContext, "The 'applicationContext' property must not be null");
        if (this.evaluationContext == null) {
            this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
        }
        onInit();
    }

    /**
     * Subclasses may implement this method to perform any necessary initialization. It
     * will be invoked from {@link #afterPropertiesSet()} which is itself {@code final}.
     */
    protected void onInit() throws Exception {
        // no-op default
    }

    @Override
    public final Binding<T> bindConsumer(String name, String group, T target, C properties) {
        if (StringUtils.isEmpty(group)) {
            Assert.isTrue(!properties.isPartitioned(),
                    "A consumer group is required for a partitioned subscription");
        }
        return doBindConsumer(name, group, target, properties);
    }

    protected abstract Binding<T> doBindConsumer(String name, String group, T inputTarget, C properties);

    @Override
    public final Binding<T> bindProducer(String name, T outboundBindTarget, P properties) {
        return doBindProducer(name, outboundBindTarget, properties);
    }

    protected abstract Binding<T> doBindProducer(String name, T outboundBindTarget, P properties);

    /**
     * Construct a name comprised of the name and group.
     *
     * @param name  the name.
     * @param group the group.
     * @return the constructed name.
     */
    protected final String groupedName(String name, String group) {
        return name + GROUP_INDEX_DELIMITER + (StringUtils.hasText(group) ? group : "default");
    }

    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, ObjectUtils.nullSafeToString(originalContentType)).toString();
        Object payload = serializePayloadIfNecessary(originalPayload);
        MessageValues messageValues = new MessageValues(message);
        messageValues.setPayload(payload);
        messageValues.put(MessageHeaders.CONTENT_TYPE, contentType);
        if (originalContentType != null && !originalContentType.toString().equals(contentType.toString())) {
            messageValues.put(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE, originalContentType.toString());
        }
        return messageValues;
    }

    protected final 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 SerializationFailedException(
                        "unable to serialize payload [" + originalPayload.getClass().getName() + "]", e);
            }
        }
    }

    protected final MessageValues deserializePayloadIfNecessary(Message<?> message) {
        return deserializePayloadIfNecessary(new MessageValues(message));
    }

    protected final MessageValues deserializePayloadIfNecessary(MessageValues messageValues) {
        Object originalPayload = messageValues.getPayload();
        MimeType contentType = this.contentTypeResolver.resolve(messageValues);
        Object payload = deserializePayload(originalPayload, contentType);
        if (payload != null) {
            messageValues.setPayload(payload);
            Object originalContentType = messageValues.get(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE);
            // Reset content-type only if the original content type is not null (when
            // receiving messages from
            // non-SCSt applications).
            if (originalContentType != null) {
                messageValues.put(MessageHeaders.CONTENT_TYPE, originalContentType);
                messageValues.remove(BinderHeaders.BINDER_ORIGINAL_CONTENT_TYPE);
            }
        }
        return messageValues;
    }

    private Object deserializePayload(Object payload, MimeType contentType) {
        if (payload instanceof byte[]) {
            if (contentType == null || MimeTypeUtils.APPLICATION_OCTET_STREAM.equals(contentType)) {
                return payload;
            } else {
                return deserializePayload((byte[]) payload, contentType);
            }
        }
        return payload;
    }

    private Object deserializePayload(byte[] bytes, MimeType contentType) {
        if ("text".equalsIgnoreCase(contentType.getType()) || MimeTypeUtils.APPLICATION_JSON.equals(contentType)) {
            try {
                return new String(bytes, "UTF-8");
            } catch (UnsupportedEncodingException e) {
                String errorMessage = "unable to deserialize [java.lang.String]. Encoding not supported. "
                        + e.getMessage();
                logger.error(errorMessage);
                throw new SerializationFailedException(errorMessage, e);
            }
        } else {
            String className = JavaClassMimeTypeConversion.classNameFromMimeType(contentType);
            try {
                // Cache types to avoid unnecessary ClassUtils.forName calls.
                Class<?> targetType = this.payloadTypeCache.get(className);
                if (targetType == null) {
                    targetType = ClassUtils.forName(className, null);
                    this.payloadTypeCache.put(className, targetType);
                }
                return this.codec.decode(bytes, targetType);
            } // catch all exceptions that could occur during de-serialization
            catch (Exception e) {
                String errorMessage = "Unable to deserialize [" + className + "] using the contentType ["
                        + contentType + "] " + e.getMessage();
                logger.error(errorMessage);
                throw new SerializationFailedException(errorMessage, e);
            }
        }
    }

    protected String buildPartitionRoutingExpression(String expressionRoot) {
        return "'" + expressionRoot + "-' + headers['" + BinderHeaders.PARTITION_HEADER + "']";
    }

    /**
     * Create and configure a retry template.
     *
     * @param properties The properties.
     * @return The retry template
     */
    public RetryTemplate buildRetryTemplate(ConsumerProperties properties) {
        RetryTemplate template = new RetryTemplate();
        SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
        retryPolicy.setMaxAttempts(properties.getMaxAttempts());
        ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
        backOffPolicy.setInitialInterval(properties.getBackOffInitialInterval());
        backOffPolicy.setMultiplier(properties.getBackOffMultiplier());
        backOffPolicy.setMaxInterval(properties.getBackOffMaxInterval());
        template.setRetryPolicy(retryPolicy);
        template.setBackOffPolicy(backOffPolicy);
        return template;
    }

    /**
     * Handles representing any java class as a {@link MimeType}.
     *
     * @author David Turanski
     * @author Ilayaperumal Gopinathan
     */
    public abstract static class JavaClassMimeTypeConversion {

        private static ConcurrentMap<String, MimeType> mimeTypesCache = new ConcurrentHashMap<>();

        static MimeType mimeTypeFromObject(Object payload, String originalContentType) {
            Assert.notNull(payload, "payload object cannot be null.");
            if (payload instanceof byte[]) {
                return MimeTypeUtils.APPLICATION_OCTET_STREAM;
            }
            if (payload instanceof String) {
                return MimeTypeUtils.APPLICATION_JSON_VALUE.equals(originalContentType)
                        ? MimeTypeUtils.APPLICATION_JSON
                        : MimeTypeUtils.TEXT_PLAIN;
            }
            String className = payload.getClass().getName();
            MimeType mimeType = mimeTypesCache.get(className);
            if (mimeType == null) {
                String modifiedClassName = className;
                if (payload.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;
        }

    }
}