org.springframework.integration.mongodb.store.MongoDbMessageStore.java Source code

Java tutorial

Introduction

Here is the source code for org.springframework.integration.mongodb.store.MongoDbMessageStore.java

Source

/*
 * Copyright 2002-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.integration.mongodb.store;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;

import org.springframework.beans.BeansException;
import org.springframework.beans.DirectFieldAccessor;
import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.core.serializer.support.DeserializingConverter;
import org.springframework.core.serializer.support.SerializingConverter;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.domain.Sort;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mongodb.MongoDbFactory;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.IndexOperations;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.CustomConversions;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.index.Index;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.integration.history.MessageHistory;
import org.springframework.integration.message.AdviceMessage;
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.MutableMessageBuilder;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.messaging.support.GenericMessage;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;

import com.mongodb.BasicDBList;
import com.mongodb.BasicDBObject;
import com.mongodb.BulkWriteOperation;
import com.mongodb.DBObject;

/**
 * An implementation of both the {@link MessageStore} and {@link MessageGroupStore}
 * strategies that relies upon MongoDB for persistence.
 *
 * @author Mark Fisher
 * @author Oleg Zhurakousky
 * @author Sean Brandt
 * @author Jodie StJohn
 * @author Gary Russell
 * @author Artem Bilan
 * @since 2.1
 */
public class MongoDbMessageStore extends AbstractMessageGroupStore
        implements MessageStore, BeanClassLoaderAware, ApplicationContextAware, InitializingBean {

    private final static String DEFAULT_COLLECTION_NAME = "messages";

    public final static String SEQUENCE_NAME = "messagesSequence";

    /**
     * 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 = ConfigurableMongoDbMessageStore.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 = ConfigurableMongoDbMessageStore.class.getSimpleName()
            + ".CREATED_DATE";

    private final static String GROUP_ID_KEY = "_groupId";

    private final static String GROUP_COMPLETE_KEY = "_group_complete";

    private final static String LAST_RELEASED_SEQUENCE_NUMBER = "_last_released_sequence";

    private final static String GROUP_TIMESTAMP_KEY = "_group_timestamp";

    private final static String GROUP_UPDATE_TIMESTAMP_KEY = "_group_update_timestamp";

    private final static String CREATED_DATE = "_createdDate";

    private static final String SEQUENCE = "sequence";

    private final MongoTemplate template;

    private final MessageReadingMongoConverter converter;

    private final String collectionName;

    private volatile ClassLoader classLoader = ClassUtils.getDefaultClassLoader();

    private ApplicationContext applicationContext;

    /**
     * Create a MongoDbMessageStore using the provided {@link MongoDbFactory}.and the default collection name.
     * @param mongoDbFactory The mongodb factory.
     */
    public MongoDbMessageStore(MongoDbFactory mongoDbFactory) {
        this(mongoDbFactory, null);
    }

    /**
     * Create a MongoDbMessageStore using the provided {@link MongoDbFactory} and collection name.
     * @param mongoDbFactory The mongodb factory.
     * @param collectionName The collection name.
     */
    public MongoDbMessageStore(MongoDbFactory mongoDbFactory, String collectionName) {
        Assert.notNull(mongoDbFactory, "mongoDbFactory must not be null");
        this.converter = new MessageReadingMongoConverter(mongoDbFactory, new MongoMappingContext());
        this.template = new MongoTemplate(mongoDbFactory, this.converter);
        this.collectionName = (StringUtils.hasText(collectionName)) ? collectionName : DEFAULT_COLLECTION_NAME;
    }

    @Override
    public void setBeanClassLoader(ClassLoader classLoader) {
        Assert.notNull(classLoader, "classLoader must not be null");
        this.classLoader = classLoader;
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    @Override
    public void afterPropertiesSet() throws Exception {
        if (this.applicationContext != null) {
            this.template.setApplicationContext(this.applicationContext);
            this.converter.setApplicationContext(this.applicationContext);
        }
        this.converter.afterPropertiesSet();

        IndexOperations indexOperations = this.template.indexOps(this.collectionName);

        indexOperations.ensureIndex(new Index(GROUP_ID_KEY, Sort.Direction.ASC)
                .on(GROUP_UPDATE_TIMESTAMP_KEY, Sort.Direction.DESC).on(SEQUENCE, Sort.Direction.DESC));
    }

    @Override
    public <T> Message<T> addMessage(Message<T> message) {
        Assert.notNull(message, "'message' must not be null");
        this.addMessageDocument(new MessageWrapper(message));
        return message;
    }

    private void addMessageDocument(final MessageWrapper document) {
        Message<?> message = document.getMessage();
        if (message.getHeaders().containsKey(SAVED_KEY)) {
            Message<?> saved = getMessage(message.getHeaders().getId());
            if (saved != null) {
                if (saved.equals(message)) {
                    return;
                } // We need to save it under its own id
            }
        }

        final long createdDate = document.get_Group_timestamp() == 0 ? System.currentTimeMillis()
                : document.get_Group_timestamp();

        Message<?> result = getMessageBuilderFactory().fromMessage(message).setHeader(SAVED_KEY, Boolean.TRUE)
                .setHeader(CREATED_DATE_KEY, createdDate).build();

        @SuppressWarnings("unchecked")
        Map<String, Object> innerMap = (Map<String, Object>) 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));
        innerMap.put(MessageHeaders.TIMESTAMP, message.getHeaders().get(MessageHeaders.TIMESTAMP));

        document.set_Group_timestamp(createdDate);
        template.insert(document, collectionName);
    }

    @Override
    public Message<?> getMessage(UUID id) {
        Assert.notNull(id, "'id' must not be null");
        MessageWrapper messageWrapper = this.template.findOne(whereMessageIdIs(id), MessageWrapper.class,
                this.collectionName);
        return (messageWrapper != null) ? messageWrapper.getMessage() : null;
    }

    @Override
    @ManagedAttribute
    public long getMessageCount() {
        return this.template.getCollection(this.collectionName).getCount();
    }

    @Override
    public Message<?> removeMessage(UUID id) {
        Assert.notNull(id, "'id' must not be null");
        MessageWrapper messageWrapper = this.template.findAndRemove(whereMessageIdIs(id), MessageWrapper.class,
                this.collectionName);
        return (messageWrapper != null ? messageWrapper.getMessage() : null);
    }

    @Override
    public MessageGroup getMessageGroup(Object groupId) {
        Assert.notNull(groupId, "'groupId' must not be null");
        Query query = whereGroupIdOrder(groupId);
        List<MessageWrapper> messageWrappers = this.template.find(query, MessageWrapper.class, this.collectionName);
        List<Message<?>> messages = new ArrayList<Message<?>>();
        long timestamp = 0;
        long lastModified = 0;
        int lastReleasedSequenceNumber = 0;
        boolean completeGroup = false;
        if (messageWrappers.size() > 0) {
            MessageWrapper messageWrapper = messageWrappers.get(0);
            timestamp = messageWrapper.get_Group_timestamp();
            lastModified = messageWrapper.get_Group_update_timestamp();
            completeGroup = messageWrapper.get_Group_complete();
            lastReleasedSequenceNumber = messageWrapper.get_LastReleasedSequenceNumber();
        }

        for (MessageWrapper messageWrapper : messageWrappers) {
            messages.add(messageWrapper.getMessage());
        }

        SimpleMessageGroup messageGroup = new SimpleMessageGroup(messages, groupId, timestamp, completeGroup);
        messageGroup.setLastModified(lastModified);
        if (lastReleasedSequenceNumber > 0) {
            messageGroup.setLastReleasedMessageSequenceNumber(lastReleasedSequenceNumber);
        }

        return messageGroup;
    }

    @Override
    public MessageGroup addMessageToGroup(final Object groupId, final Message<?> message) {
        Assert.notNull(groupId, "'groupId' must not be null");
        Assert.notNull(message, "'message' must not be null");
        Query query = whereGroupIdOrder(groupId);
        MessageWrapper messageDocument = template.findOne(query, MessageWrapper.class, collectionName);

        long createdTime = 0;
        int lastReleasedSequence = 0;
        boolean complete = false;

        if (messageDocument != null) {
            createdTime = messageDocument.get_Group_timestamp();
            lastReleasedSequence = messageDocument.get_LastReleasedSequenceNumber();
            complete = messageDocument.get_Group_complete();
        }

        MessageWrapper wrapper = new MessageWrapper(message);
        wrapper.set_GroupId(groupId);
        wrapper.set_Group_timestamp(createdTime == 0 ? System.currentTimeMillis() : createdTime);
        wrapper.set_Group_update_timestamp(System.currentTimeMillis());
        wrapper.set_Group_complete(complete);
        wrapper.set_LastReleasedSequenceNumber(lastReleasedSequence);
        wrapper.setSequence(getNextId());

        addMessageDocument(wrapper);
        return getMessageGroup(groupId);

    }

    @Override
    public MessageGroup removeMessageFromGroup(final Object groupId, final Message<?> messageToRemove) {
        Assert.notNull(groupId, "'groupId' must not be null");
        Assert.notNull(messageToRemove, "'messageToRemove' must not be null");

        template.findAndRemove(whereMessageIdIsAndGroupIdIs(messageToRemove.getHeaders().getId(), groupId),
                MessageWrapper.class, collectionName);
        updateGroup(groupId, lastModifiedUpdate());
        return getMessageGroup(groupId);
    }

    @Override
    public void removeMessagesFromGroup(Object groupId, Collection<Message<?>> messages) {
        Assert.notNull(groupId, "'groupId' must not be null");
        Assert.notNull(messages, "'messageToRemove' must not be null");

        Collection<UUID> ids = new ArrayList<UUID>();
        for (Message<?> messageToRemove : messages) {
            ids.add(messageToRemove.getHeaders().getId());
            if (ids.size() >= getRemoveBatchSize()) {
                bulkRemove(groupId, ids);
                ids.clear();
            }
        }
        if (ids.size() > 0) {
            bulkRemove(groupId, ids);
        }
        updateGroup(groupId, lastModifiedUpdate());
    }

    private void bulkRemove(Object groupId, Collection<UUID> ids) {
        BulkWriteOperation bulkOp = this.template.getCollection(this.collectionName)
                .initializeOrderedBulkOperation();
        for (UUID id : ids) {
            bulkOp.find(whereMessageIdIsAndGroupIdIs(id, groupId).getQueryObject()).remove();
        }
        bulkOp.execute();
    }

    @Override
    public void removeMessageGroup(Object groupId) {
        this.template.remove(whereGroupIdIs(groupId), this.collectionName);
    }

    @Override
    public Iterator<MessageGroup> iterator() {
        List<MessageGroup> messageGroups = new ArrayList<MessageGroup>();

        Query query = Query.query(Criteria.where(GROUP_ID_KEY).exists(true));

        @SuppressWarnings("rawtypes")
        List groupIds = template.getCollection(collectionName).distinct(GROUP_ID_KEY, query.getQueryObject());

        for (Object groupId : groupIds) {
            messageGroups.add(getMessageGroup(groupId));
        }

        return messageGroups.iterator();
    }

    @Override
    public Message<?> pollMessageFromGroup(final Object groupId) {
        Assert.notNull(groupId, "'groupId' must not be null");
        Query query = whereGroupIdIs(groupId).with(new Sort(GROUP_UPDATE_TIMESTAMP_KEY, SEQUENCE));
        MessageWrapper messageWrapper = template.findAndRemove(query, MessageWrapper.class, collectionName);
        Message<?> message = null;
        if (messageWrapper != null) {
            message = messageWrapper.getMessage();
        }
        updateGroup(groupId, lastModifiedUpdate());
        return message;
    }

    @Override
    public int messageGroupSize(Object groupId) {
        long lCount = this.template.count(new Query(Criteria.where(GROUP_ID_KEY).is(groupId)), this.collectionName);
        Assert.isTrue(lCount <= Integer.MAX_VALUE, "Message count is out of Integer's range");
        return (int) lCount;
    }

    @Override
    public void setLastReleasedSequenceNumberForGroup(Object groupId, int sequenceNumber) {
        this.updateGroup(groupId, lastModifiedUpdate().set(LAST_RELEASED_SEQUENCE_NUMBER, sequenceNumber));
    }

    @Override
    public void completeGroup(Object groupId) {
        this.updateGroup(groupId, lastModifiedUpdate().set(GROUP_COMPLETE_KEY, true));
    }

    @Override
    @ManagedAttribute
    public int getMessageCountForAllMessageGroups() {
        Query query = Query.query(Criteria.where(MessageDocumentFields.MESSAGE_ID).exists(true)
                .and(MessageDocumentFields.GROUP_ID).exists(true));
        long count = this.template.count(query, this.collectionName);
        Assert.isTrue(count <= Integer.MAX_VALUE, "Message count is out of Integer's range");
        return (int) count;
    }

    @Override
    @ManagedAttribute
    public int getMessageGroupCount() {
        Query query = Query.query(Criteria.where(MessageDocumentFields.GROUP_ID).exists(true));
        return this.template.getCollection(this.collectionName)
                .distinct(MessageDocumentFields.GROUP_ID, query.getQueryObject()).size();
    }

    private static Update lastModifiedUpdate() {
        return Update.update(GROUP_UPDATE_TIMESTAMP_KEY, System.currentTimeMillis());
    }

    /*
     * Common Queries
     */

    private static Query whereMessageIdIs(UUID id) {
        return new Query(Criteria.where("headers.id._value").is(id.toString()));
    }

    private static Query whereMessageIdIsAndGroupIdIs(UUID id, Object groupId) {
        return new Query(Criteria.where("headers.id._value").is(id.toString()).and(GROUP_ID_KEY).is(groupId));
    }

    private static Query whereGroupIdOrder(Object groupId) {
        return whereGroupIdIs(groupId).with(new Sort(Sort.Direction.DESC, GROUP_UPDATE_TIMESTAMP_KEY, SEQUENCE));
    }

    private static Query whereGroupIdIs(Object groupId) {
        return new Query(Criteria.where(GROUP_ID_KEY).is(groupId));
    }

    private void updateGroup(Object groupId, Update update) {
        Query query = whereGroupIdIs(groupId)
                .with(new Sort(Sort.Direction.DESC, GROUP_UPDATE_TIMESTAMP_KEY, SEQUENCE));
        this.template.updateFirst(query, update, this.collectionName);
    }

    private int getNextId() {
        Query query = Query.query(Criteria.where("_id").is(SEQUENCE_NAME));
        query.fields().include(SEQUENCE);
        return (Integer) this.template
                .findAndModify(query, new Update().inc(SEQUENCE, 1),
                        FindAndModifyOptions.options().returnNew(true).upsert(true), Map.class, this.collectionName)
                .get(SEQUENCE);
    }

    /**
     * Custom implementation of the {@link MappingMongoConverter} strategy.
     */
    private class MessageReadingMongoConverter extends MappingMongoConverter {

        public MessageReadingMongoConverter(MongoDbFactory mongoDbFactory,
                MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
            super(new DefaultDbRefResolver(mongoDbFactory), mappingContext);
        }

        @Override
        public void afterPropertiesSet() {
            List<Object> customConverters = new ArrayList<Object>();
            customConverters.add(new UuidToDBObjectConverter());
            customConverters.add(new DBObjectToUUIDConverter());
            customConverters.add(new MessageHistoryToDBObjectConverter());
            customConverters.add(new DBObjectToGenericMessageConverter());
            customConverters.add(new DBObjectToMutableMessageConverter());
            customConverters.add(new DBObjectToErrorMessageConverter());
            customConverters.add(new DBObjectToAdviceMessageConverter());
            customConverters.add(new ThrowableToBytesConverter());
            this.setCustomConversions(new CustomConversions(customConverters));
            super.afterPropertiesSet();
        }

        @Override
        public void write(Object source, DBObject target) {
            Assert.isInstanceOf(MessageWrapper.class, source);

            target.put(CREATED_DATE, System.currentTimeMillis());

            super.write(source, target);
        }

        @Override
        @SuppressWarnings({ "unchecked" })
        public <S> S read(Class<S> clazz, DBObject source) {
            if (!MessageWrapper.class.equals(clazz)) {
                return super.read(clazz, source);
            }
            if (source != null) {
                Message<?> message = null;
                Object messageType = source.get("_messageType");
                if (messageType == null) {
                    messageType = GenericMessage.class.getName();
                }
                try {
                    message = (Message<?>) this.read(ClassUtils.forName(messageType.toString(), classLoader),
                            source);
                } catch (ClassNotFoundException e) {
                    throw new IllegalStateException("failed to load class: " + messageType, e);
                }

                Long groupTimestamp = (Long) source.get(GROUP_TIMESTAMP_KEY);
                Long lastModified = (Long) source.get(GROUP_UPDATE_TIMESTAMP_KEY);
                Integer lastReleasedSequenceNumber = (Integer) source.get(LAST_RELEASED_SEQUENCE_NUMBER);
                Boolean completeGroup = (Boolean) source.get(GROUP_COMPLETE_KEY);

                MessageWrapper wrapper = new MessageWrapper(message);

                if (source.containsField(GROUP_ID_KEY)) {
                    wrapper.set_GroupId(source.get(GROUP_ID_KEY));
                }
                if (groupTimestamp != null) {
                    wrapper.set_Group_timestamp(groupTimestamp);
                }
                if (lastModified != null) {
                    wrapper.set_Group_update_timestamp(lastModified);
                }
                if (lastReleasedSequenceNumber != null) {
                    wrapper.set_LastReleasedSequenceNumber(lastReleasedSequenceNumber);
                }

                if (completeGroup != null) {
                    wrapper.set_Group_complete(completeGroup);
                }

                return (S) wrapper;
            }
            return null;
        }

        private Map<String, Object> normalizeHeaders(Map<String, Object> headers) {
            Map<String, Object> normalizedHeaders = new HashMap<String, Object>();
            for (Entry<String, Object> entry : headers.entrySet()) {
                String headerName = entry.getKey();
                Object headerValue = entry.getValue();
                if (headerValue instanceof DBObject) {
                    DBObject source = (DBObject) headerValue;
                    try {
                        Class<?> typeClass = null;
                        if (source.containsField("_class")) {
                            Object type = source.get("_class");
                            typeClass = ClassUtils.forName(type.toString(), classLoader);
                        } else if (source instanceof BasicDBList) {
                            typeClass = List.class;
                        } else {
                            throw new IllegalStateException("Unsupported 'DBObject' type: " + source.getClass());
                        }
                        normalizedHeaders.put(headerName, super.read(typeClass, source));
                    } catch (Exception e) {
                        logger.warn("Header '" + headerName + "' could not be deserialized.", e);
                    }
                } else {
                    normalizedHeaders.put(headerName, headerValue);
                }
            }
            return normalizedHeaders;
        }

        private Object extractPayload(DBObject source) {
            Object payload = source.get("payload");
            if (payload instanceof DBObject) {
                DBObject payloadObject = (DBObject) payload;
                Object payloadType = payloadObject.get("_class");
                try {
                    Class<?> payloadClass = ClassUtils.forName(payloadType.toString(), classLoader);
                    payload = this.read(payloadClass, payloadObject);
                } catch (Exception e) {
                    throw new IllegalStateException("failed to load class: " + payloadType, e);
                }
            }
            return payload;
        }

    }

    @SuppressWarnings("unchecked")
    private static void enhanceHeaders(MessageHeaders messageHeaders, Map<String, Object> headers) {
        Map<String, Object> innerMap = (Map<String, Object>) new DirectFieldAccessor(messageHeaders)
                .getPropertyValue("headers");
        // using reflection to set ID and TIMESTAMP since they are immutable through MessageHeaders
        innerMap.put(MessageHeaders.ID, headers.get(MessageHeaders.ID));
        innerMap.put(MessageHeaders.TIMESTAMP, headers.get(MessageHeaders.TIMESTAMP));
    }

    private static class UuidToDBObjectConverter implements Converter<UUID, DBObject> {
        @Override
        public DBObject convert(UUID source) {
            BasicDBObject dbObject = new BasicDBObject();
            dbObject.put("_value", source.toString());
            dbObject.put("_class", source.getClass().getName());
            return dbObject;
        }
    }

    private static class DBObjectToUUIDConverter implements Converter<DBObject, UUID> {
        @Override
        public UUID convert(DBObject source) {
            return UUID.fromString((String) source.get("_value"));
        }
    }

    private static class MessageHistoryToDBObjectConverter implements Converter<MessageHistory, DBObject> {

        @Override
        public DBObject convert(MessageHistory source) {
            BasicDBObject obj = new BasicDBObject();
            obj.put("_class", MessageHistory.class.getName());
            BasicDBList dbList = new BasicDBList();
            for (Properties properties : source) {
                BasicDBObject dbo = new BasicDBObject();
                dbo.put(MessageHistory.NAME_PROPERTY, properties.getProperty(MessageHistory.NAME_PROPERTY));
                dbo.put(MessageHistory.TYPE_PROPERTY, properties.getProperty(MessageHistory.TYPE_PROPERTY));
                dbo.put(MessageHistory.TIMESTAMP_PROPERTY,
                        properties.getProperty(MessageHistory.TIMESTAMP_PROPERTY));
                dbList.add(dbo);
            }
            obj.put("components", dbList);
            return obj;
        }
    }

    private class DBObjectToGenericMessageConverter implements Converter<DBObject, GenericMessage<?>> {

        @Override

        public GenericMessage<?> convert(DBObject source) {
            @SuppressWarnings("unchecked")
            Map<String, Object> headers = MongoDbMessageStore.this.converter
                    .normalizeHeaders((Map<String, Object>) source.get("headers"));

            GenericMessage<?> message = new GenericMessage<Object>(
                    MongoDbMessageStore.this.converter.extractPayload(source), headers);
            enhanceHeaders(message.getHeaders(), headers);
            return message;
        }

    }

    private class DBObjectToMutableMessageConverter implements GenericConverter {

        private final Class<?> mutableMessageClass;

        private DBObjectToMutableMessageConverter() {
            try {
                this.mutableMessageClass = ClassUtils.forName(
                        "org.springframework.integration.support.MutableMessage",
                        MongoDbMessageStore.this.classLoader);
            } catch (ClassNotFoundException e) {
                throw new IllegalStateException(e);
            }
        }

        @Override
        public Set<ConvertiblePair> getConvertibleTypes() {
            Set<ConvertiblePair> convertiblePairs = new HashSet<ConvertiblePair>();
            convertiblePairs.add(new ConvertiblePair(DBObject.class, this.mutableMessageClass));
            return convertiblePairs;
        }

        @Override
        public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
            DBObject dbObject = (DBObject) source;
            @SuppressWarnings("unchecked")
            Map<String, Object> headers = MongoDbMessageStore.this.converter
                    .normalizeHeaders((Map<String, Object>) dbObject.get("headers"));

            return MutableMessageBuilder.withPayload(MongoDbMessageStore.this.converter.extractPayload(dbObject))
                    .copyHeaders(headers).build();
        }
    }

    private class DBObjectToAdviceMessageConverter implements Converter<DBObject, AdviceMessage> {

        @Override
        public AdviceMessage convert(DBObject source) {
            @SuppressWarnings("unchecked")
            Map<String, Object> headers = MongoDbMessageStore.this.converter
                    .normalizeHeaders((Map<String, Object>) source.get("headers"));

            Message<?> inputMessage = null;

            if (source.get("inputMessage") != null) {
                DBObject inputMessageObject = (DBObject) source.get("inputMessage");
                Object inputMessageType = inputMessageObject.get("_class");
                try {
                    Class<?> messageClass = ClassUtils.forName(inputMessageType.toString(), classLoader);
                    inputMessage = (Message<?>) MongoDbMessageStore.this.converter.read(messageClass,
                            inputMessageObject);
                } catch (Exception e) {
                    throw new IllegalStateException("failed to load class: " + inputMessageType, e);
                }
            }

            AdviceMessage message = new AdviceMessage(MongoDbMessageStore.this.converter.extractPayload(source),
                    headers, inputMessage);
            enhanceHeaders(message.getHeaders(), headers);

            return message;
        }

    }

    private class DBObjectToErrorMessageConverter implements Converter<DBObject, ErrorMessage> {

        private final Converter<byte[], Object> deserializingConverter = new DeserializingConverter();

        @Override
        public ErrorMessage convert(DBObject source) {
            @SuppressWarnings("unchecked")
            Map<String, Object> headers = MongoDbMessageStore.this.converter
                    .normalizeHeaders((Map<String, Object>) source.get("headers"));

            Object payload = this.deserializingConverter.convert((byte[]) source.get("payload"));
            ErrorMessage message = new ErrorMessage((Throwable) payload, headers);
            enhanceHeaders(message.getHeaders(), headers);

            return message;
        }

    }

    @WritingConverter
    private class ThrowableToBytesConverter implements Converter<Throwable, byte[]> {

        private final Converter<Object, byte[]> serializingConverter = new SerializingConverter();

        @Override
        public byte[] convert(Throwable source) {
            return serializingConverter.convert(source);
        }

    }

    /**
     * Wrapper class used for storing Messages in MongoDB along with their "group" metadata.
     */
    private static final class MessageWrapper {

        /*
         * Needed as a persistence property to suppress 'Cannot determine IsNewStrategy' MappingException
         * when the application context is configured with auditing. The document is not
         * currently Auditable.
         */
        @SuppressWarnings("unused")
        @Id
        private String _id;

        private volatile Object _groupId;

        @Transient
        private final Message<?> message;

        @SuppressWarnings("unused")
        private final String _messageType;

        @SuppressWarnings("unused")
        private final Object payload;

        @SuppressWarnings("unused")
        private final Map<String, ?> headers;

        @SuppressWarnings("unused")
        private final Message<?> inputMessage;

        private volatile long _group_timestamp;

        private volatile long _group_update_timestamp;

        private volatile int _last_released_sequence;

        private volatile boolean _group_complete;

        @SuppressWarnings("unused")
        private int sequence;

        public MessageWrapper(Message<?> message) {
            Assert.notNull(message, "'message' must not be null");
            this.message = message;
            this._messageType = message.getClass().getName();
            this.payload = message.getPayload();
            this.headers = message.getHeaders();
            if (message instanceof AdviceMessage) {
                this.inputMessage = ((AdviceMessage) message).getInputMessage();
            } else {
                this.inputMessage = null;
            }
        }

        public int get_LastReleasedSequenceNumber() {
            return _last_released_sequence;
        }

        public long get_Group_timestamp() {
            return _group_timestamp;
        }

        public boolean get_Group_complete() {
            return _group_complete;
        }

        public Object get_GroupId() {
            return _groupId;
        }

        public Message<?> getMessage() {
            return message;
        }

        public void set_GroupId(Object groupId) {
            this._groupId = groupId;
        }

        public void set_Group_timestamp(long groupTimestamp) {
            this._group_timestamp = groupTimestamp;
        }

        public long get_Group_update_timestamp() {
            return _group_update_timestamp;
        }

        public void set_Group_update_timestamp(long lastModified) {
            this._group_update_timestamp = lastModified;
        }

        public void set_LastReleasedSequenceNumber(int lastReleasedSequenceNumber) {
            this._last_released_sequence = lastReleasedSequenceNumber;
        }

        public void set_Group_complete(boolean completedGroup) {
            this._group_complete = completedGroup;
        }

        public void setSequence(int sequence) {
            this.sequence = sequence;
        }

    }

}