org.axonframework.eventstore.mongo.MongoEventStore.java Source code

Java tutorial

Introduction

Here is the source code for org.axonframework.eventstore.mongo.MongoEventStore.java

Source

/*
 * 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();
        }
    }
}