chore: Initial import of FLEX training material

This commit is contained in:
Alexander Kobjolke 2024-11-07 21:02:53 +01:00
parent c01246d4f7
commit 12235acc42
1020 changed files with 53940 additions and 0 deletions

View file

@ -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>

View file

@ -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 &quot;film festival&quot;, 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>

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
}

View file

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

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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";
}

View file

@ -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
}

View file

@ -0,0 +1,10 @@
package de.accso.flexinale.common.application;
public interface Config {
int getQuoteOnline();
int getMinZeitZwischenVorfuehrungenInMinuten();
String getApplicationTitle();
String getBuildVersion();
String getBuildDate();
}

View file

@ -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
}

View file

@ -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
}
}

View file

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

View file

@ -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;
}
}

View file

@ -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);
}

View file

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

View file

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

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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;
}
}

View file

@ -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));
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}

View file

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

View file

@ -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);
}
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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 + ">";
}
}

View file

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

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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 {
}

View file

@ -0,0 +1,6 @@
package de.accso.flexinale.common.shared_kernel;
@SuppressWarnings("unused")
public interface EqualsByContent {
boolean equalsByContent(final Object o);
}

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

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

View file

@ -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));
}
}

View file

@ -0,0 +1,5 @@
package de.accso.flexinale.common.shared_kernel;
public interface Mergeable<E extends Identifiable> {
E merge(E newData);
}

View file

@ -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);
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}

View file

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

View file

@ -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));
}
}

View file

@ -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));
}
}

View file

@ -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);
}
}

View file

@ -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>

View file

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

View file

@ -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;
}
}
}

View file

@ -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);
});
}
}

View file

@ -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);
}
}

View file

@ -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");
}
}

View file

@ -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);
}
}
}

View file

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

View file

@ -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 { }

View file

@ -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");
}
}

View file

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

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

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

View file

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

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}