chore: Initial import of FLEX training material
This commit is contained in:
parent
c01246d4f7
commit
12235acc42
1020 changed files with 53940 additions and 0 deletions
|
|
@ -0,0 +1,49 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<FindBugsFilter>
|
||||
<Match>
|
||||
<!-- spotbugs message is Public method de.accso.flexinale.common.shared_kernel.EventClassHelper.getEventClazzVersion(Class)
|
||||
uses reflection to create a class it gets in its parameter which could increase the accessibility of any class. -->
|
||||
<Class name="de.accso.flexinale.common.shared_kernel.EventClassHelper" />
|
||||
<Bug pattern="REFLC_REFLECTION_MAY_INCREASE_ACCESSIBILITY_OF_CLASS" />
|
||||
</Match>
|
||||
<Match>
|
||||
<!-- spotbugs message is High: record must be non-null but is marked as nullable -->
|
||||
<Class name="de.accso.flexinale.common.infrastructure.eventbus.KafkaConsumerErrorHandler" />
|
||||
<Bug pattern="NP_PARAMETER_MUST_BE_NONNULL_BUT_MARKED_AS_NULLABLE" />
|
||||
</Match>
|
||||
<Match>
|
||||
<!-- spotbugs message is High: record must be non-null but is marked as nullable -->
|
||||
<Class name="de.accso.flexinale.common.api.event.EventContext" />
|
||||
<Bug pattern="URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD" />
|
||||
</Match>
|
||||
<Match>
|
||||
<!-- spotbugs message is Medium: may expose internal representation by returning
|
||||
InMemorySyncEventBus.eventContextHolder -->
|
||||
<Class name="de.accso.flexinale.common.infrastructure.eventbus.InMemorySyncEventBus" />
|
||||
<Bug pattern="EI_EXPOSE_REP" />
|
||||
</Match>
|
||||
<Match>
|
||||
<!-- spotbugs message is Medium: may expose internal representation by returning
|
||||
InMemorySyncEventBus.eventContextHolder -->
|
||||
<Class name="de.accso.flexinale.common.infrastructure.eventbus.KafkaAsyncEventBus" />
|
||||
<Bug pattern="EI_EXPOSE_REP" />
|
||||
</Match>
|
||||
<Match>
|
||||
<!-- spotbugs message is Medium: may expose internal representation by returning
|
||||
InMemorySyncEventBus.eventContextHolder -->
|
||||
<Class name="de.accso.flexinale.common.infrastructure.eventbus.NopeEventBusSpy" />
|
||||
<Bug pattern="EI_EXPOSE_REP" />
|
||||
</Match>
|
||||
<Match>
|
||||
<Class name="de.accso.flexinale.common.infrastructure.FlexinaleSpringConfig" />
|
||||
<Bug pattern="CT_CONSTRUCTOR_THROW"/>
|
||||
</Match>
|
||||
<Match>
|
||||
<Class name="de.accso.flexinale.common.infrastructure.eventbus.EventSubscriberReceiveDispatcher" />
|
||||
<Bug pattern="DCN_NULLPOINTER_EXCEPTION"/>
|
||||
</Match>
|
||||
<Match>
|
||||
<Class name="de.accso.flexinale.common.shared_kernel.Selection$RandomSelector" />
|
||||
<Bug pattern="PREDICTABLE_RANDOM"/>
|
||||
</Match>
|
||||
</FindBugsFilter>
|
||||
|
|
@ -0,0 +1,116 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<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/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>de.accso</groupId>
|
||||
<artifactId>flexinale-distributed</artifactId>
|
||||
<version>2024.3.0</version>
|
||||
<relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
|
||||
<artifactId>flexinale-distributed-common</artifactId>
|
||||
<version>2024.3.0</version>
|
||||
<name>Flexinale Distributed Common</name>
|
||||
<description>Flexinale - FLEX case-study "film festival", distributed services, common</description>
|
||||
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi</artifactId>
|
||||
<version>${apache-poi.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.apache.poi</groupId>
|
||||
<artifactId>poi-ooxml</artifactId>
|
||||
<version>${apache-poi.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-lang3</artifactId>
|
||||
<version>${commons-lang3.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
<version>${slf4j.version}</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.datatype</groupId>
|
||||
<artifactId>jackson-datatype-jsr310</artifactId>
|
||||
<version>${jackson.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-jar-plugin</artifactId>
|
||||
<version>${maven-jar-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>test-jar</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs-maven-plugin</artifactId>
|
||||
<version>${spotbugs-maven-plugin.version}</version>
|
||||
<configuration>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>com.h3xstream.findsecbugs</groupId>
|
||||
<artifactId>findsecbugs-plugin</artifactId>
|
||||
<version>${findsecbugs-maven-plugin.version}</version>
|
||||
</plugin>
|
||||
</plugins>
|
||||
<excludeFilterFile>SpotbugsExcludeFilter.xml</excludeFilterFile>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<!-- overwrite dependency on spotbugs if you want to specify the version of spotbugs -->
|
||||
<dependency>
|
||||
<groupId>com.github.spotbugs</groupId>
|
||||
<artifactId>spotbugs</artifactId>
|
||||
<version>${spotbugs.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>verify</phase>
|
||||
<goals>
|
||||
<goal>check</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package de.accso.flexinale.common.api.event;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonSetter;
|
||||
import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException;
|
||||
import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
|
||||
import de.accso.flexinale.common.shared_kernel.Identifiable;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@SuppressWarnings("CanBeFinal")
|
||||
public abstract class AbstractEvent implements Event {
|
||||
|
||||
public final Id id = Identifiable.Id.of();
|
||||
public final Id correlationId;
|
||||
@DoNotCheckInArchitectureTests
|
||||
protected LocalDateTime timestamp = LocalDateTime.now();
|
||||
@DoNotCheckInArchitectureTests
|
||||
protected final EventContext eventContext;
|
||||
|
||||
// event is start of a new event chain
|
||||
protected AbstractEvent() {
|
||||
this.correlationId = Identifiable.Id.of();
|
||||
this.eventContext = new EventContext( this.correlationId,
|
||||
List.of( new EventContext.EventHistoryElement(id, this.getClass())) );
|
||||
}
|
||||
|
||||
// event is part of an existing event chain but without a history
|
||||
protected AbstractEvent(final Id correlationId) {
|
||||
this.correlationId = correlationId;
|
||||
this.eventContext = new EventContext( this.correlationId,
|
||||
List.of( new EventContext.EventHistoryElement(id, this.getClass())) );
|
||||
}
|
||||
|
||||
// event is part of an existing event chain
|
||||
protected AbstractEvent(final Event predecessorEvent) {
|
||||
this(predecessorEvent.eventContext());
|
||||
}
|
||||
|
||||
// event is part of an existing event chain
|
||||
protected AbstractEvent(final EventContext predecessorEventContext) {
|
||||
this.correlationId = predecessorEventContext.correlationId;
|
||||
this.eventContext = new EventContext( this.correlationId,
|
||||
List.copyOf(predecessorEventContext.eventHistory.stream().toList()),
|
||||
new EventContext.EventHistoryElement(id, this.getClass()) // append this event to history
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Id id() {
|
||||
return id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalDateTime timestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Id correlationId() {
|
||||
return correlationId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventContext eventContext() {
|
||||
return eventContext;
|
||||
}
|
||||
|
||||
@JsonSetter("version") // needed as otherwise the static field version (in one of the subclasses) is not (de)serialized
|
||||
public void setVersion(Version newVersion) {
|
||||
String fieldName = "version";
|
||||
try {
|
||||
Field field = this.getClass().getDeclaredField(fieldName); // static field
|
||||
field.setAccessible(true);
|
||||
field.set(this, newVersion);
|
||||
}
|
||||
catch (NoSuchFieldException | IllegalAccessException ex) {
|
||||
throw new DeveloperMistakeException("version field cannot be set via reflection in an Event class", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {return true;}
|
||||
if (o == null || getClass() != o.getClass()) {return false;}
|
||||
|
||||
final AbstractEvent that = (AbstractEvent) o;
|
||||
|
||||
return Objects.equals(id, that.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return id.hashCode();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package de.accso.flexinale.common.api.event;
|
||||
|
||||
import de.accso.flexinale.common.shared_kernel.Identifiable;
|
||||
import de.accso.flexinale.common.shared_kernel.Versionable;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface Event extends Identifiable, Versionable, Serializable {
|
||||
LocalDateTime timestamp();
|
||||
Id correlationId();
|
||||
EventContext eventContext();
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
package de.accso.flexinale.common.api.event;
|
||||
|
||||
import de.accso.flexinale.common.shared_kernel.Identifiable;
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder;
|
||||
import org.apache.commons.lang3.builder.ToStringStyle;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class EventContext {
|
||||
public record EventHistoryElement(Identifiable.Id id, Class<? extends Event> eventType) {}
|
||||
|
||||
public Identifiable.Id correlationId;
|
||||
|
||||
public List<EventHistoryElement> eventHistory;
|
||||
|
||||
private EventContext() {} // used by Jackson deserialization
|
||||
|
||||
public EventContext(final Identifiable.Id correlationId, final List<EventHistoryElement> eventHistory) {
|
||||
this.correlationId = correlationId;
|
||||
this.eventHistory = List.copyOf(eventHistory);
|
||||
}
|
||||
|
||||
public EventContext(final Identifiable.Id correlationId,
|
||||
final List<EventHistoryElement> eventHistory, final EventHistoryElement additionalHistoryElement) {
|
||||
this.correlationId = correlationId;
|
||||
|
||||
// pretty ugly to copy plus add to immutable list
|
||||
List<EventHistoryElement> tempList = new ArrayList<>(eventHistory);
|
||||
tempList.add(additionalHistoryElement);
|
||||
this.eventHistory = List.copyOf(tempList);
|
||||
}
|
||||
|
||||
public boolean equalsByCorrelationId(EventContext that) {
|
||||
return this.correlationId.equals(that.correlationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {return true;}
|
||||
|
||||
if (o == null || getClass() != o.getClass()) {return false;}
|
||||
|
||||
EventContext that = (EventContext) o;
|
||||
|
||||
return new EqualsBuilder()
|
||||
.append(eventHistory, that.eventHistory)
|
||||
.isEquals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return new HashCodeBuilder(17, 37)
|
||||
.append(this.correlationId)
|
||||
.toHashCode();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package de.accso.flexinale.common.api.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
|
||||
public interface EventBus<E extends Event> {
|
||||
|
||||
// Event type specific subscription
|
||||
void subscribe(Class<E> eventType, EventSubscriber<E> subscriber, EventSubscriptionAtStart eventSubscriptionAtStart);
|
||||
void unsubscribe(Class<E> eventType, EventSubscriber<E> subscriber);
|
||||
|
||||
// subscription for _all_ Events (i.e. based on Event.class)
|
||||
void subscribe(EventSubscriber<Event> genericSubscriber, EventSubscriptionAtStart eventSubscriptionAtStart);
|
||||
void unsubscribe(EventSubscriber<Event> genericSubscriber);
|
||||
|
||||
void unsubscribeAll();
|
||||
|
||||
void publish(Class<E> eventType, E event);
|
||||
|
||||
EventContextHolder getEventContextHolder();
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package de.accso.flexinale.common.api.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
|
||||
public interface EventBusFactory {
|
||||
<E extends Event> EventBus<E> createOrGetEventBusFor(Class<E> type);
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package de.accso.flexinale.common.api.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.event.EventContext;
|
||||
|
||||
public interface EventContextHolder {
|
||||
EventContext get();
|
||||
void set(EventContext eventContext);
|
||||
void remove();
|
||||
boolean isEmpty();
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package de.accso.flexinale.common.api.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
|
||||
public interface EventNotification {
|
||||
void notify(Event event);
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
package de.accso.flexinale.common.api.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface EventPublisher<E1 extends Event> {
|
||||
|
||||
String getName();
|
||||
|
||||
void post(Class<E1> eventType, E1 event);
|
||||
|
||||
interface EventPublisher2<E1 extends Event, E2 extends Event>
|
||||
extends EventPublisher<E1> {
|
||||
void post2(Class<E2> eventType, E2 event);
|
||||
}
|
||||
|
||||
interface EventPublisher3<E1 extends Event, E2 extends Event, E3 extends Event>
|
||||
extends EventPublisher2<E1, E2> {
|
||||
void post3(Class<E3> eventType, E3 event);
|
||||
}
|
||||
|
||||
interface EventPublisher4<E1 extends Event, E2 extends Event, E3 extends Event, E4 extends Event>
|
||||
extends EventPublisher3<E1, E2, E3> {
|
||||
void post4(Class<E4> eventType, E4 event);
|
||||
}
|
||||
|
||||
interface EventPublisher5<E1 extends Event, E2 extends Event, E3 extends Event, E4 extends Event,
|
||||
E5 extends Event>
|
||||
extends EventPublisher4<E1, E2, E3, E4> {
|
||||
void post5(Class<E5> eventType, E5 event);
|
||||
}
|
||||
|
||||
interface EventPublisher6<E1 extends Event, E2 extends Event, E3 extends Event, E4 extends Event,
|
||||
E5 extends Event, E6 extends Event>
|
||||
extends EventPublisher5<E1, E2, E3, E4, E5> {
|
||||
void post6(Class<E6> eventType, E6 event);
|
||||
}
|
||||
|
||||
interface EventPublisher7<E1 extends Event, E2 extends Event, E3 extends Event, E4 extends Event,
|
||||
E5 extends Event, E6 extends Event, E7 extends Event>
|
||||
extends EventPublisher6<E1, E2, E3, E4, E5, E6> {
|
||||
void post7(Class<E7> eventType, E7 event);
|
||||
}
|
||||
|
||||
interface EventPublisher8<E1 extends Event, E2 extends Event, E3 extends Event, E4 extends Event,
|
||||
E5 extends Event, E6 extends Event, E7 extends Event, E8 extends Event>
|
||||
extends EventPublisher7<E1, E2, E3, E4, E5, E6, E7> {
|
||||
void post8(Class<E8> eventType, E8 event);
|
||||
}
|
||||
|
||||
interface EventPublisher9<E1 extends Event, E2 extends Event, E3 extends Event, E4 extends Event,
|
||||
E5 extends Event, E6 extends Event, E7 extends Event, E8 extends Event, E9 extends Event>
|
||||
extends EventPublisher8<E1, E2, E3, E4, E5, E6, E7, E8> {
|
||||
void post9(Class<E9> eventType, E9 event);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
package de.accso.flexinale.common.api.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
|
||||
@SuppressWarnings("unused") // methods are called via reflection
|
||||
public interface EventSubscriber<E1 extends Event> {
|
||||
|
||||
String getName();
|
||||
String getGroupName();
|
||||
|
||||
void receive(E1 event);
|
||||
|
||||
interface EventSubscriber2<E1 extends Event, E2 extends Event>
|
||||
extends EventSubscriber<E1> {
|
||||
void receive2(E2 event);
|
||||
}
|
||||
|
||||
interface EventSubscriber3<E1 extends Event, E2 extends Event, E3 extends Event>
|
||||
extends EventSubscriber2<E1, E2> {
|
||||
void receive3(E3 event);
|
||||
}
|
||||
|
||||
interface EventSubscriber4<E1 extends Event, E2 extends Event, E3 extends Event, E4 extends Event>
|
||||
extends EventSubscriber3<E1, E2, E3> {
|
||||
void receive4(E4 event);
|
||||
}
|
||||
|
||||
interface EventSubscriber5<E1 extends Event, E2 extends Event, E3 extends Event, E4 extends Event,
|
||||
E5 extends Event>
|
||||
extends EventSubscriber4<E1, E2, E3, E4> {
|
||||
void receive5(E5 event);
|
||||
}
|
||||
|
||||
interface EventSubscriber6<E1 extends Event, E2 extends Event, E3 extends Event, E4 extends Event,
|
||||
E5 extends Event, E6 extends Event>
|
||||
extends EventSubscriber5<E1, E2, E3, E4, E5> {
|
||||
void receive6(E6 event);
|
||||
}
|
||||
|
||||
interface EventSubscriber7<E1 extends Event, E2 extends Event, E3 extends Event, E4 extends Event,
|
||||
E5 extends Event, E6 extends Event, E7 extends Event>
|
||||
extends EventSubscriber6<E1, E2, E3, E4, E5, E6> {
|
||||
void receive7(E7 event);
|
||||
}
|
||||
|
||||
interface EventSubscriber8<E1 extends Event, E2 extends Event, E3 extends Event, E4 extends Event,
|
||||
E5 extends Event, E6 extends Event, E7 extends Event, E8 extends Event>
|
||||
extends EventSubscriber7<E1, E2, E3, E4, E5, E6, E7> {
|
||||
void receive8(E8 event);
|
||||
}
|
||||
|
||||
interface EventSubscriber9<E1 extends Event, E2 extends Event, E3 extends Event, E4 extends Event,
|
||||
E5 extends Event, E6 extends Event, E7 extends Event, E8 extends Event, E9 extends Event>
|
||||
extends EventSubscriber8<E1, E2, E3, E4, E5, E6, E7, E8> {
|
||||
void receive9(E9 event);
|
||||
}
|
||||
|
||||
String RECEIVE = "receive";
|
||||
String RECEIVE2 = "receive2";
|
||||
String RECEIVE3 = "receive3";
|
||||
String RECEIVE4 = "receive4";
|
||||
String RECEIVE5 = "receive5";
|
||||
String RECEIVE6 = "receive6";
|
||||
String RECEIVE7 = "receive7";
|
||||
String RECEIVE8 = "receive8";
|
||||
String RECEIVE9 = "receive9";
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package de.accso.flexinale.common.api.eventbus;
|
||||
|
||||
// Behaviour on how a EventSubscriber (i.e. Consumer) reads new messages, after having subscribed to the EventBus
|
||||
// - How much of the old messages are read or re-read (if any)?
|
||||
// - Where do we start, at which offset?
|
||||
public enum EventSubscriptionAtStart {
|
||||
// (re)read all messages from the EventBus from the very beginning (i.e. from offset 0)
|
||||
// In Kafka this is - for a new ConsumerGroup - "earliest".
|
||||
START_READING_FROM_BEGINNING,
|
||||
|
||||
// read all messages from now on (so don't (re)read anything published before now)
|
||||
// In Kafka this is - for a new ConsumerGroup - "latest".
|
||||
START_READING_FROM_NOW,
|
||||
|
||||
// read all messages from the EventBus, starting where we left off last time (so last offset we had + 1)
|
||||
// (if we had not connected before at all: start from beginning, i.e. from offset 0)
|
||||
// In Kafka this is - for a known ConsumerGroup - the default.
|
||||
START_READING_FROM_LAST_TIME
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
package de.accso.flexinale.common.application;
|
||||
|
||||
public interface Config {
|
||||
int getQuoteOnline();
|
||||
int getMinZeitZwischenVorfuehrungenInMinuten();
|
||||
|
||||
String getApplicationTitle();
|
||||
String getBuildVersion();
|
||||
String getBuildDate();
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package de.accso.flexinale.common.application;
|
||||
|
||||
public enum PersistMode {
|
||||
// updates on existing entities allowed (beware of inconsistencies with derived entities!)
|
||||
UPDATE,
|
||||
|
||||
// only new data can be added, no updates on existing entities allowed
|
||||
ADD_ONLY
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package de.accso.flexinale.common.application;
|
||||
|
||||
import de.accso.flexinale.common.shared_kernel.Identifiable;
|
||||
|
||||
public record PersistedEntityAndResult<E extends Identifiable>
|
||||
(E entity, PersistedResult result) {
|
||||
|
||||
public enum PersistedResult {
|
||||
UPDATED,
|
||||
ADDED
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
package de.accso.flexinale.common.application.caching;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public final class InMemoryCache<ID, OBJECT> {
|
||||
// as an alternative: extract interface, use Cache2K as a backing implementation
|
||||
// see https://cache2k.org/docs/latest/user-guide.html#getting-started
|
||||
private final ConcurrentHashMap<ID, OBJECT> backingMap = new ConcurrentHashMap<>();
|
||||
|
||||
private final ReentrantLock lock = new ReentrantLock();
|
||||
|
||||
public OBJECT get(final ID id) {
|
||||
lock.lock();
|
||||
try {
|
||||
return backingMap.get(id);
|
||||
}
|
||||
finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public OBJECT put(final ID id, final OBJECT o) {
|
||||
lock.lock();
|
||||
try {
|
||||
return backingMap.put(id, o);
|
||||
}
|
||||
finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public OBJECT remove(final ID id) {
|
||||
lock.lock();
|
||||
try {
|
||||
return backingMap.remove(id);
|
||||
}
|
||||
finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public Set<ID> keys() {
|
||||
lock.lock();
|
||||
try {
|
||||
return backingMap.keySet().stream().collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public Set<OBJECT> values() {
|
||||
lock.lock();
|
||||
try {
|
||||
return backingMap.values().stream().collect(Collectors.toUnmodifiableSet());
|
||||
}
|
||||
finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public Set<Map.Entry<ID, OBJECT>> entrySet() {
|
||||
lock.lock();
|
||||
try {
|
||||
return backingMap.entrySet();
|
||||
}
|
||||
finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public int size() {
|
||||
lock.lock();
|
||||
try {
|
||||
return backingMap.size();
|
||||
}
|
||||
finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
lock.lock();
|
||||
try {
|
||||
return backingMap.isEmpty();
|
||||
}
|
||||
finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean contains(final OBJECT o) {
|
||||
lock.lock();
|
||||
try {
|
||||
return backingMap.contains(o);
|
||||
}
|
||||
finally {
|
||||
lock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean containsKey(final ID key) {
|
||||
return backingMap.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {return true;}
|
||||
if (o == null || getClass() != o.getClass()) {return false;}
|
||||
|
||||
final InMemoryCache<?, ?> that = (InMemoryCache<?, ?>) o;
|
||||
|
||||
return backingMap.equals(that.backingMap);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return backingMap.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return backingMap.toString();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,131 @@
|
|||
package de.accso.flexinale.common.application.services;
|
||||
|
||||
import de.accso.flexinale.common.application.PersistMode;
|
||||
import de.accso.flexinale.common.application.PersistedEntityAndResult;
|
||||
import de.accso.flexinale.common.domain.model.AbstractDao;
|
||||
import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests;
|
||||
import de.accso.flexinale.common.shared_kernel.Identifiable;
|
||||
import de.accso.flexinale.common.shared_kernel.Mergeable;
|
||||
import org.apache.poi.ss.usermodel.Cell;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
import org.apache.poi.ss.usermodel.Workbook;
|
||||
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Optional;
|
||||
|
||||
import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.ADDED;
|
||||
import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.UPDATED;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@DoNotCheckInArchitectureTests
|
||||
public abstract class AbstractExcelDataUploadService<E extends Identifiable & Mergeable<E>> implements ExcelDataUploadService<E> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractExcelDataUploadService.class);
|
||||
|
||||
@Override
|
||||
public void beforeLoad(Object... o) {}
|
||||
|
||||
@Override
|
||||
public void afterLoad(Object... o) {}
|
||||
|
||||
@Override
|
||||
public final Collection<E> loadDataFromExcelSheet(final String resourceName) throws IOException {
|
||||
try (InputStream stream = AbstractExcelDataUploadService.class.getResourceAsStream(resourceName)) {
|
||||
if (stream == null) {
|
||||
throw new FileNotFoundException("resource '%s' not found!".formatted(resourceName));
|
||||
}
|
||||
return loadDataFromExcelSheet(stream);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final Collection<E> loadDataFromExcelSheet(final InputStream stream) throws IOException {
|
||||
Collection<E> dataSet = new HashSet<>();
|
||||
|
||||
// Excel sheet (the sheet, not the file!) needs to be named exactly like the entity's class name
|
||||
// (example: "Kino")
|
||||
String sheetName = getNameOfExcelDataType();
|
||||
|
||||
try (Workbook workbook = new XSSFWorkbook(stream)) {
|
||||
// iterate through each row of first/desired sheet from the workbook
|
||||
for (Row excelRow : workbook.getSheet(sheetName)) {
|
||||
// ignore comments, starting with # , load all other rows
|
||||
Cell firstCell = excelRow.getCell(0);
|
||||
if (firstCell != null && !firstCell.getStringCellValue().startsWith("#")) {
|
||||
E data = createDataFromExcelRow(excelRow);
|
||||
dataSet.add(data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return dataSet;
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int loadDataFromExcelSheetAndPersist(
|
||||
final String resourceName, final PersistMode mode) throws IOException {
|
||||
try (InputStream stream = AbstractExcelDataUploadService.class.getResourceAsStream(resourceName)) {
|
||||
if (stream == null) {
|
||||
throw new IOException("resource '%s' not found!".formatted(resourceName));
|
||||
}
|
||||
return loadDataFromExcelSheetAndPersist(stream, mode);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public final int loadDataFromExcelSheetAndPersist(
|
||||
final InputStream stream, final PersistMode mode) throws IOException {
|
||||
return loadDataFromExcelSheetAndPersistAndReturn(stream, mode).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PersistedEntityAndResult<E>> loadDataFromExcelSheetAndPersistAndReturn(
|
||||
final InputStream stream, final PersistMode mode) throws IOException {
|
||||
Collection<E> dataCollectionFromExcelSheet = loadDataFromExcelSheet(stream);
|
||||
return loadDataAndPersistAndReturn(dataCollectionFromExcelSheet, mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<PersistedEntityAndResult<E>> loadDataAndPersistAndReturn(
|
||||
final Collection<E> dataCollection, final PersistMode mode) {
|
||||
Collection<PersistedEntityAndResult<E>> dataAddedAndUpdated = new HashSet<>();
|
||||
|
||||
AbstractDao<E> dao = getDao();
|
||||
|
||||
String sheetName = getNameOfExcelDataType();
|
||||
|
||||
for (E dataFromExcelSheet : dataCollection) {
|
||||
Identifiable.Id id = dataFromExcelSheet.id();
|
||||
|
||||
Optional<E> optionalEntityFromDatabase = dao.findById(id);
|
||||
if (optionalEntityFromDatabase.isPresent()) {
|
||||
E entityFromDatabase = optionalEntityFromDatabase.get();
|
||||
switch(mode) {
|
||||
case ADD_ONLY ->
|
||||
LOGGER.warn("%s %s already exists. Not persisted as mode 'ADD_ONLY' is active".formatted(sheetName, id));
|
||||
case UPDATE -> {
|
||||
LOGGER.info("%s %s already exists. Merged as mode 'UPDATE' is active".formatted(sheetName, id));
|
||||
E mergedEntity = entityFromDatabase.merge(dataFromExcelSheet);
|
||||
dao.save(mergedEntity);
|
||||
|
||||
dataAddedAndUpdated.add( new PersistedEntityAndResult<>(dataFromExcelSheet, UPDATED) );
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
LOGGER.info("%s %s does not yet exist. Added as new entity".formatted(sheetName, id));
|
||||
dao.save(dataFromExcelSheet);
|
||||
|
||||
dataAddedAndUpdated.add( new PersistedEntityAndResult<>(dataFromExcelSheet, ADDED) );
|
||||
}
|
||||
}
|
||||
|
||||
return dataAddedAndUpdated;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package de.accso.flexinale.common.application.services;
|
||||
|
||||
import de.accso.flexinale.common.application.PersistMode;
|
||||
import de.accso.flexinale.common.application.PersistedEntityAndResult;
|
||||
import de.accso.flexinale.common.domain.model.AbstractDao;
|
||||
import de.accso.flexinale.common.shared_kernel.Identifiable;
|
||||
import org.apache.poi.ss.usermodel.Row;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collection;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface ExcelDataUploadService<E extends Identifiable> {
|
||||
Collection<E> loadDataFromExcelSheet(final String resourceName) throws IOException;
|
||||
Collection<E> loadDataFromExcelSheet(final InputStream stream) throws IOException;
|
||||
|
||||
int loadDataFromExcelSheetAndPersist(final String resourceName, PersistMode mode) throws IOException;
|
||||
int loadDataFromExcelSheetAndPersist(final InputStream stream, PersistMode mode) throws IOException;
|
||||
|
||||
Collection<PersistedEntityAndResult<E>>
|
||||
loadDataFromExcelSheetAndPersistAndReturn(final InputStream stream, PersistMode mode) throws IOException;
|
||||
Collection<PersistedEntityAndResult<E>>
|
||||
loadDataAndPersistAndReturn(Collection<E> data, PersistMode mode);
|
||||
|
||||
AbstractDao<E> getDao();
|
||||
String getNameOfExcelDataType();
|
||||
|
||||
void beforeLoad(Object... o);
|
||||
void afterLoad(Object... o);
|
||||
E createDataFromExcelRow(final Row row);
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package de.accso.flexinale.common.domain.model;
|
||||
|
||||
import de.accso.flexinale.common.shared_kernel.Identifiable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface AbstractDao<E extends Identifiable> {
|
||||
List<E> findAll();
|
||||
|
||||
Optional<E> findById(final Identifiable.Id id);
|
||||
|
||||
E save(final E entity);
|
||||
|
||||
void delete(final E data);
|
||||
void deleteById(final Identifiable.Id id);
|
||||
void deleteAll();
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package de.accso.flexinale.common.infrastructure;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventBusFactory;
|
||||
import de.accso.flexinale.common.infrastructure.eventbus.*;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Profile;
|
||||
|
||||
@Configuration
|
||||
public class FlexinaleCommonSpringFactory {
|
||||
|
||||
// Eventbus via Kafka
|
||||
|
||||
// default: used for scenarios using real distributed remote communication via Kafka
|
||||
// so when starting all three apps Besucherportal, Backoffice and Ticketing and also for "test-distributed"
|
||||
@Bean
|
||||
@Profile({"!test-integrated & !testdata & !configtest & !smoketest & !local"})
|
||||
public EventBusFactory createKafkaAsyncEventBusFactory(final KafkaAvailabilityChecker kafkaAvailabilityChecker) {
|
||||
return new KafkaAsyncEventBusFactory(kafkaAvailabilityChecker);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Profile({"!test-integrated & !testdata & !configtest & !smoketest & !local"})
|
||||
public KafkaAvailabilityChecker createKafkaAvailabilityChecker() {
|
||||
return new KafkaAvailabilityChecker();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Eventbus as in-memory with Spy
|
||||
|
||||
// used in tests where publishers and subscribers need to communicate via an in-memory event bus
|
||||
// and where we need to look into the event chain via the spy
|
||||
@Bean
|
||||
@Profile({"test-integrated"})
|
||||
public EventBusFactory createInMemorySyncEventBusSpyFactory() {
|
||||
return new InMemorySyncEventBusSpyFactory();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Eventbus as in-memory
|
||||
|
||||
// used in scenarios where publishers and subscribers need to communicated via an in-memory event bus
|
||||
@Bean
|
||||
@Profile({"local"})
|
||||
public EventBusFactory createInMemorySyncEventBusFactory() {
|
||||
return new InMemorySyncEventBusFactory();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// Eventbus doing nothing
|
||||
|
||||
// used in scenarios to clean and load test data where we do not want to send any event
|
||||
@Bean
|
||||
@Profile({"testdata | configtest | smoketest"})
|
||||
public EventBusFactory createNopeEventBusSpyFactory() {
|
||||
return new NopeEventBusSpyFactory();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
package de.accso.flexinale.common.infrastructure;
|
||||
|
||||
import de.accso.flexinale.common.application.Config;
|
||||
import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalArgumentException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class FlexinaleSpringConfig implements Config {
|
||||
@Value("${application.title}")
|
||||
public String applicationTitle;
|
||||
|
||||
@Value("${build.version}")
|
||||
public String buildVersion;
|
||||
|
||||
@Value("${build.date}")
|
||||
public String buildDate;
|
||||
|
||||
@Override
|
||||
public String getApplicationTitle() {
|
||||
return applicationTitle;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBuildVersion() {
|
||||
return buildVersion;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBuildDate() {
|
||||
return buildDate;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
private final int quoteOnline;
|
||||
|
||||
@Override
|
||||
public int getQuoteOnline() {
|
||||
return quoteOnline;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
private final int minZeitZwischenVorfuehrungenInMinuten;
|
||||
|
||||
@Override
|
||||
public int getMinZeitZwischenVorfuehrungenInMinuten() {
|
||||
return minZeitZwischenVorfuehrungenInMinuten;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
public FlexinaleSpringConfig(@Value("${de.accso.flexinale.kontingent.quote.online:33}") final int quoteOnline,
|
||||
@Value("${de.accso.flexinale.vorfuehrung.min-zeit-zwischen-vorfuehrungen-in-minuten:30}") final int minZeitZwischenVorfuehrungenInMinuten) {
|
||||
if (quoteOnline < 0 || quoteOnline > 100) {
|
||||
String message = "Quote online should be a percentage, i.e. 0 <= quote online <= 100, but was " + quoteOnline;
|
||||
throw new FlexinaleIllegalArgumentException(message);
|
||||
}
|
||||
this.quoteOnline = quoteOnline;
|
||||
|
||||
if (minZeitZwischenVorfuehrungenInMinuten < 0) {
|
||||
String message = "minZeitZwischenVorfuehrungenInMinuten should be positiv, but was "
|
||||
+ minZeitZwischenVorfuehrungenInMinuten;
|
||||
throw new FlexinaleIllegalArgumentException(message);
|
||||
}
|
||||
this.minZeitZwischenVorfuehrungenInMinuten = minZeitZwischenVorfuehrungenInMinuten;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAutoDetect;
|
||||
import com.fasterxml.jackson.annotation.PropertyAccessor;
|
||||
import com.fasterxml.jackson.core.JsonGenerator;
|
||||
import com.fasterxml.jackson.core.JsonParser;
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.*;
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public final class EventSerializationHelper {
|
||||
private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME;
|
||||
|
||||
private static final ObjectMapper jsonMapper;
|
||||
|
||||
static {
|
||||
jsonMapper = new ObjectMapper();
|
||||
jsonMapper.findAndRegisterModules();
|
||||
jsonMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
|
||||
|
||||
// register module for date/time (de)serialization, in ISO format ISO8601, e.g. 2022-05-06T11:12:13Z
|
||||
SimpleModule localDateTimeModule = new SimpleModule();
|
||||
localDateTimeModule.addSerializer(LocalDateTime.class, new JsonSerializer<>() {
|
||||
@Override
|
||||
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
gen.writeString(value.format(dateTimeFormatter));
|
||||
}
|
||||
});
|
||||
localDateTimeModule.addDeserializer(LocalDateTime.class, new JsonDeserializer<>() {
|
||||
@Override
|
||||
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
|
||||
return LocalDateTime.parse(p.getValueAsString(), dateTimeFormatter);
|
||||
}
|
||||
});
|
||||
jsonMapper.registerModule(localDateTimeModule);
|
||||
}
|
||||
|
||||
public static String serializeEvent2JsonString(final Event event) throws JsonProcessingException {
|
||||
return jsonMapper.writeValueAsString(event);
|
||||
}
|
||||
|
||||
public static <E extends Event> E deserializeJsonString2Event(
|
||||
final String jsonString, final Class<E> eventType) throws JsonProcessingException {
|
||||
return jsonMapper.readValue(jsonString, eventType);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
final class EventSubscriberReceiveDispatcher<E extends Event> {
|
||||
|
||||
void callSubscriberReceiveMethod(final Class<E> eventType, final E event, final EventSubscriber<E> subscriber) {
|
||||
boolean called = false;
|
||||
|
||||
if (subscriber instanceof EventSubscriber.EventSubscriber9<?, ?, ?, ?, ?, ?, ?, ?, ?>) {
|
||||
called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE9);
|
||||
}
|
||||
if (!called && (subscriber instanceof EventSubscriber.EventSubscriber8<?, ?, ?, ?, ?, ?, ?, ?>)) {
|
||||
called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE8);
|
||||
}
|
||||
if (!called && (subscriber instanceof EventSubscriber.EventSubscriber7<?, ?, ?, ?, ?, ?, ?>)) {
|
||||
called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE7);
|
||||
}
|
||||
if (!called && (subscriber instanceof EventSubscriber.EventSubscriber6<?, ?, ?, ?, ?, ?>)) {
|
||||
called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE6);
|
||||
}
|
||||
if (!called && (subscriber instanceof EventSubscriber.EventSubscriber5<?, ?, ?, ?, ?>)) {
|
||||
called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE5);
|
||||
}
|
||||
if (!called && (subscriber instanceof EventSubscriber.EventSubscriber4<?, ?, ?, ?>)) {
|
||||
called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE4);
|
||||
}
|
||||
if (!called && (subscriber instanceof EventSubscriber.EventSubscriber3<?, ?, ?>)) {
|
||||
called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE3);
|
||||
}
|
||||
if (!called && (subscriber instanceof EventSubscriber.EventSubscriber2<?, ?>)) {
|
||||
called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE2);
|
||||
}
|
||||
if (!called) {
|
||||
subscriber.receive(event);
|
||||
}
|
||||
}
|
||||
|
||||
private record ReflectionMethodCacheKey<E extends Event>(
|
||||
EventSubscriber<E> subscriber, String methodName, Class<E> eventType) {}
|
||||
|
||||
private final Map<ReflectionMethodCacheKey<E>, Method> reflectionMethodCache = new HashMap<>();
|
||||
|
||||
private boolean tryToGetAndInvokeReceiveMethod(
|
||||
final Class<E> eventType, final E event, final EventSubscriber<E> subscriber, final String methodName)
|
||||
{
|
||||
if (subscriber == null) {
|
||||
throw new DeveloperMistakeException("could not call a method for a null subscriber for event type %s, event=%s"
|
||||
.formatted(eventType, event.toString()));
|
||||
}
|
||||
|
||||
ReflectionMethodCacheKey<E> key = new ReflectionMethodCacheKey<>(subscriber, methodName, eventType);
|
||||
Method receiveMethod = reflectionMethodCache.get(key);
|
||||
try {
|
||||
// we could extend this that also receive methods are found with have an event type as parameter as a
|
||||
// superclass of 'eventtype', cf test case
|
||||
// InMemorySyncEventBusAndEventBusSpyTest.testGenericEventSubscriberWithExtraSubscriptionCannotBeUsedDueToDispatchingError()
|
||||
if (receiveMethod == null) {
|
||||
receiveMethod = subscriber.getClass().getMethod(methodName, eventType);
|
||||
receiveMethod.setAccessible(true);
|
||||
reflectionMethodCache.put(key, receiveMethod);
|
||||
}
|
||||
receiveMethod.invoke(subscriber, event);
|
||||
return true;
|
||||
}
|
||||
catch (NoSuchMethodException nsmex) {
|
||||
return false;
|
||||
}
|
||||
// any exception which is thrown because of "wrong" reflection due to a developer mistake (there is no test for that, could not create a test setup for this)
|
||||
catch (IllegalAccessException | IllegalArgumentException | NullPointerException | ExceptionInInitializerError ex) {
|
||||
throw new DeveloperMistakeException(("DeveloperMistakeException when calling the receive method %s for the subscriber %s " +
|
||||
"for event type %s, event=%s").formatted(methodName, subscriber, eventType, event.toString()), ex);
|
||||
}
|
||||
// Any other exception which is thrown at runtime - which is ok and hence not a developer mistake
|
||||
// Such exceptions are always wrapped in a InvocationTargetException
|
||||
catch (InvocationTargetException ex) {
|
||||
throw new RuntimeException("Exception when calling the receive method '%s' for the subscriber %s for event type %s, event=%s"
|
||||
.formatted(methodName, subscriber, eventType, event.toString()), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,144 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventBus;
|
||||
import de.accso.flexinale.common.api.eventbus.EventContextHolder;
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException;
|
||||
import de.accso.flexinale.common.shared_kernel.Selection;
|
||||
import de.accso.flexinale.common.shared_kernel.SelectionMultiMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class InMemorySyncEventBus<E extends Event> implements EventBus<E> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(InMemorySyncEventBus.class);
|
||||
|
||||
protected record SubscriberGroup(String consumerGroupName) {
|
||||
static SubscriberGroup of(EventSubscriber<?> subscriber) {
|
||||
return new SubscriberGroup(subscriber.getGroupName());
|
||||
}
|
||||
}
|
||||
|
||||
protected final Class<E> eventType;
|
||||
|
||||
protected static final EventContextHolder eventContextHolder = new InMemorySyncEventContextHolder();
|
||||
|
||||
protected final EventSubscriberReceiveDispatcher<E> subscriberReceiveDispatcher = new EventSubscriberReceiveDispatcher<>();
|
||||
protected final EventSubscriberReceiveDispatcher<Event> genericSubscriberReceiveDispatcher = new EventSubscriberReceiveDispatcher<>();
|
||||
|
||||
protected final SelectionMultiMap<SubscriberGroup, EventSubscriber<E>> subscribersPerGroupUsingSelection =
|
||||
new SelectionMultiMap<>(Selection.SelectionAlgorithm.ROUND_ROBIN);
|
||||
protected final SelectionMultiMap<SubscriberGroup, EventSubscriber<Event>> genericSubscribersPerGroupUsingSelection =
|
||||
new SelectionMultiMap<>(Selection.SelectionAlgorithm.ROUND_ROBIN);
|
||||
|
||||
InMemorySyncEventBus(final Class<E> eventType) {
|
||||
this.eventType = eventType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventContextHolder getEventContextHolder() {
|
||||
return eventContextHolder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(final Class<E> eventType, final EventSubscriber<E> subscriber,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
LOGGER.debug("InMemorySyncEventBus<%s>: subscriber %s registered".formatted(eventType.getSimpleName(), subscriber));
|
||||
|
||||
subscribersPerGroupUsingSelection.put(SubscriberGroup.of(subscriber), subscriber);
|
||||
|
||||
handle_SubscriptionAtStart(eventSubscriptionAtStart);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(final EventSubscriber<Event> genericSubscriber,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
LOGGER.debug("InMemorySyncEventBus<%s>: generic subscriber %s registered".formatted(eventType.getSimpleName(), genericSubscriber));
|
||||
|
||||
genericSubscribersPerGroupUsingSelection.put(SubscriberGroup.of(genericSubscriber), genericSubscriber);
|
||||
|
||||
handle_SubscriptionAtStart(eventSubscriptionAtStart);
|
||||
}
|
||||
|
||||
private void handle_SubscriptionAtStart(final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
switch (eventSubscriptionAtStart) {
|
||||
case START_READING_FROM_BEGINNING, START_READING_FROM_LAST_TIME -> {
|
||||
throw new DeveloperMistakeException("not implemented. Probably want to use InMemorySyncEventBusSpy");
|
||||
}
|
||||
case START_READING_FROM_NOW -> {
|
||||
// nope, nothing to do
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe(final Class<E> eventType, final EventSubscriber<E> subscriber) {
|
||||
LOGGER.debug("InMemorySyncEventBus<%s>: subscriber %s unregistered".formatted(eventType.getSimpleName(), subscriber));
|
||||
|
||||
subscribersPerGroupUsingSelection.removeMapping(SubscriberGroup.of(subscriber), subscriber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe(EventSubscriber<Event> genericSubscriber) {
|
||||
LOGGER.debug("InMemorySyncEventBus<%s>: subscriber %s unregistered".formatted(eventType.getSimpleName(), genericSubscriber));
|
||||
|
||||
genericSubscribersPerGroupUsingSelection.removeMapping(SubscriberGroup.of(genericSubscriber), genericSubscriber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribeAll() {
|
||||
LOGGER.debug("InMemorySyncEventBus<%s>: all subscribers and generic subscribers unregistered".formatted(eventType.getSimpleName()));
|
||||
|
||||
subscribersPerGroupUsingSelection.clear();
|
||||
genericSubscribersPerGroupUsingSelection.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(final Class<E> eventType, final E event) {
|
||||
LOGGER.info("InMemorySyncEventBus<%s>: publish new event %s".formatted(eventType, event));
|
||||
|
||||
// publish event to subscribers (depending on the selection algorithm, e.g. RoundRobin)
|
||||
try {
|
||||
eventContextHolder.set(event.eventContext());
|
||||
|
||||
// for each group of the subscribers, select one (or more) subscribers, using the Selection algorithm
|
||||
subscribersPerGroupUsingSelection.keySet()
|
||||
.forEach(subscriberGroup -> {
|
||||
Selection<EventSubscriber<E>> allSubscribersInGroup =
|
||||
(Selection<EventSubscriber<E>>) subscribersPerGroupUsingSelection.get(subscriberGroup);
|
||||
Set<EventSubscriber<E>> subscribersToSendTo = allSubscribersInGroup.next();
|
||||
subscribersToSendTo.forEach(subscriberToSendTo ->
|
||||
internal_sendToSubscriber(eventType, event, subscriberToSendTo));
|
||||
});
|
||||
|
||||
// for each group of the generic subscribers, select one (or more) subscribers, using the Selection algorithm
|
||||
genericSubscribersPerGroupUsingSelection.keySet()
|
||||
.forEach(subscriberGroup -> {
|
||||
Selection<EventSubscriber<Event>> allGenericSubscribersInGroup =
|
||||
(Selection<EventSubscriber<Event>>) genericSubscribersPerGroupUsingSelection.get(subscriberGroup);
|
||||
Set<EventSubscriber<Event>> genericSubscribersToSendTo = allGenericSubscribersInGroup.next();
|
||||
genericSubscribersToSendTo.forEach(genericSubscriberToSendTo ->
|
||||
internal_sendToGenericSubscriber(event, genericSubscriberToSendTo));
|
||||
});
|
||||
}
|
||||
finally {
|
||||
eventContextHolder.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// send event to one subscriber
|
||||
protected void internal_sendToSubscriber(Class<E> eventType, E event, EventSubscriber<E> subscriber) {
|
||||
subscriberReceiveDispatcher.callSubscriberReceiveMethod(eventType, event, subscriber);
|
||||
}
|
||||
|
||||
// send event to one generic subscriber
|
||||
protected void internal_sendToGenericSubscriber(E event, EventSubscriber<Event> subscriber) {
|
||||
genericSubscriberReceiveDispatcher.callSubscriberReceiveMethod(Event.class, event, subscriber);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventBus;
|
||||
import de.accso.flexinale.common.api.eventbus.EventBusFactory;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class InMemorySyncEventBusFactory implements EventBusFactory {
|
||||
protected final Map<Class<? extends Event>, EventBus<? extends Event>> eventTypeToEventBusMap = new HashMap<>();
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <E extends Event> EventBus<E> createOrGetEventBusFor(final Class<E> eventType) {
|
||||
EventBus<? extends Event> eventBus = eventTypeToEventBusMap.get(eventType);
|
||||
if (eventBus == null) {
|
||||
eventBus = new InMemorySyncEventBus<>(eventType);
|
||||
eventTypeToEventBusMap.put(eventType, eventBus);
|
||||
}
|
||||
return (EventBus<E>) eventBus;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,130 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import de.accso.flexinale.common.shared_kernel.ThreadSafeCounterMap;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public class InMemorySyncEventBusSpy<E extends Event> extends InMemorySyncEventBus<E> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(InMemorySyncEventBusSpy.class);
|
||||
|
||||
// localEventQueue is used as an in-memory storage and in tests as a spy
|
||||
private final ConcurrentLinkedQueue<E> localEventQueue = new ConcurrentLinkedQueue<>(); // TODO is currently never cleared - might want to use Cache implementation with automatic time-to-live?
|
||||
|
||||
private final ThreadSafeCounterMap<SubscriberGroup> consumerGroupOffsetMap = new ThreadSafeCounterMap<>();
|
||||
|
||||
InMemorySyncEventBusSpy(final Class<E> eventType) {
|
||||
super(eventType);
|
||||
}
|
||||
|
||||
public List<E> allEvents() {
|
||||
return localEventQueue.stream().toList();
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
localEventQueue.clear();
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return localEventQueue.size();
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
@Override
|
||||
public void subscribe(final Class<E> eventType, final EventSubscriber<E> subscriber,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
LOGGER.debug("InMemorySyncEventBusSpy<%s>: subscriber %s registered".formatted(eventType.getSimpleName(), subscriber));
|
||||
|
||||
subscribersPerGroupUsingSelection.put(SubscriberGroup.of(subscriber), subscriber);
|
||||
|
||||
handle_SubscriptionAtStart(eventType, subscriber, eventSubscriptionAtStart);
|
||||
}
|
||||
|
||||
private void handle_SubscriptionAtStart(final Class<E> eventType,
|
||||
final EventSubscriber<E> subscriber,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
switch (eventSubscriptionAtStart) {
|
||||
case START_READING_FROM_BEGINNING -> {
|
||||
// re-publish all events from beginning
|
||||
localEventQueue.forEach(event -> this.internal_sendToSubscriber(eventType, event, subscriber));
|
||||
}
|
||||
case START_READING_FROM_NOW -> {
|
||||
// nope, nothing to do
|
||||
}
|
||||
case START_READING_FROM_LAST_TIME -> {
|
||||
long offset = consumerGroupOffsetMap.get(SubscriberGroup.of(subscriber));
|
||||
|
||||
// includes the case, that a consumer group was not known before -
|
||||
// as then offset from the map is 0
|
||||
// so nothing skipped but same behaviour as START_READING_FROM_BEGINNING
|
||||
localEventQueue.stream().
|
||||
skip(offset).
|
||||
forEach(event -> this.internal_sendToSubscriber(eventType, event, subscriber));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(EventSubscriber<Event> genericSubscriber,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
LOGGER.debug("InMemorySyncEventBusSpy<%s>: generic subscriber %s registered".formatted(eventType.getSimpleName(), genericSubscriber));
|
||||
|
||||
genericSubscribersPerGroupUsingSelection.put(SubscriberGroup.of(genericSubscriber), genericSubscriber);
|
||||
|
||||
handle_SubscriptionAtStart(genericSubscriber, eventSubscriptionAtStart);
|
||||
}
|
||||
|
||||
private void handle_SubscriptionAtStart(final EventSubscriber<Event> genericSubscriber,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
switch (eventSubscriptionAtStart) {
|
||||
case START_READING_FROM_BEGINNING -> {
|
||||
// re-publish all events from beginning
|
||||
localEventQueue.forEach(event -> this.internal_sendToGenericSubscriber(event, genericSubscriber));
|
||||
}
|
||||
case START_READING_FROM_NOW -> {
|
||||
// nope, nothing to do
|
||||
}
|
||||
case START_READING_FROM_LAST_TIME -> {
|
||||
long offset = consumerGroupOffsetMap.get(SubscriberGroup.of(genericSubscriber));
|
||||
|
||||
// includes the case, that a consumer group was not known before, ie. offset is 0,
|
||||
// so nothing skipped but same behaviour as START_READING_FROM_BEGINNING
|
||||
localEventQueue.stream().
|
||||
skip(offset).
|
||||
forEach(event -> this.internal_sendToGenericSubscriber(event, genericSubscriber));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(final Class<E> eventType, final E event) {
|
||||
LOGGER.info("InMemorySyncEventBusSpy<%s>: publish new event %s".formatted(eventType, event));
|
||||
|
||||
super.publish(eventType, event);
|
||||
|
||||
// persist event in local (in-memory) storage (i.e. spy)
|
||||
localEventQueue.add(event);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------------
|
||||
|
||||
// send event to one subscriber
|
||||
@Override
|
||||
protected void internal_sendToSubscriber(final Class<E> eventType, final E event, final EventSubscriber<E> subscriber) {
|
||||
subscriberReceiveDispatcher.callSubscriberReceiveMethod(eventType, event, subscriber);
|
||||
consumerGroupOffsetMap.increment(SubscriberGroup.of(subscriber));
|
||||
}
|
||||
|
||||
// send event to one generic subscriber
|
||||
@Override
|
||||
protected void internal_sendToGenericSubscriber(final E event, final EventSubscriber<Event> genericSubscriber) {
|
||||
genericSubscriberReceiveDispatcher.callSubscriberReceiveMethod(Event.class, event, genericSubscriber);
|
||||
consumerGroupOffsetMap.increment(SubscriberGroup.of(genericSubscriber));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventBus;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
|
||||
public class InMemorySyncEventBusSpyFactory extends InMemorySyncEventBusFactory {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <E extends Event> EventBus<E> createOrGetEventBusFor(final Class<E> eventType) {
|
||||
EventBus<? extends Event> eventBus = eventTypeToEventBusMap.get(eventType);
|
||||
if (eventBus == null) {
|
||||
eventBus = new InMemorySyncEventBusSpy<>(eventType);
|
||||
eventTypeToEventBusMap.put(eventType, eventBus);
|
||||
}
|
||||
return (EventBus<E>) eventBus;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventContextHolder;
|
||||
import de.accso.flexinale.common.api.event.EventContext;
|
||||
import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
public class InMemorySyncEventContextHolder implements EventContextHolder {
|
||||
|
||||
// this implementation holds only one context:
|
||||
// - as long as a context with the same correlation id is used, it is kept, i.e. counter is increased (so that a remove is not deleting it yet)
|
||||
// - as soon as a context with a different correlation id is used, it is reset, i.e. counter is set to 1
|
||||
// -> So it is not possible to use a separate event chain and go back - for that, we could change that to a stack
|
||||
// where context instances are pushed as new and popped to be cleared
|
||||
private EventContext CONTEXT = null;
|
||||
private AtomicInteger counter = new AtomicInteger(0);
|
||||
|
||||
@Override
|
||||
public EventContext get() {
|
||||
return CONTEXT;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(final EventContext eventContext) {
|
||||
if (CONTEXT != null && CONTEXT.equalsByCorrelationId(eventContext)) {
|
||||
counter.incrementAndGet();
|
||||
}
|
||||
else {
|
||||
counter = new AtomicInteger(1);
|
||||
CONTEXT = eventContext;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
if (counter.get() == 0) {
|
||||
throw new DeveloperMistakeException("remove on InMemorySyncEventContextHolder once too much, should not happen");
|
||||
}
|
||||
else {
|
||||
counter.decrementAndGet();
|
||||
if (counter.get() == 0) {
|
||||
CONTEXT = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return CONTEXT == null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventBus;
|
||||
import de.accso.flexinale.common.api.eventbus.EventContextHolder;
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException;
|
||||
import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class KafkaAsyncEventBus<E extends Event> implements EventBus<E> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KafkaAsyncEventBus.class);
|
||||
|
||||
private static final EventContextHolder eventContextHolder = new ThreadLocalEventContextHolder();
|
||||
|
||||
private KafkaProducerAdapter<E> kafkaProducerAdapter;
|
||||
private KafkaConsumerAdapter<E> kafkaConsumerAdapter;
|
||||
|
||||
private final KafkaAvailabilityChecker kafkaAvailabilityChecker;
|
||||
|
||||
private final Class<E> eventType; // TODO might want to use this in order to simplify the interface and get rid of the method's first parameter
|
||||
|
||||
KafkaAsyncEventBus(final Class<E> eventType, final KafkaAvailabilityChecker kafkaAvailabilityChecker) {
|
||||
this.eventType = eventType;
|
||||
this.kafkaAvailabilityChecker = kafkaAvailabilityChecker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(final Class<E> eventType, final EventSubscriber<E> subscriber,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
LOGGER.info("KafkaAsyncEventBus<%s>: subscriber %s registered".formatted(eventType, subscriber));
|
||||
|
||||
if (kafkaConsumerAdapter == null) {
|
||||
kafkaConsumerAdapter = createKafkaConsumerAdapter(eventType, subscriber, eventSubscriptionAtStart);
|
||||
startKafkaConsumerAdapterInSeparateThread(kafkaConsumerAdapter);
|
||||
}
|
||||
kafkaConsumerAdapter.addSubscriber(subscriber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(EventSubscriber<Event> genericSubscriber,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
LOGGER.info("KafkaAsyncEventBus<%s>: generic subscriber %s registered".formatted(eventType, genericSubscriber));
|
||||
|
||||
if (kafkaConsumerAdapter == null) {
|
||||
kafkaConsumerAdapter = createKafkaConsumerAdapter(eventType, genericSubscriber, eventSubscriptionAtStart);
|
||||
startKafkaConsumerAdapterInSeparateThread(kafkaConsumerAdapter);
|
||||
}
|
||||
kafkaConsumerAdapter.addGenericSubscriber(genericSubscriber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe(final Class<E> eventType, final EventSubscriber<E> subscriber) {
|
||||
LOGGER.info("KafkaAsyncEventBus<%s>: subscriber %s unregistered".formatted(eventType, subscriber));
|
||||
|
||||
if (kafkaConsumerAdapter == null) {
|
||||
throw new DeveloperMistakeException("unsubscribe a subscriber from non-existing kafkaConsumerAdapter");
|
||||
}
|
||||
kafkaConsumerAdapter.removeSubscriber(subscriber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe(EventSubscriber<Event> genericSubscriber) {
|
||||
LOGGER.info("KafkaAsyncEventBus<%s>: generic subscriber %s unregistered".formatted(eventType, genericSubscriber));
|
||||
|
||||
if (kafkaConsumerAdapter == null) {
|
||||
throw new DeveloperMistakeException("unsubscribe a generic subscriber from non-existing kafkaConsumerAdapter");
|
||||
}
|
||||
kafkaConsumerAdapter.removeGenericSubscriber(genericSubscriber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribeAll() {
|
||||
LOGGER.info("KafkaAsyncEventBus<%s>: all subscribers unregistered".formatted(eventType));
|
||||
|
||||
if (kafkaConsumerAdapter == null) {
|
||||
throw new DeveloperMistakeException("unsubscribe all subscribers from non-existing kafkaConsumerAdapter");
|
||||
}
|
||||
kafkaConsumerAdapter.removeAllSubscribers();
|
||||
kafkaConsumerAdapter.removeAllGenericSubscribers();
|
||||
kafkaConsumerAdapter.shutdown(); // shutdown adapter with last subscriber
|
||||
kafkaConsumerAdapter = null;
|
||||
|
||||
// might want to add a common shutdown method?
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(final Class<E> eventType, final E event) {
|
||||
LOGGER.info("KafkaAsyncEventBus<%s>: publish new event %s".formatted(eventType, event));
|
||||
|
||||
if (kafkaProducerAdapter == null) {
|
||||
kafkaProducerAdapter = createKafkaProducerAdapter(eventType);
|
||||
}
|
||||
|
||||
kafkaProducerAdapter.publish(eventType, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventContextHolder getEventContextHolder() {
|
||||
return eventContextHolder;
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------
|
||||
|
||||
private KafkaConsumerAdapter<E> createKafkaConsumerAdapter(final Class<E> eventType, final EventSubscriber<?> subscriber,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
// check this once for every new KafkaConsumerAdapter
|
||||
checkKafkaAvailability();
|
||||
return new KafkaConsumerAdapter<>(eventType, subscriber, eventContextHolder, eventSubscriptionAtStart);
|
||||
}
|
||||
|
||||
private void startKafkaConsumerAdapterInSeparateThread(final KafkaConsumerAdapter<E> kafkaConsumerAdapter) {
|
||||
String threadName = "thread4%s<%s>".formatted(kafkaConsumerAdapter.getClass().getName(), eventType);
|
||||
new Thread(this.kafkaConsumerAdapter, threadName).start();
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------
|
||||
|
||||
private KafkaProducerAdapter<E> createKafkaProducerAdapter(final Class<E> eventType) {
|
||||
// check this once for every new KafkaProducerAdapter
|
||||
checkKafkaAvailability();
|
||||
return new KafkaProducerAdapter<>(eventType);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------
|
||||
|
||||
private void checkKafkaAvailability() {
|
||||
try {
|
||||
if (! kafkaAvailabilityChecker.isKafkaAvailable()) {
|
||||
throw new FlexinaleIllegalStateException("Kafka is not available.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new FlexinaleIllegalStateException("Kafka is not available.", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventBus;
|
||||
import de.accso.flexinale.common.api.eventbus.EventBusFactory;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class KafkaAsyncEventBusFactory implements EventBusFactory {
|
||||
private final Map < Class<? extends Event>,
|
||||
EventBus<? extends Event> > eventTypeToEventBusMap = new HashMap<>();
|
||||
|
||||
private final KafkaAvailabilityChecker kafkaAvailabilityChecker;
|
||||
|
||||
public KafkaAsyncEventBusFactory(final KafkaAvailabilityChecker kafkaAvailabilityChecker) {
|
||||
this.kafkaAvailabilityChecker = kafkaAvailabilityChecker;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <E extends Event> EventBus<E> createOrGetEventBusFor(final Class<E> eventType) {
|
||||
EventBus<? extends Event> eventBus = eventTypeToEventBusMap.get(eventType);
|
||||
if (eventBus == null) {
|
||||
eventBus = new KafkaAsyncEventBus<>(eventType, kafkaAvailabilityChecker);
|
||||
eventTypeToEventBusMap.put(eventType, eventBus);
|
||||
}
|
||||
return (EventBus<E>) eventBus;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
|
||||
import org.apache.kafka.clients.admin.AdminClient;
|
||||
import org.apache.kafka.clients.admin.DescribeClusterOptions;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
import static de.accso.flexinale.common.infrastructure.eventbus.KafkaConfiguration.adminConfig;
|
||||
import static de.accso.flexinale.common.infrastructure.eventbus.KafkaConfiguration.kafkaTimeoutInMs;
|
||||
|
||||
public final class KafkaAvailabilityChecker {
|
||||
|
||||
// Kafka availability is checked intentionally in Spring init phase
|
||||
// so the whole application (or test) won't start if no Kafka is available
|
||||
|
||||
public KafkaAvailabilityChecker() {
|
||||
try {
|
||||
if (!isKafkaAvailable()) {
|
||||
// intentionally not written to Log as this is checked in init phase where not even a logger
|
||||
// might be available yet.
|
||||
System.err.println("ERROR - Kafka is not available! Start Kafka before starting this application or test!");
|
||||
throw new FlexinaleIllegalStateException("Kafka is not available.");
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
System.err.println("ERROR - Kafka is not available! Start Kafka before starting this application or test!");
|
||||
throw new FlexinaleIllegalStateException("Kafka is not available.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings({"SameReturnValue", "BooleanMethodIsAlwaysInverted"})
|
||||
public boolean isKafkaAvailable() throws ExecutionException {
|
||||
Map<String, Object> kafkaAdminConfig = adminConfig();
|
||||
|
||||
try (var adminClient = AdminClient.create( kafkaAdminConfig )) {
|
||||
adminClient.describeCluster(new DescribeClusterOptions().timeoutMs(kafkaTimeoutInMs)).nodes().get();
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
public static void main(String[] args) throws ExecutionException {
|
||||
boolean kafkaAvailable = new KafkaAvailabilityChecker().isKafkaAvailable();
|
||||
if (kafkaAvailable) {
|
||||
System.out.println("All fine, Kafka is available");
|
||||
}
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart;
|
||||
import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import static de.accso.flexinale.common.shared_kernel.Identifiable.Id.uuidString;
|
||||
import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG;
|
||||
import static org.apache.kafka.clients.consumer.ConsumerConfig.*;
|
||||
import static org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG;
|
||||
import static org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG;
|
||||
|
||||
//TODO externalize to kafka properties file (but than we have it fixed for all topics - in the Java code we could use different configs)
|
||||
//Note that in the application.properties there is something configured already (like the (De)Serializer classes)
|
||||
public final class KafkaConfiguration {
|
||||
|
||||
public static final int kafkaTimeoutInMs = 30_000;
|
||||
|
||||
public static final String KEY_BOOTSTRAP_SERVERS = BOOTSTRAP_SERVERS_CONFIG;
|
||||
public static final String defaultBootstrapServers = "localhost:29092";
|
||||
|
||||
public static final String KEY_SERIALIZER = KEY_SERIALIZER_CLASS_CONFIG;
|
||||
public static final String keySerializer = "org.apache.kafka.common.serialization.StringSerializer";
|
||||
public static final String VALUE_SERIALIZER = VALUE_SERIALIZER_CLASS_CONFIG;
|
||||
public static final String valueSerializer = "org.apache.kafka.common.serialization.StringSerializer";
|
||||
|
||||
public static final String KEY_DESERIALIZER = KEY_DESERIALIZER_CLASS_CONFIG;
|
||||
public static final String keyDeserializer = "org.apache.kafka.common.serialization.StringDeserializer";
|
||||
public static final String VALUE_DESERIALIZER = VALUE_DESERIALIZER_CLASS_CONFIG;
|
||||
public static final String valueDeserializer = "org.apache.kafka.common.serialization.StringDeserializer";
|
||||
|
||||
public static final String CONSUMER_GROUP_ID_CONFIG = GROUP_ID_CONFIG;
|
||||
|
||||
public static Map<String, Object> producerConfig() {
|
||||
return Map.of(
|
||||
KEY_BOOTSTRAP_SERVERS, getBootstrapServers(),
|
||||
KEY_SERIALIZER, getClassFor(keySerializer),
|
||||
VALUE_SERIALIZER, getClassFor(valueSerializer)
|
||||
);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SameParameterValue")
|
||||
public static Map<String, Object> consumerConfig(final Class<?> eventType, final EventSubscriber<?> subscriber,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
|
||||
config.put(KEY_BOOTSTRAP_SERVERS, getBootstrapServers());
|
||||
config.put(KEY_DESERIALIZER, getClassFor(keyDeserializer));
|
||||
config.put(VALUE_DESERIALIZER, getClassFor(valueDeserializer));
|
||||
|
||||
switch (eventSubscriptionAtStart) {
|
||||
case START_READING_FROM_BEGINNING -> {
|
||||
// make new consumer group and read from earliest offset (works only for new consumer groups)
|
||||
config.put(CONSUMER_GROUP_ID_CONFIG, subscriber.getGroupName() + "<" + eventType + ">" + uuidString());
|
||||
config.put(AUTO_OFFSET_RESET_CONFIG, "earliest");
|
||||
}
|
||||
case START_READING_FROM_NOW -> {
|
||||
// make new consumer group and read from latest offset (works only for new consumer groups)
|
||||
config.put(CONSUMER_GROUP_ID_CONFIG, subscriber.getGroupName() + "<" + eventType + ">" + uuidString());
|
||||
config.put(AUTO_OFFSET_RESET_CONFIG, "latest");
|
||||
}
|
||||
case START_READING_FROM_LAST_TIME -> {
|
||||
config.put(CONSUMER_GROUP_ID_CONFIG, subscriber.getGroupName() + "<" + eventType + ">");
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public static String getBootstrapServers() {
|
||||
String systemPropertyBootstrapServers = System.getProperty(BOOTSTRAP_SERVERS_CONFIG);
|
||||
return (systemPropertyBootstrapServers == null) ? defaultBootstrapServers : systemPropertyBootstrapServers;
|
||||
}
|
||||
|
||||
public static Map<String, Object> adminConfig() {
|
||||
return Map.of(
|
||||
KEY_BOOTSTRAP_SERVERS, getBootstrapServers()
|
||||
);
|
||||
}
|
||||
|
||||
private static Class<?> getClassFor(final String classAsString) {
|
||||
try {
|
||||
return Class.forName(classAsString);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
throw new FlexinaleIllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventContextHolder;
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecords;
|
||||
import org.apache.kafka.clients.consumer.KafkaConsumer;
|
||||
import org.apache.kafka.common.errors.WakeupException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.kafka.listener.CommonErrorHandler;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import static de.accso.flexinale.common.infrastructure.eventbus.KafkaConfiguration.CONSUMER_GROUP_ID_CONFIG;
|
||||
|
||||
final class KafkaConsumerAdapter<E extends Event> implements Runnable {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KafkaConsumerAdapter.class);
|
||||
|
||||
private final KafkaConsumer<String, String> internal_KafkaConsumer;
|
||||
private final static Lock lock4KafkaConsumer = new ReentrantLock(); // the internal_KafkaConsumer is not thread-safe
|
||||
|
||||
private final String kafkaTopicName;
|
||||
|
||||
private final AtomicBoolean consumerIsShutdownAndNoLongerConsumingFromKafka = new AtomicBoolean(false);
|
||||
private final CommonErrorHandler errorHandler = new KafkaConsumerErrorHandler();
|
||||
|
||||
private final Class<E> eventType;
|
||||
private final EventContextHolder eventContextHolder;
|
||||
|
||||
private final ConcurrentLinkedQueue<EventSubscriber<E>> subscribers = new ConcurrentLinkedQueue<>();
|
||||
private final ConcurrentLinkedQueue<EventSubscriber<Event>> genericSubscribers = new ConcurrentLinkedQueue<>();
|
||||
|
||||
private final EventSubscriberReceiveDispatcher<E> eventSubscriberReceiveDispatcher = new EventSubscriberReceiveDispatcher<>();
|
||||
private final EventSubscriberReceiveDispatcher<Event> eventGenericSubscriberReceiveDispatcher = new EventSubscriberReceiveDispatcher<>();
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------
|
||||
|
||||
KafkaConsumerAdapter(final Class<E> eventType, final EventSubscriber<?> subscriber, EventContextHolder eventContextHolder,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
this.eventType = eventType;
|
||||
this.eventContextHolder = eventContextHolder; //TODO This is probably not working correctly.
|
||||
// As each KafkaConsumerAdapter is running in its own thread, they cannot share the same ThreadLocal.
|
||||
// The eventContextHolder must not be given from the bus but instantiated in each KafkaConsumerAdapter separately.
|
||||
|
||||
this.kafkaTopicName = KafkaTopicHelper.createTopicNameFromEventClazz(eventType);
|
||||
|
||||
final Map<String, Object> kafkaConfig = KafkaConfiguration.consumerConfig(eventType, subscriber, eventSubscriptionAtStart);
|
||||
final String kafkaConsumerGroupId = (String) kafkaConfig.get(CONSUMER_GROUP_ID_CONFIG);
|
||||
|
||||
LOGGER.info("create new subscription from %s (%s), for Kafka topic %s, using Kafka consumer group id %s".
|
||||
formatted(subscriber.getName(),
|
||||
subscriber.getGroupName(),
|
||||
kafkaTopicName,
|
||||
kafkaConsumerGroupId));
|
||||
|
||||
internal_KafkaConsumer = new KafkaConsumer<>(kafkaConfig);
|
||||
|
||||
addShutDownHook();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
// keep running forever or until shutdown() is called from another thread.
|
||||
try {
|
||||
internal_KafkaConsumer.subscribe(List.of(kafkaTopicName));
|
||||
Duration kafkaTimeoutInMs = Duration.ofMillis( KafkaConfiguration.kafkaTimeoutInMs );
|
||||
|
||||
// does not necessarily read messages "from beginning", i.e. already existing records which had
|
||||
// been published before the consumer was connected to the topic!
|
||||
while (!consumerIsShutdownAndNoLongerConsumingFromKafka.get()) {
|
||||
ConsumerRecords<String, String> kafkaRecords = internal_KafkaConsumer.poll(kafkaTimeoutInMs);
|
||||
processAllKafkaRecords(kafkaTopicName, kafkaRecords);
|
||||
}
|
||||
}
|
||||
catch (WakeupException ex) {
|
||||
if (!consumerIsShutdownAndNoLongerConsumingFromKafka.get()) {
|
||||
throw ex; // ignore exception if closing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processAllKafkaRecords(final String kafkaTopicName, final ConsumerRecords<String, String> records) {
|
||||
LOGGER.debug("consuming from %s now %d records ...".formatted(kafkaTopicName, records.count()));
|
||||
records.forEach(record -> {
|
||||
try {
|
||||
LOGGER.debug("consuming from %s: %s".formatted(kafkaTopicName, record));
|
||||
E event = EventSerializationHelper.deserializeJsonString2Event(record.value(), eventType);
|
||||
processEventAndCallReceiveOnAllSubscribers(eventType, event);
|
||||
}
|
||||
catch (Exception ex) {
|
||||
this.errorHandler.handleOne(ex, record,
|
||||
/* not used */ null,
|
||||
/* not used */ null);
|
||||
}
|
||||
});
|
||||
LOGGER.debug("consuming from %s now %d records ... done".formatted(kafkaTopicName, records.count()));
|
||||
}
|
||||
|
||||
private void processEventAndCallReceiveOnAllSubscribers(final Class<E> eventType, final E event) {
|
||||
eventContextHolder.set(event.eventContext());
|
||||
|
||||
try {
|
||||
subscribers.forEach(subscriber ->
|
||||
eventSubscriberReceiveDispatcher.callSubscriberReceiveMethod(eventType, event, subscriber));
|
||||
genericSubscribers.forEach(subscriber ->
|
||||
eventGenericSubscriberReceiveDispatcher.callSubscriberReceiveMethod(Event.class, event, subscriber));
|
||||
}
|
||||
finally {
|
||||
eventContextHolder.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------
|
||||
|
||||
void addSubscriber(final EventSubscriber<E> subscriber) {
|
||||
subscribers.add(subscriber);
|
||||
}
|
||||
void addGenericSubscriber(final EventSubscriber<Event> genericSubscriber) {
|
||||
genericSubscribers.add(genericSubscriber);
|
||||
}
|
||||
|
||||
void removeSubscriber(final EventSubscriber<E> subscriber) {
|
||||
subscribers.remove(subscriber);
|
||||
}
|
||||
void removeGenericSubscriber(final EventSubscriber<Event> genericSubscriber) {
|
||||
genericSubscribers.remove(genericSubscriber);
|
||||
}
|
||||
|
||||
void removeAllSubscribers() {
|
||||
subscribers.forEach(subscribers::remove);
|
||||
}
|
||||
void removeAllGenericSubscribers() {
|
||||
genericSubscribers.forEach(genericSubscribers::remove);
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------------------------------------------
|
||||
|
||||
private void addShutDownHook() {
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown));
|
||||
}
|
||||
|
||||
// note that currently there is no way of reactiving the KafkaConsumerAdapter once it has been shutdown
|
||||
void shutdown() {
|
||||
try {
|
||||
|
||||
/**
|
||||
* the lock does not seem to help, one still sees (with Java 21) these kind of errors:
|
||||
*
|
||||
* 20240327 14:42:20.284 [Thread-5] ERROR o.a.k.clients.consumer.KafkaConsumer - caught exception during shutdown
|
||||
* java.util.ConcurrentModificationException: KafkaConsumer is not safe for multi-threaded access. currentThread(name: Thread-5, id: 43) otherThread(id: 44)
|
||||
* at org.apache.kafka.clients.consumer.KafkaConsumer.acquire(KafkaConsumer.java:2484)
|
||||
* at org.apache.kafka.clients.consumer.KafkaConsumer.close(KafkaConsumer.java:2343)
|
||||
* at org.apache.kafka.clients.consumer.KafkaConsumer.close(KafkaConsumer.java:2321)
|
||||
* at de.accso.flexinale.common.infrastructure.eventbus.KafkaConsumerAdapter.shutdown(KafkaConsumerAdapter.java:147)
|
||||
* at java.base/java.lang.Thread.run(Thread.java:1583)
|
||||
*/
|
||||
|
||||
boolean isLockAcquired = lock4KafkaConsumer.tryLock(1, TimeUnit.SECONDS);
|
||||
if (isLockAcquired) {
|
||||
try {
|
||||
consumerIsShutdownAndNoLongerConsumingFromKafka.set(true);
|
||||
internal_KafkaConsumer.close();
|
||||
} finally {
|
||||
lock4KafkaConsumer.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
LOGGER.error("caught exception during shutdown", ex);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,76 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import org.apache.kafka.clients.consumer.Consumer;
|
||||
import org.apache.kafka.clients.consumer.ConsumerRecord;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
import org.springframework.kafka.listener.CommonErrorHandler;
|
||||
import org.springframework.kafka.listener.DeadLetterPublishingRecoverer;
|
||||
import org.springframework.kafka.listener.MessageListenerContainer;
|
||||
|
||||
// Error handling for Kafka consumers using retry pattern and finally writing error messages to DLT
|
||||
|
||||
/*
|
||||
A typical Spring Kafka error handler would look like below, but this only works for consumers
|
||||
which are annotated with @KafkaListener
|
||||
|
||||
...
|
||||
private int numberOfMaxRetriesForRetryableErrors = 3;
|
||||
|
||||
@Bean
|
||||
DefaultErrorHandler kafkaConsumerErrorHandler() {
|
||||
DefaultErrorHandler errorHandler = new DefaultErrorHandler((record, ex) -> {
|
||||
LOGGER.error("error while processing Kafka record " + record + ex);
|
||||
|
||||
// finally send to dead letter topics
|
||||
new DeadLetterPublishingRecoverer(internal_KafkaErrorDLTTemplate).accept(record, ex);
|
||||
},
|
||||
// Default error handling is to retry
|
||||
new ExponentialBackOffWithMaxRetries(numberOfMaxRetriesForRetryableErrors));
|
||||
|
||||
// ... but do not retry on FlexinaleKafkaNotRetryableException and its subclasses
|
||||
errorHandler.addNotRetryableExceptions(FlexinaleKafkaNotRetryableException.class);
|
||||
|
||||
return errorHandler;
|
||||
}
|
||||
...
|
||||
|
||||
*/
|
||||
|
||||
public final class KafkaConsumerErrorHandler implements CommonErrorHandler {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KafkaConsumerErrorHandler.class);
|
||||
|
||||
// Kafka template for sending error message to DLT
|
||||
static final KafkaTemplate<String, String> internal_KafkaErrorDLTTemplate = new KafkaTemplate<>(
|
||||
new DefaultKafkaProducerFactory<>(
|
||||
KafkaConfiguration.producerConfig()
|
||||
));
|
||||
|
||||
@Override
|
||||
public boolean handleOne(Exception thrownException, ConsumerRecord<?, ?> record, Consumer<?, ?> consumer,
|
||||
MessageListenerContainer container) {
|
||||
try {
|
||||
LOGGER.error("error while processing Kafka record %s, now sending to DLT, error: %s".formatted(record, thrownException));
|
||||
new DeadLetterPublishingRecoverer(internal_KafkaErrorDLTTemplate).accept(record, thrownException);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class FlexinaleKafkaNotRetryableException extends RuntimeException {
|
||||
public FlexinaleKafkaNotRetryableException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
class FlexinaleKafkaMessageDeserializationException extends FlexinaleKafkaNotRetryableException {
|
||||
public FlexinaleKafkaMessageDeserializationException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import de.accso.flexinale.common.shared_kernel.Identifiable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
|
||||
import org.springframework.kafka.core.KafkaTemplate;
|
||||
|
||||
final class KafkaProducerAdapter<E extends Event> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KafkaProducerAdapter.class);
|
||||
|
||||
private final KafkaTemplate<String, String> internal_KafkaTemplate;
|
||||
private final String kafkaTopicName;
|
||||
|
||||
private final Class<E> eventType;
|
||||
|
||||
public KafkaProducerAdapter(final Class<E> eventType) {
|
||||
this.eventType = eventType;
|
||||
|
||||
this.kafkaTopicName = KafkaTopicHelper.createTopicNameFromEventClazz(eventType);
|
||||
LOGGER.info("create new %s for Kafka topic %s".formatted(getName(), kafkaTopicName));
|
||||
|
||||
this.internal_KafkaTemplate = new KafkaTemplate<>(
|
||||
new DefaultKafkaProducerFactory<>(
|
||||
KafkaConfiguration.producerConfig()
|
||||
));
|
||||
}
|
||||
|
||||
public void publish(final Class<E> eventType, final E event) {
|
||||
try {
|
||||
Identifiable.Id kafkaKey = event.id();
|
||||
String kafkaMessage = EventSerializationHelper.serializeEvent2JsonString(event);
|
||||
internal_KafkaTemplate
|
||||
.send(kafkaTopicName, kafkaKey.id(), kafkaMessage)
|
||||
.whenComplete((result, ex) -> {
|
||||
if (ex == null) {
|
||||
LOGGER.debug(String.format("message was sent to topic %s " +
|
||||
"for event type %s, event id %s",
|
||||
kafkaTopicName,
|
||||
eventType.getCanonicalName(),
|
||||
event.id()));
|
||||
} else {
|
||||
LOGGER.error(String.format("Kafka message could not be sent to topic %s " +
|
||||
"for event type %s, event id %s - error: %s",
|
||||
kafkaTopicName, eventType.getCanonicalName(),
|
||||
event.id(),
|
||||
ex.getMessage()));
|
||||
}
|
||||
});
|
||||
}
|
||||
catch (Exception ex) {
|
||||
LOGGER.error("Kafka message will not sent out for event " + event, ex);
|
||||
}
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return "KafkaProducerAdapter" + "<" + eventType + ">";
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import de.accso.flexinale.common.shared_kernel.EventClassHelper;
|
||||
import org.apache.kafka.clients.admin.AdminClient;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.TimeoutException;
|
||||
|
||||
import static de.accso.flexinale.common.infrastructure.eventbus.KafkaConfiguration.adminConfig;
|
||||
import static de.accso.flexinale.common.infrastructure.eventbus.KafkaConfiguration.kafkaTimeoutInMs;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class KafkaTopicHelper {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(KafkaTopicHelper.class);
|
||||
|
||||
static <E extends Event> String createTopicNameFromEventClazz(final Class<E> eventType) {
|
||||
return EventClassHelper.getEventClazzName(eventType)
|
||||
+ ".v"
|
||||
+ EventClassHelper.getEventClazzVersion(eventType).version();
|
||||
}
|
||||
|
||||
static <E extends Event> String createTopicNameFromEventClazzAndInstance(final Class<E> eventType, final Event event) {
|
||||
return eventType.getCanonicalName()
|
||||
+ ".v"
|
||||
+ event.version().version();
|
||||
}
|
||||
|
||||
public static void deleteKafkaTopicsWithPrefixAndSuffix(final String prefix, final String suffix)
|
||||
throws ExecutionException, TimeoutException
|
||||
{
|
||||
Map<String, Object> kafkaAdminConfig = adminConfig();
|
||||
|
||||
try (var adminClient = AdminClient.create( kafkaAdminConfig )) {
|
||||
List<String> topicNames = adminClient.listTopics().names().get()
|
||||
.stream()
|
||||
.filter(name -> name.startsWith(prefix) && name.endsWith(suffix))
|
||||
.toList();
|
||||
adminClient.deleteTopics(topicNames).all().get(kafkaTimeoutInMs, TimeUnit.SECONDS);
|
||||
|
||||
if (topicNames.isEmpty()) {
|
||||
LOGGER.info("did not delete any Kafka topics");
|
||||
}
|
||||
else {
|
||||
LOGGER.info("tried to delete these Kafka topics: " + topicNames);
|
||||
}
|
||||
}
|
||||
catch (InterruptedException ex) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventContextHolder;
|
||||
import de.accso.flexinale.common.api.event.EventContext;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
public final class NopeContextHolder implements EventContextHolder {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(NopeContextHolder.class);
|
||||
|
||||
@Override
|
||||
public EventContext get() {
|
||||
LOGGER.debug("NopeContextHolder.get() does nothing, always returns null");
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(EventContext eventContext) {
|
||||
LOGGER.debug("NopeContextHolder.set() does nothing");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
LOGGER.debug("NopeContextHolder.remove() does nothing");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
LOGGER.debug("NopeContextHolder.isEmpty() does nothing, always returns true");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventBus;
|
||||
import de.accso.flexinale.common.api.eventbus.EventContextHolder;
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriber;
|
||||
import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class NopeEventBusSpy<E extends Event> implements EventBus<E> {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(NopeEventBusSpy.class);
|
||||
|
||||
private static final EventContextHolder eventContextHolder = new NopeContextHolder();
|
||||
|
||||
private final Class<E> eventType; // might want to use this simplify the interface and get rid of the method's first parameter
|
||||
|
||||
NopeEventBusSpy(final Class<E> eventType) {
|
||||
this.eventType = eventType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(final Class<E> eventType, final EventSubscriber<E> subscriber,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
LOGGER.debug("NopeEventBusSpy<%s>: subscriber %s registered but does nothing"
|
||||
.formatted(eventType.getSimpleName(), subscriber));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void subscribe(final EventSubscriber<Event> genericSubscriber,
|
||||
final EventSubscriptionAtStart eventSubscriptionAtStart) {
|
||||
LOGGER.debug("NopeEventBusSpy<%s>: subscriber %s registered but does nothing"
|
||||
.formatted(eventType.getSimpleName(), genericSubscriber));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe(final Class<E> eventType, final EventSubscriber<E> subscriber) {
|
||||
LOGGER.debug("NopeEventBusSpy<%s>: subscriber %s unregistered but does nothing"
|
||||
.formatted(eventType.getSimpleName(), subscriber));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribe(EventSubscriber<Event> genericSubscriber) {
|
||||
LOGGER.debug("NopeEventBusSpy<%s>: subscriber %s unregistered but does nothing"
|
||||
.formatted(eventType.getSimpleName(), genericSubscriber));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unsubscribeAll() {
|
||||
LOGGER.debug("NopeEventBusSpy<%s>: all subscribers unregistered but does nothing"
|
||||
.formatted(eventType.getSimpleName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publish(final Class<E> eventType, final E event) {
|
||||
LOGGER.info("NopeEventBusSpy<%s>: publish new event %s but does nothing".formatted(eventType, event));
|
||||
}
|
||||
|
||||
@Override
|
||||
public EventContextHolder getEventContextHolder() {
|
||||
return eventContextHolder;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventBus;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
|
||||
public final class NopeEventBusSpyFactory extends InMemorySyncEventBusFactory {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <E extends Event> EventBus<E> createOrGetEventBusFor(final Class<E> eventType) {
|
||||
EventBus<? extends Event> eventBus = eventTypeToEventBusMap.get(eventType);
|
||||
if (eventBus == null) {
|
||||
eventBus = new NopeEventBusSpy<>(eventType);
|
||||
eventTypeToEventBusMap.put(eventType, eventBus);
|
||||
}
|
||||
return (EventBus<E>) eventBus;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventContextHolder;
|
||||
import de.accso.flexinale.common.api.event.EventContext;
|
||||
|
||||
public final class ThreadLocalEventContextHolder implements EventContextHolder {
|
||||
private static final ThreadLocal<EventContext> CONTEXT = new ThreadLocal<>();
|
||||
|
||||
@Override
|
||||
public EventContext get() {
|
||||
return CONTEXT.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void set(EventContext eventContext) {
|
||||
CONTEXT.set(eventContext);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove() {
|
||||
CONTEXT.remove();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return CONTEXT.get() == null;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class ClazzHelper {
|
||||
public static boolean clazzImplementsInterface(final Class<?> implementationClazz, final Class<?> interfaceClazz) {
|
||||
if (implementationClazz.equals(Object.class)) return false;
|
||||
|
||||
Class<?>[] directInterfaces = implementationClazz.getInterfaces(); // _only_ the direct interfaces, not from super class!
|
||||
boolean implementsEvent = Arrays.stream(directInterfaces).toList().contains(interfaceClazz);
|
||||
if (implementsEvent) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
// recursive call
|
||||
return clazzImplementsInterface(implementationClazz.getSuperclass(), interfaceClazz);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneOffset;
|
||||
|
||||
public final class DateTimeHelper {
|
||||
public static LocalDateTime fromEpochSeconds(final long epochSeconds) {
|
||||
return LocalDateTime.ofEpochSecond(epochSeconds, 0, ZoneOffset.UTC);
|
||||
}
|
||||
|
||||
public static long toEpochSeconds(final LocalDateTime dateTime) {
|
||||
return dateTime.toEpochSecond(ZoneOffset.UTC);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
public class DeveloperMistakeException extends RuntimeException {
|
||||
public DeveloperMistakeException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public DeveloperMistakeException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public DeveloperMistakeException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public DeveloperMistakeException(final Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
protected DeveloperMistakeException(final String message, final Throwable cause,
|
||||
final boolean enableSuppression, final boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(value = RetentionPolicy.RUNTIME)
|
||||
public @interface DoNotCheckInArchitectureTests {
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public interface EqualsByContent {
|
||||
boolean equalsByContent(final Object o);
|
||||
}
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
|
||||
import static de.accso.flexinale.common.shared_kernel.ClazzHelper.clazzImplementsInterface;
|
||||
|
||||
public final class EventClassHelper {
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <E extends Event> Class<E> getClassFor(final String clazzName) {
|
||||
try {
|
||||
return (Class<E>) Class.forName(clazzName);
|
||||
}
|
||||
catch (ClassNotFoundException ex) {
|
||||
throw new FlexinaleIllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public static <E extends Event> String getEventClazzName(final String clazzName) {
|
||||
Class<? extends Event> clazz = getClassFor(clazzName);
|
||||
|
||||
if (!clazzImplementsInterface(clazz, Event.class)) {
|
||||
throw new FlexinaleIllegalArgumentException("wrong clazz %s used to retrieve Event's clazz name and version"
|
||||
.formatted(clazzName));
|
||||
}
|
||||
|
||||
return clazz.getCanonicalName();
|
||||
}
|
||||
|
||||
public static <E extends Event> String getEventClazzName(final Class<E> clazz) {
|
||||
return clazz.getCanonicalName();
|
||||
}
|
||||
|
||||
public static <E extends Event> Versionable.Version getEventClazzVersion(String clazzName) {
|
||||
Class<? extends Event> clazz = getClassFor(clazzName);
|
||||
|
||||
if (!clazzImplementsInterface(clazz, Event.class)) {
|
||||
throw new FlexinaleIllegalArgumentException("need to use Event class %s to retrieve class name"
|
||||
.formatted(clazzName));
|
||||
}
|
||||
|
||||
return getEventClazzVersion(clazz);
|
||||
}
|
||||
|
||||
public static <E extends Event> Versionable.Version getEventClazzVersion(final Class<E> clazz) {
|
||||
try {
|
||||
Constructor<E> constructor = clazz.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Event e = constructor.newInstance();
|
||||
return e.version();
|
||||
}
|
||||
catch (Exception ex) {
|
||||
throw new FlexinaleIllegalArgumentException("error when instantiating Event class %s or retrieving its version"
|
||||
.formatted(getEventClazzName(clazz)), ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
public class FlexinaleIllegalArgumentException extends DeveloperMistakeException {
|
||||
public FlexinaleIllegalArgumentException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public FlexinaleIllegalArgumentException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FlexinaleIllegalArgumentException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public FlexinaleIllegalArgumentException(final Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
protected FlexinaleIllegalArgumentException(final String message, final Throwable cause,
|
||||
final boolean enableSuppression, final boolean writableStackTrace) {
|
||||
super(message, cause, enableSuppression, writableStackTrace);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class FlexinaleIllegalStateException extends IllegalStateException {
|
||||
public FlexinaleIllegalStateException() {
|
||||
super();
|
||||
}
|
||||
|
||||
public FlexinaleIllegalStateException(final String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
public FlexinaleIllegalStateException(final String message, final Throwable cause) {
|
||||
super(message, cause);
|
||||
}
|
||||
|
||||
public FlexinaleIllegalStateException(final Throwable cause) {
|
||||
super(cause);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.UUID;
|
||||
|
||||
public interface Identifiable {
|
||||
record Id(String id) implements Serializable, RawWrapper<String> {
|
||||
public static Id of() {
|
||||
return uuid();
|
||||
}
|
||||
|
||||
public static Id of(String id) {
|
||||
return new Id(id);
|
||||
}
|
||||
|
||||
public static Id uuid() {
|
||||
return Id.of(UUID.randomUUID().toString());
|
||||
}
|
||||
|
||||
public static String uuidString() {
|
||||
return UUID.randomUUID().toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String raw() {
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
Id id();
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public final class MapWithCounter<K> {
|
||||
private final ConcurrentHashMap<K, AtomicLong> backingMap = new ConcurrentHashMap<>();
|
||||
|
||||
public long increment(K key) {
|
||||
return backingMap.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet();
|
||||
}
|
||||
|
||||
public long get(K key) {
|
||||
AtomicLong counter = backingMap.get(key);
|
||||
return counter != null ? counter.get() : 0;
|
||||
}
|
||||
|
||||
public void set(K key, Long value) {
|
||||
backingMap.put(key, new AtomicLong(value));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
public interface Mergeable<E extends Identifiable> {
|
||||
E merge(E newData);
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public interface RawWrapper<T> extends Serializable {
|
||||
T raw();
|
||||
|
||||
default String print() { // unfortunately one cannot override toString() here, results in compiler error
|
||||
return raw().toString();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
static <T> T getRawOrNull(Object o) {
|
||||
if (o == null)
|
||||
return null;
|
||||
else
|
||||
try {
|
||||
RawWrapper<T> castedObject = (RawWrapper<T>)o;
|
||||
return castedObject.raw();
|
||||
}
|
||||
catch (ClassCastException ccex) {
|
||||
throw new DeveloperMistakeException(ccex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import java.util.*;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
|
||||
public interface Selection<T> extends Set<T> {
|
||||
Set<T> next(); // select one or more options from the list based on a selection algorithm
|
||||
|
||||
enum SelectionAlgorithm {
|
||||
RANDOM, CONSTANT_FIRST, CONSTANT_LAST, ROUND_ROBIN, ALL
|
||||
}
|
||||
|
||||
final class RandomSelector<T> extends SynchronizedSet<T> implements Selection<T> {
|
||||
@Override
|
||||
public Set<T> next() {
|
||||
if (this.size() == 0) return Set.of();
|
||||
|
||||
int randomIndex = ThreadLocalRandom.current().nextInt(this.size());
|
||||
return Set.of(this.stream()
|
||||
.skip(randomIndex)
|
||||
.findFirst()
|
||||
.orElseThrow(DeveloperMistakeException::new));
|
||||
}
|
||||
}
|
||||
|
||||
final class ConstantFirstSelector<T> extends SynchronizedSet<T> implements Selection<T> {
|
||||
@Override
|
||||
public Set<T> next() {
|
||||
if (this.size() == 0) return Set.of();
|
||||
|
||||
return Set.of(this.stream()
|
||||
.findFirst()
|
||||
.orElseThrow(DeveloperMistakeException::new));
|
||||
}
|
||||
}
|
||||
|
||||
final class ConstantLastSelector<T> extends SynchronizedSet<T> implements Selection<T> {
|
||||
@Override
|
||||
public Set<T> next() {
|
||||
if (this.size() == 0) return Set.of();
|
||||
|
||||
return Set.of(this.stream()
|
||||
.skip(this.size()-1)
|
||||
.findFirst()
|
||||
.orElseThrow(DeveloperMistakeException::new));
|
||||
}
|
||||
}
|
||||
|
||||
final class RoundRobinSelector<T> extends SynchronizedSet<T> implements Selection<T> {
|
||||
private int roundRobinIndex;
|
||||
|
||||
@Override
|
||||
public Set<T> next() {
|
||||
if (this.size() == 0) return Set.of();
|
||||
|
||||
T result = this.stream().toList().get(roundRobinIndex++);
|
||||
if (roundRobinIndex == this.size()) roundRobinIndex = 0;
|
||||
|
||||
return Set.of(result);
|
||||
}
|
||||
}
|
||||
|
||||
final class AllSelector<T> extends SynchronizedSet<T> implements Selection<T> {
|
||||
@Override
|
||||
public Set<T> next() {
|
||||
if (this.size() == 0) return Set.of();
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import org.apache.commons.collections4.multimap.AbstractSetValuedMap;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public final class SelectionMultiMap<K, V> extends AbstractSetValuedMap<K, V> {
|
||||
private final Selection.SelectionAlgorithm selectionAlgorithm;
|
||||
|
||||
public SelectionMultiMap(Selection.SelectionAlgorithm selectionAlgorithm) {
|
||||
super(new HashMap<K, Selection<V>>());
|
||||
this.selectionAlgorithm = selectionAlgorithm;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Set<V> createCollection() {
|
||||
return switch (selectionAlgorithm) {
|
||||
case RANDOM -> new Selection.RandomSelector<>();
|
||||
case CONSTANT_FIRST -> new Selection.ConstantFirstSelector<>();
|
||||
case CONSTANT_LAST -> new Selection.ConstantLastSelector<>();
|
||||
case ROUND_ROBIN -> new Selection.RoundRobinSelector<>();
|
||||
case ALL -> new Selection.AllSelector<>();
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<V> get(final K key) {
|
||||
return getMap().get(key);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.Set;
|
||||
|
||||
// needed as one cannot derive from Collections.synchronizedSet()'s result type (as package-protected, sigh)
|
||||
class SynchronizedSet<T> implements Set<T> {
|
||||
|
||||
private final Set<T> backingSet = new HashSet<T>();
|
||||
private final Object lock = new Object();
|
||||
|
||||
@Override
|
||||
public boolean add(T object) {
|
||||
synchronized (lock) {
|
||||
return backingSet.add(object);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean addAll(Collection<? extends T> collection) {
|
||||
synchronized (lock) {
|
||||
return backingSet.addAll(collection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clear() {
|
||||
synchronized (lock) {
|
||||
backingSet.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(Object object) {
|
||||
synchronized (lock) {
|
||||
return backingSet.contains(object);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean containsAll(Collection<?> collection) {
|
||||
synchronized (lock) {
|
||||
return backingSet.containsAll(collection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
synchronized (lock) {
|
||||
return backingSet.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
// needs to get synchronized outside!
|
||||
@Override
|
||||
public Iterator<T> iterator() {
|
||||
return backingSet.iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public T[] toArray() {
|
||||
synchronized (lock) {
|
||||
return (T[]) backingSet.toArray();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public T[] toArray(Object[] object) {
|
||||
synchronized (lock) {
|
||||
return (T[]) backingSet.toArray(object);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean remove(Object object) {
|
||||
synchronized (lock) {
|
||||
return backingSet.remove(object);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeAll(Collection<?> collection) {
|
||||
synchronized (lock) {
|
||||
return backingSet.removeAll(collection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean retainAll(Collection<?> collection) {
|
||||
synchronized (lock) {
|
||||
return backingSet.retainAll(collection);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
synchronized (lock) {
|
||||
return backingSet.size();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object object) {
|
||||
synchronized (lock) {
|
||||
if (object == this) {
|
||||
return true;
|
||||
}
|
||||
return backingSet.equals(object);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
synchronized (lock) {
|
||||
return backingSet.hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
synchronized (lock) {
|
||||
return backingSet.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
public final class ThreadSafeCounterMap<K> {
|
||||
private final ConcurrentHashMap<K, AtomicLong> backingMap = new ConcurrentHashMap<>();
|
||||
|
||||
public long increment(K key) {
|
||||
return backingMap.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet();
|
||||
}
|
||||
|
||||
public long get(K key) {
|
||||
AtomicLong counter = backingMap.get(key);
|
||||
return counter != null ? counter.get() : 0;
|
||||
}
|
||||
|
||||
public void set(K key, Long value) {
|
||||
backingMap.put(key, new AtomicLong(value));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
public class TimeFormatter {
|
||||
public static String calculateString(final LocalDateTime from, final Integer durationInMinutes) {
|
||||
DateTimeFormatter hourMinuteFormatter = DateTimeFormatter.ofPattern("HH:mm");
|
||||
|
||||
return hourMinuteFormatter.format(from)
|
||||
+ " - "
|
||||
+ hourMinuteFormatter.format(from.plusMinutes(durationInMinutes));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
public interface Versionable {
|
||||
record Version(Integer version) implements Serializable, RawWrapper<Integer> {
|
||||
public static Version of(Integer version) {
|
||||
return new Version(version);
|
||||
}
|
||||
|
||||
public Version inc() {
|
||||
return new Version(this.version()+1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer raw() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
Version version();
|
||||
|
||||
static Version unknownVersion() {
|
||||
Integer UNKNOWN = -1;
|
||||
return Version.of(UNKNOWN);
|
||||
}
|
||||
|
||||
static Version initialVersion() {
|
||||
Integer INITIAL = 0;
|
||||
return Version.of(INITIAL);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
|
||||
<logger name="org.springframework.orm.jpa" level="INFO"/>
|
||||
<logger name="org.springframework.boot.autoconfigure.domain.EntityScan" level="INFO"/>
|
||||
|
||||
<logger name="org.apache.kafka" level="WARN"/>
|
||||
<logger name="org.apache.kafka.clients.admin.AdminClient" level="INFO"/>
|
||||
<logger name="org.apache.kafka.clients.consumer.ConsumerConfig" level="INFO"/>
|
||||
<logger name="org.apache.kafka.clients.producer.ProducerConfig" level="INFO"/>
|
||||
|
||||
<logger name="de.accso" level="INFO"/>
|
||||
<root level="WARN">
|
||||
<appender-ref ref="STDOUT" />
|
||||
</root>
|
||||
</configuration>
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
package de.accso.flexinale.common.api.event;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
class EventTest {
|
||||
|
||||
@Test
|
||||
void testEventConstructor() {
|
||||
// arrange
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// act
|
||||
EventTestdata.MyNumberEvent event = new EventTestdata.MyNumberEvent(11, 12);
|
||||
|
||||
// assert
|
||||
assertThat(event.id).isNotNull();
|
||||
assertThat(event.correlationId()).isNotNull();
|
||||
assertThat(event.timestamp).isAfterOrEqualTo(now);
|
||||
assertThat(event.eventContext().eventHistory.size()).isEqualTo(1);
|
||||
assertThat(event.eventContext().eventHistory.getFirst()).isEqualTo(
|
||||
new EventContext.EventHistoryElement(event.id, EventTestdata.MyNumberEvent.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEventConstructorWithHistory() {
|
||||
// arrange
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
|
||||
// act
|
||||
EventTestdata.MyNumberEvent startEvent =
|
||||
new EventTestdata.MyNumberEvent(11, 12);
|
||||
|
||||
EventTestdata.MyMixedEvent nextEvent =
|
||||
new EventTestdata.MyMixedEvent(startEvent, "13", 14);
|
||||
|
||||
EventTestdata.MyNumberEvent lastEvent =
|
||||
new EventTestdata.MyNumberEvent(nextEvent, 15, 16);
|
||||
|
||||
// assert
|
||||
assertThat(lastEvent.id).isNotNull();
|
||||
assertThat(lastEvent.id).isNotEqualTo( nextEvent.id);
|
||||
assertThat(lastEvent.id).isNotEqualTo(startEvent.id);
|
||||
assertThat(nextEvent.id).isNotEqualTo(startEvent.id);
|
||||
|
||||
assertThat(lastEvent.correlationId()).isNotNull();
|
||||
assertThat(lastEvent.correlationId()).isEqualTo( nextEvent.correlationId());
|
||||
assertThat(nextEvent.correlationId()).isEqualTo(startEvent.correlationId());
|
||||
|
||||
assertThat( lastEvent.timestamp).isAfterOrEqualTo( nextEvent.timestamp);
|
||||
assertThat( nextEvent.timestamp).isAfterOrEqualTo(startEvent.timestamp);
|
||||
assertThat(startEvent.timestamp).isAfterOrEqualTo(now);
|
||||
|
||||
assertThat( lastEvent.eventContext().eventHistory.size()).isEqualTo(3);
|
||||
assertThat( nextEvent.eventContext().eventHistory.size()).isEqualTo(2);
|
||||
assertThat(startEvent.eventContext().eventHistory.size()).isEqualTo(1);
|
||||
|
||||
assertThat(lastEvent.eventContext().eventHistory.get(0)).isEqualTo(
|
||||
new EventContext.EventHistoryElement(startEvent.id, startEvent.getClass()));
|
||||
assertThat(lastEvent.eventContext().eventHistory.get(1)).isEqualTo(
|
||||
new EventContext.EventHistoryElement(nextEvent.id, nextEvent.getClass()));
|
||||
assertThat(lastEvent.eventContext().eventHistory.get(2)).isEqualTo(
|
||||
new EventContext.EventHistoryElement(lastEvent.id, lastEvent.getClass()));
|
||||
|
||||
assertThat(nextEvent.eventContext().eventHistory.get(0)).isEqualTo(
|
||||
new EventContext.EventHistoryElement(startEvent.id, startEvent.getClass()));
|
||||
assertThat(nextEvent.eventContext().eventHistory.get(1)).isEqualTo(
|
||||
new EventContext.EventHistoryElement(nextEvent.id, nextEvent.getClass()));
|
||||
|
||||
assertThat(startEvent.eventContext().eventHistory.getFirst()).isEqualTo(
|
||||
new EventContext.EventHistoryElement(startEvent.id, startEvent.getClass()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
package de.accso.flexinale.common.api.event;
|
||||
|
||||
import de.accso.flexinale.common.api.event.AbstractEvent;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import de.accso.flexinale.common.shared_kernel.Versionable;
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
|
||||
public final class EventTestdata {
|
||||
|
||||
public static final class MyNumberEvent extends AbstractEvent {
|
||||
public final Integer value1;
|
||||
public final Integer value2;
|
||||
|
||||
public final Version version = Versionable.initialVersion();
|
||||
|
||||
public MyNumberEvent(final Integer value1, final Integer value2) {
|
||||
this.value1 = value1;
|
||||
this.value2 = value2;
|
||||
}
|
||||
|
||||
public MyNumberEvent(final Event predecessorEvent, final Integer value1, final Integer value2) {
|
||||
super(predecessorEvent);
|
||||
this.value1 = value1;
|
||||
this.value2 = value2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {return true;}
|
||||
|
||||
if (o == null || getClass() != o.getClass()) {return false;}
|
||||
|
||||
final MyNumberEvent that = (MyNumberEvent) o;
|
||||
|
||||
return new EqualsBuilder()
|
||||
.appendSuper(super.equals(o))
|
||||
.append(this.version(), that.version())
|
||||
.append(value1, that.value1)
|
||||
.append(value2, that.value2)
|
||||
.isEquals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return new HashCodeBuilder(17, 37)
|
||||
.appendSuper(super.hashCode())
|
||||
.append(this.version())
|
||||
.append(value1).append(value2)
|
||||
.toHashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version version() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MyTextEvent extends AbstractEvent {
|
||||
public final String text1;
|
||||
public final String text2;
|
||||
|
||||
public final Version version = Versionable.initialVersion();
|
||||
|
||||
public MyTextEvent(final String text1, final String text2) {
|
||||
this.text1 = text1;
|
||||
this.text2 = text2;
|
||||
}
|
||||
|
||||
public MyTextEvent(final Id correlationId, final String text1, final String text2) {
|
||||
super(correlationId);
|
||||
this.text1 = text1;
|
||||
this.text2 = text2;
|
||||
}
|
||||
|
||||
public MyTextEvent(final Event predecessorEvent, final String text1, final String text2) {
|
||||
super(predecessorEvent);
|
||||
this.text1 = text1;
|
||||
this.text2 = text2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {return true;}
|
||||
|
||||
if (o == null || getClass() != o.getClass()) {return false;}
|
||||
|
||||
final MyTextEvent that = (MyTextEvent) o;
|
||||
|
||||
return new EqualsBuilder()
|
||||
.appendSuper(super.equals(o))
|
||||
.append(this.version(), that.version())
|
||||
.append(text1, that.text1)
|
||||
.append(text2, that.text2)
|
||||
.isEquals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return new HashCodeBuilder(17, 37)
|
||||
.appendSuper(super.hashCode())
|
||||
.append(this.version())
|
||||
.append(text1).append(text2)
|
||||
.toHashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version version() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MyMixedEvent extends AbstractEvent {
|
||||
public final String text;
|
||||
public final Integer number;
|
||||
|
||||
public final Version version = Versionable.initialVersion();
|
||||
|
||||
public MyMixedEvent(final String text, final Integer number) {
|
||||
this.text = text;
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
public MyMixedEvent(final Event predecessorEvent, final String text, final Integer number) {
|
||||
super(predecessorEvent);
|
||||
this.text = text;
|
||||
this.number = number;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {return true;}
|
||||
|
||||
if (o == null || getClass() != o.getClass()) {return false;}
|
||||
|
||||
final MyMixedEvent that = (MyMixedEvent) o;
|
||||
|
||||
return new EqualsBuilder()
|
||||
.appendSuper(super.equals(o))
|
||||
.append(this.version(), that.version())
|
||||
.append(text, that.text)
|
||||
.append(number, that.number).isEquals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return new HashCodeBuilder(17, 37)
|
||||
.appendSuper(super.hashCode())
|
||||
.append(this.version())
|
||||
.append(text).append(number)
|
||||
.toHashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version version() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,282 @@
|
|||
package de.accso.flexinale.common.api.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.MyNumberAndMixedEventPublisher;
|
||||
import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.MyNumberAndMixedEventSubscriber;
|
||||
import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.MyNumberAndTextAndMixedEventSubscriber;
|
||||
import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.MyTextEventPublisher;
|
||||
import de.accso.flexinale.common.api.event.AbstractEvent;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import de.accso.flexinale.common.api.event.EventTestdata.MyMixedEvent;
|
||||
import de.accso.flexinale.common.api.event.EventTestdata.MyNumberEvent;
|
||||
import de.accso.flexinale.common.api.event.EventTestdata.MyTextEvent;
|
||||
import de.accso.flexinale.common.infrastructure.eventbus.InMemorySyncEventBus;
|
||||
import de.accso.flexinale.common.infrastructure.eventbus.InMemorySyncEventBusFactory;
|
||||
import de.accso.flexinale.common.shared_kernel.Versionable;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
|
||||
import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_NOW;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
class InMemorySyncEventBusAndSpyHandlingMultipleEventTypesTest {
|
||||
|
||||
InMemorySyncEventBusFactory factory;
|
||||
|
||||
InMemorySyncEventBus<MyNumberEvent> numberBus;
|
||||
InMemorySyncEventBus<MyTextEvent> textBus;
|
||||
InMemorySyncEventBus<MyMixedEvent> mixedBus;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
factory = new InMemorySyncEventBusFactory();
|
||||
|
||||
numberBus = (InMemorySyncEventBus<MyNumberEvent>) factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
textBus = (InMemorySyncEventBus<MyTextEvent>) factory.createOrGetEventBusFor(MyTextEvent.class);
|
||||
mixedBus = (InMemorySyncEventBus<MyMixedEvent>) factory.createOrGetEventBusFor(MyMixedEvent.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testOneEventTypeWithTwoInOnePublisherAndTwoInOneSubscriber() {
|
||||
// arrange
|
||||
Random random = new Random();
|
||||
|
||||
int numEventsToPublish = 10;
|
||||
|
||||
MyNumberAndMixedEventPublisher twoInOnePublisher = new MyNumberAndMixedEventPublisher(numberBus, mixedBus);
|
||||
MyNumberAndMixedEventSubscriber twoInOneSubscriber = new MyNumberAndMixedEventSubscriber();
|
||||
|
||||
// act
|
||||
numberBus.subscribe(MyNumberEvent.class, twoInOneSubscriber, START_READING_FROM_NOW);
|
||||
|
||||
MyNumberEvent lastNumberEvent = null;
|
||||
for (int counter=0; counter<numEventsToPublish; counter++) {
|
||||
lastNumberEvent = new MyNumberEvent(random.nextInt(), random.nextInt());
|
||||
twoInOnePublisher.post(MyNumberEvent.class, lastNumberEvent);
|
||||
}
|
||||
|
||||
// assert
|
||||
assertThat(twoInOneSubscriber.lastReceivedNumberEvent).isEqualTo(lastNumberEvent);
|
||||
assertThat(twoInOneSubscriber.lastReceivedMixedEvent).isNull();
|
||||
assertThat(twoInOneSubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"})
|
||||
@Test
|
||||
void testTwoEventTypesWithTwoInOnePublisherAndOneExtraPublisherAndAThreeInOneSubscriber() {
|
||||
// arrange
|
||||
Random random = new Random();
|
||||
|
||||
int numEventsToPublish = 100;
|
||||
|
||||
MyNumberAndMixedEventPublisher numberAndMixedPublisher = new MyNumberAndMixedEventPublisher(numberBus, mixedBus);
|
||||
MyTextEventPublisher textPublisher = new MyTextEventPublisher(textBus);
|
||||
MyNumberAndTextAndMixedEventSubscriber threeInOneSubscriber = new MyNumberAndTextAndMixedEventSubscriber();
|
||||
|
||||
// act
|
||||
numberBus.subscribe(MyNumberEvent.class, (EventSubscriber) threeInOneSubscriber, START_READING_FROM_NOW);
|
||||
textBus.subscribe( MyTextEvent.class, (EventSubscriber) threeInOneSubscriber, START_READING_FROM_NOW);
|
||||
mixedBus.subscribe( MyMixedEvent.class, (EventSubscriber) threeInOneSubscriber, START_READING_FROM_NOW);
|
||||
|
||||
MyNumberEvent lastNumberEvent = null;
|
||||
MyTextEvent lastTextEvent = null;
|
||||
MyMixedEvent lastMixedEvent = null;
|
||||
for (int counter=0; counter<numEventsToPublish; counter++) {
|
||||
lastNumberEvent = new MyNumberEvent(random.nextInt(), random.nextInt());
|
||||
numberAndMixedPublisher.post(MyNumberEvent.class, lastNumberEvent);
|
||||
|
||||
lastMixedEvent = new MyMixedEvent(""+random.nextInt(), random.nextInt());
|
||||
numberAndMixedPublisher.post2(MyMixedEvent.class, lastMixedEvent);
|
||||
|
||||
lastTextEvent = new MyTextEvent(""+random.nextInt(), ""+random.nextInt());
|
||||
textPublisher.post(MyTextEvent.class, lastTextEvent);
|
||||
}
|
||||
|
||||
// assert
|
||||
assertThat(threeInOneSubscriber.lastReceivedNumberEvent).isEqualTo(lastNumberEvent);
|
||||
assertThat(threeInOneSubscriber.lastReceivedTextEvent).isEqualTo(lastTextEvent);
|
||||
assertThat(threeInOneSubscriber.lastReceivedMixedEvent).isEqualTo(lastMixedEvent);
|
||||
assertThat(threeInOneSubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish * 3);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
@Test
|
||||
void testNineEventTypesWithAllInOnePublishersAndAllInOneSubscriber() {
|
||||
// arrange, 1
|
||||
|
||||
class E1 extends AbstractEvent implements Event {
|
||||
@Override
|
||||
public Version version() {
|
||||
return Versionable.initialVersion();
|
||||
}
|
||||
}
|
||||
class E2 extends AbstractEvent implements Event {
|
||||
@Override
|
||||
public Version version() {
|
||||
return Versionable.initialVersion();
|
||||
}
|
||||
}
|
||||
class E3 extends AbstractEvent implements Event {
|
||||
@Override
|
||||
public Version version() {
|
||||
return Versionable.initialVersion();
|
||||
}
|
||||
}
|
||||
class E4 extends AbstractEvent implements Event {
|
||||
@Override
|
||||
public Version version() {
|
||||
return Versionable.initialVersion();
|
||||
}
|
||||
}
|
||||
class E5 extends AbstractEvent implements Event {
|
||||
@Override
|
||||
public Version version() {
|
||||
return Versionable.initialVersion();
|
||||
}
|
||||
}
|
||||
class E6 extends AbstractEvent implements Event {
|
||||
@Override
|
||||
public Version version() {
|
||||
return Versionable.initialVersion();
|
||||
}
|
||||
}
|
||||
class E7 extends AbstractEvent implements Event {
|
||||
@Override
|
||||
public Version version() {
|
||||
return Versionable.initialVersion();
|
||||
}
|
||||
}
|
||||
class E8 extends AbstractEvent implements Event {
|
||||
@Override
|
||||
public Version version() {
|
||||
return Versionable.initialVersion();
|
||||
}
|
||||
}
|
||||
class E9 extends AbstractEvent implements Event {
|
||||
@Override
|
||||
public Version version() {
|
||||
return Versionable.initialVersion();
|
||||
}
|
||||
}
|
||||
|
||||
EventBus<E1> E1bus = factory.createOrGetEventBusFor(E1.class);
|
||||
EventBus<E2> E2bus = factory.createOrGetEventBusFor(E2.class);
|
||||
EventBus<E3> E3bus = factory.createOrGetEventBusFor(E3.class);
|
||||
EventBus<E4> E4bus = factory.createOrGetEventBusFor(E4.class);
|
||||
EventBus<E5> E5bus = factory.createOrGetEventBusFor(E5.class);
|
||||
EventBus<E6> E6bus = factory.createOrGetEventBusFor(E6.class);
|
||||
EventBus<E7> E7bus = factory.createOrGetEventBusFor(E7.class);
|
||||
EventBus<E8> E8bus = factory.createOrGetEventBusFor(E8.class);
|
||||
EventBus<E9> E9bus = factory.createOrGetEventBusFor(E9.class);
|
||||
|
||||
class Publisher9 implements EventPublisher.EventPublisher9<E1,E2,E3,E4,E5,E6,E7,E8,E9> {
|
||||
@Override
|
||||
public String getName() {
|
||||
return Publisher9.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void post (final Class<E1> eventType, final E1 event) { E1bus.publish(eventType, event); }
|
||||
@Override
|
||||
public void post2(final Class<E2> eventType, final E2 event) { E2bus.publish(eventType, event); }
|
||||
@Override
|
||||
public void post3(final Class<E3> eventType, final E3 event) { E3bus.publish(eventType, event); }
|
||||
@Override
|
||||
public void post4(final Class<E4> eventType, final E4 event) { E4bus.publish(eventType, event); }
|
||||
@Override
|
||||
public void post5(final Class<E5> eventType, final E5 event) { E5bus.publish(eventType, event); }
|
||||
@Override
|
||||
public void post6(final Class<E6> eventType, final E6 event) { E6bus.publish(eventType, event); }
|
||||
@Override
|
||||
public void post7(final Class<E7> eventType, final E7 event) { E7bus.publish(eventType, event); }
|
||||
@Override
|
||||
public void post8(final Class<E8> eventType, final E8 event) { E8bus.publish(eventType, event); }
|
||||
@Override
|
||||
public void post9(final Class<E9> eventType, final E9 event) { E9bus.publish(eventType, event); }
|
||||
}
|
||||
|
||||
class Subscriber9 implements EventSubscriber.EventSubscriber9<E1,E2,E3,E4,E5,E6,E7,E8,E9> {
|
||||
final Map<Class<?>,Integer> event2CounterMap = new HashMap<>();
|
||||
|
||||
private void incEventCounter(Class<?> eventType) {
|
||||
Integer counter = event2CounterMap.get(eventType);
|
||||
if (event2CounterMap.get(eventType) == null) {
|
||||
event2CounterMap.put(eventType, 1);
|
||||
} else {
|
||||
event2CounterMap.put(eventType, counter+1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return Subscriber9.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
return InMemorySyncEventBusAndSpyHandlingMultipleEventTypesTest.class.getPackageName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive (final E1 event) { incEventCounter(E1.class); }
|
||||
@Override
|
||||
public void receive2(final E2 event) { incEventCounter(E2.class); }
|
||||
@Override
|
||||
public void receive3(final E3 event) { incEventCounter(E3.class); }
|
||||
@Override
|
||||
public void receive4(final E4 event) { incEventCounter(E4.class); }
|
||||
@Override
|
||||
public void receive5(final E5 event) { incEventCounter(E5.class); }
|
||||
@Override
|
||||
public void receive6(final E6 event) { incEventCounter(E6.class); }
|
||||
@Override
|
||||
public void receive7(final E7 event) { incEventCounter(E7.class); }
|
||||
@Override
|
||||
public void receive8(final E8 event) { incEventCounter(E8.class); }
|
||||
@Override
|
||||
public void receive9(final E9 event) { incEventCounter(E9.class); }
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------------------------------------
|
||||
|
||||
// arrange, 2
|
||||
|
||||
Publisher9 publisher9 = new Publisher9();
|
||||
Subscriber9 subscriber9 = new Subscriber9();
|
||||
|
||||
E1bus.subscribe(E1.class, subscriber9, START_READING_FROM_NOW);
|
||||
E2bus.subscribe(E2.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW);
|
||||
E3bus.subscribe(E3.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW);
|
||||
E4bus.subscribe(E4.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW);
|
||||
E5bus.subscribe(E5.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW);
|
||||
E6bus.subscribe(E6.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW);
|
||||
E7bus.subscribe(E7.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW);
|
||||
E8bus.subscribe(E8.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW);
|
||||
E9bus.subscribe(E9.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW);
|
||||
|
||||
// act
|
||||
int numEventsToPublish = 100;
|
||||
for (int counter=0; counter<numEventsToPublish; counter++) {
|
||||
publisher9.post (E1.class, new E1());
|
||||
publisher9.post2(E2.class, new E2());
|
||||
publisher9.post3(E3.class, new E3());
|
||||
publisher9.post4(E4.class, new E4());
|
||||
publisher9.post5(E5.class, new E5());
|
||||
publisher9.post6(E6.class, new E6());
|
||||
publisher9.post7(E7.class, new E7());
|
||||
publisher9.post8(E8.class, new E8());
|
||||
publisher9.post9(E9.class, new E9());
|
||||
}
|
||||
|
||||
// assert
|
||||
Set<Class<?>> classes = subscriber9.event2CounterMap.keySet();
|
||||
classes.forEach(eventClass -> {
|
||||
assertThat(subscriber9.event2CounterMap.get(eventClass)).isEqualTo(numEventsToPublish);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,423 @@
|
|||
package de.accso.flexinale.common.api.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.*;
|
||||
import de.accso.flexinale.common.api.event.EventTestdata.MyNumberEvent;
|
||||
import de.accso.flexinale.common.infrastructure.eventbus.InMemorySyncEventBusFactory;
|
||||
import de.accso.flexinale.common.infrastructure.eventbus.InMemorySyncEventBusSpy;
|
||||
import de.accso.flexinale.common.infrastructure.eventbus.InMemorySyncEventBusSpyFactory;
|
||||
import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException;
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.*;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
class InMemorySyncEventBusAndSpySubscriptionTest {
|
||||
|
||||
@Test
|
||||
void testSubscriptionAtStart_START_READING_FROM_NOW() {
|
||||
// arrange
|
||||
int numEventsToPublish = 100;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
internal_testSubscriberReceivesMultipleEvents_START_READING_FROM_NOW(numEventsToPublish, sut);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscriptionAtStart_START_READING_FROM_NOW_Spy() {
|
||||
// arrange
|
||||
int numEventsToPublish = 100;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
MyNumberEvent myEvent = internal_testSubscriberReceivesMultipleEvents_START_READING_FROM_NOW(numEventsToPublish, sut);
|
||||
|
||||
// ... and assert spy functionality
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
|
||||
List<MyNumberEvent> allEvents = spy.allEvents();
|
||||
assertThat(allEvents.getLast()).isEqualTo(myEvent);
|
||||
assertThat(spy.size()).isEqualTo(numEventsToPublish);
|
||||
}
|
||||
|
||||
private static MyNumberEvent internal_testSubscriberReceivesMultipleEvents_START_READING_FROM_NOW(
|
||||
final int numEventsToPublish, final EventBus<MyNumberEvent> sut) {
|
||||
|
||||
MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);
|
||||
|
||||
MyNumberEventSubscriber mySubscriberSubscribedBeforePublish = new MyNumberEventSubscriber(true);
|
||||
MyNumberEventSubscriber mySubscriberSubscribedAfterPublish = new MyNumberEventSubscriber(true);
|
||||
|
||||
// act
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedBeforePublish, START_READING_FROM_NOW);
|
||||
MyNumberEvent myEvent = null;
|
||||
for (int counter = 0; counter < numEventsToPublish; counter++) {
|
||||
myEvent = new MyNumberEvent(counter, counter);
|
||||
myPublisher.post(MyNumberEvent.class, myEvent);
|
||||
}
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedAfterPublish, START_READING_FROM_NOW);
|
||||
|
||||
// assert
|
||||
assertThat(mySubscriberSubscribedBeforePublish.lastReceivedEvent).isEqualTo(myEvent);
|
||||
assertThat(mySubscriberSubscribedBeforePublish.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
|
||||
|
||||
assertThat(mySubscriberSubscribedAfterPublish.lastReceivedEvent).isNull();
|
||||
assertThat(mySubscriberSubscribedAfterPublish.counterOfRetrievedEvents).isEqualTo(0);
|
||||
|
||||
return myEvent;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void testSubscriptionAtStart_START_READING_FROM_BEGINNING_throwsException() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(true);
|
||||
|
||||
// act and assert
|
||||
Assertions.assertThatThrownBy(() -> {
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_BEGINNING);
|
||||
})
|
||||
.isInstanceOf(DeveloperMistakeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscriptionAtStart_START_READING_FROM_BEGINNING_Spy() {
|
||||
// arrange
|
||||
int numEventsToPublish = 100;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
|
||||
MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);
|
||||
|
||||
MyNumberEventSubscriber mySubscriberSubscribedBeforePublish = new MyNumberEventSubscriber(true);
|
||||
MyNumberEventSubscriber mySubscriberSubscribedAfterPublish = new MyNumberEventSubscriber(true);
|
||||
|
||||
// act - one subscriber before the publish, the other afterwards - with START_READING_FROM_BEGINNING both get the same
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedBeforePublish, START_READING_FROM_BEGINNING);
|
||||
MyNumberEvent myEvent = null;
|
||||
for (int counter = 0; counter < numEventsToPublish; counter++) {
|
||||
myEvent = new MyNumberEvent(counter, counter);
|
||||
myPublisher.post(MyNumberEvent.class, myEvent);
|
||||
}
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedAfterPublish, START_READING_FROM_BEGINNING);
|
||||
|
||||
// assert
|
||||
assertThat(mySubscriberSubscribedBeforePublish.lastReceivedEvent).isEqualTo(myEvent);
|
||||
assertThat(mySubscriberSubscribedBeforePublish.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
|
||||
|
||||
assertThat(mySubscriberSubscribedAfterPublish.lastReceivedEvent).isEqualTo(myEvent);
|
||||
assertThat(mySubscriberSubscribedAfterPublish.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
|
||||
|
||||
// ... and assert spy functionality
|
||||
List<MyNumberEvent> allEvents = spy.allEvents();
|
||||
assertThat(allEvents.getLast()).isEqualTo(myEvent);
|
||||
assertThat(spy.size()).isEqualTo(numEventsToPublish);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void testSubscriptionAtStart_START_READING_FROM_LAST_TIME_throwsException() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(true);
|
||||
|
||||
// act and assert
|
||||
Assertions.assertThatThrownBy(() -> {
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_LAST_TIME);
|
||||
})
|
||||
.isInstanceOf(DeveloperMistakeException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscriptionAtStart_START_READING_FROM_LAST_TIME_Spy_oneConsumer() {
|
||||
// arrange
|
||||
int numEventsToPublish = 100;
|
||||
int counter;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
|
||||
MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);
|
||||
|
||||
MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(true);
|
||||
|
||||
// act
|
||||
|
||||
// 1) subscribe
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_LAST_TIME);
|
||||
|
||||
// 2) sent first half
|
||||
MyNumberEvent myEvent = null;
|
||||
for (counter = 0; counter < numEventsToPublish / 2; counter++) {
|
||||
myEvent = new MyNumberEvent(counter, counter);
|
||||
myPublisher.post(MyNumberEvent.class, myEvent);
|
||||
}
|
||||
|
||||
assertThat(mySubscriber.lastReceivedEvent).isEqualTo(myEvent);
|
||||
assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish / 2);
|
||||
|
||||
// 3) unsubscribe
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriber);
|
||||
|
||||
// 4) sent second half
|
||||
for (; counter < numEventsToPublish; counter++) {
|
||||
myEvent = new MyNumberEvent(counter, counter);
|
||||
myPublisher.post(MyNumberEvent.class, myEvent);
|
||||
}
|
||||
|
||||
// 5) subscribe again -> get the second half
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_LAST_TIME);
|
||||
|
||||
// 6) unsubscribe
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriber);
|
||||
assertThat(mySubscriber.lastReceivedEvent).isEqualTo(myEvent);
|
||||
assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
|
||||
|
||||
// 7) subscribe again -> get nothing else
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_LAST_TIME);
|
||||
assertThat(mySubscriber.lastReceivedEvent).isEqualTo(myEvent);
|
||||
assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
|
||||
|
||||
// ... and assert spy functionality
|
||||
List<MyNumberEvent> allEvents = spy.allEvents();
|
||||
assertThat(allEvents.getLast()).isEqualTo(myEvent);
|
||||
assertThat(spy.size()).isEqualTo(numEventsToPublish);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscriptionAtStart_START_READING_FROM_LAST_TIME_Spy_twoConsumersInOneGroup() {
|
||||
// arrange
|
||||
int numEventsToPublish = 100;
|
||||
int counter;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
|
||||
|
||||
MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);
|
||||
|
||||
MyNumberEventSubscriber mySubscriber1 = new MyNumberEventSubscriber(true);
|
||||
MyNumberEventSubscriber mySubscriber2 = new MyNumberEventSubscriber(true);
|
||||
|
||||
// act
|
||||
|
||||
// 1) subscribe
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber1, START_READING_FROM_LAST_TIME);
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber2, START_READING_FROM_LAST_TIME);
|
||||
|
||||
// 2) sent first half
|
||||
MyNumberEvent myEvent50 = null;
|
||||
for (counter = 0; counter < numEventsToPublish / 2; counter++) {
|
||||
myEvent50 = new MyNumberEvent(counter, counter);
|
||||
myPublisher.post(MyNumberEvent.class, myEvent50);
|
||||
}
|
||||
|
||||
// only one of the subscribers has the last event
|
||||
Set<MyNumberEvent> lastReceivedEvents = Set.of(mySubscriber1.lastReceivedEvent, mySubscriber2.lastReceivedEvent);
|
||||
assertThat(lastReceivedEvents.size()).isEqualTo(2); // Set.of() already guarantees that the two elements are different (otherwise "java.lang.IllegalArgumentException: duplicate element")
|
||||
assertThat(lastReceivedEvents.contains(myEvent50)).isTrue();
|
||||
|
||||
// both subscribers received all events in total
|
||||
assertThat(mySubscriber1.counterOfRetrievedEvents + mySubscriber2.counterOfRetrievedEvents)
|
||||
.isEqualTo(numEventsToPublish / 2);
|
||||
|
||||
int counterOfRetrievedEventsOfSubscriber1AfterFirstHalf = mySubscriber1.counterOfRetrievedEvents;
|
||||
MyNumberEvent mySubscriber1LastReceivedEventAfterFirstHalf = mySubscriber1.lastReceivedEvent;
|
||||
int counterOfRetrievedEventsOfSubscriber2AfterFirstHalf = mySubscriber2.counterOfRetrievedEvents;
|
||||
MyNumberEvent mySubscriber2LastReceivedEventAfterFirstHalf = mySubscriber2.lastReceivedEvent;
|
||||
|
||||
// 3) unsubscribe
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriber1);
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriber2);
|
||||
|
||||
// 4) sent second half
|
||||
MyNumberEvent myEvent100 = null;
|
||||
for (; counter < numEventsToPublish; counter++) {
|
||||
myEvent100 = new MyNumberEvent(counter, counter);
|
||||
myPublisher.post(MyNumberEvent.class, myEvent100);
|
||||
}
|
||||
|
||||
// 5) subscribe subscriber 1 again -> gets the second half completely
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber1, START_READING_FROM_LAST_TIME);
|
||||
assertThat(mySubscriber1.lastReceivedEvent).isEqualTo(myEvent100);
|
||||
assertThat(mySubscriber1.counterOfRetrievedEvents)
|
||||
.isEqualTo(counterOfRetrievedEventsOfSubscriber1AfterFirstHalf + numEventsToPublish/2);
|
||||
|
||||
// the second subscriber get's nothing as both subscribers are in one group
|
||||
// (wait fail, when the following subscribe comes to early -> then add to wait a little here)
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber2, START_READING_FROM_LAST_TIME);
|
||||
assertThat(mySubscriber2.lastReceivedEvent).isEqualTo(mySubscriber2LastReceivedEventAfterFirstHalf);
|
||||
assertThat(mySubscriber2.counterOfRetrievedEvents)
|
||||
.isEqualTo(counterOfRetrievedEventsOfSubscriber2AfterFirstHalf);
|
||||
|
||||
int counterOfRetrievedEventsOfSubscriber1AfterSecondHalf = mySubscriber1.counterOfRetrievedEvents;
|
||||
int counterOfRetrievedEventsOfSubscriber2AfterSecondHalf = mySubscriber2.counterOfRetrievedEvents;
|
||||
|
||||
assertThat(counterOfRetrievedEventsOfSubscriber1AfterSecondHalf + counterOfRetrievedEventsOfSubscriber2AfterSecondHalf)
|
||||
.isEqualTo(numEventsToPublish);
|
||||
|
||||
// 6) unsubscribe
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriber1);
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriber2);
|
||||
|
||||
// 7) subscribe again -> get nothing else
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber1, START_READING_FROM_LAST_TIME);
|
||||
assertThat(mySubscriber1.lastReceivedEvent).isEqualTo(myEvent100);
|
||||
assertThat(mySubscriber1.counterOfRetrievedEvents)
|
||||
.isEqualTo(counterOfRetrievedEventsOfSubscriber1AfterSecondHalf);
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber2, START_READING_FROM_LAST_TIME);
|
||||
assertThat(mySubscriber2.lastReceivedEvent)
|
||||
.isEqualTo(mySubscriber2LastReceivedEventAfterFirstHalf);
|
||||
assertThat(mySubscriber2.counterOfRetrievedEvents)
|
||||
.isEqualTo(counterOfRetrievedEventsOfSubscriber2AfterSecondHalf);
|
||||
|
||||
// ... and assert spy functionality
|
||||
List<MyNumberEvent> allEvents = spy.allEvents();
|
||||
assertThat(allEvents.getLast()).isEqualTo(myEvent100);
|
||||
assertThat(spy.size()).isEqualTo(numEventsToPublish);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscriptionAtStart_START_READING_FROM_LAST_TIME_Spy_twoConsumersInSeparateGroups() {
|
||||
// arrange
|
||||
int numEventsToPublish = 100;
|
||||
int counter;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
|
||||
|
||||
MyNumberEventSubscriber mySubscriber1 = new MyNumberEventSubscriber(true);
|
||||
MyEventSubscriberEachInstanceInItsOwnGroup mySubscriber2 = new MyEventSubscriberEachInstanceInItsOwnGroup(true);
|
||||
MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);
|
||||
|
||||
// act
|
||||
|
||||
// 1) subscribe
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber1, START_READING_FROM_LAST_TIME);
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber2, START_READING_FROM_LAST_TIME);
|
||||
|
||||
// 2) sent first half
|
||||
MyNumberEvent myEvent50 = null;
|
||||
for (counter = 0; counter < numEventsToPublish / 2; counter++) {
|
||||
myEvent50 = new MyNumberEvent(counter, counter);
|
||||
myPublisher.post(MyNumberEvent.class, myEvent50);
|
||||
}
|
||||
|
||||
assertThat(mySubscriber1.lastReceivedEvent).isEqualTo(myEvent50);
|
||||
assertThat(mySubscriber1.counterOfRetrievedEvents).isEqualTo(numEventsToPublish / 2);
|
||||
assertThat(mySubscriber2.lastReceivedEvent).isEqualTo(myEvent50);
|
||||
assertThat(mySubscriber2.counterOfRetrievedEvents).isEqualTo(numEventsToPublish / 2);
|
||||
|
||||
// 3) unsubscribe
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriber1);
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriber2);
|
||||
|
||||
// 4) sent second half
|
||||
MyNumberEvent myEvent100 = null;
|
||||
for (; counter < numEventsToPublish; counter++) {
|
||||
myEvent100 = new MyNumberEvent(counter, counter);
|
||||
myPublisher.post(MyNumberEvent.class, myEvent100);
|
||||
}
|
||||
|
||||
// 5) subscribe again -> get the second half
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber1, START_READING_FROM_LAST_TIME);
|
||||
assertThat(mySubscriber1.lastReceivedEvent).isEqualTo(myEvent100);
|
||||
assertThat(mySubscriber1.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
|
||||
|
||||
// the second subscriber get's same as both subscribers are in separate groups
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber2, START_READING_FROM_LAST_TIME);
|
||||
assertThat(mySubscriber2.lastReceivedEvent).isEqualTo(myEvent100);
|
||||
assertThat(mySubscriber2.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
|
||||
|
||||
// 6) unsubscribe
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriber1);
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriber2);
|
||||
|
||||
// 7) subscribe again -> get nothing else
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber1, START_READING_FROM_LAST_TIME);
|
||||
assertThat(mySubscriber1.lastReceivedEvent).isEqualTo(myEvent100);
|
||||
assertThat(mySubscriber1.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber2, START_READING_FROM_LAST_TIME);
|
||||
assertThat(mySubscriber2.lastReceivedEvent).isEqualTo(myEvent100);
|
||||
assertThat(mySubscriber2.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
|
||||
|
||||
// ... and assert spy functionality
|
||||
List<MyNumberEvent> allEvents = spy.allEvents();
|
||||
assertThat(allEvents.getLast()).isEqualTo(myEvent100);
|
||||
assertThat(spy.size()).isEqualTo(numEventsToPublish);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscriptionAtStart_START_READING_FROM_BEGINNING_and_LAST_TIME_Spy() {
|
||||
// arrange
|
||||
int numEventsToPublish = 100;
|
||||
int counter;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
|
||||
|
||||
// false, as we get some events twice
|
||||
MyNumberEventSubscriber mySubscriberSubscribedBeforeAndAfterPublish = new MyNumberEventSubscriber(false);
|
||||
MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);
|
||||
|
||||
// act
|
||||
|
||||
// 1) subscribe
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedBeforeAndAfterPublish, START_READING_FROM_LAST_TIME);
|
||||
|
||||
// 2) sent first half
|
||||
MyNumberEvent myEvent = null;
|
||||
for (counter = 0; counter < numEventsToPublish / 2; counter++) {
|
||||
myEvent = new MyNumberEvent(counter, counter);
|
||||
myPublisher.post(MyNumberEvent.class, myEvent);
|
||||
}
|
||||
|
||||
// 3) unsubscribe
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriberSubscribedBeforeAndAfterPublish);
|
||||
|
||||
// 4) sent second half
|
||||
for (; counter < numEventsToPublish; counter++) {
|
||||
myEvent = new MyNumberEvent(counter, counter);
|
||||
myPublisher.post(MyNumberEvent.class, myEvent);
|
||||
}
|
||||
|
||||
// 5) subscribe again -> get it all, i.e. the first half and the second half
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedBeforeAndAfterPublish, START_READING_FROM_BEGINNING);
|
||||
|
||||
// assert
|
||||
assertThat(mySubscriberSubscribedBeforeAndAfterPublish.lastReceivedEvent).isEqualTo(myEvent);
|
||||
assertThat(mySubscriberSubscribedBeforeAndAfterPublish.counterOfRetrievedEvents)
|
||||
.isEqualTo(numEventsToPublish/2 + numEventsToPublish);
|
||||
|
||||
// 6) unsubscribe
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriberSubscribedBeforeAndAfterPublish);
|
||||
|
||||
// 7) subscribe again -> get nothing else
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedBeforeAndAfterPublish, START_READING_FROM_LAST_TIME);
|
||||
|
||||
// assert
|
||||
assertThat(mySubscriberSubscribedBeforeAndAfterPublish.lastReceivedEvent).isEqualTo(myEvent);
|
||||
assertThat(mySubscriberSubscribedBeforeAndAfterPublish.counterOfRetrievedEvents)
|
||||
.isEqualTo(numEventsToPublish/2 + numEventsToPublish);
|
||||
|
||||
// ... and assert spy functionality
|
||||
List<MyNumberEvent> allEvents = spy.allEvents();
|
||||
assertThat(allEvents.getLast()).isEqualTo(myEvent);
|
||||
assertThat(spy.size()).isEqualTo(numEventsToPublish);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,571 @@
|
|||
package de.accso.flexinale.common.api.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.*;
|
||||
import de.accso.flexinale.common.api.event.EventTestdata.MyMixedEvent;
|
||||
import de.accso.flexinale.common.api.event.EventTestdata.MyNumberEvent;
|
||||
import de.accso.flexinale.common.api.event.EventTestdata.MyTextEvent;
|
||||
import de.accso.flexinale.common.infrastructure.eventbus.*;
|
||||
import de.accso.flexinale.common.shared_kernel.Identifiable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.*;
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
class InMemorySyncEventBusAndSpyTest {
|
||||
|
||||
@Test
|
||||
void testCreateAndGetEventBus() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory sut = new InMemorySyncEventBusFactory();
|
||||
|
||||
// act
|
||||
InMemorySyncEventBus<MyNumberEvent> eventBus =
|
||||
(InMemorySyncEventBus<MyNumberEvent>) sut.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
InMemorySyncEventBus<MyNumberEvent> retrievedEventBus =
|
||||
(InMemorySyncEventBus<MyNumberEvent>) sut.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
// assert
|
||||
assertThat(eventBus).isNotNull();
|
||||
assertThat(retrievedEventBus).isEqualTo(eventBus);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCreateAndGetEventBusSpy() {
|
||||
// arrange
|
||||
InMemorySyncEventBusSpyFactory sut = new InMemorySyncEventBusSpyFactory();
|
||||
|
||||
// act
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> eventBus =
|
||||
(InMemorySyncEventBusSpy<MyNumberEvent>) sut.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> retrievedEventBus =
|
||||
(InMemorySyncEventBusSpy<MyNumberEvent>) sut.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
// assert
|
||||
assertThat(eventBus).isNotNull();
|
||||
assertThat(retrievedEventBus).isEqualTo(eventBus);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void testSubscriberReceivesEventOfCorrectType() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
internal_testSubscriberReceivesEventOfCorrectType(sut);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscriberReceivesEventOfCorrectTypeSpy() {
|
||||
// arrange
|
||||
InMemorySyncEventBusSpyFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
MyNumberEvent myEvent = internal_testSubscriberReceivesEventOfCorrectType(sut);
|
||||
|
||||
// ... and assert spy functionality
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
|
||||
assertThat(spy.allEvents()).isEqualTo(List.of(myEvent));
|
||||
assertThat(spy.size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
private MyNumberEvent internal_testSubscriberReceivesEventOfCorrectType(final EventBus<MyNumberEvent> sut) {
|
||||
MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber();
|
||||
MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);
|
||||
|
||||
// act
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
|
||||
MyNumberEvent myEvent = new MyNumberEvent(11, 22);
|
||||
myPublisher.post(MyNumberEvent.class, myEvent);
|
||||
|
||||
// assert
|
||||
assertThat(mySubscriber.lastReceivedEvent).isEqualTo(myEvent);
|
||||
assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(1);
|
||||
return myEvent;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
void testEventSubscriberProducesRuntimeException() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
EventBus<MyNumberEvent> numberBus = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
MyNumberEventPublisher myNumberPublisher = new MyNumberEventPublisher(numberBus);
|
||||
|
||||
MyNumberEventThrowingRuntimeExceptionSubscriber erroneousSubscriber = new MyNumberEventThrowingRuntimeExceptionSubscriber();
|
||||
numberBus.subscribe(MyNumberEvent.class, (EventSubscriber) erroneousSubscriber, START_READING_FROM_NOW);
|
||||
|
||||
MyNumberEvent myNumberEvent = new MyNumberEvent(11, 22);
|
||||
Identifiable.Id id = myNumberEvent.id();
|
||||
|
||||
// act and assert that receive method throws RuntimeException (wrapped in a RuntimeException by the dispatcher)
|
||||
Exception caughtException = null;
|
||||
|
||||
try {
|
||||
myNumberPublisher.post(MyNumberEvent.class, myNumberEvent);
|
||||
} catch (Exception ex) {
|
||||
caughtException = ex;
|
||||
}
|
||||
assertThat(caughtException).isNotNull(); // assertThatThrownBy would be nicer, but we need to the check the exception's contents
|
||||
|
||||
Throwable cause = caughtException.getCause();
|
||||
// ... and wrapped again in a InvocationTargetException
|
||||
assertThat(cause).isInstanceOf(java.lang.reflect.InvocationTargetException.class);
|
||||
// so here we go with the root cause:
|
||||
assertThat(cause.getCause()).isInstanceOf(MyNumberEventRuntimeException.class);
|
||||
assertThat(cause.getCause()).hasMessageContaining("RuntimeException for event with Id=" + id);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void testGenericSubscriberReceivesAllEventsIndependentOfItsType() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
EventBus<MyNumberEvent> numberBus = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
EventBus<MyTextEvent> textBus = factory.createOrGetEventBusFor(MyTextEvent.class);
|
||||
|
||||
MyNumberEventSubscriber myNumberSubscriber = new MyNumberEventSubscriber();
|
||||
MyNumberEventPublisher myNumberPublisher = new MyNumberEventPublisher(numberBus);
|
||||
MyTextEventSubscriber myTextSubscriber = new MyTextEventSubscriber();
|
||||
MyTextEventPublisher myTextPublisher = new MyTextEventPublisher(textBus);
|
||||
|
||||
numberBus.subscribe(MyNumberEvent.class, myNumberSubscriber, START_READING_FROM_NOW);
|
||||
textBus.subscribe(MyTextEvent.class, myTextSubscriber, START_READING_FROM_NOW);
|
||||
|
||||
GenericEventSubscriber myGenericSubscriber = new GenericEventSubscriber();
|
||||
|
||||
numberBus.subscribe(myGenericSubscriber, START_READING_FROM_NOW);
|
||||
textBus.subscribe(myGenericSubscriber, START_READING_FROM_NOW);
|
||||
|
||||
// act
|
||||
MyNumberEvent myNumberEvent = new MyNumberEvent(11, 22);
|
||||
myNumberPublisher.post(MyNumberEvent.class, myNumberEvent);
|
||||
|
||||
// assert
|
||||
assertThat(myNumberSubscriber.lastReceivedEvent).isEqualTo(myNumberEvent);
|
||||
assertThat(myNumberSubscriber.counterOfRetrievedEvents).isEqualTo(1);
|
||||
|
||||
assertThat(myGenericSubscriber.lastReceivedEvent.getClass()).isEqualTo(MyNumberEvent.class);
|
||||
assertThat(myGenericSubscriber.lastReceivedEvent).isEqualTo(myNumberEvent);
|
||||
assertThat(myGenericSubscriber.counterOfRetrievedEvents).isEqualTo(1);
|
||||
|
||||
// act
|
||||
MyTextEvent myTextEvent = new MyTextEvent("11", "22");
|
||||
myTextPublisher.post(MyTextEvent.class, myTextEvent);
|
||||
|
||||
// assert
|
||||
assertThat(myTextSubscriber.lastReceivedEvent).isEqualTo(myTextEvent);
|
||||
assertThat(myTextSubscriber.counterOfRetrievedEvents).isEqualTo(1);
|
||||
|
||||
assertThat(myGenericSubscriber.lastReceivedEvent.getClass()).isEqualTo(MyTextEvent.class);
|
||||
assertThat(myGenericSubscriber.lastReceivedEvent).isEqualTo(myTextEvent);
|
||||
assertThat(myGenericSubscriber.counterOfRetrievedEvents).isEqualTo(2);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void testEventChainKeepsCorrelationIdAndEventHistory() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
|
||||
EventBus<MyTextEvent> textBus = factory.createOrGetEventBusFor(MyTextEvent.class);
|
||||
EventBus<MyNumberEvent> numberBus = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
MyNumberEventSubscriberAndMyTextEventPublisher numberSubscriberAndTextPublisher =
|
||||
new MyNumberEventSubscriberAndMyTextEventPublisher(textBus);
|
||||
MyTextEventSubscriber myTextEventSubscriber = new MyTextEventSubscriber();
|
||||
|
||||
numberBus.subscribe(MyNumberEvent.class, numberSubscriberAndTextPublisher, START_READING_FROM_NOW);
|
||||
textBus.subscribe(MyTextEvent.class, myTextEventSubscriber, START_READING_FROM_NOW);
|
||||
|
||||
// act - send MyNumberEvent so that internally a MyTextEvent is sent
|
||||
MyNumberEvent firstEvent = new MyNumberEvent(11, 22);
|
||||
new MyNumberEventPublisher(numberBus).post(MyNumberEvent.class, firstEvent);
|
||||
assertThat(numberSubscriberAndTextPublisher.lastReceivedEvent).isEqualTo(firstEvent);
|
||||
|
||||
// assert that the event chain keeps the correlation Id and the event history
|
||||
assertThat(numberSubscriberAndTextPublisher.lastSentEvent).isEqualTo(myTextEventSubscriber.lastReceivedEvent);
|
||||
MyTextEvent secondEvent = numberSubscriberAndTextPublisher.lastSentEvent;
|
||||
|
||||
assertThat(secondEvent.correlationId()).isEqualTo(firstEvent.correlationId());
|
||||
assertThat(secondEvent.eventContext().eventHistory.getFirst().id()).isEqualTo(firstEvent.id());
|
||||
assertThat(secondEvent.eventContext().eventHistory.getFirst().eventType()).isEqualTo(MyNumberEvent.class);
|
||||
assertThat(secondEvent.text1).isEqualTo("11");
|
||||
assertThat(secondEvent.text2).isEqualTo("22");
|
||||
|
||||
// assert that the context of the bus is cleared
|
||||
assertThat(textBus.getEventContextHolder().isEmpty()).isTrue();
|
||||
assertThat(numberBus.getEventContextHolder().isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void testSubscriberReceivesMultipleEvents() {
|
||||
// arrange
|
||||
int numEventsToPublish = 100;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
internal_testSubscriberReceivesMultipleEvents(numEventsToPublish, sut);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscriberReceivesMultipleEventsSpy() {
|
||||
// arrange
|
||||
int numEventsToPublish = 100;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
MyNumberEvent myEvent = internal_testSubscriberReceivesMultipleEvents(numEventsToPublish, sut);
|
||||
|
||||
// ... and assert spy functionality
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
|
||||
List<MyNumberEvent> allEvents = spy.allEvents();
|
||||
assertThat(allEvents.getLast()).isEqualTo(myEvent);
|
||||
assertThat(spy.size()).isEqualTo(numEventsToPublish);
|
||||
}
|
||||
|
||||
private static MyNumberEvent internal_testSubscriberReceivesMultipleEvents(
|
||||
final int numEventsToPublish, final EventBus<MyNumberEvent> sut) {
|
||||
|
||||
MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(true);
|
||||
MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);
|
||||
|
||||
// act
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
|
||||
MyNumberEvent myEvent = null;
|
||||
for (int counter = 0; counter < numEventsToPublish; counter++) {
|
||||
myEvent = new MyNumberEvent(counter, counter);
|
||||
myPublisher.post(MyNumberEvent.class, myEvent);
|
||||
}
|
||||
|
||||
// assert
|
||||
assertThat(mySubscriber.lastReceivedEvent).isEqualTo(myEvent);
|
||||
assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
|
||||
return myEvent;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void testSubscriberDoesNotReceiveOtherEventTypes() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
EventBus<MyNumberEvent> bus1 = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
EventBus<MyMixedEvent> bus2 = factory.createOrGetEventBusFor(MyMixedEvent.class);
|
||||
|
||||
internal_testSubscriberDoesNotReceiveOtherEventTypes(bus1, bus2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscriberDoesNotReceiveOtherEventTypesSpy() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> bus1 = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
EventBus<MyMixedEvent> bus2 = factory.createOrGetEventBusFor(MyMixedEvent.class);
|
||||
|
||||
MyMixedEvent myEvent = internal_testSubscriberDoesNotReceiveOtherEventTypes(bus1, bus2);
|
||||
|
||||
// ... and assert spy functionality
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> spy1 = (InMemorySyncEventBusSpy<MyNumberEvent>) bus1;
|
||||
InMemorySyncEventBusSpy<MyMixedEvent> spy2 = (InMemorySyncEventBusSpy<MyMixedEvent>) bus2;
|
||||
assertThat(spy1.allEvents().isEmpty()).isTrue();
|
||||
assertThat(spy1.size()).isEqualTo(0);
|
||||
assertThat(spy2.allEvents()).isEqualTo(List.of(myEvent));
|
||||
assertThat(spy2.size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
private static MyMixedEvent internal_testSubscriberDoesNotReceiveOtherEventTypes(
|
||||
final EventBus<MyNumberEvent> bus1, final EventBus<MyMixedEvent> bus2) {
|
||||
|
||||
MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber();
|
||||
MyMixedEventPublisher myPublisher = new MyMixedEventPublisher(bus2);
|
||||
|
||||
// act
|
||||
bus1.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
|
||||
MyMixedEvent myEvent = new MyMixedEvent("abc", 22);
|
||||
myPublisher.post(MyMixedEvent.class, myEvent);
|
||||
|
||||
// assert
|
||||
assertThat(mySubscriber.lastReceivedEvent).isNull();
|
||||
assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(0);
|
||||
return myEvent;
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void testSubscriberReceivedEventsOnlyAfterSubscribing() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
MyNumberEvent firstEvent = new MyNumberEvent(11, 22);
|
||||
MyNumberEvent secondEvent = new MyNumberEvent(12, 23);
|
||||
|
||||
internal_testSubscriberReceivedEventsOnlyAfterSubscribing(sut, firstEvent, secondEvent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscriberReceivedEventsOnlyAfterSubscribingSpy() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
MyNumberEvent firstEvent = new MyNumberEvent(11, 22);
|
||||
MyNumberEvent secondEvent = new MyNumberEvent(12, 23);
|
||||
|
||||
internal_testSubscriberReceivedEventsOnlyAfterSubscribing(sut, firstEvent, secondEvent);
|
||||
|
||||
// ... and assert spy functionality
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
|
||||
assertThat(spy.allEvents()).isEqualTo(List.of(firstEvent, secondEvent));
|
||||
}
|
||||
|
||||
private static void internal_testSubscriberReceivedEventsOnlyAfterSubscribing(
|
||||
final EventBus<MyNumberEvent> sut,
|
||||
final MyNumberEvent firstEvent, final MyNumberEvent secondEvent) {
|
||||
MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber();
|
||||
MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);
|
||||
|
||||
// act
|
||||
myPublisher.post(MyNumberEvent.class, firstEvent); // published but not received
|
||||
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
|
||||
|
||||
myPublisher.post(MyNumberEvent.class, secondEvent); // published and received
|
||||
|
||||
// assert
|
||||
assertThat(mySubscriber.lastReceivedEvent).isEqualTo(secondEvent);
|
||||
assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(1);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void testSubscriberReceivesNoMoreEventsAfterUnsubscribing() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
MyNumberEvent firstEvent = new MyNumberEvent(11, 22);
|
||||
MyNumberEvent secondEvent = new MyNumberEvent(12, 23);
|
||||
|
||||
internal_testSubscriberReceivesNoMoreEventsAfterUnsubscribing(sut, firstEvent, secondEvent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSubscriberReceivesNoMoreEventsAfterUnsubscribingSpy() {
|
||||
// arrange
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
MyNumberEvent firstEvent = new MyNumberEvent(11, 22);
|
||||
MyNumberEvent secondEvent = new MyNumberEvent(12, 23);
|
||||
|
||||
internal_testSubscriberReceivesNoMoreEventsAfterUnsubscribing(sut, firstEvent, secondEvent);
|
||||
|
||||
// ... and assert spy functionality
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
|
||||
assertThat(spy.allEvents()).isEqualTo(List.of(firstEvent, secondEvent));
|
||||
}
|
||||
|
||||
private static void internal_testSubscriberReceivesNoMoreEventsAfterUnsubscribing(
|
||||
final EventBus<MyNumberEvent> sut,
|
||||
final MyNumberEvent firstEvent, final MyNumberEvent secondEvent) {
|
||||
MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber();
|
||||
MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);
|
||||
|
||||
// act
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
|
||||
myPublisher.post(MyNumberEvent.class, firstEvent); // published but not received
|
||||
assertThat(mySubscriber.lastReceivedEvent).isEqualTo(firstEvent);
|
||||
sut.unsubscribe(MyNumberEvent.class, mySubscriber);
|
||||
myPublisher.post(MyNumberEvent.class, secondEvent); // published and received
|
||||
|
||||
// assert
|
||||
assertThat(mySubscriber.lastReceivedEvent).isEqualTo(firstEvent);
|
||||
assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(1);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void testMultipleSubscribersInSeparateGroupsAllReceiveTheSameEvents() {
|
||||
// arrange
|
||||
int numSubscribers = 5;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
List<MyNumberEvent> events = List.of(new MyNumberEvent(11, 22), new MyNumberEvent(12, 23),
|
||||
new MyNumberEvent(13, 24), new MyNumberEvent(14, 25));
|
||||
|
||||
internal_testListOfSubscribersAllReceiveTheSameEvents(numSubscribers, sut, events.toArray(new MyNumberEvent[0]));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleSubscribersInSeparateGroupsAllReceiveTheSameEventsSpy() {
|
||||
// arrange
|
||||
int numSubscribers = 5;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> sut = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
|
||||
List<MyNumberEvent> events = List.of(new MyNumberEvent(11, 22), new MyNumberEvent(12, 23),
|
||||
new MyNumberEvent(13, 24), new MyNumberEvent(14, 25));
|
||||
|
||||
internal_testListOfSubscribersAllReceiveTheSameEvents(numSubscribers, sut, events.toArray(new MyNumberEvent[0]));
|
||||
|
||||
// ... and assert spy functionality
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> spy = (InMemorySyncEventBusSpy<MyNumberEvent>) sut;
|
||||
assertThat(spy.allEvents()).isEqualTo(events);
|
||||
}
|
||||
|
||||
private static void internal_testListOfSubscribersAllReceiveTheSameEvents(
|
||||
final int numSubscribers, final EventBus<MyNumberEvent> sut,
|
||||
final MyNumberEvent... events) {
|
||||
|
||||
List<MyNumberEventSubscriber> mySubscribers = new ArrayList<>();
|
||||
for (int counter = 0; counter < numSubscribers; counter++) {
|
||||
MyEventSubscriberEachInstanceInItsOwnGroup mySubscriberEachInItsOwnGroup = new MyEventSubscriberEachInstanceInItsOwnGroup(true);
|
||||
mySubscribers.add(mySubscriberEachInItsOwnGroup);
|
||||
sut.subscribe(MyNumberEvent.class, mySubscriberEachInItsOwnGroup, START_READING_FROM_NOW);
|
||||
}
|
||||
|
||||
MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut);
|
||||
|
||||
for (MyNumberEvent event: events) {
|
||||
// act - first event
|
||||
myPublisher.post(MyNumberEvent.class, event);
|
||||
|
||||
// assert
|
||||
for (MyNumberEventSubscriber mySubscriber : mySubscribers) {
|
||||
assertThat(mySubscriber.lastReceivedEvent).isEqualTo(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@Test
|
||||
void testMultipleSubscribersReceiveMultipleEventsForMultipleTypes() {
|
||||
// arrange
|
||||
int numEventsToPublish = 9_999;
|
||||
int numSubscribersPerType = 1_999;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory();
|
||||
EventBus<MyNumberEvent> numberEventBus = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
EventBus<MyMixedEvent> mixedEventBus = factory.createOrGetEventBusFor(MyMixedEvent.class);
|
||||
|
||||
// create subscribers
|
||||
internal_testMultipleSubscribersReceiveMultipleEventsForMultipleTypes(numEventsToPublish, numSubscribersPerType,
|
||||
numberEventBus, mixedEventBus);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMultipleSubscribersReceiveMultipleEventsForMultipleTypesSpy() {
|
||||
// arrange
|
||||
int numEventsToPublish = 9_999;
|
||||
int numSubscribersPerType = 1_999;
|
||||
|
||||
InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory();
|
||||
EventBus<MyNumberEvent> numberEventBus = factory.createOrGetEventBusFor(MyNumberEvent.class);
|
||||
EventBus<MyMixedEvent> mixedEventBus = factory.createOrGetEventBusFor(MyMixedEvent.class);
|
||||
|
||||
internal_testMultipleSubscribersReceiveMultipleEventsForMultipleTypes(numEventsToPublish, numSubscribersPerType,
|
||||
numberEventBus, mixedEventBus);
|
||||
|
||||
// ... and assert spy functionality
|
||||
InMemorySyncEventBusSpy<MyNumberEvent> numberEventBusSpy = (InMemorySyncEventBusSpy<MyNumberEvent>) numberEventBus;
|
||||
InMemorySyncEventBusSpy<MyMixedEvent> mixedEventBusSpy = (InMemorySyncEventBusSpy<MyMixedEvent>) mixedEventBus;
|
||||
assertThat(numberEventBusSpy.size()).isEqualTo(numEventsToPublish);
|
||||
assertThat(mixedEventBusSpy.size()).isEqualTo(numEventsToPublish);
|
||||
}
|
||||
|
||||
private static void internal_testMultipleSubscribersReceiveMultipleEventsForMultipleTypes(
|
||||
final int numEventsToPublish, final int numSubscribersPerType,
|
||||
final EventBus<MyNumberEvent> numberEventBus, final EventBus<MyMixedEvent> mixedEventBus) {
|
||||
|
||||
// LocalDateTime start = LocalDateTime.now();
|
||||
|
||||
Random random = new Random();
|
||||
|
||||
// create subscribers, all instances in the same group
|
||||
List<MyNumberEventSubscriber> myNumberEventSubscribers = new ArrayList<>();
|
||||
List<MyMixedEventSubscriber> myMixedEventSubscribers = new ArrayList<>();
|
||||
|
||||
// create subscribers, all instances in the separate groups (each in one group)
|
||||
List<MyEventSubscriberEachInstanceInItsOwnGroup> mySubscribersEachInItsOwnGroup = new ArrayList<>();
|
||||
|
||||
for (int counter = 0; counter < numSubscribersPerType; counter++) {
|
||||
MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber();
|
||||
myNumberEventSubscribers.add(mySubscriber);
|
||||
numberEventBus.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
|
||||
}
|
||||
for (int counter = 0; counter < numSubscribersPerType; counter++) {
|
||||
MyMixedEventSubscriber mySubscriber = new MyMixedEventSubscriber();
|
||||
myMixedEventSubscribers.add(mySubscriber);
|
||||
mixedEventBus.subscribe(MyMixedEvent.class, mySubscriber, START_READING_FROM_NOW);
|
||||
}
|
||||
for (int counter = 0; counter < numSubscribersPerType; counter++) {
|
||||
MyEventSubscriberEachInstanceInItsOwnGroup mySubscriber = new MyEventSubscriberEachInstanceInItsOwnGroup();
|
||||
mySubscribersEachInItsOwnGroup.add(mySubscriber);
|
||||
numberEventBus.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW);
|
||||
}
|
||||
|
||||
// create publisher and post events
|
||||
MyNumberEventPublisher myNumberEventPublisher = new MyNumberEventPublisher(numberEventBus);
|
||||
MyMixedEventPublisher myMixedEventPublisher = new MyMixedEventPublisher(mixedEventBus);
|
||||
|
||||
MyNumberEvent lastNumberEvent = null;
|
||||
for (int eventCounter = 0; eventCounter < numEventsToPublish; eventCounter++) {
|
||||
lastNumberEvent = new MyNumberEvent(random.nextInt(), random.nextInt());
|
||||
myNumberEventPublisher.post(MyNumberEvent.class, lastNumberEvent);
|
||||
}
|
||||
MyMixedEvent lastMixedEvent = null;
|
||||
for (int eventCounter = 0; eventCounter < numEventsToPublish; eventCounter++) {
|
||||
lastMixedEvent = new MyMixedEvent("" + random.nextInt(), random.nextInt());
|
||||
myMixedEventPublisher.post(MyMixedEvent.class, lastMixedEvent);
|
||||
}
|
||||
|
||||
// assert
|
||||
List<MyNumberEvent> lastNumberReceivedEvents = new ArrayList<>();
|
||||
int counterOfRetrievedNumberEvents = 0;
|
||||
for (MyNumberEventSubscriber mySubscriber : myNumberEventSubscribers) {
|
||||
lastNumberReceivedEvents.add(mySubscriber.lastReceivedEvent);
|
||||
counterOfRetrievedNumberEvents += mySubscriber.counterOfRetrievedEvents;
|
||||
}
|
||||
assertThat(lastNumberReceivedEvents.contains(lastNumberEvent)).isTrue();
|
||||
assertThat(counterOfRetrievedNumberEvents).isEqualTo(numEventsToPublish);
|
||||
|
||||
List<MyMixedEvent> lastMixedReceivedEvents = new ArrayList<>();
|
||||
int counterOfRetrievedMixedEvents = 0;
|
||||
for (MyMixedEventSubscriber mySubscriber : myMixedEventSubscribers) {
|
||||
lastMixedReceivedEvents.add(mySubscriber.lastReceivedEvent);
|
||||
counterOfRetrievedMixedEvents += mySubscriber.counterOfRetrievedEvents;
|
||||
}
|
||||
assertThat(lastMixedReceivedEvents.contains(lastMixedEvent)).isTrue();
|
||||
assertThat(counterOfRetrievedMixedEvents).isEqualTo(numEventsToPublish);
|
||||
|
||||
// assert
|
||||
for (MyEventSubscriberEachInstanceInItsOwnGroup mySubscriber : mySubscribersEachInItsOwnGroup) {
|
||||
assertThat(mySubscriber.lastReceivedEvent).isEqualTo(lastNumberEvent);
|
||||
assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish);
|
||||
}
|
||||
|
||||
// LocalDateTime end = LocalDateTime.now();
|
||||
// System.out.println("test duration was " + (Duration.between(start,end).toMillis()) + " ms");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,398 @@
|
|||
package de.accso.flexinale.common.api.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import de.accso.flexinale.common.api.event.EventTestdata.MyMixedEvent;
|
||||
import de.accso.flexinale.common.api.event.EventTestdata.MyNumberEvent;
|
||||
import de.accso.flexinale.common.api.event.EventTestdata.MyTextEvent;
|
||||
import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException;
|
||||
|
||||
import static de.accso.flexinale.common.shared_kernel.Identifiable.Id.uuidString;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public final class PublisherAndSubscriberTestdata {
|
||||
public static class MyNumberEventSubscriber implements EventSubscriber<MyNumberEvent> {
|
||||
MyNumberEvent lastReceivedEvent = null;
|
||||
int counterOfRetrievedEvents = 0;
|
||||
|
||||
final boolean checkForAscendingNumbersInEvents;
|
||||
|
||||
MyNumberEventSubscriber() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
MyNumberEventSubscriber(boolean checkForAscendingNumbersInEvents) {
|
||||
this.checkForAscendingNumbersInEvents = checkForAscendingNumbersInEvents;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyNumberEventSubscriber.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(final MyNumberEvent event) {
|
||||
if (checkForAscendingNumbersInEvents && lastReceivedEvent != null) {
|
||||
if ( this.lastReceivedEvent.value1 >= event.value1
|
||||
|| this.lastReceivedEvent.value2 >= event.value2) {
|
||||
throw new FlexinaleIllegalStateException(
|
||||
("no ascending value when receiving next MyNumberEvent " +
|
||||
"(last received event = %s, next received event = %s")
|
||||
.formatted(lastReceivedEvent, event));
|
||||
}
|
||||
}
|
||||
this.lastReceivedEvent = event;
|
||||
counterOfRetrievedEvents++;
|
||||
}
|
||||
}
|
||||
|
||||
public static class MyEventSubscriberEachInstanceInItsOwnGroup extends MyNumberEventSubscriber {
|
||||
// not static, as otherwise all instances of MyDifferentEventSubscriber would be in the same group,
|
||||
// but we want them in different groups
|
||||
private final String RANDOM_SUFFIX = uuidString();
|
||||
|
||||
MyEventSubscriberEachInstanceInItsOwnGroup() {
|
||||
this(false);
|
||||
}
|
||||
|
||||
MyEventSubscriberEachInstanceInItsOwnGroup(boolean checkForAscendingNumbersInEvents) {
|
||||
super(checkForAscendingNumbersInEvents);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
// don't use anything random here, as called in each subscribe() method (so group name would change)
|
||||
return getName() + RANDOM_SUFFIX;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final class MyTextEventSubscriber implements EventSubscriber<MyTextEvent> {
|
||||
MyTextEvent lastReceivedEvent = null;
|
||||
int counterOfRetrievedEvents = 0;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyTextEventSubscriber.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(final MyTextEvent event) {
|
||||
this.lastReceivedEvent = event;
|
||||
counterOfRetrievedEvents++;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final class MyNumberEventThrowingRuntimeExceptionSubscriber
|
||||
implements EventSubscriber.EventSubscriber2<MyTextEvent, MyNumberEvent> {
|
||||
MyNumberEvent lastReceivedEvent = null;
|
||||
int counterOfRetrievedEvents = 0;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyNumberEventThrowingRuntimeExceptionSubscriber.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(final MyTextEvent event) {
|
||||
// do nothing
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive2(final MyNumberEvent event) {
|
||||
this.lastReceivedEvent = event;
|
||||
counterOfRetrievedEvents++;
|
||||
throw new MyNumberEventRuntimeException("RuntimeException for event with Id=" + event.id());
|
||||
}
|
||||
}
|
||||
|
||||
public static class MyNumberEventRuntimeException extends RuntimeException {
|
||||
public MyNumberEventRuntimeException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final class MyMixedEventSubscriber implements EventSubscriber<MyMixedEvent> {
|
||||
MyMixedEvent lastReceivedEvent = null;
|
||||
int counterOfRetrievedEvents = 0;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyMixedEventSubscriber.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(final MyMixedEvent event) {
|
||||
this.lastReceivedEvent = event;
|
||||
counterOfRetrievedEvents++;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final class MyNumberAndMixedEventSubscriber implements
|
||||
EventSubscriber.EventSubscriber2<MyNumberEvent, MyMixedEvent> {
|
||||
MyNumberEvent lastReceivedNumberEvent = null;
|
||||
MyMixedEvent lastReceivedMixedEvent = null;
|
||||
int counterOfRetrievedEvents = 0;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyNumberAndMixedEventSubscriber.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(final MyNumberEvent event) {
|
||||
this.lastReceivedNumberEvent = event;
|
||||
counterOfRetrievedEvents++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive2(final MyMixedEvent event) {
|
||||
this.lastReceivedMixedEvent = event;
|
||||
counterOfRetrievedEvents++;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final class MyNumberAndTextAndMixedEventSubscriber implements
|
||||
EventSubscriber.EventSubscriber3<MyNumberEvent, MyTextEvent, MyMixedEvent> {
|
||||
MyNumberEvent lastReceivedNumberEvent = null;
|
||||
MyTextEvent lastReceivedTextEvent = null;
|
||||
MyMixedEvent lastReceivedMixedEvent = null;
|
||||
int counterOfRetrievedEvents = 0;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyNumberAndTextAndMixedEventSubscriber.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(final MyNumberEvent event) {
|
||||
this.lastReceivedNumberEvent = event;
|
||||
counterOfRetrievedEvents++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive2(final MyTextEvent event) {
|
||||
this.lastReceivedTextEvent = event;
|
||||
counterOfRetrievedEvents++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive3(final MyMixedEvent event) {
|
||||
this.lastReceivedMixedEvent = event;
|
||||
counterOfRetrievedEvents++;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final class GenericEventSubscriber implements EventSubscriber<Event> {
|
||||
Event lastReceivedEvent = null;
|
||||
int counterOfRetrievedEvents = 0;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return GenericEventSubscriber.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(final Event event) {
|
||||
this.lastReceivedEvent = event;
|
||||
counterOfRetrievedEvents++;
|
||||
}
|
||||
}
|
||||
|
||||
// does not work, see expected exception in test
|
||||
@SuppressWarnings("unused")
|
||||
public static final class GenericEventSubscriberWithExtraSubscription
|
||||
implements EventSubscriber.EventSubscriber2<Event, MyTextEvent> { // Event has to be the first generic type otherwise does not compile
|
||||
Event lastReceivedEvent = null;
|
||||
int counterOfRetrievedEvents = 0;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return GenericEventSubscriberWithExtraSubscription.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(final Event event) {
|
||||
this.lastReceivedEvent = event;
|
||||
counterOfRetrievedEvents++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive2(final MyTextEvent event) {
|
||||
this.lastReceivedEvent = event;
|
||||
counterOfRetrievedEvents++;
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
public static final class MyNumberEventPublisher implements EventPublisher<MyNumberEvent> {
|
||||
private final EventBus<MyNumberEvent> bus;
|
||||
|
||||
public MyNumberEventPublisher(EventBus<MyNumberEvent> bus) {
|
||||
this.bus = bus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyNumberEventPublisher.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void post(final Class<MyNumberEvent> eventType, final MyNumberEvent event) {
|
||||
bus.publish(MyNumberEvent.class, event);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MyTextEventPublisher implements EventPublisher<MyTextEvent> {
|
||||
private final EventBus<MyTextEvent> bus;
|
||||
|
||||
public MyTextEventPublisher(EventBus<MyTextEvent> bus) {
|
||||
this.bus = bus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyTextEventPublisher.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void post(final Class<MyTextEvent> eventType, final MyTextEvent event) {
|
||||
bus.publish(MyTextEvent.class, event);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MyMixedEventPublisher implements EventPublisher<MyMixedEvent> {
|
||||
private final EventBus<MyMixedEvent> bus;
|
||||
|
||||
MyMixedEventPublisher(EventBus<MyMixedEvent> bus) {
|
||||
this.bus = bus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyMixedEventPublisher.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void post(final Class<MyMixedEvent> eventType, final MyMixedEvent event) {
|
||||
bus.publish(MyMixedEvent.class, event);
|
||||
}
|
||||
}
|
||||
|
||||
public static final class MyNumberAndMixedEventPublisher implements
|
||||
EventPublisher.EventPublisher2<MyNumberEvent, MyMixedEvent> {
|
||||
private final EventBus<MyNumberEvent> numberBus;
|
||||
private final EventBus<MyMixedEvent> mixedBus;
|
||||
|
||||
public MyNumberAndMixedEventPublisher(final EventBus<MyNumberEvent> numberBus,
|
||||
final EventBus<MyMixedEvent> mixedBus) {
|
||||
this.numberBus = numberBus;
|
||||
this.mixedBus = mixedBus;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyNumberAndMixedEventPublisher.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void post(final Class<MyNumberEvent> eventType, final MyNumberEvent event) {
|
||||
numberBus.publish(MyNumberEvent.class, event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void post2(final Class<MyMixedEvent> eventType, final MyMixedEvent event) {
|
||||
mixedBus.publish(MyMixedEvent.class, event);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------------------------------------
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public static final class MyNumberEventSubscriberAndMyTextEventPublisher implements
|
||||
EventSubscriber<MyNumberEvent>,
|
||||
EventPublisher<MyTextEvent> {
|
||||
|
||||
private final EventBus<MyTextEvent> textBus;
|
||||
|
||||
public MyNumberEventSubscriberAndMyTextEventPublisher(final EventBus<MyTextEvent> textBus) {
|
||||
this.textBus = textBus;
|
||||
}
|
||||
|
||||
MyNumberEvent lastReceivedEvent = null;
|
||||
MyTextEvent lastSentEvent = null;
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return MyNumberEventSubscriberAndMyTextEventPublisher.class.getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getGroupName() {
|
||||
return getName();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void receive(final MyNumberEvent event) {
|
||||
this.lastReceivedEvent = event;
|
||||
|
||||
// create and post new Event using the received event as the predecessor, implicitely using propagating its correlationId
|
||||
this.lastSentEvent = new MyTextEvent(event, ""+event.value1, ""+ event.value2);
|
||||
post(MyTextEvent.class, lastSentEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void post(final Class<MyTextEvent> eventType, final MyTextEvent event) {
|
||||
textBus.publish(MyTextEvent.class, event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,293 @@
|
|||
package de.accso.flexinale.common.application.caching;
|
||||
|
||||
import de.accso.flexinale.common.application.caching.InMemoryCache;
|
||||
import de.accso.flexinale.common.shared_kernel.Identifiable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
class InMemoryCacheTest {
|
||||
|
||||
private record Data(String text, int number) { }
|
||||
|
||||
@Test
|
||||
void testEmptyCacheContainsNothing() {
|
||||
// arrange
|
||||
Random random = new Random();
|
||||
|
||||
// act
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
|
||||
// assert
|
||||
assertThat(sut.size()).isEqualTo(0);
|
||||
|
||||
Data data = new Data("" + random.nextInt(), random.nextInt());
|
||||
assertThat(sut.contains(data)).isFalse();
|
||||
|
||||
assertThat(sut.keys().isEmpty()).isTrue();
|
||||
assertThat(sut.values().isEmpty()).isTrue();
|
||||
assertThat(sut.entrySet().isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGet() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
Identifiable.Id id = Identifiable.Id.of();
|
||||
Data data = new Data("text", 42);
|
||||
sut.put(id, data);
|
||||
|
||||
// act
|
||||
Data retrievedData = sut.get(id);
|
||||
|
||||
// assert
|
||||
assertThat(retrievedData).isEqualTo(data);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetForNotPuttedId() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
Identifiable.Id id = Identifiable.Id.of();
|
||||
Data data = new Data("text", 42);
|
||||
sut.put(id, data);
|
||||
|
||||
// act
|
||||
Identifiable.Id newId = Identifiable.Id.of();
|
||||
Data retrievedData = sut.get(newId);
|
||||
|
||||
// assert
|
||||
assertThat(retrievedData).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPutOnce() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
Identifiable.Id id = Identifiable.Id.of();
|
||||
Data data = new Data("text", 42);
|
||||
|
||||
// act
|
||||
Data oldData = sut.put(id, data);
|
||||
|
||||
// assert
|
||||
assertThat(oldData).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testPutTwice() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
Identifiable.Id id = Identifiable.Id.of();
|
||||
Data data1 = new Data("text1", 42);
|
||||
Data data2 = new Data("text2", 21);
|
||||
|
||||
// act
|
||||
Data oldData0 = sut.put(id, data1);
|
||||
Data oldData1 = sut.put(id, data2);
|
||||
|
||||
// assert
|
||||
assertThat(oldData0).isNull();
|
||||
assertThat(oldData1).isEqualTo(data1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveExistingData() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
Identifiable.Id id = Identifiable.Id.of();
|
||||
Data data = new Data("text", 42);
|
||||
|
||||
// act
|
||||
Data oldData0 = sut.put(id, data);
|
||||
Data removedData = sut.remove(id);
|
||||
Data notFoundData = sut.get(id);
|
||||
|
||||
// assert
|
||||
assertThat(oldData0).isNull();
|
||||
assertThat(removedData).isEqualTo(data);
|
||||
assertThat(notFoundData).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveNotExistingDataDoesNothing() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
Identifiable.Id id = Identifiable.Id.of();
|
||||
|
||||
// act
|
||||
Data removedData = sut.remove(id);
|
||||
Data notFoundData = sut.get(id);
|
||||
|
||||
// assert
|
||||
assertThat(removedData).isNull();
|
||||
assertThat(notFoundData).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testKeys() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
Identifiable.Id id1 = Identifiable.Id.of();
|
||||
Data data1 = new Data("text", 42);
|
||||
Identifiable.Id id2 = Identifiable.Id.of();
|
||||
Data data2 = new Data("abc", 21);
|
||||
|
||||
sut.put(id1, data1);
|
||||
sut.put(id2, data2);
|
||||
|
||||
// act
|
||||
Set<Identifiable.Id> retrievedKeys = sut.keys();
|
||||
|
||||
// assert
|
||||
assertThat(retrievedKeys).isEqualTo(Set.of(id1, id2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testValues() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
Identifiable.Id id1 = Identifiable.Id.of();
|
||||
Data data1 = new Data("text", 42);
|
||||
Identifiable.Id id2 = Identifiable.Id.of();
|
||||
Data data2 = new Data("abc", 21);
|
||||
|
||||
sut.put(id1, data1);
|
||||
sut.put(id2, data2);
|
||||
|
||||
// act
|
||||
Set<Data> retrievedKeys = sut.values();
|
||||
|
||||
// assert
|
||||
assertThat(retrievedKeys).isEqualTo(Set.of(data1, data2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEntrySet() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
Identifiable.Id id1 = Identifiable.Id.of();
|
||||
Data data1 = new Data("text", 42);
|
||||
Identifiable.Id id2 = Identifiable.Id.of();
|
||||
Data data2 = new Data("abc", 21);
|
||||
|
||||
sut.put(id1, data1);
|
||||
sut.put(id2, data2);
|
||||
|
||||
// act
|
||||
Set<Map.Entry<Identifiable.Id, Data>> retrievedEntries = sut.entrySet();
|
||||
|
||||
// assert
|
||||
assertThat(retrievedEntries.size()).isEqualTo(2);
|
||||
Set<Identifiable.Id> retrievedKeys = retrievedEntries.stream()
|
||||
.map(Map.Entry::getKey).collect(Collectors.toUnmodifiableSet());
|
||||
Set<Data> retrievedValues = retrievedEntries.stream()
|
||||
.map(Map.Entry::getValue).collect(Collectors.toSet());
|
||||
|
||||
assertThat(retrievedKeys).isEqualTo(Set.of(id1, id2));
|
||||
assertThat(retrievedValues).isEqualTo(Set.of(data1, data2));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSize() {
|
||||
// arrange
|
||||
Random random = new Random();
|
||||
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
|
||||
for (int counter=0; counter<100; counter++) {
|
||||
Identifiable.Id id = Identifiable.Id.of();
|
||||
Data data = new Data("" + random.nextInt(), random.nextInt());
|
||||
sut.put(id, data);
|
||||
}
|
||||
|
||||
// act
|
||||
int expectedSize = sut.size();
|
||||
|
||||
// assert
|
||||
assertThat(expectedSize).isEqualTo(100);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSizeDoesNotChangeAfterPutWithSameId() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
|
||||
Identifiable.Id id = Identifiable.Id.of();
|
||||
Data data = new Data("text", 42);
|
||||
sut.put(id, data);
|
||||
int size = sut.size();
|
||||
assertThat(size).isEqualTo(1);
|
||||
|
||||
// act
|
||||
sut.put(id, data);
|
||||
int expectedSize = sut.size();
|
||||
|
||||
// assert
|
||||
assertThat(expectedSize).isEqualTo(size);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsEmptyInitially() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
|
||||
// act
|
||||
boolean expectedEmpty = sut.isEmpty();
|
||||
|
||||
// assert
|
||||
assertThat(expectedEmpty).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testIsNotEmptyAfterPuttingData() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
Identifiable.Id id = Identifiable.Id.of();
|
||||
Data data = new Data("text", 42);
|
||||
sut.put(id, data);
|
||||
|
||||
// act
|
||||
boolean expectedEmpty = sut.isEmpty();
|
||||
|
||||
// assert
|
||||
assertThat(expectedEmpty).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContains() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
Identifiable.Id id = Identifiable.Id.of();
|
||||
Data data = new Data("text", 42);
|
||||
sut.put(id, data);
|
||||
|
||||
// act
|
||||
boolean expectedContains = sut.contains(data);
|
||||
|
||||
// assert
|
||||
assertThat(expectedContains).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testContainsForNotPuttedId() {
|
||||
// arrange
|
||||
InMemoryCache<Identifiable.Id, Data> sut = new InMemoryCache<>();
|
||||
Identifiable.Id id = Identifiable.Id.of();
|
||||
Data data = new Data("text", 42);
|
||||
sut.put(id, data);
|
||||
|
||||
// act
|
||||
Data newData = new Data("def", 21);
|
||||
boolean expectedContains = sut.contains(newData);
|
||||
|
||||
// assert
|
||||
assertThat(expectedContains).isFalse();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,167 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import de.accso.flexinale.common.api.event.AbstractEvent;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import de.accso.flexinale.common.api.event.EventContext;
|
||||
import de.accso.flexinale.common.shared_kernel.Identifiable;
|
||||
import de.accso.flexinale.common.shared_kernel.Versionable;
|
||||
import org.apache.commons.lang3.builder.EqualsBuilder;
|
||||
import org.apache.commons.lang3.builder.HashCodeBuilder;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class EventSerializationHelperTest {
|
||||
|
||||
@Test
|
||||
void testSerializeEventClass2JsonString() throws JsonProcessingException {
|
||||
// arrange
|
||||
MyNumberEventImplementedAsSubclassOfAbstractEvent myNumberEvent =
|
||||
new MyNumberEventImplementedAsSubclassOfAbstractEvent(21, 42);
|
||||
|
||||
// act
|
||||
String jsonString = EventSerializationHelper.serializeEvent2JsonString(myNumberEvent);
|
||||
|
||||
// assert
|
||||
String expectedJsonString =
|
||||
("{\"id\":{\"id\":\"%s\"}," +
|
||||
"\"correlationId\":{\"id\":\"%s\"}," +
|
||||
"\"timestamp\":\"%s\"," +
|
||||
"\"eventContext\":{\"correlationId\":{\"id\":\"%s\"}," +
|
||||
"\"eventHistory\":[{\"id\":{\"id\":\"%s\"}," +
|
||||
"\"eventType\":\"%s\"}]}," +
|
||||
"\"value1\":21,\"value2\":42,\"version\":{\"version\":0}}")
|
||||
.formatted(myNumberEvent.id().id(),
|
||||
myNumberEvent.correlationId().id(),
|
||||
timestamp2String(myNumberEvent.timestamp()),
|
||||
myNumberEvent.correlationId().id(),
|
||||
myNumberEvent.id().id(),
|
||||
MyNumberEventImplementedAsSubclassOfAbstractEvent.class.getName());
|
||||
|
||||
assertThat(jsonString).isEqualTo(expectedJsonString);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSerializeEventRecord2JsonString() throws JsonProcessingException {
|
||||
// arrange
|
||||
MyNumberEventImplementedAsRecord myNumberEvent =
|
||||
new MyNumberEventImplementedAsRecord(
|
||||
Identifiable.Id.of(), Versionable.initialVersion(), LocalDateTime.now(), Identifiable.Id.of(), null,
|
||||
21, 42);
|
||||
|
||||
// act
|
||||
String jsonString = EventSerializationHelper.serializeEvent2JsonString(myNumberEvent);
|
||||
|
||||
// assert
|
||||
String expectedJsonString = ("{\"id\":{\"id\":\"%s\"}," +
|
||||
"\"version\":{\"version\":0}," +
|
||||
"\"timestamp\":\"%s\"," +
|
||||
"\"correlationId\":{\"id\":\"%s\"}," +
|
||||
"\"eventContext\":null,\"value1\":21,\"value2\":42}")
|
||||
.formatted(myNumberEvent.id().id(),
|
||||
timestamp2String(myNumberEvent.timestamp()),
|
||||
myNumberEvent.correlationId().id());
|
||||
|
||||
assertThat(jsonString).isEqualTo(expectedJsonString);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeJsonString2EventClass() throws JsonProcessingException {
|
||||
// arrange
|
||||
MyNumberEventImplementedAsSubclassOfAbstractEvent myNumberEvent =
|
||||
new MyNumberEventImplementedAsSubclassOfAbstractEvent(21, 42);
|
||||
String jsonString = EventSerializationHelper.serializeEvent2JsonString(myNumberEvent);
|
||||
|
||||
// act
|
||||
MyNumberEventImplementedAsSubclassOfAbstractEvent deserializedEvent =
|
||||
EventSerializationHelper.deserializeJsonString2Event(
|
||||
jsonString, MyNumberEventImplementedAsSubclassOfAbstractEvent.class);
|
||||
|
||||
// assert
|
||||
assertThat(deserializedEvent).isEqualTo(myNumberEvent);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testDeserializeJsonString2EventRecord() throws JsonProcessingException {
|
||||
// arrange
|
||||
MyNumberEventImplementedAsRecord myNumberEvent =
|
||||
new MyNumberEventImplementedAsRecord(
|
||||
Identifiable.Id.of(), Versionable.initialVersion(), LocalDateTime.now(), Identifiable.Id.of(), null,
|
||||
21, 42);
|
||||
String jsonString = EventSerializationHelper.serializeEvent2JsonString(myNumberEvent);
|
||||
|
||||
// act
|
||||
MyNumberEventImplementedAsRecord deserializedEvent =
|
||||
EventSerializationHelper.deserializeJsonString2Event(
|
||||
jsonString, MyNumberEventImplementedAsRecord.class);
|
||||
|
||||
// assert
|
||||
assertThat(deserializedEvent).isEqualTo(myNumberEvent);
|
||||
}
|
||||
|
||||
private static String timestamp2String(final LocalDateTime timestamp) {
|
||||
return timestamp.format(DateTimeFormatter.ISO_DATE_TIME);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
final class MyNumberEventImplementedAsSubclassOfAbstractEvent extends AbstractEvent {
|
||||
public Integer value1;
|
||||
public Integer value2;
|
||||
|
||||
private final Version version = Versionable.initialVersion();
|
||||
|
||||
// needed for Jackson de/serialization
|
||||
private MyNumberEventImplementedAsSubclassOfAbstractEvent() {}
|
||||
|
||||
public MyNumberEventImplementedAsSubclassOfAbstractEvent(final Integer value1, final Integer value2) {
|
||||
this.value1 = value1;
|
||||
this.value2 = value2;
|
||||
}
|
||||
|
||||
public MyNumberEventImplementedAsSubclassOfAbstractEvent(final Event predecessorEvent, final Integer value1, final Integer value2) {
|
||||
super(predecessorEvent);
|
||||
this.value1 = value1;
|
||||
this.value2 = value2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(final Object o) {
|
||||
if (this == o) {return true;}
|
||||
|
||||
if (o == null || getClass() != o.getClass()) {return false;}
|
||||
|
||||
final MyNumberEventImplementedAsSubclassOfAbstractEvent that = (MyNumberEventImplementedAsSubclassOfAbstractEvent) o;
|
||||
|
||||
return new EqualsBuilder()
|
||||
.appendSuper(super.equals(o))
|
||||
.append(this.version(), that.version())
|
||||
.append(value1, that.value1)
|
||||
.append(value2, that.value2)
|
||||
.isEquals();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return new HashCodeBuilder(17, 37)
|
||||
.appendSuper(super.hashCode())
|
||||
.append(this.version())
|
||||
.append(value1).append(value2)
|
||||
.toHashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Version version() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("ALL")
|
||||
record MyNumberEventImplementedAsRecord(
|
||||
Id id, Version version, LocalDateTime timestamp, Id correlationId, EventContext eventContext,
|
||||
Integer value1, Integer value2) implements Event { }
|
||||
|
|
@ -0,0 +1,105 @@
|
|||
package de.accso.flexinale.common.infrastructure.eventbus;
|
||||
|
||||
import de.accso.flexinale.common.api.eventbus.EventContextHolder;
|
||||
import de.accso.flexinale.common.api.event.EventContext;
|
||||
import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException;
|
||||
import de.accso.flexinale.common.shared_kernel.Identifiable;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
class InMemorySyncEventContextHolderTest {
|
||||
|
||||
@Test
|
||||
void testInitialHolderDoesNotContainAnything() {
|
||||
// arrange
|
||||
EventContextHolder sut = new InMemorySyncEventContextHolder();
|
||||
|
||||
// act
|
||||
EventContext eventContext = sut.get();
|
||||
|
||||
// assert
|
||||
assertThat(eventContext).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAndGetOnce() {
|
||||
// arrange
|
||||
EventContextHolder sut = new InMemorySyncEventContextHolder();
|
||||
EventContext newContext = new EventContext(Identifiable.Id.of("123"), Collections.emptyList());
|
||||
|
||||
// act
|
||||
sut.set(newContext);
|
||||
EventContext eventContext = sut.get();
|
||||
|
||||
// assert
|
||||
assertThat(eventContext).isEqualTo(newContext);
|
||||
|
||||
// act
|
||||
sut.remove();
|
||||
eventContext = sut.get();
|
||||
|
||||
// assert
|
||||
assertThat(eventContext).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testSetAndGetTwice() {
|
||||
// arrange
|
||||
EventContextHolder sut = new InMemorySyncEventContextHolder();
|
||||
EventContext newContext = new EventContext(Identifiable.Id.of("123"), Collections.emptyList());
|
||||
|
||||
// act set twice
|
||||
sut.set(newContext);
|
||||
sut.set(newContext);
|
||||
|
||||
// assert
|
||||
assertThat(sut.get()).isEqualTo(newContext);
|
||||
|
||||
// act remove first time
|
||||
sut.remove();
|
||||
|
||||
// assert
|
||||
assertThat(sut.get()).isEqualTo(newContext);
|
||||
|
||||
// act second first time
|
||||
sut.remove();
|
||||
|
||||
// assert
|
||||
assertThat(sut.get()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRemoveOnceToOftenIsAnError() {
|
||||
// arrange
|
||||
int maxSets = 10;
|
||||
|
||||
EventContextHolder sut = new InMemorySyncEventContextHolder();
|
||||
EventContext newContext = new EventContext(Identifiable.Id.of("123"), Collections.emptyList());
|
||||
|
||||
// act
|
||||
for (int i=0; i<maxSets; i++) {
|
||||
sut.set(newContext);
|
||||
}
|
||||
|
||||
// assert
|
||||
assertThat(sut.get()).isEqualTo(newContext);
|
||||
|
||||
// act
|
||||
for (int i=0; i<maxSets; i++) {
|
||||
sut.remove();
|
||||
}
|
||||
|
||||
// assert
|
||||
assertThat(sut.get()).isNull();
|
||||
|
||||
// assert error second time
|
||||
assertThatThrownBy(() -> {
|
||||
sut.remove();
|
||||
}).isInstanceOf(DeveloperMistakeException.class)
|
||||
.hasMessageContaining("remove on InMemorySyncEventContextHolder once too much, should not happen");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,118 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ClazzHelperTest {
|
||||
|
||||
interface MyInterface1 { }
|
||||
interface MyInterface2 { }
|
||||
|
||||
static class MyClazzImplementingTheInterface1 implements MyInterface1 { }
|
||||
static class MyClazzImplementingTheInterface2 implements MyInterface2 { }
|
||||
static class MyClazzImplementingTheInterfaces implements MyInterface1, MyInterface2 { }
|
||||
static class MyClazzNotImplementingAnyInterface { }
|
||||
|
||||
record MyRecordImplementingTheInterface1() implements MyInterface1 { }
|
||||
record MyRecordImplementingTheInterface2() implements MyInterface2 { }
|
||||
record MyRecordImplementingTheInterfaces() implements MyInterface1, MyInterface2 { }
|
||||
record MyRecordNotImplementingAnyInterface() { }
|
||||
|
||||
@Test
|
||||
void testClazzImplementsInterface() {
|
||||
boolean result;
|
||||
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyClazzImplementingTheInterface1.class, MyInterface1.class);
|
||||
// assert
|
||||
assertThat(result).isTrue();
|
||||
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyClazzImplementingTheInterface2.class, MyInterface2.class);
|
||||
// assert
|
||||
assertThat(result).isTrue();
|
||||
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyClazzImplementingTheInterfaces.class, MyInterface1.class);
|
||||
// assert
|
||||
assertThat(result).isTrue();
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyClazzImplementingTheInterfaces.class, MyInterface2.class);
|
||||
// assert
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testClazzNotImplementsInterface() {
|
||||
boolean result;
|
||||
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyClazzImplementingTheInterface1.class, MyInterface2.class);
|
||||
// assert
|
||||
assertThat(result).isFalse();
|
||||
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyClazzImplementingTheInterface2.class, MyInterface1.class);
|
||||
// assert
|
||||
assertThat(result).isFalse();
|
||||
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyClazzNotImplementingAnyInterface.class, MyInterface1.class);
|
||||
// assert
|
||||
assertThat(result).isFalse();
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyClazzNotImplementingAnyInterface.class, MyInterface2.class);
|
||||
// assert
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testRecordImplementsInterface() {
|
||||
boolean result;
|
||||
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyRecordImplementingTheInterface1.class, MyInterface1.class);
|
||||
// assert
|
||||
assertThat(result).isTrue();
|
||||
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyRecordImplementingTheInterface2.class, MyInterface2.class);
|
||||
// assert
|
||||
assertThat(result).isTrue();
|
||||
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyRecordImplementingTheInterfaces.class, MyInterface1.class);
|
||||
// assert
|
||||
assertThat(result).isTrue();
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyRecordImplementingTheInterfaces.class, MyInterface2.class);
|
||||
// assert
|
||||
assertThat(result).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRecordNotImplementsInterface() {
|
||||
boolean result;
|
||||
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyRecordImplementingTheInterface1.class, MyInterface2.class);
|
||||
// assert
|
||||
assertThat(result).isFalse();
|
||||
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyRecordImplementingTheInterface2.class, MyInterface1.class);
|
||||
// assert
|
||||
assertThat(result).isFalse();
|
||||
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyRecordNotImplementingAnyInterface.class, MyInterface1.class);
|
||||
// assert
|
||||
assertThat(result).isFalse();
|
||||
// arrange, act
|
||||
result = ClazzHelper.clazzImplementsInterface(MyRecordNotImplementingAnyInterface.class, MyInterface2.class);
|
||||
// assert
|
||||
assertThat(result).isFalse();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,142 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import de.accso.flexinale.common.api.event.AbstractEvent;
|
||||
import de.accso.flexinale.common.api.event.Event;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
public class EventClassHelperTest {
|
||||
public static final Versionable.Version VERSION = Versionable.Version.of(42);
|
||||
|
||||
private static Stream<Arguments> allEventClassesToTest() {
|
||||
return Stream.of(
|
||||
Arguments.of(MyEvent.class)
|
||||
, Arguments.of(MyEventWithHierarchyClass.class)
|
||||
, Arguments.of(MyEventExtendsAbstractClassButDoesNotImplementInterface.class)
|
||||
, Arguments.of(MyEventWithHierarchySuperClass.class)
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("allEventClassesToTest")
|
||||
void testGetClassForEventClass(Class<?> eventClass) {
|
||||
// arrange
|
||||
String clazzName = eventClass.getCanonicalName();
|
||||
|
||||
// act
|
||||
Class<?> classForClazzName = EventClassHelper.getClassFor(clazzName);
|
||||
|
||||
// assert
|
||||
assertThat(classForClazzName).isEqualTo(eventClass);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetClassForNonEventClass() {
|
||||
// arrange
|
||||
String clazzName = String.class.getCanonicalName();
|
||||
|
||||
// act
|
||||
Class<?> classForClazzName = EventClassHelper.getClassFor(clazzName);
|
||||
|
||||
// assert
|
||||
assertThat(classForClazzName).isEqualTo(String.class);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("allEventClassesToTest")
|
||||
void testGetClassNameForEventClass(Class<?> eventClass) {
|
||||
// arrange
|
||||
String clazzName = eventClass.getCanonicalName();
|
||||
|
||||
// act
|
||||
String eventClazzName = EventClassHelper.getEventClazzName(clazzName);
|
||||
|
||||
// assert
|
||||
assertThat(eventClazzName).isEqualTo(clazzName);
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testGetClassNameForNonEventClassThrowsException() {
|
||||
// arrange
|
||||
String clazzName = Long.class.getCanonicalName();
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
// act
|
||||
EventClassHelper.getEventClazzName(clazzName);
|
||||
|
||||
// assert
|
||||
}).isInstanceOf(FlexinaleIllegalArgumentException.class)
|
||||
.hasMessageContaining("wrong clazz");
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("allEventClassesToTest")
|
||||
void testGetVersionForEventClass(Class<?> eventClass) {
|
||||
// arrange
|
||||
String clazzName = eventClass.getCanonicalName();
|
||||
|
||||
// act
|
||||
Versionable.Version eventClazzVersion = EventClassHelper.getEventClazzVersion(clazzName);
|
||||
|
||||
// assert
|
||||
assertThat(eventClazzVersion).isEqualTo(VERSION);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetVersionForNonEventClassThrowsException() {
|
||||
// arrange
|
||||
String clazzName = Integer.class.getCanonicalName();
|
||||
|
||||
assertThatThrownBy(() -> {
|
||||
// act
|
||||
EventClassHelper.getEventClazzVersion(clazzName);
|
||||
|
||||
// assert
|
||||
}).isInstanceOf(FlexinaleIllegalArgumentException.class)
|
||||
.hasMessageContaining("need to use Event class");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class MyEvent extends AbstractEvent implements Event {
|
||||
static final Version version = EventClassHelperTest.VERSION;
|
||||
|
||||
@Override
|
||||
public Version version() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
class MyEventExtendsAbstractClassButDoesNotImplementInterface extends AbstractEvent {
|
||||
static final Version version = EventClassHelperTest.VERSION;
|
||||
|
||||
@Override
|
||||
public Version version() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
||||
class MyEventWithHierarchySuperClass extends AbstractEvent implements Event {
|
||||
static final Version version = EventClassHelperTest.VERSION;
|
||||
|
||||
@Override
|
||||
public Version version() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
class MyEventWithHierarchyClass extends MyEventWithHierarchySuperClass {
|
||||
static final Version version = EventClassHelperTest.VERSION;
|
||||
|
||||
@Override
|
||||
public Version version() {
|
||||
return version;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
class MapWithCounterTest {
|
||||
|
||||
@Test
|
||||
public void testInitialMap() {
|
||||
// arrange
|
||||
MapWithCounter<String> sut = new MapWithCounter<>();
|
||||
|
||||
// act
|
||||
Long key = sut.get("nothere");
|
||||
|
||||
// assert
|
||||
assertThat(key).isEqualTo(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetAndGet() {
|
||||
// arrange
|
||||
MapWithCounter<String> sut = new MapWithCounter<>();
|
||||
Long fortyTwo = 42L;
|
||||
|
||||
// act
|
||||
sut.set("key", fortyTwo);
|
||||
|
||||
// assert
|
||||
assertThat(sut.get("key")).isEqualTo(fortyTwo);
|
||||
assertThat(sut.get("notthere")).isEqualTo(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncrementInitial() {
|
||||
// arrange
|
||||
MapWithCounter<String> sut = new MapWithCounter<>();
|
||||
|
||||
// act
|
||||
long value = sut.increment("key");
|
||||
|
||||
// assert
|
||||
assertThat(value).isEqualTo(1);
|
||||
assertThat(sut.get("key")).isEqualTo(1);
|
||||
|
||||
// act
|
||||
value = sut.increment("key");
|
||||
|
||||
// assert
|
||||
assertThat(value).isEqualTo(1+1);
|
||||
assertThat(sut.get("key")).isEqualTo(1+1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncrementAfterSet() {
|
||||
// arrange
|
||||
MapWithCounter<String> sut = new MapWithCounter<>();
|
||||
Long fortyTwo = 42L;
|
||||
sut.set("key", fortyTwo);
|
||||
|
||||
// act
|
||||
long value = sut.increment("key");
|
||||
|
||||
// assert
|
||||
assertThat(value).isEqualTo(fortyTwo+1);
|
||||
assertThat(sut.get("key")).isEqualTo(fortyTwo+1);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
class RawWrapperTest {
|
||||
|
||||
private record StringWrapper(String raw) implements RawWrapper<String> {}
|
||||
private record IntegerWrapper(Integer raw) implements RawWrapper<Integer> {}
|
||||
|
||||
@Test
|
||||
public void testStringWrapperWithString() {
|
||||
// arrange
|
||||
StringWrapper sut = new StringWrapper("test");
|
||||
|
||||
// act
|
||||
String rawOrNull = RawWrapper.getRawOrNull(sut);
|
||||
|
||||
// assert
|
||||
assertThat(rawOrNull.getClass()).isEqualTo(String.class);
|
||||
assertThat(rawOrNull).isEqualTo("test");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStringWrapperWithNull() {
|
||||
// arrange
|
||||
StringWrapper sut = new StringWrapper(null);
|
||||
|
||||
// act
|
||||
String rawOrNull = RawWrapper.getRawOrNull(sut);
|
||||
|
||||
// assert
|
||||
assertThat(rawOrNull).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntegerWrapperWithInteger() {
|
||||
// arrange
|
||||
IntegerWrapper sut = new IntegerWrapper(1);
|
||||
|
||||
// act
|
||||
Integer rawOrNull = RawWrapper.getRawOrNull(sut);
|
||||
|
||||
// assert
|
||||
assertThat(rawOrNull.getClass()).isEqualTo(Integer.class);
|
||||
assertThat(rawOrNull).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntegerWrapperWithNull() {
|
||||
// arrange
|
||||
IntegerWrapper sut = new IntegerWrapper(null);
|
||||
|
||||
// act
|
||||
Integer rawOrNull = RawWrapper.getRawOrNull(sut);
|
||||
|
||||
// assert
|
||||
assertThat(rawOrNull).isNull();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
class SelectionMultiMapTest {
|
||||
|
||||
private record VALUE(String s) {}
|
||||
|
||||
private static Stream<Arguments> allAlgorithms() {
|
||||
return Stream.of(
|
||||
Arguments.of(Selection.SelectionAlgorithm.RANDOM),
|
||||
Arguments.of(Selection.SelectionAlgorithm.CONSTANT_FIRST),
|
||||
Arguments.of(Selection.SelectionAlgorithm.CONSTANT_LAST),
|
||||
Arguments.of(Selection.SelectionAlgorithm.ROUND_ROBIN),
|
||||
Arguments.of(Selection.SelectionAlgorithm.ALL)
|
||||
);
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("allAlgorithms")
|
||||
public void testInitialMap(Selection.SelectionAlgorithm algorithm) {
|
||||
// arrange
|
||||
SelectionMultiMap<String, VALUE> sut = new SelectionMultiMap<>(algorithm);
|
||||
|
||||
// act
|
||||
Set<?> result = sut.get("notthere");
|
||||
|
||||
// assert
|
||||
assertThat(result).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("allAlgorithms")
|
||||
public void testSetAndGet(Selection.SelectionAlgorithm algorithm) {
|
||||
// arrange
|
||||
SelectionMultiMap<String, VALUE> sut = new SelectionMultiMap<>(algorithm);
|
||||
VALUE fortyTwo = new VALUE("42");
|
||||
|
||||
// act
|
||||
sut.put("key", fortyTwo);
|
||||
|
||||
// assert
|
||||
assertThat(sut.get("key").size()).isEqualTo(1);
|
||||
assertThat(sut.get("key").contains(fortyTwo)).isTrue();
|
||||
assertThat(sut.get("notthere")).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("allAlgorithms")
|
||||
public void testSetDuplicatesAndGet(Selection.SelectionAlgorithm algorithm) {
|
||||
// arrange
|
||||
SelectionMultiMap<String, VALUE> sut = new SelectionMultiMap<>(algorithm);
|
||||
VALUE fortyTwo = new VALUE("42");
|
||||
|
||||
// act
|
||||
sut.put("key", fortyTwo);
|
||||
sut.put("key", fortyTwo);
|
||||
|
||||
// assert
|
||||
assertThat(sut.get("key").size()).isEqualTo(1);
|
||||
assertThat(sut.get("key").contains(fortyTwo)).isTrue();
|
||||
assertThat(sut.get("notthere")).isNull();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("allAlgorithms")
|
||||
public void testSetAndGetMultipleValues(Selection.SelectionAlgorithm algorithm) {
|
||||
// arrange
|
||||
SelectionMultiMap<String, VALUE> sut = new SelectionMultiMap<>(algorithm);
|
||||
VALUE fortyTwo = new VALUE("42");
|
||||
VALUE fortyThree = new VALUE("43");
|
||||
VALUE fortyFour = new VALUE("44");
|
||||
|
||||
// act
|
||||
sut.put("key", fortyTwo);
|
||||
sut.put("key", fortyThree);
|
||||
sut.put("key", fortyFour);
|
||||
|
||||
// assert
|
||||
assertThat(sut.get("key").size()).isEqualTo(3);
|
||||
assertThat(sut.get("key").contains(fortyTwo)).isTrue();
|
||||
assertThat(sut.get("key").contains(fortyThree)).isTrue();
|
||||
assertThat(sut.get("key").contains(fortyFour)).isTrue();
|
||||
assertThat(sut.get("notthere")).isNull();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,568 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
class SelectionTest {
|
||||
|
||||
private record OPTION(Integer i) {}
|
||||
|
||||
@Test
|
||||
void testRandomSelectorOneOption() {
|
||||
// arrange
|
||||
Selection.RandomSelector<OPTION> sut = new Selection.RandomSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options).isEqualTo(selected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRandomSelectorAddOption() {
|
||||
// arrange
|
||||
Selection.RandomSelector<OPTION> sut = new Selection.RandomSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options).isEqualTo(selected);
|
||||
|
||||
// act
|
||||
sut.add(new OPTION(2));
|
||||
|
||||
// assert
|
||||
selected = sut.next();
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRandomSelectorRemoveOnlyOption() {
|
||||
// arrange
|
||||
Selection.RandomSelector<OPTION> sut = new Selection.RandomSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options).isEqualTo(selected);
|
||||
|
||||
// act
|
||||
sut.remove(new OPTION(1));
|
||||
|
||||
Set<OPTION> selectedAgain = sut.next();
|
||||
assertThat(selectedAgain.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRandomSelectorRemoveOption() {
|
||||
// arrange
|
||||
Selection.RandomSelector<OPTION> sut = new Selection.RandomSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1), new OPTION(2));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
sut.remove(new OPTION(1));
|
||||
|
||||
// assert
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected.contains(new OPTION(2))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRandomSelectorMultipleOptions() {
|
||||
// arrange
|
||||
Selection.RandomSelector<OPTION> sut = new Selection.RandomSelector<>();
|
||||
int numOptions = 100;
|
||||
HashSet<OPTION> options = new HashSet<>();
|
||||
for (int counter=0; counter<numOptions; counter++) {
|
||||
options.add(new OPTION(counter));
|
||||
}
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options.containsAll(selected)).isTrue();
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testConstantFirstSelectorOneOption() {
|
||||
// arrange
|
||||
Selection.ConstantFirstSelector<OPTION> sut = new Selection.ConstantFirstSelector<>();
|
||||
HashSet<OPTION> options = new HashSet<>() {{ add(new OPTION(0)); }};
|
||||
sut.addAll(options);
|
||||
|
||||
// act, first choice
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected).isEqualTo(options);
|
||||
|
||||
// act, second choice does not change
|
||||
selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(options);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstantFirstSelectorAddOption() {
|
||||
// arrange
|
||||
Selection.ConstantFirstSelector<OPTION> sut = new Selection.ConstantFirstSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options).isEqualTo(selected);
|
||||
|
||||
// act
|
||||
sut.add(new OPTION(2));
|
||||
|
||||
// assert
|
||||
selected = sut.next();
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options).isEqualTo(selected);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstantFirstSelectorRemoveOnlyOption() {
|
||||
// arrange
|
||||
Selection.ConstantFirstSelector<OPTION> sut = new Selection.ConstantFirstSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options).isEqualTo(selected);
|
||||
|
||||
// act
|
||||
sut.remove(new OPTION(1));
|
||||
|
||||
Set<OPTION> selectedAgain = sut.next();
|
||||
assertThat(selectedAgain.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstantFirstSelectorRemoveOption() {
|
||||
// arrange
|
||||
Selection.ConstantFirstSelector<OPTION> sut = new Selection.ConstantFirstSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1), new OPTION(2));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
sut.remove(new OPTION(1));
|
||||
|
||||
// assert
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options.contains(new OPTION(2))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstantFirstFirstSelectorMultipleOptions() {
|
||||
// arrange
|
||||
Selection.ConstantFirstSelector<OPTION> sut = new Selection.ConstantFirstSelector<>();
|
||||
int numOptions = 100;
|
||||
HashSet<OPTION> options = new HashSet<>();
|
||||
for (int counter=0; counter<numOptions; counter++) {
|
||||
options.add(new OPTION(counter));
|
||||
}
|
||||
sut.addAll(options);
|
||||
|
||||
// act, first choice
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(0)));
|
||||
|
||||
// act, second choice does not change
|
||||
selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstantLastSelectorOneOption() {
|
||||
// arrange
|
||||
Selection.ConstantLastSelector<OPTION> sut = new Selection.ConstantLastSelector<>();
|
||||
HashSet<OPTION> options = new HashSet<>() {{ add(new OPTION(0)); }};
|
||||
sut.addAll(options);
|
||||
|
||||
// act, first choice
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(0)));
|
||||
|
||||
// act, second choice does not change
|
||||
selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstantLastSelectorAddOption() {
|
||||
// arrange
|
||||
Selection.ConstantLastSelector<OPTION> sut = new Selection.ConstantLastSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options).isEqualTo(selected);
|
||||
|
||||
// act
|
||||
sut.add(new OPTION(2));
|
||||
|
||||
// assert
|
||||
selected = sut.next();
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstantLastSelectorRemoveOnlyOption() {
|
||||
// arrange
|
||||
Selection.ConstantLastSelector<OPTION> sut = new Selection.ConstantLastSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options).isEqualTo(selected);
|
||||
|
||||
// act
|
||||
sut.remove(new OPTION(1));
|
||||
|
||||
Set<OPTION> selectedAgain = sut.next();
|
||||
assertThat(selectedAgain.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstantLastSelectorRemoveOption() {
|
||||
// arrange
|
||||
Selection.ConstantLastSelector<OPTION> sut = new Selection.ConstantLastSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1), new OPTION(2));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
sut.remove(new OPTION(1));
|
||||
|
||||
// assert
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options.contains(new OPTION(2))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testConstantLastSelectorMultipleOptions() {
|
||||
// arrange
|
||||
Selection.ConstantLastSelector<OPTION> sut = new Selection.ConstantLastSelector<>();
|
||||
int numOptions = 100;
|
||||
HashSet<OPTION> options = new HashSet<>();
|
||||
for (int counter=0; counter<numOptions; counter++) {
|
||||
options.add(new OPTION(counter));
|
||||
}
|
||||
sut.addAll(options);
|
||||
|
||||
// act, first choice
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(numOptions - 1)));
|
||||
|
||||
// act, second choice does not change
|
||||
selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(numOptions - 1)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoundRobinSelectorOneOption() {
|
||||
// arrange
|
||||
Selection.RoundRobinSelector<OPTION> sut = new Selection.RoundRobinSelector<>();
|
||||
HashSet<OPTION> options = new HashSet<>() {{ add(new OPTION(0)); }};
|
||||
sut.addAll(options);
|
||||
|
||||
// act, first choice
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(0)));
|
||||
|
||||
// act, second choice does not change
|
||||
selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(0)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoundRobinSelectorAddOption() {
|
||||
// arrange
|
||||
Selection.RoundRobinSelector<OPTION> sut = new Selection.RoundRobinSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options).isEqualTo(selected);
|
||||
|
||||
// act
|
||||
sut.add(new OPTION(2));
|
||||
|
||||
// assert
|
||||
selected = sut.next();
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(options); // start from scratch
|
||||
selected = sut.next();
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(2)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoundRobinSelectorRemoveOnlyOption() {
|
||||
// arrange
|
||||
Selection.RoundRobinSelector<OPTION> sut = new Selection.RoundRobinSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options).isEqualTo(selected);
|
||||
|
||||
// act
|
||||
sut.remove(new OPTION(1));
|
||||
|
||||
Set<OPTION> selectedAgain = sut.next();
|
||||
assertThat(selectedAgain.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoundRobinSelectorRemoveOption() {
|
||||
// arrange
|
||||
Selection.RoundRobinSelector<OPTION> sut = new Selection.RoundRobinSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1), new OPTION(2));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
sut.remove(new OPTION(1));
|
||||
|
||||
// assert
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected.contains(new OPTION(2))).isTrue();
|
||||
selected = sut.next();
|
||||
assertThat(selected.contains(new OPTION(2))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoundRobinSelectorMultipleOptions() {
|
||||
// arrange
|
||||
Selection.RoundRobinSelector<OPTION> sut = new Selection.RoundRobinSelector<>();
|
||||
int numOptions = 100;
|
||||
HashSet<OPTION> options = new HashSet<>();
|
||||
for (int counter=0; counter<numOptions; counter++) {
|
||||
options.add(new OPTION(counter));
|
||||
}
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
for (int counter=0; counter<numOptions; counter++) {
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(counter)));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void testRoundRobinSelectorMultipleOptionsSelectingMoreThanOptions() {
|
||||
// arrange
|
||||
Selection.RoundRobinSelector<OPTION> sut = new Selection.RoundRobinSelector<>();
|
||||
int numOptions = 100;
|
||||
HashSet<OPTION> options = new HashSet<>();
|
||||
for (int counter=0; counter<numOptions; counter++) {
|
||||
options.add(new OPTION(counter));
|
||||
}
|
||||
sut.addAll(options);
|
||||
|
||||
// act once - selection goes through all <numOptions>
|
||||
List<OPTION> selectedFirstTime = new ArrayList<>();
|
||||
for (int counter=0; counter<numOptions; counter++) {
|
||||
selectedFirstTime.addAll(sut.next());
|
||||
}
|
||||
|
||||
// act again - selection starts from scratch, goes again through all <numOptions>
|
||||
List<OPTION> selectedSecondTime = new ArrayList<>();
|
||||
for (int counter=0; counter<numOptions; counter++) {
|
||||
selectedSecondTime.addAll(sut.next());
|
||||
}
|
||||
|
||||
// assert
|
||||
assertThat(selectedFirstTime.size()).isEqualTo(numOptions);
|
||||
assertThat(selectedFirstTime.size()).isEqualTo(selectedSecondTime.size());
|
||||
assertThat(selectedFirstTime).isEqualTo(selectedSecondTime);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAllSelectorOneOption() {
|
||||
// arrange
|
||||
Selection.AllSelector<OPTION> sut = new Selection.AllSelector<>();
|
||||
HashSet<OPTION> options = new HashSet<>() {{ add(new OPTION(0)); }};
|
||||
sut.addAll(options);
|
||||
|
||||
// act, first choice
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(0)));
|
||||
|
||||
// act, second choice does not change
|
||||
selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected).isEqualTo(Set.of(new OPTION(0)));
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
void testAllSelectorAddOption() {
|
||||
// arrange
|
||||
Selection.AllSelector<OPTION> sut = new Selection.AllSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options).isEqualTo(selected);
|
||||
|
||||
// act
|
||||
sut.add(new OPTION(2));
|
||||
|
||||
// assert
|
||||
selected = sut.next();
|
||||
assertThat(selected.size()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAllSelectorRemoveOnlyOption() {
|
||||
// arrange
|
||||
Selection.AllSelector<OPTION> sut = new Selection.AllSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(options).isEqualTo(selected);
|
||||
|
||||
// act
|
||||
sut.remove(new OPTION(1));
|
||||
|
||||
Set<OPTION> selectedAgain = sut.next();
|
||||
assertThat(selectedAgain.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAllSelectorRemoveOption() {
|
||||
// arrange
|
||||
Selection.AllSelector<OPTION> sut = new Selection.AllSelector<>();
|
||||
Set<OPTION> options = Set.of(new OPTION(1), new OPTION(2));
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
sut.remove(new OPTION(1));
|
||||
|
||||
// assert
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
assertThat(selected.size()).isEqualTo(1);
|
||||
assertThat(selected.contains(new OPTION(2))).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testAllSelectorMultipleOptions() {
|
||||
// arrange
|
||||
Selection.AllSelector<OPTION> sut = new Selection.AllSelector<>();
|
||||
int numOptions = 100;
|
||||
HashSet<OPTION> options = new HashSet<>();
|
||||
for (int counter=0; counter<numOptions; counter++) {
|
||||
options.add(new OPTION(counter));
|
||||
}
|
||||
sut.addAll(options);
|
||||
|
||||
// act
|
||||
for (int counter=0; counter<numOptions; counter++) {
|
||||
Set<OPTION> selected = sut.next();
|
||||
|
||||
// assert
|
||||
assertThat(selected.size()).isEqualTo(numOptions);
|
||||
assertThat(selected).isEqualTo(options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,233 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
class SynchronizedSetTest {
|
||||
private SynchronizedSet<String> set;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
set = new SynchronizedSet<>();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAdd() {
|
||||
// arrange, act, assert
|
||||
assertThat(set.add("Hello")).isTrue();
|
||||
assertThat(set.add("Hello")).isFalse();
|
||||
assertThat(set.add("World")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAddAll() {
|
||||
// arrange
|
||||
Set<String> collection = Set.of("Hello", "World");
|
||||
|
||||
// act, assert
|
||||
assertThat(set.addAll(collection)).isTrue();
|
||||
assertThat(set.contains("Hello")).isTrue();
|
||||
assertThat(set.contains("World")).isTrue();
|
||||
assertThat(set.contains("NonExistent")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemove() {
|
||||
// arrange
|
||||
set.add("Hello");
|
||||
set.add("World");
|
||||
|
||||
// act, assert
|
||||
assertThat(set.remove("Hello")).isTrue();
|
||||
assertThat(set.remove("Hello")).isFalse();
|
||||
assertThat(set.remove("NonExistent")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveAll() {
|
||||
// arrange
|
||||
set.add("Hello");
|
||||
set.add("World");
|
||||
set.add("Something");
|
||||
|
||||
Set<String> collection = Set.of("Hello", "World");
|
||||
|
||||
// act, assert
|
||||
assertThat(set.removeAll(collection)).isTrue();
|
||||
assertThat(set.contains("Hello")).isFalse();
|
||||
assertThat(set.contains("World")).isFalse();
|
||||
assertThat(set.remove("Something")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testClear() {
|
||||
// arrange
|
||||
set.add("Hello");
|
||||
set.add("World");
|
||||
|
||||
// act
|
||||
set.clear();
|
||||
|
||||
// assert
|
||||
assertThat(set.contains("Hello")).isFalse();
|
||||
assertThat(set.contains("World")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContains() {
|
||||
// arrange
|
||||
set.add("Hello");
|
||||
|
||||
// act, assert
|
||||
assertThat(set.contains("Hello")).isTrue();
|
||||
assertThat(set.contains("NonExistent")).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testContainsAll() {
|
||||
// arrange
|
||||
set.add("Hello");
|
||||
set.add("World");
|
||||
|
||||
Set<String> collection = Set.of("Hello", "World");
|
||||
|
||||
// act, assert
|
||||
assertThat(set.containsAll(collection)).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIsEmpty() {
|
||||
// arrange
|
||||
set.add("Hello");
|
||||
set.add("World");
|
||||
|
||||
// act, assert
|
||||
assertThat(set.isEmpty()).isFalse();
|
||||
|
||||
// act, assert
|
||||
set.clear();
|
||||
assertThat(set.isEmpty()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRetainsAll() {
|
||||
// arrange
|
||||
set.add("Hello");
|
||||
set.add("World");
|
||||
|
||||
Set<String> collection = Set.of("Hello");
|
||||
|
||||
// act
|
||||
assertThat(set.retainAll(collection)).isTrue();
|
||||
|
||||
// assert
|
||||
assertThat(set.contains("Hello")).isTrue();
|
||||
assertThat(set.contains("World")).isFalse();
|
||||
assertThat(set.add("NonExistent")).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSize() {
|
||||
// act, assert
|
||||
set.add("Hello");
|
||||
assertThat(set.size()).isEqualTo(1);
|
||||
|
||||
// act, assert
|
||||
set.add("Hello");
|
||||
assertThat(set.size()).isEqualTo(1);
|
||||
|
||||
// act, assert
|
||||
set.add("World");
|
||||
assertThat(set.size()).isEqualTo(2);
|
||||
|
||||
// act, assert
|
||||
set.remove("World");
|
||||
assertThat(set.size()).isEqualTo(1);
|
||||
|
||||
// act, assert
|
||||
set.clear();
|
||||
assertThat(set.size()).isEqualTo(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testEqualsAndHashCodeAndToString() {
|
||||
// arrange
|
||||
SynchronizedSet<String> anotherSet = new SynchronizedSet<>();
|
||||
|
||||
// act
|
||||
set.add("Hello");
|
||||
anotherSet.add("Hello");
|
||||
|
||||
// assert
|
||||
assertThat(set.equals(anotherSet)).isTrue();
|
||||
assertThat(set.hashCode()).isEqualTo(anotherSet.hashCode());
|
||||
assertThat(set.toString()).isEqualTo(anotherSet.toString());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToArray() {
|
||||
// arrange
|
||||
set.add("Hello");
|
||||
set.add("World");
|
||||
|
||||
// act
|
||||
String[] array = set.toArray(new String[0]);
|
||||
|
||||
// assert
|
||||
assertThat(new String[] { "Hello", "World" }).isEqualTo(array);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToArrayWithProvidedTooLargeArray() {
|
||||
// arrange
|
||||
set.add("Hello");
|
||||
set.add("World");
|
||||
|
||||
String[] largerArray = new String[5];
|
||||
|
||||
// act - Scenario 1: Provided array is larger than the set size
|
||||
String[] resultArray = set.toArray(largerArray);
|
||||
|
||||
// assert
|
||||
assertThat(largerArray == resultArray).isTrue();
|
||||
assertThat("Hello").isEqualTo(resultArray[0]);
|
||||
assertThat("World").isEqualTo(resultArray[1]);
|
||||
assertThat(resultArray[2]).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToArrayWithProvidedArray() {
|
||||
// arrange
|
||||
set.add("Hello");
|
||||
set.add("World");
|
||||
|
||||
String[] exactSizeArray = new String[2];
|
||||
|
||||
// act - Scenario 2: Provided array is exactly the set size
|
||||
String[] resultArray = set.toArray(exactSizeArray);
|
||||
|
||||
// assert
|
||||
assertThat(exactSizeArray == resultArray).isTrue();
|
||||
assertThat(new String[]{ "Hello", "World" }).isEqualTo(resultArray);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testToArrayWithProvidedTooSmallArray() {
|
||||
// arrange
|
||||
set.add("Hello");
|
||||
set.add("World");
|
||||
|
||||
String[] smallerArray = new String[1];
|
||||
|
||||
// act - Scenario 3: Provided array is smaller than the set size
|
||||
String[] resultArray = set.toArray(smallerArray);
|
||||
|
||||
// assert
|
||||
assertThat(smallerArray == resultArray).isFalse();
|
||||
assertThat(new String[]{ "Hello", "World" }).isEqualTo(resultArray);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
package de.accso.flexinale.common.shared_kernel;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
|
||||
|
||||
class ThreadSafeCounterMapTest {
|
||||
|
||||
@Test
|
||||
public void testInitialMap() {
|
||||
// arrange
|
||||
ThreadSafeCounterMap<String> sut = new ThreadSafeCounterMap<>();
|
||||
|
||||
// act
|
||||
Long key = sut.get("nothere");
|
||||
|
||||
// assert
|
||||
assertThat(key).isEqualTo(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSetAndGet() {
|
||||
// arrange
|
||||
ThreadSafeCounterMap<String> sut = new ThreadSafeCounterMap<>();
|
||||
Long fortyTwo = 42L;
|
||||
|
||||
// act
|
||||
sut.set("key", fortyTwo);
|
||||
|
||||
// assert
|
||||
assertThat(sut.get("key")).isEqualTo(fortyTwo);
|
||||
assertThat(sut.get("notthere")).isEqualTo(0L);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncrementInitial() {
|
||||
// arrange
|
||||
ThreadSafeCounterMap<String> sut = new ThreadSafeCounterMap<>();
|
||||
|
||||
// act
|
||||
long value = sut.increment("key");
|
||||
|
||||
// assert
|
||||
assertThat(value).isEqualTo(1);
|
||||
assertThat(sut.get("key")).isEqualTo(1);
|
||||
|
||||
// act
|
||||
value = sut.increment("key");
|
||||
|
||||
// assert
|
||||
assertThat(value).isEqualTo(1+1);
|
||||
assertThat(sut.get("key")).isEqualTo(1+1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIncrementAfterSet() {
|
||||
// arrange
|
||||
ThreadSafeCounterMap<String> sut = new ThreadSafeCounterMap<>();
|
||||
Long fortyTwo = 42L;
|
||||
sut.set("key", fortyTwo);
|
||||
|
||||
// act
|
||||
long value = sut.increment("key");
|
||||
|
||||
// assert
|
||||
assertThat(value).isEqualTo(fortyTwo+1);
|
||||
assertThat(sut.get("key")).isEqualTo(fortyTwo+1);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue