org.springframework.integration.redis.outbound.RedisCollectionPopulatingMessageHandler.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.integration.redis.outbound.RedisCollectionPopulatingMessageHandler.java

Source

/*
 * Copyright 2007-2012 the original author or authors
 *
 *     Licensed under the Apache License, Version 2.0 (the "License");
 *     you may not use this file except in compliance with the License.
 *     You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 *     Unless required by applicable law or agreed to in writing, software
 *     distributed under the License is distributed on an "AS IS" BASIS,
 *     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *     See the License for the specific language governing permissions and
 *     limitations under the License.
 */
package org.springframework.integration.redis.outbound;

import java.util.Collection;
import java.util.Map;
import java.util.Properties;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.BoundZSetOperations;
import org.springframework.data.redis.core.RedisConnectionUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.data.redis.support.collections.RedisCollectionFactoryBean;
import org.springframework.data.redis.support.collections.RedisCollectionFactoryBean.CollectionType;
import org.springframework.data.redis.support.collections.RedisList;
import org.springframework.data.redis.support.collections.RedisMap;
import org.springframework.data.redis.support.collections.RedisProperties;
import org.springframework.data.redis.support.collections.RedisSet;
import org.springframework.data.redis.support.collections.RedisStore;
import org.springframework.data.redis.support.collections.RedisZSet;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.integration.Message;
import org.springframework.integration.MessageHandlingException;
import org.springframework.integration.core.MessageHandler;
import org.springframework.integration.handler.AbstractMessageHandler;
import org.springframework.integration.redis.support.RedisHeaders;
import org.springframework.integration.util.ExpressionUtils;
import org.springframework.util.Assert;
import org.springframework.util.NumberUtils;

/**
 * Implementation of {@link MessageHandler} which writes Message data into a Redis store
 * identified by a key {@link String}.
 * It supports the collection types identified by {@link CollectionType}.
 *
 * It also supports batch updates and single item entry.
 *
 * "Batch updates" means that the payload of the Message may be a Map or Collection.
 * With such a payload, individual items from it are added to the corresponding Redis store.
 * See {@link #handleMessage(Message)} for more details.
 *
 * You can also choose to persist such a payload as a single item if the {@link #extractPayloadElements}
 * property is set to false (default is true).
 *
 * @author Oleg Zhurakousky
 * @author Gary Russell
 * @since 2.2
 */
public class RedisCollectionPopulatingMessageHandler extends AbstractMessageHandler {

    private final Log logger = LogFactory.getLog(this.getClass());

    private volatile StandardEvaluationContext evaluationContext;

    private volatile Expression keyExpression = new SpelExpressionParser()
            .parseExpression("headers." + RedisHeaders.KEY);

    private volatile Expression mapKeyExpression = new SpelExpressionParser()
            .parseExpression("headers." + RedisHeaders.MAP_KEY);

    private volatile boolean mapKeyExpressionExplicitlySet;

    private final RedisTemplate<String, ?> redisTemplate;

    private volatile CollectionType collectionType = CollectionType.LIST;

    private volatile boolean extractPayloadElements = true;

    /**
     * Will construct this instance using fully created and initialized instance of
     * provided {@link RedisTemplate}
     *
     * The default expression 'headers.{@link RedisHeaders#KEY}'
     * will be used.
     * @param redisTemplate
     */
    public RedisCollectionPopulatingMessageHandler(RedisTemplate<String, ?> redisTemplate) {
        this(redisTemplate, null);
    }

    /**
     * Will construct this instance using
     * provided {@link RedisTemplate} and {@link #keyExpression}. The RedisTemplate must
     * be fully initialized.
     * If {@link #keyExpression} is null, the default expression 'headers.{@link RedisHeaders#KEY}'
     * will be used.
     *
     * @param redisTemplate
     * @param keyExpression
     */
    public RedisCollectionPopulatingMessageHandler(RedisTemplate<String, ?> redisTemplate,
            Expression keyExpression) {
        Assert.notNull(redisTemplate, "'redisTemplate' must not be null");
        this.redisTemplate = redisTemplate;
        if (keyExpression != null) {
            this.keyExpression = keyExpression;
        }
    }

    /**
     * Will construct this instance using the provided {@link RedisConnectionFactory}.
     * It will create an instance of {@link RedisTemplate}, initializing it with a
     * {@link StringRedisSerializer} for the keySerializer and a {@link JdkSerializationRedisSerializer}
     * for each of valueSerializer, hasKeySerializer, and hashValueSerializer.
     *
     * The default expression 'headers.{@link RedisHeaders#KEY}'
     * will be used.
     * @param connectionFactory
     */
    public RedisCollectionPopulatingMessageHandler(RedisConnectionFactory connectionFactory) {
        this(connectionFactory, null);
    }

    /**
     * Will construct this instance using the provided {@link RedisConnectionFactory} and {@link #keyExpression}
     * It will create an instance of {@link RedisTemplate} initializing it with a
     * {@link StringRedisSerializer} for the keySerializer and a {@link JdkSerializationRedisSerializer}
     * for each of valueSerializer, hasKeySerializer, and hashValueSerializer.
     *
     * If {@link #keyExpression} is null, the default expression 'headers.{@link RedisHeaders#KEY}'
     * will be used.
     *
     * @param connectionFactory
     * @param keyExpression
     */
    public RedisCollectionPopulatingMessageHandler(RedisConnectionFactory connectionFactory,
            Expression keyExpression) {
        Assert.notNull(connectionFactory, "'connectionFactory' must not be null");

        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setHashKeySerializer(new JdkSerializationRedisSerializer());
        redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());

        this.redisTemplate = redisTemplate;
        if (keyExpression != null) {
            this.keyExpression = keyExpression;
        }
    }

    /**
     * Sets the collection type for this handler as per {@link CollectionType}
     *
     * @param collectionType
     */
    public void setCollectionType(CollectionType collectionType) {
        this.collectionType = collectionType;
    }

    /**
     * Sets the flag signifying that if the payload is a "multivalue" (i.e., Collection or Map),
     * it should be saved using addAll/putAll semantics. Default is 'true'.
     * If set to 'false' the payload will be saved as a single entry regardless of its type.
     * If the payload is not an instance of "multivalue" (i.e., Collection or Map)
     * the value of this attribute is meaningless as the payload will always be
     * stored as a single entry.
     *
     * @see #setExtractPayloadElements(boolean)
     *
     * @param extractPayloadElements
     */
    public void setExtractPayloadElements(boolean extractPayloadElements) {
        this.extractPayloadElements = extractPayloadElements;
    }

    /**
     * Sets the expression used as the key for Map and Properties entries.
     * Default is 'headers.{@link RedisHeaders#MAP_KEY}'
     * @param mapKeyExpression
     */
    public void setMapKeyExpression(Expression mapKeyExpression) {
        Assert.notNull(mapKeyExpression, "'mapKeyExpression' must not be null");
        this.mapKeyExpression = mapKeyExpression;
        this.mapKeyExpressionExplicitlySet = true;
    }

    @Override
    public String getComponentType() {
        return "redis:store-outbound-channel-adapter";
    }

    @Override
    protected void onInit() throws Exception {
        if (this.getBeanFactory() != null) {
            this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(this.getBeanFactory());
        } else {
            this.evaluationContext = ExpressionUtils.createStandardEvaluationContext();
        }
        Assert.state(!this.mapKeyExpressionExplicitlySet
                || (this.collectionType == CollectionType.MAP || this.collectionType == CollectionType.PROPERTIES),
                "'mapKeyExpression' can only be set for CollectionType.MAP or CollectionType.PROPERTIES");
    }

    /**
     * Will extract payload from the Message storing it in the collection identified by the
     * {@link #collectionType}. The default CollectinType is LIST.
     * <p/>
     * The rules for storing payload are:
     * <p/>
     * <b>LIST/SET</b>
     * If payload is of type Collection and {@link #extractPayloadElements} is 'true' (default),
     * the payload will be added using the addAll() method. If {@link #extractPayloadElements} is set to 'false' then,
     * regardless of the payload type, the payload will be added using add();
     * <p/>
     * <b>ZSET</b>
     * In addition to rules described for LIST/SET, ZSET allows 'score' information
     * to be provided. The score can be provided using the {@link RedisHeaders#ZSET_SCORE} message header,
     * when the payload is a Collection, or
     * by sending a Map as the payload, where the Map 'key' is the value to be saved and the 'value' is
     * the score assigned to this value.
     * If {@link #extractPayloadElements} is set to 'false' the map will be stored as a single entry.
     * If the 'score' can not be determined, the default value (1) will be used.
     * <p/>
     * <b>MAP/PROPERTIES</b>
     * You can also store a payload of type Map or Properties following the same rules as above.
     * If payload itself needs to be stored as a value of the map/property then the map key must be
     * specified via the mapKeyExpression (default {@link RedisHeaders#MAP_KEY} Message header).
     */
    @SuppressWarnings("unchecked")
    @Override
    protected void handleMessageInternal(Message<?> message) throws Exception {
        String key = this.keyExpression.getValue(this.evaluationContext, message, String.class);

        Assert.hasText(key, "Can not determine a 'key' for a Redis store. The key can be provided via the "
                + "'key' or 'key-expression' attributes.");

        RedisStore store = this.createStoreView(key);

        try {
            if (collectionType == CollectionType.ZSET) {
                this.handleZset((RedisZSet<Object>) store, message);
            } else if (collectionType == CollectionType.SET) {
                this.handleSet((RedisSet<Object>) store, message);
            } else if (collectionType == CollectionType.LIST) {
                this.handleList((RedisList<Object>) store, message);
            } else if (collectionType == CollectionType.MAP) {
                this.handleMap((RedisMap<Object, Object>) store, message);
            } else if (collectionType == CollectionType.PROPERTIES) {
                this.handleProperties((RedisProperties) store, message);
            }
        } catch (Exception e) {
            throw new MessageHandlingException(message, "Failed to store Message data in Redis collection", e);
        }
    }

    @SuppressWarnings("unchecked")
    private void handleZset(RedisZSet<Object> zset, final Message<?> message) throws Exception {
        final Object payload = message.getPayload();

        if (this.extractPayloadElements) {
            final BoundZSetOperations<String, Object> ops = (BoundZSetOperations<String, Object>) this.redisTemplate
                    .boundZSetOps(zset.getKey());

            if ((payload instanceof Map<?, ?> && this.isMapValuesOfTypeNumber((Map<?, ?>) payload))) {
                final Map<Object, Number> pyloadAsMap = (Map<Object, Number>) payload;
                this.processInPipeline(new PipelineCallback() {
                    public void process() {
                        for (Object key : pyloadAsMap.keySet()) {
                            Number d = pyloadAsMap.get(key);
                            ops.add(key, d == null ? determineScore(message)
                                    : NumberUtils.convertNumberToTargetClass(d, Double.class));
                        }
                    }
                });
            } else if (payload instanceof Collection<?>) {
                this.processInPipeline(new PipelineCallback() {
                    public void process() {
                        for (Object object : ((Collection<?>) payload)) {
                            ops.add(object, determineScore(message));
                        }
                    }
                });
            } else {
                this.addToZset(zset, payload, this.determineScore(message));
            }
        } else {
            this.addToZset(zset, payload, this.determineScore(message));
        }
    }

    @SuppressWarnings("unchecked")
    private void handleList(RedisList<Object> list, Message<?> message) {
        Object payload = message.getPayload();
        if (this.extractPayloadElements) {
            if (payload instanceof Collection<?>) {
                list.addAll((Collection<? extends Object>) payload);
            } else {
                list.add(payload);
            }
        } else {
            list.add(payload);
        }
    }

    @SuppressWarnings("unchecked")
    private void handleSet(final RedisSet<Object> set, Message<?> message) {
        final Object payload = message.getPayload();
        if (this.extractPayloadElements && payload instanceof Collection<?>) {
            final BoundSetOperations<String, Object> ops = (BoundSetOperations<String, Object>) this.redisTemplate
                    .boundSetOps(set.getKey());

            this.processInPipeline(new PipelineCallback() {
                public void process() {
                    for (Object object : ((Collection<?>) payload)) {
                        ops.add(object);
                    }
                }
            });
        } else {
            set.add(payload);
        }
    }

    @SuppressWarnings("unchecked")
    private void handleMap(final RedisMap<Object, Object> map, Message<?> message) {
        final Object payload = message.getPayload();
        if (this.extractPayloadElements && payload instanceof Map<?, ?>) {
            this.processInPipeline(new PipelineCallback() {
                public void process() {
                    map.putAll((Map<? extends Object, ? extends Object>) payload);
                }
            });
        } else {
            Object key = this.assertMapEntry(message, false);
            map.put(key, payload);
        }
    }

    private void handleProperties(final RedisProperties properties, Message<?> message) {
        final Object payload = message.getPayload();
        if (this.extractPayloadElements && payload instanceof Properties) {
            this.processInPipeline(new PipelineCallback() {
                public void process() {
                    properties.putAll((Properties) payload);
                }
            });
        } else {
            Object key = this.assertMapEntry(message, true);
            properties.put(key, payload);
        }
    }

    private void processInPipeline(PipelineCallback callback) {
        RedisConnection connection = RedisConnectionUtils.bindConnection(redisTemplate.getConnectionFactory());
        try {
            connection.openPipeline();
            callback.process();
        } finally {
            connection.closePipeline();
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
    }

    private Object assertMapEntry(Message<?> message, boolean property) {
        Object mapKey = this.mapKeyExpression.getValue(this.evaluationContext, message);
        Assert.notNull(mapKey, "Can not determine a map key for the entry. The key is determined by evaluating "
                + "the 'mapKeyExpression' property.");
        Object payload = message.getPayload();
        if (property) {
            Assert.isInstanceOf(String.class, mapKey, "For property, key must be a String");
            Assert.isInstanceOf(String.class, payload, "For property, payload must be a String");
        }
        Assert.isTrue(mapKey != null,
                "Failed to determine the key for the " + "Redis Map entry. Payload is not a Map and '"
                        + RedisHeaders.MAP_KEY + "' header is not provided");
        return mapKey;
    }

    private void addToZset(RedisZSet<Object> zset, Object objectToAdd, Double score) {
        if (score != null) {
            zset.add(objectToAdd, score);
        } else {
            logger.debug("Zset Score could not be determined. Using default score of 1");
            zset.add(objectToAdd);
        }
    }

    private boolean isMapValuesOfTypeNumber(Map<?, ?> map) {
        for (Object value : map.values()) {
            if (!(value instanceof Number)) {
                logger.warn("Failed to extract payload elements because one of its values '" + value
                        + "' is not of type Number");
                return false;
            }
        }
        return true;
    }

    private RedisStore createStoreView(String key) {
        RedisCollectionFactoryBean fb = new RedisCollectionFactoryBean();
        fb.setKey(key);
        fb.setTemplate(this.redisTemplate);
        fb.setType(this.collectionType);
        fb.afterPropertiesSet();
        return fb.getObject();
    }

    private double determineScore(Message<?> message) {
        Object scoreHeader = message.getHeaders().get(RedisHeaders.ZSET_SCORE);
        if (scoreHeader == null) {
            return Double.valueOf(1);
        } else {
            Assert.isInstanceOf(Number.class, scoreHeader,
                    "Header " + RedisHeaders.ZSET_SCORE + " must be a Number");
            Number score = (Number) scoreHeader;
            return Double.valueOf(score.toString());
        }
    }

    private interface PipelineCallback {
        public void process();
    }
}