JAMES-2393 Introduce dedicated modules for event sourcing This intend to ease future usage, external usage as well as allow a clear view.
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/189490a4 Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/189490a4 Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/189490a4 Branch: refs/heads/master Commit: 189490a4eaabaa9876a0c73b3f5f035ec6565c65 Parents: c2b85a0 Author: benwa <btell...@linagora.com> Authored: Thu May 10 11:29:21 2018 +0700 Committer: Matthieu Baechler <matth...@apache.org> Committed: Thu May 17 09:50:00 2018 +0200 ---------------------------------------------------------------------- event-sourcing/event-sourcing-core/pom.xml | 90 +++++++ .../james/eventsourcing/CommandDispatcher.java | 110 ++++++++ .../james/eventsourcing/CommandHandler.java | 29 +++ .../apache/james/eventsourcing/EventBus.java | 63 +++++ .../eventsourcing/EventSourcingSystem.java | 37 +++ .../apache/james/eventsourcing/Subscriber.java | 24 ++ .../eventsourcing/DataCollectorSubscriber.java | 46 ++++ .../eventsourcing/EventSourcingSystemTest.java | 252 +++++++++++++++++++ event-sourcing/event-sourcing-pojo/pom.xml | 67 +++++ .../apache/james/eventsourcing/AggregateId.java | 24 ++ .../org/apache/james/eventsourcing/Command.java | 23 ++ .../org/apache/james/eventsourcing/Event.java | 43 ++++ .../org/apache/james/eventsourcing/EventId.java | 86 +++++++ .../apache/james/eventsourcing/EventIdTest.java | 85 +++++++ .../james/eventsourcing/TestAggregateId.java | 68 +++++ .../apache/james/eventsourcing/TestEvent.java | 82 ++++++ event-sourcing/event-store-api/pom.xml | 77 ++++++ .../eventsourcing/eventstore/EventStore.java | 47 ++++ .../eventstore/EventStoreFailedException.java | 23 ++ .../james/eventsourcing/eventstore/History.java | 94 +++++++ .../eventstore/EventStoreTest.java | 88 +++++++ .../eventsourcing/eventstore/HistoryTest.java | 91 +++++++ event-sourcing/event-store-cassandra/pom.xml | 131 ++++++++++ .../cassandra/CassandraEventStore.java | 64 +++++ .../cassandra/CassandraEventStoreModule.java | 62 +++++ .../cassandra/CassandraEventStoreTable.java | 27 ++ .../eventstore/cassandra/EventStoreDao.java | 122 +++++++++ .../cassandra/JsonEventSerializer.java | 98 ++++++++ .../eventstore/cassandra/dto/EventDTO.java | 26 ++ .../cassandra/dto/EventDTOModule.java | 32 +++ .../CassandraEventSourcingSystemTest.java | 28 +++ .../cassandra/CassandraEventStoreExtension.java | 30 +++ .../cassandra/CassandraEventStoreTest.java | 28 +++ .../CassandraGenericEventStoreExtension.java | 87 +++++++ .../cassandra/JsonEventSerializerTest.java | 102 ++++++++ .../eventstore/cassandra/dto/OtherEvent.java | 50 ++++ .../cassandra/dto/OtherTestEventDTO.java | 72 ++++++ .../cassandra/dto/OtherTestEventDTOModule.java | 55 ++++ .../eventstore/cassandra/dto/TestEventDTO.java | 73 ++++++ .../cassandra/dto/TestEventDTOModule.java | 56 +++++ event-sourcing/event-store-memory/pom.xml | 86 +++++++ .../eventstore/memory/InMemoryEventStore.java | 99 ++++++++ .../memory/InMemoryEventSourcingSystemTest.java | 28 +++ .../memory/InMemoryEventStoreExtension.java | 39 +++ .../memory/InMemoryEventStoreTest.java | 28 +++ event-sourcing/pom.xml | 44 ++++ mailbox/plugin/quota-mailing-cassandra/pom.xml | 21 ++ .../cassandra/CassandraEventStore.java | 62 ----- .../cassandra/CassandraEventStoreModule.java | 62 ----- .../cassandra/CassandraEventStoreTable.java | 27 -- .../eventsourcing/cassandra/EventStoreDao.java | 122 --------- .../cassandra/JsonEventSerializer.java | 98 -------- .../eventsourcing/cassandra/dto/EventDTO.java | 26 -- .../cassandra/dto/EventDTOModule.java | 32 --- .../dto/QuotaThresholdChangedEventDTO.java | 2 +- .../QuotaThresholdChangedEventDTOModule.java | 4 +- .../CassandraEventSourcingSystemTest.java | 28 --- .../cassandra/CassandraEventStoreExtension.java | 86 ------- .../cassandra/CassandraEventStoreTest.java | 28 --- .../cassandra/JsonEventSerializerTest.java | 102 -------- .../eventsourcing/cassandra/dto/OtherEvent.java | 50 ---- .../cassandra/dto/OtherTestEventDTO.java | 72 ------ .../cassandra/dto/OtherTestEventDTOModule.java | 55 ---- .../cassandra/dto/TestEventDTO.java | 73 ------ .../cassandra/dto/TestEventDTOModule.java | 56 ----- .../mailbox/quota/cassandra/dto/DTOTest.java | 30 +-- .../listeners/CassandraEventStoreExtension.java | 32 +++ ...draQuotaMailingListenersIntegrationTest.java | 1 - mailbox/plugin/quota-mailing-memory/pom.xml | 21 ++ .../james/eventsource/InMemoryEventStore.java | 97 ------- .../InMemoryEventSourcingSystemTest.java | 27 -- .../InMemoryEventStoreExtension.java | 39 --- .../eventsourcing/InMemoryEventStoreTest.java | 27 -- ...oryQuotaMailingListenersIntegrationTest.java | 2 +- ...yQuotaThresholdConfigurationChangesTest.java | 2 +- mailbox/plugin/quota-mailing/pom.xml | 14 ++ .../apache/james/eventsourcing/AggregateId.java | 24 -- .../james/eventsourcing/CommandDispatcher.java | 117 --------- .../org/apache/james/eventsourcing/Event.java | 43 ---- .../apache/james/eventsourcing/EventBus.java | 61 ----- .../org/apache/james/eventsourcing/EventId.java | 86 ------- .../eventsourcing/EventSourcingSystem.java | 36 --- .../apache/james/eventsourcing/EventStore.java | 114 --------- .../apache/james/eventsourcing/Subscriber.java | 24 -- .../mailing/aggregates/UserQuotaThresholds.java | 8 +- .../commands/DetectThresholdCrossing.java | 4 +- .../DetectThresholdCrossingHandler.java | 9 +- .../eventsourcing/DataCollectorSubscriber.java | 46 ---- .../apache/james/eventsourcing/EventIdTest.java | 85 ------- .../eventsourcing/EventSourcingSystemTest.java | 250 ------------------ .../james/eventsourcing/EventStoreTest.java | 75 ------ .../apache/james/eventsourcing/HistoryTest.java | 87 ------- .../james/eventsourcing/TestAggregateId.java | 68 ----- .../apache/james/eventsourcing/TestEvent.java | 82 ------ .../QuotaThresholdConfigurationChangesTest.java | 2 +- .../QuotaThresholdListenersTestSystem.java | 2 +- .../QuotaThresholdMailingIntegrationTest.java | 2 +- pom.xml | 61 +++++ 98 files changed, 3199 insertions(+), 2401 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-core/pom.xml ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-core/pom.xml b/event-sourcing/event-sourcing-core/pom.xml new file mode 100644 index 0000000..7b7c6e1 --- /dev/null +++ b/event-sourcing/event-sourcing-core/pom.xml @@ -0,0 +1,90 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.james</groupId> + <artifactId>event-sourcing</artifactId> + <version>3.1.0-SNAPSHOT</version> + </parent> + + <artifactId>event-sourcing-core</artifactId> + + <name>Apache James :: Event sourcing :: core</name> + <description>James Event Sourcing system</description> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>event-sourcing-event-store-api</artifactId> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>event-sourcing-pojo</artifactId> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>event-sourcing-pojo</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.github.steveash.guavate</groupId> + <artifactId>guavate</artifactId> + </dependency> + <dependency> + <groupId>javax.inject</groupId> + <artifactId>javax.inject</artifactId> + </dependency> + <dependency> + <groupId>nl.jqno.equalsverifier</groupId> + <artifactId>equalsverifier</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + </dependencies> + +</project> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/CommandDispatcher.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/CommandDispatcher.java b/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/CommandDispatcher.java new file mode 100644 index 0000000..15d128c --- /dev/null +++ b/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/CommandDispatcher.java @@ -0,0 +1,110 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.IntStream; + +import javax.inject.Inject; + +import org.apache.james.eventsourcing.eventstore.EventStoreFailedException; + +import com.github.steveash.guavate.Guavate; + +public class CommandDispatcher { + + private static final int MAX_RETRY = 10; + + public class UnknownCommandException extends RuntimeException { + private final Command command; + + public UnknownCommandException(Command command) { + super(String.format("Unknown command %s", command)); + this.command = command; + } + + public Command getCommand() { + return command; + } + } + + public class TooManyRetries extends RuntimeException { + private final Command command; + private final int retries; + + + public TooManyRetries(Command command, int retries) { + super(String.format("Too much retries for command %s. Store failure after %d retries", command, retries)); + this.command = command; + this.retries = retries; + } + + + public Command getCommand() { + return command; + } + + public int getRetries() { + return retries; + } + } + + private final EventBus eventBus; + @SuppressWarnings("rawtypes") + private final Map<Class, CommandHandler> handlers; + + @Inject + public CommandDispatcher(EventBus eventBus, Set<CommandHandler<?>> handlers) { + this.eventBus = eventBus; + this.handlers = handlers.stream() + .collect(Guavate.toImmutableMap(CommandHandler::handledClass, handler -> handler)); + } + + public void dispatch(Command c) { + trySeveralTimes(() -> tryDispatch(c)) + .orElseThrow(() -> new TooManyRetries(c, MAX_RETRY)); + } + + public Optional<Integer> trySeveralTimes(Supplier<Boolean> singleTry) { + return IntStream.range(0, MAX_RETRY) + .boxed() + .filter(any -> singleTry.get()) + .findFirst(); + } + + @SuppressWarnings("unchecked") + private boolean tryDispatch(Command c) { + try { + List<Event> events = + Optional.ofNullable(handlers.get(c.getClass())) + .map(f -> f.handle(c)) + .orElseThrow(() -> new UnknownCommandException(c)); + + eventBus.publish(events); + return true; + } catch (EventStoreFailedException e) { + return false; + } + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/CommandHandler.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/CommandHandler.java b/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/CommandHandler.java new file mode 100644 index 0000000..6b8aacf --- /dev/null +++ b/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/CommandHandler.java @@ -0,0 +1,29 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +import java.util.List; + +public interface CommandHandler<C> { + + Class<C> handledClass(); + + List<? extends Event> handle(C c); +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/EventBus.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/EventBus.java b/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/EventBus.java new file mode 100644 index 0000000..289433e --- /dev/null +++ b/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/EventBus.java @@ -0,0 +1,63 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; + +import org.apache.commons.lang3.tuple.Pair; +import org.apache.james.eventsourcing.eventstore.EventStore; +import org.apache.james.eventsourcing.eventstore.EventStoreFailedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.collect.ImmutableSet; + +public class EventBus { + + public static final Logger LOGGER = LoggerFactory.getLogger(EventBus.class); + private final EventStore eventStore; + private final Set<Subscriber> subscribers; + + @Inject + public EventBus(EventStore eventStore, Set<Subscriber> subscribers) { + this.eventStore = eventStore; + this.subscribers = ImmutableSet.copyOf(subscribers); + } + + public void publish(List<Event> events) throws EventStoreFailedException { + eventStore.appendAll(events); + events.stream() + .flatMap(event -> subscribers.stream().map(subscriber -> Pair.of(event, subscriber))) + .forEach(this::handle); + } + + public void handle(Pair<Event, Subscriber> pair) { + Subscriber subscriber = pair.getRight(); + Event event = pair.getLeft(); + try { + subscriber.handle(event); + } catch (Exception e) { + LOGGER.error("Error while calling {} for {}", subscriber, event, e); + } + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/EventSourcingSystem.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/EventSourcingSystem.java b/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/EventSourcingSystem.java new file mode 100644 index 0000000..e077fe6 --- /dev/null +++ b/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/EventSourcingSystem.java @@ -0,0 +1,37 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +import java.util.Set; + +import org.apache.james.eventsourcing.eventstore.EventStore; + +public class EventSourcingSystem { + private final CommandDispatcher commandDispatcher; + + public EventSourcingSystem(Set<CommandHandler<?>> handlers, Set<Subscriber> subscribers, EventStore eventStore) { + EventBus eventBus = new EventBus(eventStore, subscribers); + this.commandDispatcher = new CommandDispatcher(eventBus, handlers); + } + + public void dispatch(Command c) { + commandDispatcher.dispatch(c); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/Subscriber.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/Subscriber.java b/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/Subscriber.java new file mode 100644 index 0000000..42a804d --- /dev/null +++ b/event-sourcing/event-sourcing-core/src/main/java/org/apache/james/eventsourcing/Subscriber.java @@ -0,0 +1,24 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +public interface Subscriber { + void handle(Event event); +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-core/src/test/java/org/apache/james/eventsourcing/DataCollectorSubscriber.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-core/src/test/java/org/apache/james/eventsourcing/DataCollectorSubscriber.java b/event-sourcing/event-sourcing-core/src/test/java/org/apache/james/eventsourcing/DataCollectorSubscriber.java new file mode 100644 index 0000000..85fccd1 --- /dev/null +++ b/event-sourcing/event-sourcing-core/src/test/java/org/apache/james/eventsourcing/DataCollectorSubscriber.java @@ -0,0 +1,46 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +import java.util.ArrayList; +import java.util.List; + +import com.google.common.collect.ImmutableList; + +public class DataCollectorSubscriber implements Subscriber { + + private final List<String> data; + + public DataCollectorSubscriber() { + data = new ArrayList<>(); + } + + @Override + public void handle(Event event) { + if (event instanceof TestEvent) { + data.add(((TestEvent) event).getData()); + } + } + + + public List<String> getData() { + return ImmutableList.copyOf(data); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-core/src/test/java/org/apache/james/eventsourcing/EventSourcingSystemTest.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-core/src/test/java/org/apache/james/eventsourcing/EventSourcingSystemTest.java b/event-sourcing/event-sourcing-core/src/test/java/org/apache/james/eventsourcing/EventSourcingSystemTest.java new file mode 100644 index 0000000..02c9eec --- /dev/null +++ b/event-sourcing/event-sourcing-core/src/test/java/org/apache/james/eventsourcing/EventSourcingSystemTest.java @@ -0,0 +1,252 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyListOf; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.List; + +import org.apache.james.eventsourcing.eventstore.EventStore; +import org.apache.james.eventsourcing.eventstore.History; +import org.junit.jupiter.api.Test; + +import com.github.steveash.guavate.Guavate; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; + +public interface EventSourcingSystemTest { + + String PAYLOAD_1 = "payload1"; + String PAYLOAD_2 = "payload2"; + TestAggregateId AGGREGATE_ID = TestAggregateId.testId(42); + + class MyCommand implements Command { + private final String payload; + + public MyCommand(String payload) { + this.payload = payload; + } + + public String getPayload() { + return payload; + } + } + + @Test + default void dispatchShouldApplyCommandHandlerThenCallSubscribers(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(simpleDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore); + + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1)); + + assertThat(subscriber.getData()).containsExactly(PAYLOAD_1); + } + + @Test + default void throwingSubscribersShouldNotAbortSubscriberChain(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(simpleDispatcher(eventStore)), + ImmutableSet.of( + events -> { + throw new RuntimeException(); + }, + subscriber), + eventStore); + + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1)); + + assertThat(subscriber.getData()).containsExactly(PAYLOAD_1); + } + + @Test + default void throwingStoreShouldNotLeadToPusblishing() { + EventStore eventStore = mock(EventStore.class); + doThrow(new RuntimeException()).when(eventStore).appendAll(anyListOf(Event.class)); + when(eventStore.getEventsOfAggregate(any())).thenReturn(History.empty()); + + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(simpleDispatcher(eventStore)), + ImmutableSet.of( + events -> { + throw new RuntimeException(); + }, + subscriber), + eventStore); + + assertThatThrownBy(() -> eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1))) + .isInstanceOf(RuntimeException.class); + + assertThat(subscriber.getData()).isEmpty(); + } + + @Test + default void dispatchShouldApplyCommandHandlerThenStoreGeneratedEvents(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(simpleDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore); + + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1)); + + TestEvent expectedEvent = new TestEvent(EventId.first(), AGGREGATE_ID, PAYLOAD_1); + assertThat(eventStore.getEventsOfAggregate(AGGREGATE_ID).getEvents()) + .containsOnly(expectedEvent); + } + + @Test + default void dispatchShouldCallSubscriberForSubsequentCommands(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(simpleDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore); + + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1)); + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_2)); + + assertThat(subscriber.getData()).containsExactly(PAYLOAD_1, PAYLOAD_2); + } + + @Test + default void dispatchShouldStoreEventsForSubsequentCommands(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(simpleDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore); + + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_1)); + eventSourcingSystem.dispatch(new MyCommand(PAYLOAD_2)); + + TestEvent expectedEvent1 = new TestEvent(EventId.first(), AGGREGATE_ID, PAYLOAD_1); + TestEvent expectedEvent2 = new TestEvent(expectedEvent1.eventId().next(), AGGREGATE_ID, PAYLOAD_2); + assertThat(eventStore.getEventsOfAggregate(AGGREGATE_ID).getEvents()) + .containsOnly(expectedEvent1, expectedEvent2); + } + + @Test + default void dispatcherShouldBeAbleToReturnSeveralEvents(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(wordCuttingDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore); + + eventSourcingSystem.dispatch(new MyCommand("This is a test")); + + assertThat(subscriber.getData()).containsExactly("This", "is", "a", "test"); + } + + @Test + default void unknownCommandsShouldBeIgnored(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + EventSourcingSystem eventSourcingSystem = new EventSourcingSystem( + ImmutableSet.of(wordCuttingDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore); + + assertThatThrownBy(() -> eventSourcingSystem.dispatch(new Command() {})) + .isInstanceOf(CommandDispatcher.UnknownCommandException.class); + } + + @Test + default void constructorShouldThrowWhenSeveralHandlersForTheSameCommand(EventStore eventStore) { + DataCollectorSubscriber subscriber = new DataCollectorSubscriber(); + + assertThatThrownBy(() -> + new EventSourcingSystem( + ImmutableSet.of(wordCuttingDispatcher(eventStore), + simpleDispatcher(eventStore)), + ImmutableSet.of(subscriber), + eventStore)) + .isInstanceOf(IllegalArgumentException.class); + } + + default CommandHandler<MyCommand> simpleDispatcher(EventStore eventStore) { + return new CommandHandler<MyCommand>() { + @Override + public Class<MyCommand> handledClass() { + return MyCommand.class; + } + + @Override + public List<? extends Event> handle(MyCommand myCommand) { + History history = eventStore.getEventsOfAggregate(AGGREGATE_ID); + + return ImmutableList.of(new TestEvent( + history.getNextEventId(), + AGGREGATE_ID, + myCommand.getPayload())); + } + }; + } + + default CommandHandler<MyCommand> wordCuttingDispatcher(EventStore eventStore) { + return new CommandHandler<MyCommand>() { + @Override + public Class<MyCommand> handledClass() { + return MyCommand.class; + } + + @Override + public List<? extends Event> handle(MyCommand myCommand) { + History history = eventStore.getEventsOfAggregate(AGGREGATE_ID); + + EventIdIncrementer eventIdIncrementer = new EventIdIncrementer(history.getNextEventId()); + + return Splitter.on(" ") + .splitToList(myCommand.getPayload()) + .stream() + .map(word -> new TestEvent( + eventIdIncrementer.next(), + AGGREGATE_ID, + word)) + .collect(Guavate.toImmutableList()); + } + }; + } + + class EventIdIncrementer { + private EventId currentEventId; + + public EventIdIncrementer(EventId base) { + this.currentEventId = base; + } + + public EventId next() { + currentEventId = currentEventId.next(); + return currentEventId; + } + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-pojo/pom.xml ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-pojo/pom.xml b/event-sourcing/event-sourcing-pojo/pom.xml new file mode 100644 index 0000000..c27130f --- /dev/null +++ b/event-sourcing/event-sourcing-pojo/pom.xml @@ -0,0 +1,67 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.james</groupId> + <artifactId>event-sourcing</artifactId> + <version>3.1.0-SNAPSHOT</version> + </parent> + + <artifactId>event-sourcing-pojo</artifactId> + + <name>Apache James :: Event sourcing :: pojo</name> + <description>James event sourcing types</description> + + <dependencies> + <dependency> + <groupId>com.github.steveash.guavate</groupId> + <artifactId>guavate</artifactId> + </dependency> + <dependency> + <groupId>nl.jqno.equalsverifier</groupId> + <artifactId>equalsverifier</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + </dependencies> + +</project> http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/AggregateId.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/AggregateId.java b/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/AggregateId.java new file mode 100644 index 0000000..18c6224 --- /dev/null +++ b/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/AggregateId.java @@ -0,0 +1,24 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +public interface AggregateId { + String asAggregateKey(); +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/Command.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/Command.java b/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/Command.java new file mode 100644 index 0000000..dad8332 --- /dev/null +++ b/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/Command.java @@ -0,0 +1,23 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +public interface Command { +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/Event.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/Event.java b/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/Event.java new file mode 100644 index 0000000..2b31374 --- /dev/null +++ b/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/Event.java @@ -0,0 +1,43 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +import java.util.List; + +public interface Event extends Comparable<Event> { + + static boolean belongsToSameAggregate(List<? extends Event> events) { + return events.stream() + .map(Event::getAggregateId) + .distinct() + .limit(2) + .count() == 1; + } + + EventId eventId(); + + AggregateId getAggregateId(); + + @Override + default int compareTo(Event o) { + return eventId().compareTo(o.eventId()); + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/EventId.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/EventId.java b/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/EventId.java new file mode 100644 index 0000000..cb5bd1e --- /dev/null +++ b/event-sourcing/event-sourcing-pojo/src/main/java/org/apache/james/eventsourcing/EventId.java @@ -0,0 +1,86 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +import java.util.Objects; +import java.util.Optional; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Preconditions; + +public class EventId implements Comparable<EventId> { + + public static EventId fromSerialized(int value) { + return new EventId(value); + } + + public static EventId first() { + return new EventId(0); + } + + private final int value; + + private EventId(int value) { + Preconditions.checkArgument(value >= 0, "EventId can not be negative"); + this.value = value; + } + + public EventId next() { + return new EventId(value + 1); + } + + public Optional<EventId> previous() { + if (value > 0) { + return Optional.of(new EventId(value - 1)); + } + return Optional.empty(); + } + + @Override + public int compareTo(EventId o) { + return Long.compare(value, o.value); + } + + @Override + public final boolean equals(Object o) { + if (o instanceof EventId) { + EventId eventId = (EventId) o; + + return Objects.equals(this.value, eventId.value); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(value); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("value", value) + .toString(); + } + + public int serialize() { + return value; + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-pojo/src/test/java/org/apache/james/eventsourcing/EventIdTest.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-pojo/src/test/java/org/apache/james/eventsourcing/EventIdTest.java b/event-sourcing/event-sourcing-pojo/src/test/java/org/apache/james/eventsourcing/EventIdTest.java new file mode 100644 index 0000000..1b46fbf --- /dev/null +++ b/event-sourcing/event-sourcing-pojo/src/test/java/org/apache/james/eventsourcing/EventIdTest.java @@ -0,0 +1,85 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +class EventIdTest { + + @Test + void shouldMatchBeanContract() { + EqualsVerifier.forClass(EventId.class) + .allFieldsShouldBeUsed() + .verify(); + } + + @Test + void firstShouldReturnAConstant() { + assertThat(EventId.first()) + .isEqualTo(EventId.first()); + } + + @Test + void previousShouldReturnEmptyWhenBeforeFirst() { + assertThat(EventId.first().previous()) + .isEmpty(); + } + + @Test + void compareToShouldReturnNegativeWhenComparedToNext() { + assertThat(EventId.first()) + .isLessThan(EventId.first().next()); + } + + @Test + void compareToShouldReturnNegativeWhenComparedToPrevious() { + assertThat(EventId.first().next()) + .isGreaterThan(EventId.first()); + } + + @Test + void nextShouldAlwaysHaveTheSameIncrement() { + assertThat(EventId.first().next()) + .isEqualTo(EventId.first().next()); + } + + @Test + void previousShouldRevertNext() { + assertThat(EventId.first().next().previous()) + .contains(EventId.first()); + } + + @Test + void compareToShouldReturnNegativeWhenComparedToNextWithPreviousCall() { + assertThat(EventId.first().next().previous().get()) + .isLessThan(EventId.first().next()); + } + + @Test + void compareToShouldReturnNegativeWhenComparedToPreviousWithPreviousCall() { + assertThat(EventId.first().next()) + .isGreaterThan(EventId.first().next().previous().get()); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-pojo/src/test/java/org/apache/james/eventsourcing/TestAggregateId.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-pojo/src/test/java/org/apache/james/eventsourcing/TestAggregateId.java b/event-sourcing/event-sourcing-pojo/src/test/java/org/apache/james/eventsourcing/TestAggregateId.java new file mode 100644 index 0000000..b3ae78c --- /dev/null +++ b/event-sourcing/event-sourcing-pojo/src/test/java/org/apache/james/eventsourcing/TestAggregateId.java @@ -0,0 +1,68 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +import java.util.Objects; + +import com.google.common.base.MoreObjects; + +public class TestAggregateId implements AggregateId { + + public static TestAggregateId testId(int id) { + return new TestAggregateId(id); + } + + private final int id; + + private TestAggregateId(int id) { + this.id = id; + } + + @Override + public String asAggregateKey() { + return "TestAggregateId-" + id; + } + + public int getId() { + return id; + } + + @Override + public final boolean equals(Object o) { + if (o instanceof TestAggregateId) { + TestAggregateId that = (TestAggregateId) o; + + return Objects.equals(this.id, that.id); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .toString(); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-sourcing-pojo/src/test/java/org/apache/james/eventsourcing/TestEvent.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-sourcing-pojo/src/test/java/org/apache/james/eventsourcing/TestEvent.java b/event-sourcing/event-sourcing-pojo/src/test/java/org/apache/james/eventsourcing/TestEvent.java new file mode 100644 index 0000000..c46f804 --- /dev/null +++ b/event-sourcing/event-sourcing-pojo/src/test/java/org/apache/james/eventsourcing/TestEvent.java @@ -0,0 +1,82 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing; + +import java.util.Comparator; +import java.util.Objects; + +import com.google.common.base.MoreObjects; + +public class TestEvent implements Event { + private final EventId id; + private final TestAggregateId aggregateId; + private final String data; + + public TestEvent(EventId id, TestAggregateId aggregateId, String data) { + this.id = id; + this.aggregateId = aggregateId; + this.data = data; + } + + @Override + public EventId eventId() { + return id; + } + + @Override + public TestAggregateId getAggregateId() { + return aggregateId; + } + + public String getData() { + return data; + } + + @Override + public int compareTo(Event o) { + return Comparator.<EventId>naturalOrder().compare(id, o.eventId()); + } + + @Override + public final boolean equals(Object o) { + if (o instanceof TestEvent) { + TestEvent testEvent = (TestEvent) o; + + return Objects.equals(this.id, testEvent.id) + && Objects.equals(this.aggregateId, testEvent.aggregateId) + && Objects.equals(this.data, testEvent.data); + } + return false; + } + + @Override + public int hashCode() { + return Objects.hash(id, aggregateId, data); + } + + @Override + public String toString() { + return MoreObjects.toStringHelper(this) + .add("id", id) + .add("aggregateId", aggregateId) + .add("data", data) + .toString(); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-store-api/pom.xml ---------------------------------------------------------------------- diff --git a/event-sourcing/event-store-api/pom.xml b/event-sourcing/event-store-api/pom.xml new file mode 100644 index 0000000..2cfd710 --- /dev/null +++ b/event-sourcing/event-store-api/pom.xml @@ -0,0 +1,77 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.james</groupId> + <artifactId>event-sourcing</artifactId> + <version>3.1.0-SNAPSHOT</version> + </parent> + + <artifactId>event-sourcing-event-store-api</artifactId> + + <name>Apache James :: Event Sourcing :: Event Store :: API</name> + <description>James Event Sourcing interface for Event Store implementations</description> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>event-sourcing-pojo</artifactId> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>event-sourcing-pojo</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.github.steveash.guavate</groupId> + <artifactId>guavate</artifactId> + </dependency> + <dependency> + <groupId>nl.jqno.equalsverifier</groupId> + <artifactId>equalsverifier</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.slf4j</groupId> + <artifactId>slf4j-api</artifactId> + </dependency> + </dependencies> + +</project> \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-store-api/src/main/java/org/apache/james/eventsourcing/eventstore/EventStore.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-store-api/src/main/java/org/apache/james/eventsourcing/eventstore/EventStore.java b/event-sourcing/event-store-api/src/main/java/org/apache/james/eventsourcing/eventstore/EventStore.java new file mode 100644 index 0000000..3b3a95e --- /dev/null +++ b/event-sourcing/event-store-api/src/main/java/org/apache/james/eventsourcing/eventstore/EventStore.java @@ -0,0 +1,47 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing.eventstore; + +import java.util.List; + +import org.apache.james.eventsourcing.AggregateId; +import org.apache.james.eventsourcing.Event; + +import com.google.common.collect.ImmutableList; + +public interface EventStore { + + default void append(Event event) { + appendAll(ImmutableList.of(event)); + } + + default void appendAll(Event... events) { + appendAll(ImmutableList.copyOf(events)); + } + + /** + * This method should check that no input event has an id already stored and throw otherwise + * It should also check that all events belong to the same aggregate + */ + void appendAll(List<Event> events); + + History getEventsOfAggregate(AggregateId aggregateId); + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-store-api/src/main/java/org/apache/james/eventsourcing/eventstore/EventStoreFailedException.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-store-api/src/main/java/org/apache/james/eventsourcing/eventstore/EventStoreFailedException.java b/event-sourcing/event-store-api/src/main/java/org/apache/james/eventsourcing/eventstore/EventStoreFailedException.java new file mode 100644 index 0000000..93bcb2f --- /dev/null +++ b/event-sourcing/event-store-api/src/main/java/org/apache/james/eventsourcing/eventstore/EventStoreFailedException.java @@ -0,0 +1,23 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing.eventstore; + +public class EventStoreFailedException extends RuntimeException { +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-store-api/src/main/java/org/apache/james/eventsourcing/eventstore/History.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-store-api/src/main/java/org/apache/james/eventsourcing/eventstore/History.java b/event-sourcing/event-store-api/src/main/java/org/apache/james/eventsourcing/eventstore/History.java new file mode 100644 index 0000000..5a91306 --- /dev/null +++ b/event-sourcing/event-store-api/src/main/java/org/apache/james/eventsourcing/eventstore/History.java @@ -0,0 +1,94 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing.eventstore; + +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.apache.james.eventsourcing.Event; +import org.apache.james.eventsourcing.EventId; + +import com.github.steveash.guavate.Guavate; +import com.google.common.collect.ImmutableList; + +public class History { + public static History empty() { + return new History(ImmutableList.of()); + } + + public static History of(List<Event> events) { + return new History(ImmutableList.copyOf(events)); + } + + public static History of(Event... events) { + return of(ImmutableList.copyOf(events)); + } + + private final List<Event> events; + + private History(List<Event> events) { + if (hasEventIdDuplicates(events)) { + throw new EventStoreFailedException(); + } + this.events = events; + } + + public boolean hasEventIdDuplicates(List<Event> events) { + Set<EventId> eventIds = events.stream() + .map(Event::eventId) + .collect(Guavate.toImmutableSet()); + + return eventIds.size() != events.size(); + } + + public Optional<EventId> getVersion() { + return events.stream() + .map(Event::eventId) + .max(Comparator.naturalOrder()); + } + + public List<Event> getEvents() { + return events; + } + + public EventId getNextEventId() { + return getVersion() + .map(EventId::next) + .orElse(EventId.first()); + } + + @Override + public final boolean equals(Object o) { + if (o instanceof History) { + History history = (History) o; + + return Objects.equals(this.events, history.events); + } + return false; + } + + @Override + public final int hashCode() { + return Objects.hash(events); + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-store-api/src/test/java/org/apache/james/eventsourcing/eventstore/EventStoreTest.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-store-api/src/test/java/org/apache/james/eventsourcing/eventstore/EventStoreTest.java b/event-sourcing/event-store-api/src/test/java/org/apache/james/eventsourcing/eventstore/EventStoreTest.java new file mode 100644 index 0000000..dc312d9 --- /dev/null +++ b/event-sourcing/event-store-api/src/test/java/org/apache/james/eventsourcing/eventstore/EventStoreTest.java @@ -0,0 +1,88 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing.eventstore; + +import static org.apache.james.eventsourcing.TestAggregateId.testId; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.james.eventsourcing.EventId; +import org.apache.james.eventsourcing.TestAggregateId; +import org.apache.james.eventsourcing.TestEvent; +import org.junit.jupiter.api.Test; + +import com.google.common.collect.ImmutableList; + +public interface EventStoreTest { + + TestAggregateId AGGREGATE_1 = testId(1); + TestAggregateId AGGREGATE_2 = testId(2); + + @Test + default void getEventsOfAggregateShouldThrowOnNullAggregateId(EventStore testee) { + assertThatThrownBy(() -> testee.getEventsOfAggregate(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + default void appendShouldThrowWhenEventFromSeveralAggregates(EventStore testee) { + TestEvent event1 = new TestEvent(EventId.first(), AGGREGATE_1, "first"); + TestEvent event2 = new TestEvent(event1.eventId().next(), AGGREGATE_2, "second"); + assertThatThrownBy(() -> testee.appendAll(event1, event2)).isInstanceOf(IllegalArgumentException.class); + } + + @Test + default void appendShouldDoNothingOnEmptyEventList(EventStore testee) { + assertThatCode(testee::appendAll).doesNotThrowAnyException(); + } + + @Test + default void appendShouldThrowWhenTryingToRewriteHistory(EventStore testee) { + TestEvent event1 = new TestEvent(EventId.first(), AGGREGATE_1, "first"); + testee.append(event1); + TestEvent event2 = new TestEvent(EventId.first(), AGGREGATE_1, "second"); + assertThatThrownBy(() -> testee.append(event2)).isInstanceOf(EventStoreFailedException.class); + } + + @Test + default void getEventsOfAggregateShouldReturnEmptyHistoryWhenUnknown(EventStore testee) { + assertThat(testee.getEventsOfAggregate(AGGREGATE_1)).isEqualTo(History.empty()); + } + + @Test + default void getEventsOfAggregateShouldReturnAppendedEvent(EventStore testee) { + TestEvent event = new TestEvent(EventId.first(), AGGREGATE_1, "first"); + testee.append(event); + assertThat(testee.getEventsOfAggregate(AGGREGATE_1)) + .isEqualTo(History.of(ImmutableList.of(event))); + } + + @Test + default void getEventsOfAggregateShouldReturnAppendedEvents(EventStore testee) { + TestEvent event1 = new TestEvent(EventId.first(), AGGREGATE_1, "first"); + TestEvent event2 = new TestEvent(event1.eventId().next(), AGGREGATE_1, "second"); + testee.append(event1); + testee.append(event2); + assertThat(testee.getEventsOfAggregate(AGGREGATE_1)) + .isEqualTo(History.of(ImmutableList.of(event1, event2))); + } + +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-store-api/src/test/java/org/apache/james/eventsourcing/eventstore/HistoryTest.java ---------------------------------------------------------------------- diff --git a/event-sourcing/event-store-api/src/test/java/org/apache/james/eventsourcing/eventstore/HistoryTest.java b/event-sourcing/event-store-api/src/test/java/org/apache/james/eventsourcing/eventstore/HistoryTest.java new file mode 100644 index 0000000..2945b92 --- /dev/null +++ b/event-sourcing/event-store-api/src/test/java/org/apache/james/eventsourcing/eventstore/HistoryTest.java @@ -0,0 +1,91 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you 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.apache.james.eventsourcing.eventstore; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import org.apache.james.eventsourcing.EventId; +import org.apache.james.eventsourcing.TestAggregateId; +import org.apache.james.eventsourcing.TestEvent; +import org.junit.jupiter.api.Test; + +import nl.jqno.equalsverifier.EqualsVerifier; + +class HistoryTest { + + @Test + void shouldMatchBeanContract() { + EqualsVerifier.forClass(History.class) + .allFieldsShouldBeUsed() + .verify(); + } + + @Test + void emptyShouldGenerateAnEmptyHistory() { + assertThat(History.empty()) + .isEqualTo(History.of()); + } + + @Test + void getVersionShouldReturnEmptyWhenEmpty() { + assertThat(History.empty() + .getVersion()) + .isEmpty(); + } + + @Test + void getVersionShouldReturnSingleEventIdWhenSingleEvent() { + assertThat(History + .of(new TestEvent(EventId.first(), + TestAggregateId.testId(42), + "any")) + .getVersion()) + .contains(EventId.first()); + } + + @Test + void getVersionShouldReturnHighestEventId() { + TestEvent event1 = new TestEvent(EventId.first(), + TestAggregateId.testId(42), + "any"); + TestEvent event2 = new TestEvent(event1.eventId().next(), + TestAggregateId.testId(42), + "any"); + + assertThat(History.of(event1, event2) + .getVersion()) + .contains(event2.eventId()); + } + + @Test + void duplicateHistoryShouldThrow() { + TestEvent event1 = new TestEvent(EventId.first(), + TestAggregateId.testId(42), + "any"); + TestEvent event2 = new TestEvent(EventId.first(), + TestAggregateId.testId(42), + "any"); + + assertThatThrownBy(() -> History.of(event1, event2)) + .isInstanceOf(EventStoreFailedException.class); + } + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/189490a4/event-sourcing/event-store-cassandra/pom.xml ---------------------------------------------------------------------- diff --git a/event-sourcing/event-store-cassandra/pom.xml b/event-sourcing/event-store-cassandra/pom.xml new file mode 100644 index 0000000..cf2e6dc --- /dev/null +++ b/event-sourcing/event-store-cassandra/pom.xml @@ -0,0 +1,131 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!-- + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. +--> +<project xmlns="http://maven.apache.org/POM/4.0.0" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> + <modelVersion>4.0.0</modelVersion> + + <parent> + <groupId>org.apache.james</groupId> + <artifactId>event-sourcing</artifactId> + <version>3.1.0-SNAPSHOT</version> + </parent> + + <artifactId>event-sourcing-event-store-cassandra</artifactId> + + <name>Apache James :: Event sourcing :: Event Store :: Cassandra</name> + <description>Cassandra implementation for James Event Store</description> + + <dependencies> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>apache-james-backends-cassandra</artifactId> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>apache-james-backends-cassandra</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>event-sourcing-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>event-sourcing-event-store-api</artifactId> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>event-sourcing-event-store-api</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>event-sourcing-core</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>event-sourcing-pojo</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> + <groupId>ch.qos.logback</groupId> + <artifactId>logback-classic</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.assertj</groupId> + <artifactId>assertj-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.core</groupId> + <artifactId>jackson-databind</artifactId> + </dependency> + <dependency> + <groupId>com.fasterxml.jackson.datatype</groupId> + <artifactId>jackson-datatype-jdk8</artifactId> + </dependency> + <dependency> + <groupId>net.javacrumbs.json-unit</groupId> + <artifactId>json-unit</artifactId> + <version>1.5.5</version> + <scope>test</scope> + </dependency> + <dependency> + <groupId>net.javacrumbs.json-unit</groupId> + <artifactId>json-unit-fluent</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>nl.jqno.equalsverifier</groupId> + <artifactId>equalsverifier</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.jupiter</groupId> + <artifactId>junit-jupiter-engine</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.junit.platform</groupId> + <artifactId>junit-platform-launcher</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.mockito</groupId> + <artifactId>mockito-core</artifactId> + <scope>test</scope> + </dependency> + <dependency> + <groupId>org.testcontainers</groupId> + <artifactId>testcontainers</artifactId> + <scope>test</scope> + </dependency> + </dependencies> + + +</project> \ No newline at end of file --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org