/*
* Copyright 2002-2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.integration.mongodb.store;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.UUID;
import java.util.stream.Collectors;
import org.bson.Document;
import org.bson.conversions.Bson;
import org.bson.types.Binary;
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.converter.Converter;
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.ReadingConverter;
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.BulkOperations;
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.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
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.MessageMetadata;
import org.springframework.integration.store.MessageStore;
import org.springframework.integration.store.SimpleMessageGroup;
import org.springframework.integration.support.MutableMessage;
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.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.
* @deprecated since 5.0. This constant isn't used any more.
*/
@Deprecated
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.
* @deprecated since 5.0. This constant isn't used any more.
*/
@Deprecated
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.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(MessageWrapper document) {
UUID messageId = (UUID) document.headers.get(MessageHeaders.ID);
Query query = whereMessageIdIsAndGroupIdIs(messageId, document.get_GroupId());
if (!this.template.exists(query, MessageWrapper.class, this.collectionName)) {
if (document.get_Group_timestamp() == 0) {
document.set_Group_timestamp(System.currentTimeMillis());
}
document.set_message_timestamp(System.currentTimeMillis());
this.template.insert(document, this.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
public MessageMetadata getMessageMetadata(UUID id) {
Assert.notNull(id, "'id' must not be null");
MessageWrapper messageWrapper =
this.template.findOne(whereMessageIdIs(id), MessageWrapper.class, this.collectionName);
if (messageWrapper != null) {
MessageMetadata messageMetadata = new MessageMetadata(id);
messageMetadata.setTimestamp(messageWrapper.get_message_timestamp());
return messageMetadata;
}
else {
return null;
}
}
@Override
@ManagedAttribute
public long getMessageCount() {
return this.template.getCollection(this.collectionName).count();
}
@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);
MessageWrapper messageWrapper = this.template.findOne(query, MessageWrapper.class, this.collectionName);
if (messageWrapper != null) {
long createdTime = messageWrapper.get_Group_timestamp();
long lastModifiedTime = messageWrapper.get_Group_update_timestamp();
boolean complete = messageWrapper.get_Group_complete();
int lastReleasedSequence = messageWrapper.get_LastReleasedSequenceNumber();
MessageGroup messageGroup = getMessageGroupFactory()
.create(this, groupId, createdTime, complete);
messageGroup.setLastModified(lastModifiedTime);
messageGroup.setLastReleasedMessageSequenceNumber(lastReleasedSequence);
return messageGroup;
}
else {
return new SimpleMessageGroup(groupId);
}
}
@Override
public void addMessagesToGroup(Object groupId, Message<?>... messages) {
Assert.notNull(groupId, "'groupId' must not be null");
Assert.notNull(messages, "'message' must not be null");
Query query = whereGroupIdOrder(groupId);
MessageWrapper messageDocument = this.template.findOne(query, MessageWrapper.class, this.collectionName);
long createdTime = System.currentTimeMillis();
int lastReleasedSequence = 0;
boolean complete = false;
if (messageDocument != null) {
createdTime = messageDocument.get_Group_timestamp();
lastReleasedSequence = messageDocument.get_LastReleasedSequenceNumber();
complete = messageDocument.get_Group_complete();
}
for (Message<?> message : messages) {
MessageWrapper wrapper = new MessageWrapper(message);
wrapper.set_GroupId(groupId);
wrapper.set_Group_timestamp(createdTime);
wrapper.set_Group_update_timestamp(messageDocument == null ? createdTime : System.currentTimeMillis());
wrapper.set_Group_complete(complete);
wrapper.set_LastReleasedSequenceNumber(lastReleasedSequence);
wrapper.set_Sequence(getNextId());
addMessageDocument(wrapper);
}
}
@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<>();
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) {
BulkOperations bulkOperations = this.template.bulkOps(BulkOperations.BulkMode.ORDERED, this.collectionName);
for (UUID id : ids) {
bulkOperations.remove(whereMessageIdIsAndGroupIdIs(id, groupId));
}
bulkOperations.execute();
}
@Override
public void removeMessageGroup(Object groupId) {
this.template.remove(whereGroupIdIs(groupId), this.collectionName);
}
@Override
public Iterator<MessageGroup> iterator() {
List<MessageGroup> messageGroups = new ArrayList<>();
Query query = Query.query(Criteria.where(GROUP_ID_KEY).exists(true));
@SuppressWarnings("rawtypes")
Iterable<String> groupIds = this.template.getCollection(this.collectionName)
.distinct(GROUP_ID_KEY, query.getQueryObject(), String.class);
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(Sort.by(GROUP_UPDATE_TIMESTAMP_KEY, SEQUENCE));
MessageWrapper messageWrapper = this.template.findAndRemove(query, MessageWrapper.class, this.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
public Message<?> getOneMessageFromGroup(Object groupId) {
Assert.notNull(groupId, "'groupId' must not be null");
Query query = whereGroupIdOrder(groupId);
MessageWrapper messageWrapper = this.template.findOne(query, MessageWrapper.class, this.collectionName);
if (messageWrapper != null) {
return messageWrapper.getMessage();
}
else {
return null;
}
}
@Override
public Collection<Message<?>> getMessagesForGroup(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);
return messageWrappers.stream()
.map(MessageWrapper::getMessage)
.collect(Collectors.toList());
}
@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(), Object.class)
.into(new ArrayList<>())
.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").is(id));
}
private static Query whereMessageIdIsAndGroupIdIs(UUID id, Object groupId) {
return new Query(Criteria.where("headers.id").is(id).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);
}
@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));
}
@SuppressWarnings("unchecked")
private static Map<String, Object> asMap(Bson bson) {
if (bson instanceof Document) {
return (Document) bson;
}
if (bson instanceof DBObject) {
return ((DBObject) bson).toMap();
}
throw new IllegalArgumentException(
String.format("Cannot read %s. as map. Given Bson must be a Document or DBObject!", bson.getClass()));
}
/**
* Custom implementation of the {@link MappingMongoConverter} strategy.
*/
private final class MessageReadingMongoConverter extends MappingMongoConverter {
MessageReadingMongoConverter(MongoDbFactory mongoDbFactory,
MappingContext<? extends MongoPersistentEntity<?>, MongoPersistentProperty> mappingContext) {
super(new DefaultDbRefResolver(mongoDbFactory), mappingContext);
}
@Override
public void afterPropertiesSet() {
List<Object> customConverters = new ArrayList<>();
customConverters.add(new MessageHistoryToDocumentConverter());
customConverters.add(new DocumentToGenericMessageConverter());
customConverters.add(new DocumentToMutableMessageConverter());
customConverters.add(new DocumentToErrorMessageConverter());
customConverters.add(new DocumentToAdviceMessageConverter());
customConverters.add(new ThrowableToBytesConverter());
this.setCustomConversions(new MongoCustomConversions(customConverters));
super.afterPropertiesSet();
}
@Override
public void write(Object source, Bson target) {
Assert.isInstanceOf(MessageWrapper.class, source);
asMap(target).put(CREATED_DATE, System.currentTimeMillis());
super.write(source, target);
}
@Override
@SuppressWarnings({ "unchecked" })
public <S> S read(Class<S> clazz, Bson source) {
if (!MessageWrapper.class.equals(clazz)) {
return super.read(clazz, source);
}
if (source != null) {
Map<String, Object> sourceMap = asMap(source);
Message<?> message = null;
Object messageType = sourceMap.get("_messageType");
if (messageType == null) {
messageType = GenericMessage.class.getName();
}
try {
message = (Message<?>) read(ClassUtils.forName(messageType.toString(),
MongoDbMessageStore.this.classLoader), source);
}
catch (ClassNotFoundException e) {
throw new IllegalStateException("failed to load class: " + messageType, e);
}
Long groupTimestamp = (Long) sourceMap.get(GROUP_TIMESTAMP_KEY);
Long lastModified = (Long) sourceMap.get(GROUP_UPDATE_TIMESTAMP_KEY);
Integer lastReleasedSequenceNumber = (Integer) sourceMap.get(LAST_RELEASED_SEQUENCE_NUMBER);
Boolean completeGroup = (Boolean) sourceMap.get(GROUP_COMPLETE_KEY);
MessageWrapper wrapper = new MessageWrapper(message);
if (sourceMap.containsKey(GROUP_ID_KEY)) {
wrapper.set_GroupId(sourceMap.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 Bson) {
Bson source = (Bson) headerValue;
Map<String, Object> document = asMap(source);
try {
Class<?> typeClass = null;
if (document.containsKey("_class")) {
Object type = document.get("_class");
typeClass = ClassUtils.forName(type.toString(), MongoDbMessageStore.this.classLoader);
}
else if (source instanceof BasicDBList) {
typeClass = List.class;
}
else {
throw new IllegalStateException("Unsupported 'Bson' 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(Bson source) {
Object payload = asMap(source).get("payload");
if (payload instanceof Bson) {
Bson payloadObject = (Bson) payload;
Object payloadType = asMap(payloadObject).get("_class");
try {
Class<?> payloadClass = ClassUtils.forName(payloadType.toString(), MongoDbMessageStore.this.classLoader);
payload = read(payloadClass, payloadObject);
}
catch (Exception e) {
throw new IllegalStateException("failed to load class: " + payloadType, e);
}
}
return payload;
}
}
@WritingConverter
private static class MessageHistoryToDocumentConverter implements Converter<MessageHistory, Document> {
MessageHistoryToDocumentConverter() {
super();
}
@Override
public Document convert(MessageHistory source) {
BasicDBList dbList = new BasicDBList();
for (Properties properties : source) {
Document historyProperty = new Document()
.append(MessageHistory.NAME_PROPERTY, properties.getProperty(MessageHistory.NAME_PROPERTY))
.append(MessageHistory.TYPE_PROPERTY, properties.getProperty(MessageHistory.TYPE_PROPERTY))
.append(MessageHistory.TIMESTAMP_PROPERTY,
properties.getProperty(MessageHistory.TIMESTAMP_PROPERTY));
dbList.add(historyProperty);
}
return new Document("components", dbList)
.append("_class", MessageHistory.class.getName());
}
}
@ReadingConverter
private class DocumentToGenericMessageConverter implements Converter<Document, GenericMessage<?>> {
DocumentToGenericMessageConverter() {
super();
}
@Override
public GenericMessage<?> convert(Document source) {
@SuppressWarnings("unchecked")
Map<String, Object> headers =
MongoDbMessageStore.this.converter.normalizeHeaders((Map<String, Object>) source.get("headers"));
GenericMessage<?> message =
new GenericMessage<>(MongoDbMessageStore.this.converter.extractPayload(source), headers);
enhanceHeaders(message.getHeaders(), headers);
return message;
}
}
@ReadingConverter
private final class DocumentToMutableMessageConverter implements Converter<Document, MutableMessage<?>> {
DocumentToMutableMessageConverter() {
super();
}
@Override
public MutableMessage<?> convert(Document source) {
@SuppressWarnings("unchecked")
Map<String, Object> headers =
MongoDbMessageStore.this.converter.normalizeHeaders((Map<String, Object>) source.get("headers"));
Object payload = MongoDbMessageStore.this.converter.extractPayload(source);
return (MutableMessage<?>) MutableMessageBuilder.withPayload(payload)
.copyHeaders(headers)
.build();
}
}
@ReadingConverter
private class DocumentToAdviceMessageConverter implements Converter<Document, AdviceMessage<?>> {
DocumentToAdviceMessageConverter() {
super();
}
@Override
public AdviceMessage<?> convert(Document 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) {
Bson inputMessageObject = (Bson) source.get("inputMessage");
Object inputMessageType = asMap(inputMessageObject).get("_class");
try {
Class<?> messageClass = ClassUtils.forName(inputMessageType.toString(),
MongoDbMessageStore.this.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;
}
}
@ReadingConverter
private class DocumentToErrorMessageConverter implements Converter<Document, ErrorMessage> {
private final Converter<byte[], Object> deserializingConverter = new DeserializingConverter();
DocumentToErrorMessageConverter() {
super();
}
@Override
public ErrorMessage convert(Document source) {
@SuppressWarnings("unchecked")
Map<String, Object> headers =
MongoDbMessageStore.this.converter.normalizeHeaders((Map<String, Object>) source.get("headers"));
Object payload = this.deserializingConverter.convert(((Binary) source.get("payload")).getData());
ErrorMessage message = new ErrorMessage((Throwable) payload, headers);
enhanceHeaders(message.getHeaders(), headers);
return message;
}
}
@WritingConverter
private static class ThrowableToBytesConverter implements Converter<Throwable, byte[]> {
private final Converter<Object, byte[]> serializingConverter = new SerializingConverter();
ThrowableToBytesConverter() {
super();
}
@Override
public byte[] convert(Throwable source) {
return this.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 long _message_timestamp;
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;
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 this._last_released_sequence;
}
public long get_Group_timestamp() {
return this._group_timestamp;
}
public boolean get_Group_complete() {
return this._group_complete;
}
@SuppressWarnings("unused")
public Object get_GroupId() {
return this._groupId;
}
public Message<?> getMessage() {
return this.message;
}
public void set_GroupId(Object groupId) {
this._groupId = groupId;
}
public void set_Group_timestamp(long groupTimestamp) {
this._group_timestamp = groupTimestamp;
}
public long get_message_timestamp() {
return this._message_timestamp;
}
public void set_message_timestamp(long _message_timestamp) {
this._message_timestamp = _message_timestamp;
}
public long get_Group_update_timestamp() {
return this._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 set_Sequence(int sequence) {
this.sequence = sequence;
}
}
}