org.springframework.integration.jdbc.store.JdbcChannelMessageStore.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.integration.jdbc.store.JdbcChannelMessageStore.java

Source

/*
 * Copyright 2002-2013 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.jdbc.store;

import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.sql.DataSource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.core.serializer.Deserializer;
import org.springframework.core.serializer.Serializer;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.integration.Message;
import org.springframework.integration.MessageHeaders;
import org.springframework.integration.jdbc.JdbcMessageStore;
import org.springframework.integration.jdbc.store.channel.DerbyChannelMessageStoreQueryProvider;
import org.springframework.integration.jdbc.store.channel.MessageRowMapper;
import org.springframework.integration.jdbc.store.channel.MySqlChannelMessageStoreQueryProvider;
import org.springframework.integration.jdbc.store.channel.OracleChannelMessageStoreQueryProvider;
import org.springframework.integration.jdbc.store.channel.PostgresChannelMessageStoreQueryProvider;
import org.springframework.integration.jdbc.store.channel.ChannelMessageStoreQueryProvider;
import org.springframework.integration.store.AbstractMessageGroupStore;
import org.springframework.integration.store.MessageGroup;
import org.springframework.integration.store.MessageGroupStore;
import org.springframework.integration.store.MessageStore;
import org.springframework.integration.store.SimpleMessageGroup;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.integration.transaction.TransactionSynchronizationFactory;
import org.springframework.integration.util.UUIDConverter;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.jdbc.support.lob.DefaultLobHandler;
import org.springframework.jdbc.support.lob.LobHandler;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedMetric;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

/**
 * <p>
 * Channel-specific implementation of {@link MessageGroupStore} using a relational
 * database via JDBC.
 *
 * This message store shall be used for message channels only.
 * </p>
 * <p>
 * <strong>NOTICE</strong>: This implementation may change for Spring Integration
 * 3.0. It is provided for use-cases where the current {@link JdbcMessageStore}
 * is not delivering the desired performance characteristics.
 * </p>
 *
 * <p>
 * As such, the {@link JdbcChannelMessageStore} uses database specific SQL queries.
 * </p>
 * <p>
 * Contrary to the {@link JdbcMessageStore}, this implementation uses one single
 * database table only. The SQL scripts to create the necessary table are packaged
 * under <code>org/springframework/integration/jdbc/messagestore/channel/schema-*.sql</code>,
 * where <code>*</code> denotes the target database type.
 * </p
 * >
 * @author Gunnar Hillert
 * @author Artem Bilan
 * @since 2.2
 */
@ManagedResource
public class JdbcChannelMessageStore extends AbstractMessageGroupStore implements InitializingBean {

    private static final Log logger = LogFactory.getLog(JdbcChannelMessageStore.class);

    private final Set<String> idCache = new HashSet<String>();

    private final ReadWriteLock idCacheLock = new ReentrantReadWriteLock();

    private final Lock idCacheReadLock = idCacheLock.readLock();

    private final Lock idCacheWriteLock = idCacheLock.writeLock();

    /**
     * Default value for the table prefix property.
     */
    public static final String DEFAULT_TABLE_PREFIX = "INT_";

    /**
     * Default region property, used to partition the message store. For example,
     * a separate Spring Integration application with overlapping channel names
     * may use the same message store by providing a distinct region name.
     */
    public static final String DEFAULT_REGION = "DEFAULT";

    private ChannelMessageStoreQueryProvider channelMessageStoreQueryProvider;

    public static final int DEFAULT_LONG_STRING_LENGTH = 2500;

    /**
     * The name of the message header that stores a flag to indicate that the message has been saved. This is an
     * optimization for the put method.
     */
    public static final String SAVED_KEY = JdbcChannelMessageStore.class.getSimpleName() + ".SAVED";

    /**
     * The name of the message header that stores a timestamp for the time the message was inserted.
     */
    public static final String CREATED_DATE_KEY = JdbcChannelMessageStore.class.getSimpleName() + ".CREATED_DATE";

    private volatile String region = DEFAULT_REGION;

    private volatile String tablePrefix = DEFAULT_TABLE_PREFIX;

    private volatile JdbcTemplate jdbcTemplate;

    private volatile DeserializingConverter deserializer;

    private volatile SerializingConverter serializer;

    private volatile LobHandler lobHandler = new DefaultLobHandler();

    private volatile MessageRowMapper messageRowMapper;

    private volatile Map<String, String> queryCache = new HashMap<String, String>();

    private boolean usingIdCache = false;

    /**
     * Convenient constructor for configuration use.
     */
    public JdbcChannelMessageStore() {
        deserializer = new DeserializingConverter();
        serializer = new SerializingConverter();
    }

    /**
     * Create a {@link MessageStore} with all mandatory properties. The passed-in
     * {@link DataSource} is used to instantiate a {@link JdbcTemplate}
     *
     * with {@link JdbcTemplate#setFetchSize(int)} set to <code>1</code>
     * and with {@link JdbcTemplate#setMaxRows(int)} set to <code>1</code>.
     *
     * @param dataSource a {@link DataSource}
     */
    public JdbcChannelMessageStore(DataSource dataSource) {
        this();
        jdbcTemplate = new JdbcTemplate(dataSource);

        this.jdbcTemplate.setFetchSize(1);
        this.jdbcTemplate.setMaxRows(1);

        this.jdbcTemplate.afterPropertiesSet();
    }

    /**
     * The JDBC {@link DataSource} to use when interacting with the database.
     * The passed-in {@link DataSource} is used to instantiate a {@link JdbcTemplate}
     * with {@link JdbcTemplate#setFetchSize(int)} set to <code>1</code>
     * and with {@link JdbcTemplate#setMaxRows(int)} set to <code>1</code>.
     *
     * @param dataSource a {@link DataSource}
     */
    public void setDataSource(DataSource dataSource) {
        jdbcTemplate = new JdbcTemplate(dataSource);

        this.jdbcTemplate.setFetchSize(1);
        this.jdbcTemplate.setMaxRows(1);

        this.jdbcTemplate.afterPropertiesSet();
    }

    /**
     * A converter for deserializing byte arrays to messages.
     *
     * @param deserializer the deserializer to set
     */
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public void setDeserializer(Deserializer<? extends Message<?>> deserializer) {
        this.deserializer = new DeserializingConverter((Deserializer) deserializer);
    }

    /**
     * The {@link JdbcOperations} to use when interacting with the database. Either
     * this property can be set or the {@link #setDataSource(DataSource) dataSource}.
     *
     * Please consider passing in a {@link JdbcTemplate} with a fetchSize property
     * of 1. This is particularly important for Oracle to ensure First In, First Out (FIFO)
     * message retrieval characteristics.
     *
     * @param jdbcTemplate a {@link JdbcOperations}
     */
    public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
        Assert.notNull(jdbcTemplate, "The provided jdbcTemplate must not be null.");
        this.jdbcTemplate = jdbcTemplate;
    }

    /**
     * Method not implemented.
     * @throws UnsupportedOperationException
     */
    public void setLastReleasedSequenceNumberForGroup(Object groupId, final int sequenceNumber) {
        throw new UnsupportedOperationException("Not implemented");
    }

    /**
     * Override the {@link LobHandler} that is used to create and unpack large objects in SQL queries. The default is
     * fine for almost all platforms, but some Oracle drivers require a native implementation.
     *
     * @param lobHandler a {@link LobHandler}
     */
    public void setLobHandler(LobHandler lobHandler) {
        Assert.notNull(lobHandler, "The provided LobHandler must not be null.");
        this.lobHandler = lobHandler;
    }

    /**
     * Allows for passing in a custom {@link MessageRowMapper}. The {@link MessageRowMapper}
     * is used to convert the selected database row representing the persisted
     * message into the actual {@link Message} object.
     *
     * @param messageRowMapper Must not be null
     */
    public void setMessageRowMapper(MessageRowMapper messageRowMapper) {
        Assert.notNull(messageRowMapper, "The provided MessageRowMapper must not be null.");
        this.messageRowMapper = messageRowMapper;
    }

    /**
     * <p>
     * Sets the database specific {@link ChannelMessageStoreQueryProvider} to use. The {@link JdbcChannelMessageStore}
     * provides the SQL queries to retrieve messages from the database. The
     * following {@link ChannelMessageStoreQueryProvider} are provided:
     * </p>
     * <ul>
     *     <li>{@link DerbyChannelMessageStoreQueryProvider}</li>
     *     <li>{@link MySqlChannelMessageStoreQueryProvider}</li>
     *     <li>{@link OracleChannelMessageStoreQueryProvider}</li>
     *     <li>{@link PostgresChannelMessageStoreQueryProvider}</li>
     * </ul>
     * <p>
     * Beyond, you can provide your own query implementations, in case you need
     * to support additional databases and/or need to fine-tune the queries for
     * your requirements.
     * </p>
     *
     * @param channelMessageStoreQueryProvider Must not be null.
     */
    public void setChannelMessageStoreQueryProvider(
            ChannelMessageStoreQueryProvider channelMessageStoreQueryProvider) {
        Assert.notNull(channelMessageStoreQueryProvider,
                "The provided channelMessageStoreQueryProvider must not be null.");
        this.channelMessageStoreQueryProvider = channelMessageStoreQueryProvider;
    }

    /**
     * A unique grouping identifier for all messages persisted with this store.
     * Using multiple regions allows the store to be partitioned (if necessary)
     * for different purposes. Defaults to <code>{@link #DEFAULT_REGION}</code>.
     *
     * @param region the region name to set
     */
    public void setRegion(String region) {
        this.region = region;
    }

    /**
     * A converter for serializing messages to byte arrays for storage.
     *
     * @param serializer The serializer to set
     */
    @SuppressWarnings("unchecked")
    public void setSerializer(Serializer<? super Message<?>> serializer) {
        Assert.notNull(serializer, "The provided serializer must not be null.");
        this.serializer = new SerializingConverter((Serializer<Object>) serializer);
    }

    /**
     * Public setter for the table prefix property. This will be prefixed to all the table names before queries are
     * executed. Defaults to {@link #DEFAULT_TABLE_PREFIX}.
     *
     * @param tablePrefix the tablePrefix to set
     */
    public void setTablePrefix(String tablePrefix) {
        this.tablePrefix = tablePrefix;
    }

    /**
     * <p>Consider using this property when polling the database transactionally
     * using multiple parallel threads, meaning when the configured poller is configured
     * using a task executor.</p>
     *
     * <p>The issue is that the {@link #pollMessageFromGroup(Object)} looks for the
     * oldest entry for a giving channel (groupKey) and region ({@link #setRegion(String)}).
     * If you do that with multiple threads and you are using transactions, other
     * threads may be waiting for that same locked row.</p>
     *
     * <p>If using the provided {@link OracleChannelMessageStoreQueryProvider}, don't set {@link #usingIdCache}
     * to true, as the Oracle query will ignore locked rows.</p>
     *
     * <p>Using the id cache, the {@link JdbcChannelMessageStore} will store each
     * message id in an in-memory collection for the duration of processing. With
     * that, any polling threads will explicitly exclude those messages from
     * being polled.</p>
     *
     * <p>For this to work, you must setup the corresponding
     * {@link TransactionSynchronizationFactory}:</p>
     *
     * <pre>
     * {@code
     * <int:transaction-synchronization-factory id="syncFactory">
     *     <int:after-commit   expression="@jdbcChannelMessageStore.removeFromIdCache(headers.id.toString())" />
     *     <int:after-rollback expression="@jdbcChannelMessageStore.removeFromIdCache(headers.id.toString())" />
     * </int:transaction-synchronization-factory>
     * }
     * </pre>
     *
     * This {@link TransactionSynchronizationFactory} is then referenced in the
     * transaction configuration of the poller:
     *
     * <pre>
     * {@code
     * <int:poller fixed-delay="300" receive-timeout="500"
     *     max-messages-per-poll="1" task-executor="pool">
     *     <int:transactional propagation="REQUIRED" synchronization-factory="syncFactory"
     *         isolation="READ_COMMITTED" transaction-manager="transactionManager" />
     * </int:poller>
     * }
     * </pre>
     *
     * @param usingIdCache When <code>true</code> the id cache will be used.
     */
    public void setUsingIdCache(boolean usingIdCache) {
        this.usingIdCache = usingIdCache;
    }

    /**
     * Check mandatory properties ({@link DataSource} and
     * {@link #setChannelMessageStoreQueryProvider(ChannelMessageStoreQueryProvider)}). If no {@link MessageRowMapper} was
     * explicitly set using {@link #setMessageRowMapper(MessageRowMapper)}, the default
     * {@link MessageRowMapper} will be instantiate using the specified {@link #deserializer}
     * and {@link #lobHandler}.
     *
     * Also, if the jdbcTemplate's fetchSize property ({@link JdbcTemplate#getFetchSize()})
     * is not 1, a warning will be logged. When using the {@link JdbcChannelMessageStore}
     * with Oracle, the fetchSize value of 1 is needed to ensure FIFO characteristics
     * of polled messages. Please see the Oracle {@link ChannelMessageStoreQueryProvider} for more details.
     *
     * @throws Exception
     */
    public void afterPropertiesSet() throws Exception {
        Assert.state(jdbcTemplate != null, "A DataSource or JdbcTemplate must be provided");
        Assert.notNull(this.channelMessageStoreQueryProvider,
                "A channelMessageStoreQueryProvider must be provided.");

        if (this.messageRowMapper == null) {
            this.messageRowMapper = new MessageRowMapper(this.deserializer, this.lobHandler);
        }

        if (this.jdbcTemplate.getFetchSize() != 1 && logger.isWarnEnabled()) {
            logger.warn(
                    "The jdbcTemplate's fetchsize is not 1 but %s. This may cause FIFO issues with Oracle databases.");
        }

    }

    /**
     * Store a message in the database. The groupId identifies the channel for which
     * the message is to be stored.
     *
     * Keep in mind that the actual groupdId (Channel
     * Identifier) is converted to a String-based UUID identifier.
     *
     * @param groupId the group id to store the message under
     * @param message a message
     */
    @SuppressWarnings({ "rawtypes", "unchecked" })
    public MessageGroup addMessageToGroup(Object groupId, Message<?> message) {

        final String groupKey = getKey(groupId);

        final long createdDate = System.currentTimeMillis();
        final Message<?> result = MessageBuilder.fromMessage(message).setHeader(SAVED_KEY, Boolean.TRUE)
                .setHeader(CREATED_DATE_KEY, new Long(createdDate)).build();

        final Map innerMap = (Map) new DirectFieldAccessor(result.getHeaders()).getPropertyValue("headers");
        // using reflection to set ID since it is immutable through MessageHeaders
        innerMap.put(MessageHeaders.ID, message.getHeaders().get(MessageHeaders.ID));

        final String messageId = getKey(result.getHeaders().getId());
        final byte[] messageBytes = serializer.convert(result);

        jdbcTemplate.update(getQuery(channelMessageStoreQueryProvider.getCreateMessageQuery()),
                new PreparedStatementSetter() {
                    public void setValues(PreparedStatement ps) throws SQLException {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Inserting message with id key=" + messageId);
                        }
                        ps.setString(1, messageId);
                        ps.setString(2, groupKey);
                        ps.setString(3, region);
                        ps.setLong(4, createdDate);
                        lobHandler.getLobCreator().setBlobAsBytes(ps, 5, messageBytes);
                    }
                });

        return getMessageGroup(groupId);
    }

    /**
     * Method not implemented.
     * @throws UnsupportedOperationException
     */
    public void completeGroup(Object groupId) {
        throw new UnsupportedOperationException("Not implemented");
    }

    /**
     * This method executes a call to the DB to get the oldest Message in the
     * MessageGroup which in the context of the {@link JdbcChannelMessageStore}
     * means the channel identifier.
     *
     * @param groupIdKey String representation of message group (Channel) ID
     * @return a message; could be null if query produced no Messages
     */
    protected Message<?> doPollForMessage(String groupIdKey) {

        final NamedParameterJdbcTemplate namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate);
        final MapSqlParameterSource parameters = new MapSqlParameterSource();

        parameters.addValue("region", region);
        parameters.addValue("group_key", groupIdKey);

        final String query;

        final List<Message<?>> messages;

        this.idCacheReadLock.lock();
        try {
            if (this.usingIdCache && !this.idCache.isEmpty()) {
                query = getQuery(this.channelMessageStoreQueryProvider.getPollFromGroupExcludeIdsQuery());
                parameters.addValue("message_ids", idCache);
            } else {
                query = getQuery(this.channelMessageStoreQueryProvider.getPollFromGroupQuery());
            }
            messages = namedParameterJdbcTemplate.query(query, parameters, messageRowMapper);
        } finally {
            this.idCacheReadLock.unlock();
        }

        Assert.isTrue(messages.size() == 0 || messages.size() == 1);
        if (messages.size() > 0) {

            final Message<?> message = messages.get(0);
            final String messageId = message.getHeaders().getId().toString();

            if (this.usingIdCache) {
                this.idCacheWriteLock.lock();
                try {
                    boolean added = this.idCache.add(messageId);

                    if (logger.isDebugEnabled()) {
                        logger.debug(String.format("Polled message with id '%s' added: '%s'.", messageId, added));
                    }
                } finally {
                    this.idCacheWriteLock.unlock();
                }
            }

            return message;
        }
        return null;
    }

    /**
     * Helper method that converts the channel id to a UUID using
     * {@link UUIDConverter#getUUID(Object)}.
     *
     * @param input Parameter may be null
     * @return Returns null when the input is null otherwise the UUID as String.
     */
    private String getKey(Object input) {
        return input == null ? null : UUIDConverter.getUUID(input).toString();
    }

    /**
     * Method not implemented.
     * @throws UnsupportedOperationException
     */
    @ManagedAttribute
    public long getMessageCount() {
        throw new UnsupportedOperationException("Not implemented");
    }

    /**
     * Method not implemented.
     * @throws UnsupportedOperationException
     */
    @Override
    @ManagedAttribute
    public int getMessageCountForAllMessageGroups() {
        throw new UnsupportedOperationException("Not implemented");
    }

    /**
     * Not fully used. Only wraps the provided group id.
     */
    public MessageGroup getMessageGroup(Object groupId) {
        return new SimpleMessageGroup(groupId);
    }

    /**
     * Method not implemented.
     * @throws UnsupportedOperationException
     */
    @Override
    @ManagedAttribute
    public int getMessageGroupCount() {
        throw new UnsupportedOperationException("Not implemented");
    }

    /**
     * Replace patterns in the input to produce a valid SQL query. This implementation lazily initializes a
     * simple map-based cache, only replacing the table prefix on the first access to a named query. Further
     * accesses will be resolved from the cache.
     *
     * @param sqlQuery the SQL query to be transformed
     * @return a transformed query with replacements
     */
    protected String getQuery(String sqlQuery) {
        String query = queryCache.get(sqlQuery);

        if (query == null) {
            query = StringUtils.replace(sqlQuery, "%PREFIX%", tablePrefix);
            queryCache.put(sqlQuery, query);
        }

        return query;
    }

    /**
     * Method not implemented.
     * @throws UnsupportedOperationException
     */
    public Iterator<MessageGroup> iterator() {
        throw new UnsupportedOperationException("Not implemented");
    }

    /**
     * Returns the number of messages persisted for the specified channel id (groupId)
     * and the specified region ({@link #setRegion(String)}).
     */
    @ManagedAttribute
    public int messageGroupSize(Object groupId) {
        final String key = getKey(groupId);
        return jdbcTemplate.queryForInt(
                getQuery(channelMessageStoreQueryProvider.getCountAllMessagesInGroupQuery()), key, this.region);
    }

    /**
     * Polls the database for a new message that is persisted for the given
     * group id which represents the channel identifier.
     */
    public Message<?> pollMessageFromGroup(Object groupId) {

        final String key = getKey(groupId);
        final Message<?> polledMessage = this.doPollForMessage(key);

        if (polledMessage != null) {
            this.removeMessageFromGroup(groupId, polledMessage);
        }

        return polledMessage;
    }

    /**
     * Remove a single message from the database.
     *
     * @param groupId The channel id to remove the message from
     * @param messageToRemove The message to remove
     *
     */
    public MessageGroup removeMessageFromGroup(Object groupId, Message<?> messageToRemove) {

        final UUID id = messageToRemove.getHeaders().getId();

        int updated = jdbcTemplate.update(getQuery(channelMessageStoreQueryProvider.getDeleteMessageQuery()),
                new Object[] { getKey(id), getKey(groupId), region },
                new int[] { Types.VARCHAR, Types.VARCHAR, Types.VARCHAR });

        if (updated != 0) {
            logger.debug(String.format("Message with id '%s' was deleted.", id));
        } else {
            logger.warn(String.format("Message with id '%s' was not deleted.", id));
        }

        return getMessageGroup(groupId);
    }

    /**
     * <p>Remove a Message Id from the idCache. Should be used in conjunction
     * with the Spring Integration Transaction Synchronization feature to remove
     * a message from the Message Id cache once a transaction either succeeded or
     * rolled back.</p>
     * <p>Only applicable if {@link #setUsingIdCache(boolean)} is set to
     * <code>true</code></p>.
     *
     * @param messageId
     */
    public void removeFromIdCache(String messageId) {
        if (logger.isDebugEnabled()) {
            logger.debug("Removing Message Id:" + messageId);
        }
        this.idCacheWriteLock.lock();
        try {
            this.idCache.remove(messageId);
        } finally {
            this.idCacheWriteLock.unlock();
        }
    }

    /**
     * Returns the size of the Message Id Cache, which caches Message Ids for
     * those messages that are currently being processed.
     *
     * @return The size of the Message Id Cache
     */
    @ManagedMetric
    public int getSizeOfIdCache() {
        return this.idCache.size();
    }

    /**
     * Will remove all messages from the message channel.
     */
    public void removeMessageGroup(Object groupId) {

        final String groupKey = getKey(groupId);

        jdbcTemplate.update(getQuery(channelMessageStoreQueryProvider.getDeleteMessageGroupQuery()),
                new PreparedStatementSetter() {
                    public void setValues(PreparedStatement ps) throws SQLException {
                        if (logger.isDebugEnabled()) {
                            logger.debug("Marking messages with group key=" + groupKey);
                        }
                        ps.setString(1, groupKey);
                        ps.setString(2, region);
                    }
                });

    }

}