/* Copyright 2014 Danish Maritime Authority.
*
* 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 net.maritimecloud.common.eventsourcing.axon;
import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.List;
import java.util.SortedSet;
import java.util.concurrent.ConcurrentSkipListSet;
import org.axonframework.domain.DomainEventMessage;
import org.axonframework.domain.DomainEventStream;
import org.axonframework.eventstore.EventVisitor;
import org.axonframework.eventstore.fs.FileSystemEventStore;
import org.axonframework.eventstore.fs.SimpleEventFileResolver;
import org.axonframework.eventstore.management.Criteria;
import org.axonframework.eventstore.management.CriteriaBuilder;
import org.axonframework.eventstore.management.EventStoreManagement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Simpleminded extension of FileSystemEventStore that supports EventStoreManagement in the simplest variant (no CriteriaBuilder support!)
* <p>
* @author Christoffer Børrild
*/
public class ReplayableFileSystemEventStore extends FileSystemEventStore implements EventStoreManagement {
private final static Logger LOG = LoggerFactory.getLogger(ReplayableFileSystemEventStore.class);
private final File baseDir;
private final SortedSet<DomainEventMessage> domainEventMessagesCache = new ConcurrentSkipListSet<>((DomainEventMessage m1, DomainEventMessage m2) -> {
// Compare:
long r = m1.getTimestamp().getMillis() - m2.getTimestamp().getMillis();
int i = r < 0 ? -1 : r == 0 ? 0 : 1;
if (i == 0) {
if (m1.getAggregateIdentifier().equals(m2.getAggregateIdentifier())) {
// same aggregaste at same time are ordered by sequence number
long r2 = m1.getSequenceNumber() - m2.getSequenceNumber();
return r2 < 0 ? -1 : r2 == 0 ? 0 : 1;
} else {
// some arbitrary but conscequent ordering
long r2 = m1.getIdentifier().compareTo(m2.getIdentifier());
};
}
return i;
});
public ReplayableFileSystemEventStore(File baseDir) {
super(new SimpleEventFileResolver(baseDir));
this.baseDir = baseDir;
}
@Override
public void visitEvents(EventVisitor visitor) {
if (!baseDir.exists()) {
// skipping since no file store
LOG.info("No event store found at {}", baseDir.getAbsoluteFile());
return;
}
resetEventCache();
// scan event store for types
List<File> types = scanForTypes();
// for each type scan for aggregates
types.stream().forEach((type) -> {
List<File> aggregateFiles = scanForAggregates(type);
LOG.info("indexing {} aggregates of type '{}'", aggregateFiles.size(), type.getName());
// for each aggregate register event messages in a big sorted set ordered by timestamp
aggregateFiles.stream().forEach((aggregateFile) -> {
readAndRegisterAggregateEvents(aggregateFile);
});
});
// finally, call visitor for each message in sequence
replayEvents(visitor);
LOG.info("Replayed {} events from {} aggregates.", domainEventMessagesCache.size(), types.size());
resetEventCache();
}
@Override
public void visitEvents(Criteria criteria, EventVisitor visitor) {
throw new UnsupportedOperationException("Not supported yet.");
}
@Override
public CriteriaBuilder newCriteriaBuilder() {
throw new UnsupportedOperationException("Not supported yet.");
}
private List<File> scanForTypes() {
List<File> types = new ArrayList<>();
for (File file : baseDir.listFiles()) {
if (file.isDirectory()) {
types.add(file);
}
}
return types;
}
private List<File> scanForAggregates(File typeDir) {
List<File> aggregateFiles = new ArrayList<>();
for (File file : typeDir.listFiles()) {
if (file.isFile()) {
aggregateFiles.add(file);
}
}
return aggregateFiles;
}
private void readAndRegisterAggregateEvents(File aggregateFile) {
DomainEventStream eventStream = readEvents(getType(aggregateFile), getIdentifier(aggregateFile));
while (eventStream.hasNext()) {
register(eventStream.next());
}
}
public static String getType(File aggregateFile) {
return aggregateFile.getParentFile().getName();
}
public static String getIdentifier(File aggregateIndentifier) {
try {
return URLDecoder.decode(aggregateIndentifier.getName(), "UTF-8").replace(".events", "");
} catch (UnsupportedEncodingException ex) {
throw new RuntimeException(ex);
}
}
private void resetEventCache() {
domainEventMessagesCache.clear();
}
private void register(DomainEventMessage domainEventMessage) {
domainEventMessagesCache.add(domainEventMessage);
}
private void replayEvents(EventVisitor visitor) {
domainEventMessagesCache.stream().forEach((domainEventMessage) -> {
visitor.doWithEvent(domainEventMessage);
});
}
}