/*
* Copyright (c) 2010-2014. Axon Framework
*
* 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.axonframework.eventstore.mongo;
import com.mongodb.Bytes;
import com.mongodb.DBCursor;
import com.mongodb.DBObject;
import com.mongodb.MongoException;
import org.axonframework.domain.DomainEventMessage;
import org.axonframework.domain.DomainEventStream;
import org.axonframework.eventstore.EventStreamNotFoundException;
import org.axonframework.eventstore.EventVisitor;
import org.axonframework.eventstore.PartialStreamSupport;
import org.axonframework.eventstore.SnapshotEventStore;
import org.axonframework.eventstore.management.Criteria;
import org.axonframework.eventstore.management.EventStoreManagement;
import org.axonframework.eventstore.mongo.criteria.MongoCriteria;
import org.axonframework.eventstore.mongo.criteria.MongoCriteriaBuilder;
import org.axonframework.repository.ConcurrencyException;
import org.axonframework.serializer.Serializer;
import org.axonframework.serializer.xml.XStreamSerializer;
import org.axonframework.upcasting.SimpleUpcasterChain;
import org.axonframework.upcasting.UpcasterAware;
import org.axonframework.upcasting.UpcasterChain;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Closeable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.annotation.PostConstruct;
/**
* <p>Implementation of the <code>EventStore</code> based on a MongoDB instance or replica set. Sharding and pairing
* are not explicitly supported.</p> <p>This event store implementation needs a serializer as well as a {@link
* MongoTemplate} to interact with the mongo database.</p> <p><strong>Warning:</strong> This implementation is
* still in progress and may be subject to alterations. The implementation works, but has not been optimized to fully
* leverage MongoDB's features, yet.</p>
*
* @author Jettro Coenradie
* @since 2.0 (in incubator since 0.7)
*/
public class MongoEventStore implements SnapshotEventStore, EventStoreManagement, UpcasterAware, PartialStreamSupport {
private static final Logger logger = LoggerFactory.getLogger(MongoEventStore.class);
private final MongoTemplate mongoTemplate;
private final Serializer eventSerializer;
private final StorageStrategy storageStrategy;
private UpcasterChain upcasterChain = SimpleUpcasterChain.EMPTY;
/**
* Constructor that accepts a Serializer and the MongoTemplate. A Document-Per-Event storage strategy is used,
* causing each event to be stored in a separate Mongo Document.
* <p/>
* <em>Note: the SerializedType of Message Meta Data is not stored. Upon retrieval, it is set to the default value
* (name = "org.axonframework.domain.MetaData", revision = null). See {@link org.axonframework.serializer.SerializedMetaData#isSerializedMetaData(org.axonframework.serializer.SerializedObject)}</em>
*
* @param eventSerializer Your own Serializer
* @param mongo Mongo instance to obtain the database and the collections.
*/
public MongoEventStore(Serializer eventSerializer, MongoTemplate mongo) {
this(mongo, eventSerializer, new DocumentPerEventStorageStrategy());
}
/**
* Constructor that uses the default Serializer. A Document-Per-Event storage strategy is used, causing each event
* to be stored in a separate Mongo Document.
*
* @param mongo MongoTemplate instance to obtain the database and the collections.
*/
public MongoEventStore(MongoTemplate mongo) {
this(new XStreamSerializer(), mongo);
}
/**
* Constructor that accepts a MongoTemplate and a custom StorageStrategy.
*
* @param mongoTemplate The template giving access to the required collections
* @param storageStrategy The strategy for storing and retrieving events from the collections
*/
public MongoEventStore(MongoTemplate mongoTemplate, StorageStrategy storageStrategy) {
this(mongoTemplate, new XStreamSerializer(), storageStrategy);
}
/**
* Initialize the mongo event store with given <code>mongoTemplate</code>, <code>eventSerializer</code> and
* <code>storageStrategy</code>.
*
* @param mongoTemplate The template giving access to the required collections
* @param eventSerializer The serializer to serialize events with
* @param storageStrategy The strategy for storing and retrieving events from the collections
*/
public MongoEventStore(MongoTemplate mongoTemplate, Serializer eventSerializer, StorageStrategy storageStrategy) {
this.eventSerializer = eventSerializer;
this.mongoTemplate = mongoTemplate;
this.storageStrategy = storageStrategy;
}
/**
* Make sure an index is created on the collection that stores domain events.
*/
@PostConstruct
public void ensureIndexes() {
storageStrategy.ensureIndexes(mongoTemplate.domainEventCollection(), mongoTemplate.snapshotEventCollection());
}
@Override
public void appendEvents(String type, DomainEventStream events) {
if (!events.hasNext()) {
return;
}
List<DomainEventMessage> messages = new ArrayList<DomainEventMessage>();
while (events.hasNext()) {
messages.add(events.next());
}
try {
mongoTemplate.domainEventCollection().insert(storageStrategy.createDocuments(type,
eventSerializer,
messages));
} catch (MongoException.DuplicateKey e) {
throw new ConcurrencyException("Trying to insert an Event for an aggregate with a sequence "
+ "number that is already present in the Event Store", e);
}
if (logger.isDebugEnabled()) {
logger.debug("{} events appended", new Object[]{messages.size()});
}
}
@Override
public DomainEventStream readEvents(String type, Object identifier) {
long snapshotSequenceNumber = -1;
List<DomainEventMessage> lastSnapshotCommit = loadLastSnapshotEvent(type, identifier);
if (lastSnapshotCommit != null && !lastSnapshotCommit.isEmpty()) {
snapshotSequenceNumber = lastSnapshotCommit.get(0).getSequenceNumber();
}
final DBCursor dbCursor = storageStrategy.findEvents(mongoTemplate.domainEventCollection(),
type,
identifier.toString(),
snapshotSequenceNumber + 1);
DomainEventStream stream = new CursorBackedDomainEventStream(dbCursor, lastSnapshotCommit, identifier, false);
if (!stream.hasNext()) {
throw new EventStreamNotFoundException(type, identifier);
}
return stream;
}
@Override
public DomainEventStream readEvents(String type, Object identifier, long firstSequenceNumber) {
return readEvents(type, identifier, firstSequenceNumber, Long.MAX_VALUE);
}
@Override
public DomainEventStream readEvents(String type, Object identifier, long firstSequenceNumber,
long lastSequenceNumber) {
final DBCursor dbCursor = storageStrategy.findEvents(mongoTemplate.domainEventCollection(),
type,
identifier.toString(),
firstSequenceNumber);
DomainEventStream stream = new CursorBackedDomainEventStream(dbCursor, null, identifier, lastSequenceNumber,
false);
if (!stream.hasNext()) {
throw new EventStreamNotFoundException(type, identifier);
}
return stream;
}
@Override
public void appendSnapshotEvent(String type, DomainEventMessage snapshotEvent) {
final DBObject dbObject = storageStrategy.createDocuments(type, eventSerializer,
Collections.singletonList(snapshotEvent))[0];
try {
mongoTemplate.snapshotEventCollection().insert(dbObject);
} catch (MongoException.DuplicateKey e) {
throw new ConcurrencyException("Trying to insert a SnapshotEvent with aggregate identifier and sequence "
+ "number that is already present in the Event Store", e);
}
if (logger.isDebugEnabled()) {
logger.debug("snapshot event of type {} appended.");
}
}
@Override
public void visitEvents(EventVisitor visitor) {
visitEvents(null, visitor);
}
@Override
public void visitEvents(Criteria criteria, EventVisitor visitor) {
DBCursor cursor = storageStrategy.findEvents(mongoTemplate.domainEventCollection(),
(MongoCriteria) criteria);
cursor.addOption(Bytes.QUERYOPTION_NOTIMEOUT);
CursorBackedDomainEventStream events = new CursorBackedDomainEventStream(cursor, null, null, true);
try {
while (events.hasNext()) {
visitor.doWithEvent(events.next());
}
} finally {
events.close();
}
}
@Override
public MongoCriteriaBuilder newCriteriaBuilder() {
return new MongoCriteriaBuilder();
}
private List<DomainEventMessage> loadLastSnapshotEvent(String type, Object identifier) {
DBCursor dbCursor = storageStrategy.findLastSnapshot(mongoTemplate.snapshotEventCollection(),
type,
identifier.toString());
if (!dbCursor.hasNext()) {
return null;
}
DBObject first = dbCursor.next();
return storageStrategy.extractEventMessages(first, identifier, eventSerializer, upcasterChain, false);
}
@Override
public void setUpcasterChain(UpcasterChain upcasterChain) {
this.upcasterChain = upcasterChain;
}
private class CursorBackedDomainEventStream implements DomainEventStream, Closeable {
private Iterator<DomainEventMessage> messagesToReturn = Collections.<DomainEventMessage>emptyList().iterator();
private DomainEventMessage next;
private final DBCursor dbCursor;
private final Object actualAggregateIdentifier;
private final long lastSequenceNumber;
private boolean skipUnknownTypes;
/**
* Initializes the DomainEventStream, streaming events obtained from the given <code>dbCursor</code> and
* optionally the given <code>lastSnapshotEvent</code>.
*
* @param dbCursor The cursor providing access to the query results in the Mongo instance
* @param lastSnapshotCommit The last snapshot event read, or <code>null</code> if no snapshot is
* available
* @param actualAggregateIdentifier The actual aggregateIdentifier instance used to perform the lookup, or
* <code>null</code> if unknown
* @param skipUnknownTypes Whether or not the stream should ignore events that cannot be deserialized
*/
public CursorBackedDomainEventStream(DBCursor dbCursor, List<DomainEventMessage> lastSnapshotCommit,
Object actualAggregateIdentifier, boolean skipUnknownTypes) {
this(dbCursor, lastSnapshotCommit, actualAggregateIdentifier, Long.MAX_VALUE, skipUnknownTypes);
}
/**
* Initializes the DomainEventStream, streaming events obtained from the given <code>dbCursor</code> and
* optionally the given <code>lastSnapshotEvent</code>, which stops streaming once an event with a sequence
* number higher given than <code>lastSequenceNumber</code>.
*
* @param dbCursor The cursor providing access to the query results in the Mongo instance
* @param lastSnapshotCommit The last snapshot event read, or <code>null</code> if no snapshot is
* available
* @param actualAggregateIdentifier The actual aggregateIdentifier instance used to perform the lookup, or
* <code>null</code> if unknown
* @param lastSequenceNumber The highest sequence number this stream may return before indicating
* end-of-stream
* @param skipUnknownTypes Whether or not the stream should ignore events that cannot be deserialized
*/
public CursorBackedDomainEventStream(DBCursor dbCursor, List<DomainEventMessage> lastSnapshotCommit,
Object actualAggregateIdentifier, long lastSequenceNumber,
boolean skipUnknownTypes) {
this.dbCursor = dbCursor;
this.actualAggregateIdentifier = actualAggregateIdentifier;
this.lastSequenceNumber = lastSequenceNumber;
this.skipUnknownTypes = skipUnknownTypes;
if (lastSnapshotCommit != null) {
messagesToReturn = lastSnapshotCommit.iterator();
}
initializeNextItem();
}
@Override
public boolean hasNext() {
return next != null && next.getSequenceNumber() <= lastSequenceNumber;
}
@Override
public DomainEventMessage next() {
DomainEventMessage itemToReturn = next;
initializeNextItem();
return itemToReturn;
}
@Override
public DomainEventMessage peek() {
return next;
}
/**
* Ensures that the <code>next</code> points to the correct item, possibly reading from the dbCursor.
*/
private void initializeNextItem() {
while (!messagesToReturn.hasNext() && dbCursor.hasNext()) {
messagesToReturn = storageStrategy.extractEventMessages(dbCursor.next(), actualAggregateIdentifier,
eventSerializer, upcasterChain,
skipUnknownTypes).iterator();
}
next = messagesToReturn.hasNext() ? messagesToReturn.next() : null;
}
@Override
public void close() {
dbCursor.close();
}
}
}