diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 4662abe..13334d2 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -2,7 +2,7 @@ name: Java Maven Build on: push: - branches: + branches: - '**' paths-ignore: - '**/README.md' @@ -18,46 +18,46 @@ jobs: runs-on: ubuntu-latest steps: - - name: Checkout source - uses: actions/checkout@v3 - with: - fetch-depth: 0 - - - name: Set up JDK - uses: actions/setup-java@v3 - with: - java-version: '17' - distribution: 'zulu' - cache: maven - - - name: Show versions - run: java -version && ./mvnw -version && gpg --version - - - name: Cache SonarCloud packages - uses: actions/cache@v3 - with: - path: ~/.sonar/cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - - name: Cache Maven packages - uses: actions/cache@v3 - with: - path: ~/.m2 - key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} - restore-keys: ${{ runner.os }}-m2 - - - name: Import GPG key - uses: crazy-max/ghaction-import-gpg@v5 - with: - gpg_private_key: ${{ secrets.OSS_SONATYPE_GPG_PRIVATE_KEY }} - passphrase: ${{ secrets.OSS_SONATYPE_GPG_PASSPHRASE }} - - - name: Build with Maven - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - OSS_SONATYPE_USERNAME: michael-schnell - OSS_SONATYPE_TOKEN: ${{ secrets.OSS_SONATYPE_TOKEN }} - OSS_SONATYPE_GPG_PASSPHRASE: ${{ secrets.OSS_SONATYPE_GPG_PASSPHRASE }} - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: ./mvnw clean deploy jacoco:report sonar:sonar -U -B -P sonatype-oss-release --file pom.xml -s settings.xml + - name: Checkout source + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'zulu' + cache: maven + + - name: Show versions + run: java -version && ./mvnw -version && gpg --version + + - name: Cache SonarCloud packages + uses: actions/cache@v4 + with: + path: ~/.sonar/cache + key: ${{ runner.os }}-sonar + restore-keys: ${{ runner.os }}-sonar + + - name: Cache Maven packages + uses: actions/cache@v4 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + + - name: Import GPG key + uses: crazy-max/ghaction-import-gpg@v6 + with: + gpg_private_key: ${{ secrets.OSS_SONATYPE_GPG_PRIVATE_KEY }} + passphrase: ${{ secrets.OSS_SONATYPE_GPG_PASSPHRASE }} + + - name: Build with Maven + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + OSS_SONATYPE_USERNAME: michael-schnell + OSS_SONATYPE_TOKEN: ${{ secrets.OSS_SONATYPE_TOKEN }} + OSS_SONATYPE_GPG_PASSPHRASE: ${{ secrets.OSS_SONATYPE_GPG_PASSPHRASE }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: ./mvnw clean deploy jacoco:report sonar:sonar -U -B -P sonatype-oss-release --file pom.xml -s settings.xml diff --git a/.gitignore b/.gitignore index ff2a5cb..ec4b28e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,7 @@ !.gitkeep !.github !.mvn +!.sonarlint target -META-INF *.log /pom.xml.versionsBackup diff --git a/.sonarlint/connectedMode.json b/.sonarlint/connectedMode.json new file mode 100644 index 0000000..a662712 --- /dev/null +++ b/.sonarlint/connectedMode.json @@ -0,0 +1,5 @@ +{ + "sonarCloudOrganization": "fuinorg", + "projectKey": "org.fuin.cqrs4j:cqrs-4-java", + "region": "EU" +} \ No newline at end of file diff --git a/README.md b/README.md index 41ef8a4..20c67b4 100644 --- a/README.md +++ b/README.md @@ -4,12 +4,13 @@ Base classes for Command Query Responsibility Segregation (CQRS) with Java [![Java Maven Build](https://github.com/fuinorg/cqrs-4-java/actions/workflows/maven.yml/badge.svg)](https://github.com/fuinorg/cqrs-4-java/actions/workflows/maven.yml) -[![Coverage Status](https://sonarcloud.io/api/project_badges/measure?project=org.fuin%3Acqrs-4-java&metric=coverage)](https://sonarcloud.io/dashboard?id=org.fuin%3Acqrs-4-java) +[![Coverage Status](https://sonarcloud.io/api/project_badges/measure?project=org.fuin.cqrs4j%3Acqrs-4-java&metric=coverage)](https://sonarcloud.io/dashboard?id=org.fuin.cqrs4j%3Acqrs-4-java) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.fuin/cqrs-4-java/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.fuin/cqrs-4-java/) [![LGPLv3 License](http://img.shields.io/badge/license-LGPLv3-blue.svg)](https://www.gnu.org/licenses/lgpl.html) [![Java Development Kit 17](https://img.shields.io/badge/JDK-17-green.svg)](https://openjdk.java.net/projects/jdk/17/) ## Versions +- [0.6.0](release-notes.md) Added new [Jackson](jackson) module - 0.5.x (or later) = **Java 17** with new **jakarta** namespace - 0.3.x/0.4.x = **Java 11** before namespace change from 'javax' to 'jakarta' - 0.2.1 = **Java 8** diff --git a/core/pom.xml b/core/pom.xml new file mode 100644 index 0000000..c2b62cb --- /dev/null +++ b/core/pom.xml @@ -0,0 +1,179 @@ + + + + 4.0.0 + + + org.fuin.cqrs4j + cqrs-4-java + 0.6.0-SNAPSHOT + ../pom.xml + + + cqrs-4-java-core + jar + ${description} (CORE) + + + + + + + org.fuin.ddd4j + ddd-4-java-core + + + + org.fuin.objects4j + objects4j-common + + + + jakarta.validation + jakarta.validation-api + + + + jakarta.annotation + jakarta.annotation-api + + + + jakarta.persistence + jakarta.persistence-api + + + + + + org.junit.jupiter + junit-jupiter + test + + + + org.fuin + units4j + test + + + + org.hibernate.validator + hibernate-validator + test + + + + org.glassfish.expressly + expressly + test + + + + org.assertj + assertj-core + test + + + + org.mockito + mockito-core + test + + + + com.tngtech.archunit + archunit + test + + + + com.tngtech.archunit + archunit-junit5 + test + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.apache.maven.plugins + maven-source-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + org.apache.maven.plugins + maven-jar-plugin + + + **/* + + + + org.fuin.cqrs4j.core + + + + + + + org.apache.maven.plugins + maven-jdeps-plugin + + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + + + + io.smallrye + jandex-maven-plugin + + + + org.apache.maven.plugins + maven-dependency-plugin + + + jakarta.el:jakarta.el-api + org.glassfish.expressly:expressly + org.hibernate.validator:hibernate-validator + com.tngtech.archunit:archunit-junit5 + org.junit.jupiter:junit-jupiter + + + com.tngtech.archunit:archunit-junit5-api + org.junit.jupiter:junit-jupiter-api + + + + + + + + + diff --git a/src/main/java/org/fuin/cqrs4j/AbstractMultiCommandExecutor.java b/core/src/main/java/org/fuin/cqrs4j/core/AbstractMultiCommandExecutor.java similarity index 88% rename from src/main/java/org/fuin/cqrs4j/AbstractMultiCommandExecutor.java rename to core/src/main/java/org/fuin/cqrs4j/core/AbstractMultiCommandExecutor.java index 849e398..69dd7ad 100644 --- a/src/main/java/org/fuin/cqrs4j/AbstractMultiCommandExecutor.java +++ b/core/src/main/java/org/fuin/cqrs4j/core/AbstractMultiCommandExecutor.java @@ -1,21 +1,31 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.core; + +import jakarta.validation.constraints.NotEmpty; +import org.fuin.ddd4j.core.AggregateAlreadyExistsException; +import org.fuin.ddd4j.core.AggregateDeletedException; +import org.fuin.ddd4j.core.AggregateNotFoundException; +import org.fuin.ddd4j.core.AggregateVersionConflictException; +import org.fuin.ddd4j.core.AggregateVersionNotFoundException; +import org.fuin.ddd4j.core.EventType; +import org.fuin.objects4j.common.ConstraintViolationException; +import org.fuin.objects4j.common.Contract; import java.util.Arrays; import java.util.HashMap; @@ -23,33 +33,22 @@ import java.util.Map; import java.util.Set; -import jakarta.validation.constraints.NotEmpty; - -import org.fuin.ddd4j.ddd.AggregateAlreadyExistsException; -import org.fuin.ddd4j.ddd.AggregateDeletedException; -import org.fuin.ddd4j.ddd.AggregateNotFoundException; -import org.fuin.ddd4j.ddd.AggregateVersionConflictException; -import org.fuin.ddd4j.ddd.AggregateVersionNotFoundException; -import org.fuin.ddd4j.ddd.EventType; -import org.fuin.objects4j.common.ConstraintViolationException; -import org.fuin.objects4j.common.Contract; - /** * Handles multiple commands by delegating the call to other executors. - * + * * @param * Type of context for the command execution. * @param * Result of the command execution. */ -@SuppressWarnings({ "unchecked", "rawtypes" }) +@SuppressWarnings({"unchecked", "rawtypes"}) public abstract class AbstractMultiCommandExecutor implements CommandExecutor { private final Map commandExecutors; /** * Constructor with command handler array. - * + * * @param cmdExecutors * Array of command executors. */ @@ -59,7 +58,7 @@ public AbstractMultiCommandExecutor(@NotEmpty final CommandExecutor... cmdExecut /** * Constructor with mandatory data. - * + * * @param cmdExecutors * List of command executors. */ diff --git a/src/main/java/org/fuin/cqrs4j/AggregateCommand.java b/core/src/main/java/org/fuin/cqrs4j/core/AggregateCommand.java similarity index 84% rename from src/main/java/org/fuin/cqrs4j/AggregateCommand.java rename to core/src/main/java/org/fuin/cqrs4j/core/AggregateCommand.java index 9f8d5e6..751c7bf 100644 --- a/src/main/java/org/fuin/cqrs4j/AggregateCommand.java +++ b/core/src/main/java/org/fuin/cqrs4j/core/AggregateCommand.java @@ -1,31 +1,30 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.core; import jakarta.validation.constraints.NotNull; - -import org.fuin.ddd4j.ddd.AggregateRootId; -import org.fuin.ddd4j.ddd.DomainEvent; -import org.fuin.ddd4j.ddd.EntityId; +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.DomainEvent; +import org.fuin.ddd4j.core.EntityId; /** * Common behavior shared by all commands related to an aggregate. - * + * * @param * Type of the aggregate root identifier. * @param @@ -35,7 +34,7 @@ public interface AggregateCommand * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.core; -import org.fuin.ddd4j.ddd.Event; +import org.fuin.ddd4j.core.Event; /** * Common behavior shared by all commands. diff --git a/src/main/java/org/fuin/cqrs4j/CommandExecutionFailedException.java b/core/src/main/java/org/fuin/cqrs4j/core/CommandExecutionFailedException.java similarity index 88% rename from src/main/java/org/fuin/cqrs4j/CommandExecutionFailedException.java rename to core/src/main/java/org/fuin/cqrs4j/core/CommandExecutionFailedException.java index 28ff366..322a4aa 100644 --- a/src/main/java/org/fuin/cqrs4j/CommandExecutionFailedException.java +++ b/core/src/main/java/org/fuin/cqrs4j/core/CommandExecutionFailedException.java @@ -1,39 +1,42 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -package org.fuin.cqrs4j; - -import jakarta.validation.constraints.NotNull; - -/** - * The execution of a command failed. This exception is used for "tunneling" other checked exceptions during command execution. - */ -public final class CommandExecutionFailedException extends Exception { - - private static final long serialVersionUID = 1L; - - /** - * Constructor with all data. - * - * @param cause - * Causing exception. - */ - public CommandExecutionFailedException(@NotNull final Exception cause) { - super(cause); - } - -} +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.core; + +import jakarta.validation.constraints.NotNull; + +import java.io.Serial; + +/** + * The execution of a command failed. This exception is used for "tunneling" other checked exceptions during command execution. + */ +public final class CommandExecutionFailedException extends Exception { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * Constructor with all data. + * + * @param cause + * Causing exception. + */ + public CommandExecutionFailedException(@NotNull final Exception cause) { + super(cause); + } + +} diff --git a/core/src/main/java/org/fuin/cqrs4j/core/CommandExecutor.java b/core/src/main/java/org/fuin/cqrs4j/core/CommandExecutor.java new file mode 100644 index 0000000..74a849d --- /dev/null +++ b/core/src/main/java/org/fuin/cqrs4j/core/CommandExecutor.java @@ -0,0 +1,64 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.core; + +import jakarta.validation.constraints.NotNull; +import org.fuin.ddd4j.core.AggregateAlreadyExistsException; +import org.fuin.ddd4j.core.AggregateDeletedException; +import org.fuin.ddd4j.core.AggregateNotFoundException; +import org.fuin.ddd4j.core.AggregateVersionConflictException; +import org.fuin.ddd4j.core.AggregateVersionNotFoundException; +import org.fuin.ddd4j.core.EventType; + +import java.util.Set; + +/** + * Executes one or more commands. + * + * @param Type of context for the command execution. + * @param Result of the command execution. + * @param Type of command to execute. + */ +public interface CommandExecutor { + + /** + * Returns a list of commands this executor can handle. + * + * @return List of unique command types. + */ + @NotNull + public Set getCommandTypes(); + + /** + * Executes the given command. Only the main aggregate related exceptions are modeled via throws. All other checked exceptions must be + * wrapped into a {@link CommandExecutionFailedException}. + * + * @param ctx Context of command to execute. + * @param cmd Command to execute. + * @return Result. + * @throws AggregateVersionConflictException There is a conflict between an expected and an actual version for the aggregate targeted by the command. + * @throws AggregateNotFoundException The aggregate targeted by the command with a given type and identifier was not found in the repository. + * @throws AggregateVersionNotFoundException The requested version for the aggregate targeted by the command does not exist. + * @throws AggregateDeletedException The aggregate targeted by the command was deleted from the repository. + * @throws AggregateAlreadyExistsException The aggregate targeted by the command already exists when trying to create it. + * @throws CommandExecutionFailedException Other checked exceptions are wrapped into this one. + */ + public RESULT execute(@NotNull CONTEXT ctx, @NotNull CMD cmd) throws AggregateVersionConflictException, AggregateNotFoundException, + AggregateVersionNotFoundException, AggregateDeletedException, AggregateAlreadyExistsException, CommandExecutionFailedException; + +} diff --git a/core/src/main/java/org/fuin/cqrs4j/core/CqrsUtils.java b/core/src/main/java/org/fuin/cqrs4j/core/CqrsUtils.java new file mode 100644 index 0000000..2a38262 --- /dev/null +++ b/core/src/main/java/org/fuin/cqrs4j/core/CqrsUtils.java @@ -0,0 +1,35 @@ +package org.fuin.cqrs4j.core; + +import org.fuin.ddd4j.core.EventType; + +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.zip.Adler32; + +/** + * CQRS related helper functions. + */ +public final class CqrsUtils { + + private CqrsUtils() { + throw new UnsupportedOperationException("Utility classes cannot be instantiated"); + } + + /** + * Creates an Adler32 checksum based on event type names. + * + * @param eventTypes Types to calculate a checksum for. + * @return Checksum based on all names. + */ + public static long calculateAdler32Checksum(final Collection eventTypes) { + if (eventTypes == null || eventTypes.isEmpty()) { + throw new IllegalArgumentException("eventTypes cannot be null or empty"); + } + final Adler32 checksum = new Adler32(); + for (final EventType eventType : eventTypes) { + checksum.update(eventType.asBaseType().getBytes(StandardCharsets.US_ASCII)); + } + return checksum.getValue(); + } + +} diff --git a/src/main/java/org/fuin/cqrs4j/EventHandler.java b/core/src/main/java/org/fuin/cqrs4j/core/JpaEventHandler.java similarity index 63% rename from src/main/java/org/fuin/cqrs4j/EventHandler.java rename to core/src/main/java/org/fuin/cqrs4j/core/JpaEventHandler.java index 25688fa..ccaba5e 100644 --- a/src/main/java/org/fuin/cqrs4j/EventHandler.java +++ b/core/src/main/java/org/fuin/cqrs4j/core/JpaEventHandler.java @@ -1,46 +1,46 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.core; -import org.fuin.ddd4j.ddd.Event; -import org.fuin.ddd4j.ddd.EventType; +import jakarta.persistence.EntityManager; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventType; /** - * Does something useful using the input from an event. - * - * @param - * Event type. + * Event handler that maps an event to JPA entities. + * + * @param Event type. */ -public interface EventHandler { +public interface JpaEventHandler { /** * Returns the type of event this handler operates on. - * + * * @return Unique event type. */ public EventType getEventType(); /** * Modifies the view using the given event. - * - * @param event - * Event to use. + * + * @param entityManager Entity manager to use. + * @param event Event to use. */ - public void handle(TYPE event); + public void handle(EntityManager entityManager, TYPE event); } diff --git a/core/src/main/java/org/fuin/cqrs4j/core/JpaView.java b/core/src/main/java/org/fuin/cqrs4j/core/JpaView.java new file mode 100644 index 0000000..95e7616 --- /dev/null +++ b/core/src/main/java/org/fuin/cqrs4j/core/JpaView.java @@ -0,0 +1,40 @@ +package org.fuin.cqrs4j.core; + +import jakarta.persistence.EntityManager; +import org.fuin.ddd4j.core.Event; + +import java.util.List; + +/** + * Defines a unit that projects events read from the event store into another representation. + * The view is updated regularly by using a scheduler and the result will be stored using JPA. + */ +public interface JpaView extends View { + + /** + * Returns the CRON expression defining how often the view should be updated. + * + * @return Spring Quartz CRON expression + */ + String getCron(); + + + /** + * Number of events to read and handle in one transaction. + * + * @return Number of events (defaults to 100). + */ + default int getChunkSize() { + return 100; + } + + + /** + * Events to handle by the view. + * + * @param em Entity manager to use. + * @param events Events used to update the view. + */ + void handleEvents(EntityManager em, List events); + +} diff --git a/src/main/java/org/fuin/cqrs4j/MultiCommandExecutor.java b/core/src/main/java/org/fuin/cqrs4j/core/MultiCommandExecutor.java similarity index 92% rename from src/main/java/org/fuin/cqrs4j/MultiCommandExecutor.java rename to core/src/main/java/org/fuin/cqrs4j/core/MultiCommandExecutor.java index de768eb..b843350 100644 --- a/src/main/java/org/fuin/cqrs4j/MultiCommandExecutor.java +++ b/core/src/main/java/org/fuin/cqrs4j/core/MultiCommandExecutor.java @@ -1,29 +1,29 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; - -import java.util.List; +package org.fuin.cqrs4j.core; import jakarta.validation.constraints.NotEmpty; +import java.util.List; + /** * Handles multiple commands by delegating the call to other executors. - * + * * @param * Type of context for the command execution. * @param @@ -34,7 +34,7 @@ public final class MultiCommandExecutor extends AbstractMultiCo /** * Constructor with command handler array. - * + * * @param cmdExecutors * Array of command executors. */ @@ -44,7 +44,7 @@ public MultiCommandExecutor(@NotEmpty final CommandExecutor... cmdExecutors) { /** * Constructor with mandatory data. - * + * * @param cmdExecutors * List of command executors. */ diff --git a/src/main/java/org/fuin/cqrs4j/Result.java b/core/src/main/java/org/fuin/cqrs4j/core/Result.java similarity index 86% rename from src/main/java/org/fuin/cqrs4j/Result.java rename to core/src/main/java/org/fuin/cqrs4j/core/Result.java index ffc5828..b38f03c 100644 --- a/src/main/java/org/fuin/cqrs4j/Result.java +++ b/core/src/main/java/org/fuin/cqrs4j/core/Result.java @@ -1,31 +1,30 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.core; +import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; -import org.fuin.objects4j.common.Nullable; - /** - * Result of a request. The type signals if the execution was successful or not. In case the the result is not {@link ResultType#OK}, the + * Result of a request. The type signals if the execution was successful or not. In case the result is not {@link ResultType#OK}, the * fields code and message should contain unique information to help the user identifying the cause of the problem. A result may carry some * optional data. - * + * * @param * Type of data returned. */ @@ -33,7 +32,7 @@ public interface Result { /** * Returns the result type. - * + * * @return Type. */ @NotNull @@ -41,7 +40,7 @@ public interface Result { /** * Returns the result code. - * + * * @return Code. */ @Nullable @@ -49,7 +48,7 @@ public interface Result { /** * Returns the result message. - * + * * @return Message. */ @Nullable @@ -57,7 +56,7 @@ public interface Result { /** * Returns the result data. - * + * * @return Optional data. */ @Nullable diff --git a/src/main/java/org/fuin/cqrs4j/ResultType.java b/core/src/main/java/org/fuin/cqrs4j/core/ResultType.java similarity index 89% rename from src/main/java/org/fuin/cqrs4j/ResultType.java rename to core/src/main/java/org/fuin/cqrs4j/core/ResultType.java index 912d64b..a7a8a68 100644 --- a/src/main/java/org/fuin/cqrs4j/ResultType.java +++ b/core/src/main/java/org/fuin/cqrs4j/core/ResultType.java @@ -1,21 +1,21 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.core; /** * Type of the result. diff --git a/src/main/java/org/fuin/cqrs4j/ToResultCapable.java b/core/src/main/java/org/fuin/cqrs4j/core/ToResultCapable.java similarity index 87% rename from src/main/java/org/fuin/cqrs4j/ToResultCapable.java rename to core/src/main/java/org/fuin/cqrs4j/core/ToResultCapable.java index eccfe3c..ee4dde1 100644 --- a/src/main/java/org/fuin/cqrs4j/ToResultCapable.java +++ b/core/src/main/java/org/fuin/cqrs4j/core/ToResultCapable.java @@ -1,35 +1,35 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -package org.fuin.cqrs4j; - -/** - * Marks an object that can be converted into a result. - */ -public interface ToResultCapable { - - /** - * Returns a result for the object. - * - * @return Object expressed as result. - * - * @param - * Type of data. - */ - public Result toResult(); - -} +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.core; + +/** + * Marks an object that can be converted into a result. + */ +public interface ToResultCapable { + + /** + * Returns a result for the object. + * + * @return Object expressed as result. + * + * @param + * Type of data. + */ + public Result toResult(); + +} diff --git a/core/src/main/java/org/fuin/cqrs4j/core/UrlParamEntityIdPathNotEqualsCmdException.java b/core/src/main/java/org/fuin/cqrs4j/core/UrlParamEntityIdPathNotEqualsCmdException.java new file mode 100644 index 0000000..be0130b --- /dev/null +++ b/core/src/main/java/org/fuin/cqrs4j/core/UrlParamEntityIdPathNotEqualsCmdException.java @@ -0,0 +1,49 @@ +package org.fuin.cqrs4j.core; + +import org.fuin.ddd4j.core.EntityIdPath; + +/** + * The entity identifier path constructed from the URL does not match the one that is inside the received command. + *

+ * This can happen if URL for example contains the name of the aggregate, followed by an aggregate identifier.
+ * Example: POST /customer/f832a5a4-dd80-49df-856a-7274de82cd6b/create (Command send in the request body)
+ * The ID from the URL must match the aggregate ID that is passed via the command in the body. + */ +@SuppressWarnings("rawtypes") +public class UrlParamEntityIdPathNotEqualsCmdException extends Exception { + + private final EntityIdPath urlEntityIdPath; + + private final AggregateCommand command; + + /** + * Constructor with mandatory data. + * + * @param urlEntityIdPath Entity identifier path constructed from the URL. + * @param command Command with the entity identifier path that does not match the one from the URL. + */ + public UrlParamEntityIdPathNotEqualsCmdException(EntityIdPath urlEntityIdPath, AggregateCommand command) { + super("Entity path constructed from URL parameters '" + urlEntityIdPath.asBaseType() + "' " + + "is not the same as command's entityPath: '" + command.getEntityIdPath().asBaseType() + "'"); + this.urlEntityIdPath = urlEntityIdPath; + this.command = command; + } + + /** + * Returns the entity identifier path constructed from the URL. + * + * @return Entity ID path from URL. + */ + public EntityIdPath getUrlEntityIdPath() { + return urlEntityIdPath; + } + + /** + * Returns the command with the entity identifier path that does not match the one from the URL. + * + * @return Command with mismatching entity identifier path. + */ + public AggregateCommand getCommand() { + return command; + } +} diff --git a/core/src/main/java/org/fuin/cqrs4j/core/View.java b/core/src/main/java/org/fuin/cqrs4j/core/View.java new file mode 100644 index 0000000..59c3220 --- /dev/null +++ b/core/src/main/java/org/fuin/cqrs4j/core/View.java @@ -0,0 +1,27 @@ +package org.fuin.cqrs4j.core; + +import org.fuin.ddd4j.core.EventType; + +import java.util.Set; + +/** + * Defines a unit that projects events into another representation. + */ +public interface View { + + /** + * Unique name of the view. + * + * @return Name that is unique in this program instance. + */ + String getName(); + + /** + * Returns the type of events the view is interested in. + * + * @return List of events. + */ + Set getEventTypes(); + + +} diff --git a/src/main/java/org/fuin/cqrs4j/package-info.java b/core/src/main/java/org/fuin/cqrs4j/core/package-info.java similarity index 67% rename from src/main/java/org/fuin/cqrs4j/package-info.java rename to core/src/main/java/org/fuin/cqrs4j/core/package-info.java index 439582b..79eef5a 100644 --- a/src/main/java/org/fuin/cqrs4j/package-info.java +++ b/core/src/main/java/org/fuin/cqrs4j/core/package-info.java @@ -1,29 +1,22 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -@XmlJavaTypeAdapter(type = ZonedDateTime.class, value = ZonedDateTimeXmlAdapter.class) -package org.fuin.cqrs4j; - -import java.time.ZonedDateTime; - -import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter; - -import io.github.threetenjaxb.core.ZonedDateTimeXmlAdapter; - -/** - * Command Query Responsibility Segregation base classes. - */ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.core; + +/** + * Command Query Responsibility Segregation base classes. + */ diff --git a/core/src/test/java/org/fuin/cqrs4j/core/ArchitectureTest.java b/core/src/test/java/org/fuin/cqrs4j/core/ArchitectureTest.java new file mode 100644 index 0000000..45bfc21 --- /dev/null +++ b/core/src/test/java/org/fuin/cqrs4j/core/ArchitectureTest.java @@ -0,0 +1,39 @@ +package org.fuin.cqrs4j.core; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.library.DependencyRules.NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + +/** + * Tests architectural aspects. + */ +@AnalyzeClasses(packagesOf = ArchitectureTest.class, importOptions = ImportOption.DoNotIncludeTests.class) +public class ArchitectureTest { + + private static final String THIS_PACKAGE = ArchitectureTest.class.getPackageName(); + + @ArchTest + static final ArchRule no_accesses_to_upper_package = NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + + @ArchTest + static final ArchRule access_only_to_defined_packages = classes() + .that() + .resideInAPackage(THIS_PACKAGE) + .should() + .onlyDependOnClassesThat() + .resideInAnyPackage(THIS_PACKAGE, "java..", + "org.fuin.ddd4j.common..", + "org.fuin.ddd4j.core..", + "org.fuin.objects4j.common..", + "org.fuin.objects4j.core..", + "jakarta.validation.constraints..", + "jakarta.annotation..", + "org.slf4j..", + "javax.annotation.concurrent.." + ); + +} diff --git a/core/src/test/java/org/fuin/cqrs4j/core/BaseTest.java b/core/src/test/java/org/fuin/cqrs4j/core/BaseTest.java new file mode 100644 index 0000000..8364809 --- /dev/null +++ b/core/src/test/java/org/fuin/cqrs4j/core/BaseTest.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2013 Future Invent Informationsmanagement GmbH. All rights + * reserved. + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ +package org.fuin.cqrs4j.core; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import org.fuin.units4j.archunit.Units4JConditions; + +@AnalyzeClasses(packagesOf = BaseTest.class) +class BaseTest { + + @ArchTest + static final ArchRule all_classes_should_have_tests = Units4JConditions.ALL_CLASSES_SHOULD_HAVE_TESTS; + +} + diff --git a/core/src/test/java/org/fuin/cqrs4j/core/CommandExecutionFailedExceptionTest.java b/core/src/test/java/org/fuin/cqrs4j/core/CommandExecutionFailedExceptionTest.java new file mode 100644 index 0000000..81efaa8 --- /dev/null +++ b/core/src/test/java/org/fuin/cqrs4j/core/CommandExecutionFailedExceptionTest.java @@ -0,0 +1,28 @@ +package org.fuin.cqrs4j.core; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the {@link CommandExecutionFailedException} class. + */ +class CommandExecutionFailedExceptionTest { + + @Test + void testCreate() { + + // PREPARE + final IOException ex = new IOException("Whatever"); + + // TEST + CommandExecutionFailedException testee = new CommandExecutionFailedException(ex); + + // VERIFY + assertThat(testee.getCause()).isEqualTo(ex); + + } + +} diff --git a/core/src/test/java/org/fuin/cqrs4j/core/CqrsUtilsTest.java b/core/src/test/java/org/fuin/cqrs4j/core/CqrsUtilsTest.java new file mode 100644 index 0000000..4e243f6 --- /dev/null +++ b/core/src/test/java/org/fuin/cqrs4j/core/CqrsUtilsTest.java @@ -0,0 +1,26 @@ +package org.fuin.cqrs4j.core; + +import org.fuin.ddd4j.core.EventType; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the {@link CqrsUtils} class. + */ +class CqrsUtilsTest { + + @Test + void testCalculateAdler32Checksum() { + + assertThat(CqrsUtils.calculateAdler32Checksum(List.of(new EventType("A")))) + .isEqualTo(4325442L); + + assertThat(CqrsUtils.calculateAdler32Checksum(List.of(new EventType("A"), new EventType("B")))) + .isEqualTo(12976260L); + + } + +} \ No newline at end of file diff --git a/src/test/java/org/fuin/cqrs4j/MultiCommandExecutorTest.java b/core/src/test/java/org/fuin/cqrs4j/core/MultiCommandExecutorTest.java similarity index 86% rename from src/test/java/org/fuin/cqrs4j/MultiCommandExecutorTest.java rename to core/src/test/java/org/fuin/cqrs4j/core/MultiCommandExecutorTest.java index 091f728..eda2f47 100644 --- a/src/test/java/org/fuin/cqrs4j/MultiCommandExecutorTest.java +++ b/core/src/test/java/org/fuin/cqrs4j/core/MultiCommandExecutorTest.java @@ -1,202 +1,234 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -package org.fuin.cqrs4j; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.fail; - -import java.net.InetAddress; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.concurrent.CountDownLatch; - -import org.fuin.ddd4j.ddd.EventType; -import org.fuin.objects4j.common.ConstraintViolationException; -import org.junit.jupiter.api.Test; - -/** - * Test for {@link MultiCommandExecutor}. - */ -public class MultiCommandExecutorTest { - - @Test - @SuppressWarnings("rawtypes") - public final void testDispatch() throws Exception { - - // PREPARE - final CountDownLatch done = new CountDownLatch(2); - final CommandExecutor cmdHandler1 = new CommandExecutor() { - @Override - public final Set getCommandTypes() { - final Set set = new HashSet<>(); - set.add(MyCommand.EVENT_TYPE); - return set; - } - - @Override - public final Long execute(final MyContext ctx, final MyCommand cmd) { - done.countDown(); - return done.getCount(); - } - - }; - final List list = new ArrayList<>(); - list.add(cmdHandler1); - final MultiCommandExecutor testee = new MultiCommandExecutor<>(list); - final MyContext ctx = new MyContext(InetAddress.getLocalHost()); - - // TEST call twice - testee.execute(ctx, new MyCommand()); - testee.execute(ctx, new MyCommand()); - - // VERIFY - assertThat(done.getCount()).isEqualTo(0); - - } - - @Test - public final void testCreateNullArray() { - - try { - new MultiCommandExecutor(); - fail(); - } catch (final ConstraintViolationException ex) { - assertThat(ex.getMessage()).isEqualTo("The argument 'cmdExecutors' cannot be an empty list"); - } - - } - - @Test - @SuppressWarnings("rawtypes") - public final void testCreateNullList() { - - try { - new MultiCommandExecutor((List) null); - fail(); - } catch (final ConstraintViolationException ex) { - assertThat(ex.getMessage()).isEqualTo("The argument 'cmdExecutors' cannot be null"); - } - - } - - @Test - public final void testCreateEmptyArray() { - - try { - new MultiCommandExecutor(new CommandExecutor[] {}); - fail(); - } catch (final ConstraintViolationException ex) { - assertThat(ex.getMessage()).isEqualTo("The argument 'cmdExecutors' cannot be an empty list"); - } - - } - - @Test - @SuppressWarnings("rawtypes") - public final void testCreateEmptyList() { - - try { - new MultiCommandExecutor(new ArrayList()); - fail(); - } catch (final ConstraintViolationException ex) { - assertThat(ex.getMessage()).isEqualTo("The argument 'cmdExecutors' cannot be an empty list"); - } - - } - - @Test - @SuppressWarnings("rawtypes") - public final void testCreateDuplicate() { - - // PREPARE - final CommandExecutor cmdHandler1 = new CommandExecutor() { - @Override - public final Set getCommandTypes() { - final Set set = new HashSet<>(); - set.add(MyCommand.EVENT_TYPE); - return set; - } - - @Override - public final Void execute(final MyContext ctx, final MyCommand event) { - return null; - } - }; - final CommandExecutor cmdHandler2 = new CommandExecutor() { - @Override - public final Set getCommandTypes() { - final Set set = new HashSet<>(); - set.add(MyCommand.EVENT_TYPE); - return set; - } - - @Override - public final Void execute(final MyContext ctx, final MyCommand event) { - return null; - } - }; - final List list = new ArrayList<>(); - list.add(cmdHandler1); - list.add(cmdHandler2); - - // TEST - try { - new MultiCommandExecutor(list); - fail(); - } catch (final ConstraintViolationException ex) { - assertThat(ex.getMessage()) - .isEqualTo("The argument 'cmdExecutors' contains multiple executors for command: " + MyCommand.EVENT_TYPE); - } - - } - - public static class MyCommand extends AbstractCommand { - - private static final long serialVersionUID = 1L; - - private static final EventType EVENT_TYPE = new EventType("MyCommand"); - - public MyCommand() { - super(); - } - - @Override - public EventType getEventType() { - return EVENT_TYPE; - } - - } - - public static class MyContext { - - private InetAddress ipAddr; - - public MyContext(final InetAddress ipAddr) { - super(); - this.ipAddr = ipAddr; - } - - public InetAddress getIpAddr() { - return ipAddr; - } - - } - -} +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.core; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import org.fuin.ddd4j.core.EventId; +import org.fuin.ddd4j.core.EventType; +import org.fuin.objects4j.common.ConstraintViolationException; +import org.junit.jupiter.api.Test; + +import java.io.Serial; +import java.net.InetAddress; +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CountDownLatch; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test for {@link MultiCommandExecutor}. + */ +public class MultiCommandExecutorTest { + + @Test + @SuppressWarnings("rawtypes") + public final void testDispatch() throws Exception { + + // PREPARE + final CountDownLatch done = new CountDownLatch(2); + final CommandExecutor cmdHandler1 = new CommandExecutor() { + @Override + public final Set getCommandTypes() { + final Set set = new HashSet<>(); + set.add(MyCommand.EVENT_TYPE); + return set; + } + + @Override + public final Long execute(final MyContext ctx, final MyCommand cmd) { + done.countDown(); + return done.getCount(); + } + + }; + final List list = new ArrayList<>(); + list.add(cmdHandler1); + final MultiCommandExecutor testee = new MultiCommandExecutor<>(list); + final MyContext ctx = new MyContext(InetAddress.getLocalHost()); + + // TEST call twice + testee.execute(ctx, new MyCommand()); + testee.execute(ctx, new MyCommand()); + + // VERIFY + assertThat(done.getCount()).isEqualTo(0); + + } + + @Test + public final void testCreateNullArray() { + + try { + new MultiCommandExecutor(); + fail(); + } catch (final ConstraintViolationException ex) { + assertThat(ex.getMessage()).isEqualTo("The argument 'cmdExecutors' cannot be an empty list"); + } + + } + + @Test + @SuppressWarnings("rawtypes") + public final void testCreateNullList() { + + try { + new MultiCommandExecutor((List) null); + fail(); + } catch (final ConstraintViolationException ex) { + assertThat(ex.getMessage()).isEqualTo("The argument 'cmdExecutors' cannot be null"); + } + + } + + @Test + public final void testCreateEmptyArray() { + + try { + new MultiCommandExecutor(); + fail(); + } catch (final ConstraintViolationException ex) { + assertThat(ex.getMessage()).isEqualTo("The argument 'cmdExecutors' cannot be an empty list"); + } + + } + + @Test + @SuppressWarnings("rawtypes") + public final void testCreateEmptyList() { + + try { + new MultiCommandExecutor(new ArrayList()); + fail(); + } catch (final ConstraintViolationException ex) { + assertThat(ex.getMessage()).isEqualTo("The argument 'cmdExecutors' cannot be an empty list"); + } + + } + + @Test + @SuppressWarnings("rawtypes") + public final void testCreateDuplicate() { + + // PREPARE + final CommandExecutor cmdHandler1 = new CommandExecutor() { + @Override + public final Set getCommandTypes() { + final Set set = new HashSet<>(); + set.add(MyCommand.EVENT_TYPE); + return set; + } + + @Override + public final Void execute(final MyContext ctx, final MyCommand event) { + return null; + } + }; + final CommandExecutor cmdHandler2 = new CommandExecutor() { + @Override + public final Set getCommandTypes() { + final Set set = new HashSet<>(); + set.add(MyCommand.EVENT_TYPE); + return set; + } + + @Override + public final Void execute(final MyContext ctx, final MyCommand event) { + return null; + } + }; + final List list = new ArrayList<>(); + list.add(cmdHandler1); + list.add(cmdHandler2); + + // TEST + try { + new MultiCommandExecutor(list); + fail(); + } catch (final ConstraintViolationException ex) { + assertThat(ex.getMessage()) + .isEqualTo("The argument 'cmdExecutors' contains multiple executors for command: " + MyCommand.EVENT_TYPE); + } + + } + + public static class MyCommand implements Command { + + @Serial + private static final long serialVersionUID = 1L; + + private static final EventType EVENT_TYPE = new EventType("MyCommand"); + + private ZonedDateTime timestamp; + + public MyCommand() { + super(); + timestamp = ZonedDateTime.now(); + } + + @Override + public @NotNull EventId getEventId() { + return null; + } + + @Override + public EventType getEventType() { + return EVENT_TYPE; + } + + @Override + @NotNull + public ZonedDateTime getEventTimestamp() { + return timestamp; + } + + @Nullable + @Override + public EventId getCorrelationId() { + return null; + } + + @Nullable + @Override + public EventId getCausationId() { + return null; + } + + } + + public static class MyContext { + + private InetAddress ipAddr; + + public MyContext(final InetAddress ipAddr) { + super(); + this.ipAddr = ipAddr; + } + + public InetAddress getIpAddr() { + return ipAddr; + } + + } + +} diff --git a/core/src/test/java/org/fuin/cqrs4j/core/UrlParamEntityIdPathNotEqualsCmdExceptionTest.java b/core/src/test/java/org/fuin/cqrs4j/core/UrlParamEntityIdPathNotEqualsCmdExceptionTest.java new file mode 100644 index 0000000..135b10b --- /dev/null +++ b/core/src/test/java/org/fuin/cqrs4j/core/UrlParamEntityIdPathNotEqualsCmdExceptionTest.java @@ -0,0 +1,37 @@ +package org.fuin.cqrs4j.core; + +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityIdPath; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * Test for the {@link UrlParamEntityIdPathNotEqualsCmdException} class. + */ +public class UrlParamEntityIdPathNotEqualsCmdExceptionTest { + + @Test + void testCreate() { + + // PREPARE + final EntityId entityId = Mockito.mock(EntityId.class); + final EntityIdPath urlEntityIdPath = new EntityIdPath(entityId); + final AggregateCommand command = Mockito.mock(AggregateCommand.class); + + final EntityId cmdEntityId = Mockito.mock(EntityId.class); + final EntityIdPath cmdEntityIdPath = new EntityIdPath(cmdEntityId); + when(command.getEntityIdPath()).thenReturn(cmdEntityIdPath); + + // TEST + final UrlParamEntityIdPathNotEqualsCmdException testee = new UrlParamEntityIdPathNotEqualsCmdException(urlEntityIdPath, command); + + // VERIFY + assertThat(testee.getUrlEntityIdPath()).isEqualTo(urlEntityIdPath); + assertThat(testee.getCommand()).isEqualTo(command); + + } + +} diff --git a/cqrs-4-java.iml b/cqrs-4-java.iml new file mode 100644 index 0000000..20baf01 --- /dev/null +++ b/cqrs-4-java.iml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/esc/cqrs-4-java-esc.iml b/esc/cqrs-4-java-esc.iml new file mode 100644 index 0000000..2ddb30b --- /dev/null +++ b/esc/cqrs-4-java-esc.iml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/esc/pom.xml b/esc/pom.xml new file mode 100644 index 0000000..f776378 --- /dev/null +++ b/esc/pom.xml @@ -0,0 +1,189 @@ + + + + 4.0.0 + + + org.fuin.cqrs4j + cqrs-4-java + 0.6.0-SNAPSHOT + ../pom.xml + + + cqrs-4-java-esc + jar + ${description} (ESC) + + + + + + + org.fuin.cqrs4j + cqrs-4-java-core + + + + org.fuin.ddd4j + ddd-4-java-core + + + + org.fuin.objects4j + objects4j-common + + + + org.fuin.esc + esc-api + + + + jakarta.validation + jakarta.validation-api + + + + jakarta.persistence + jakarta.persistence-api + + + + + + org.fuin.ddd4j + ddd-4-java-jsonb + test + + + + org.junit.jupiter + junit-jupiter + test + + + + org.fuin + units4j + test + + + + org.hibernate.validator + hibernate-validator + test + + + + org.glassfish.expressly + expressly + test + + + + org.assertj + assertj-core + test + + + + com.tngtech.archunit + archunit + test + + + + com.tngtech.archunit + archunit-junit5 + test + + + + org.mockito + mockito-core + test + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.apache.maven.plugins + maven-source-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + org.apache.maven.plugins + maven-jar-plugin + + + **/* + + + + org.fuin.cqrs4j.esc + + + + + + + org.apache.maven.plugins + maven-jdeps-plugin + + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + + + + io.smallrye + jandex-maven-plugin + + + + org.apache.maven.plugins + maven-dependency-plugin + + + org.glassfish.expressly:expressly + org.hibernate.validator:hibernate-validator + com.tngtech.archunit:archunit-junit5 + org.junit.jupiter:junit-jupiter + + + com.tngtech.archunit:archunit-junit5-api + org.junit.jupiter:junit-jupiter-api + + + + + + + + + diff --git a/src/main/java/org/fuin/cqrs4j/EventDispatcher.java b/esc/src/main/java/org/fuin/cqrs4j/esc/JpaEventDispatcher.java similarity index 58% rename from src/main/java/org/fuin/cqrs4j/EventDispatcher.java rename to esc/src/main/java/org/fuin/cqrs4j/esc/JpaEventDispatcher.java index 8e3edb4..de2d566 100644 --- a/src/main/java/org/fuin/cqrs4j/EventDispatcher.java +++ b/esc/src/main/java/org/fuin/cqrs4j/esc/JpaEventDispatcher.java @@ -1,39 +1,39 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; - -import java.util.List; -import java.util.Set; +package org.fuin.cqrs4j.esc; +import jakarta.persistence.EntityManager; import jakarta.validation.constraints.NotNull; - -import org.fuin.ddd4j.ddd.Event; -import org.fuin.ddd4j.ddd.EventType; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventType; import org.fuin.esc.api.CommonEvent; +import java.util.List; +import java.util.Set; + /** - * Registry with all event handlers. + * Registry with all JPA event handlers. */ -public interface EventDispatcher { +public interface JpaEventDispatcher { /** * Returns a set of all known types. - * + * * @return All known event types. */ @NotNull @@ -41,27 +41,27 @@ public interface EventDispatcher { /** * Dispatch all common events to the appropriate event handler. - * - * @param commonEvents - * Events to dispatch. + * + * @param entityManager Entity manager to use. + * @param commonEvents Events to dispatch. */ - public void dispatchCommonEvents(@NotNull List commonEvents); + public void dispatchCommonEvents(@NotNull EntityManager entityManager, @NotNull List commonEvents); /** * Dispatch all events to the appropriate event handler. - * - * @param events - * Events to dispatch. + * + * @param entityManager Entity manager to use. + * @param events Events to dispatch. */ - public void dispatchEvents(@NotNull List events); + public void dispatchEvents(@NotNull EntityManager entityManager, @NotNull List events); /** * Dispatches the given event to the appropriate event handler. The event is ignored if no event handler can be found that is capable of * handling it. - * - * @param event - * Event to dispatch. + * + * @param entityManager Entity manager to use. + * @param event Event to dispatch. */ - public void dispatchEvent(@NotNull Event event); + public void dispatchEvent(@NotNull EntityManager entityManager, @NotNull Event event); } diff --git a/src/main/java/org/fuin/cqrs4j/ProjectionService.java b/esc/src/main/java/org/fuin/cqrs4j/esc/ProjectionService.java similarity index 92% rename from src/main/java/org/fuin/cqrs4j/ProjectionService.java rename to esc/src/main/java/org/fuin/cqrs4j/esc/ProjectionService.java index 1877902..1ec6dff 100644 --- a/src/main/java/org/fuin/cqrs4j/ProjectionService.java +++ b/esc/src/main/java/org/fuin/cqrs4j/esc/ProjectionService.java @@ -1,24 +1,23 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.esc; import jakarta.validation.constraints.NotNull; - import org.fuin.esc.api.StreamId; /** @@ -28,19 +27,19 @@ public interface ProjectionService { /** * Sets the stored position of the projection to the start position. - * + * * @param streamId * Unique ID of the stream. - * + * */ public void resetProjectionPosition(@NotNull final StreamId streamId); /** * Reads the position that was read last time. - * + * * @param streamId * Unique ID of the stream. - * + * * @return Number of the next event to read. */ @NotNull @@ -48,7 +47,7 @@ public interface ProjectionService { /** * Updates the position to read next time. - * + * * @param streamId * Unique ID of the stream. * @param nextEventNumber diff --git a/src/main/java/org/fuin/cqrs4j/SimpleEventDispatcher.java b/esc/src/main/java/org/fuin/cqrs4j/esc/SimpleJpaEventDispatcher.java similarity index 54% rename from src/main/java/org/fuin/cqrs4j/SimpleEventDispatcher.java rename to esc/src/main/java/org/fuin/cqrs4j/esc/SimpleJpaEventDispatcher.java index 0f97828..86efe07 100644 --- a/src/main/java/org/fuin/cqrs4j/SimpleEventDispatcher.java +++ b/esc/src/main/java/org/fuin/cqrs4j/esc/SimpleJpaEventDispatcher.java @@ -1,21 +1,29 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.esc; + +import jakarta.persistence.EntityManager; +import jakarta.validation.constraints.NotNull; +import org.fuin.cqrs4j.core.JpaEventHandler; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventType; +import org.fuin.esc.api.CommonEvent; +import org.fuin.objects4j.common.Contract; import java.util.ArrayList; import java.util.Arrays; @@ -24,53 +32,46 @@ import java.util.Map; import java.util.Set; -import jakarta.validation.constraints.NotNull; - -import org.fuin.ddd4j.ddd.Event; -import org.fuin.ddd4j.ddd.EventType; -import org.fuin.esc.api.CommonEvent; -import org.fuin.objects4j.common.Contract; - /** - * Registry with all event handlers. + * Registry with all JPA event handlers. */ -public final class SimpleEventDispatcher implements EventDispatcher { +public final class SimpleJpaEventDispatcher implements JpaEventDispatcher { @SuppressWarnings("rawtypes") - private final Map> eventHandlers; + private final Map> eventHandlers; /** * Constructor with array of event handlers. - * - * @param eventHandlers + * + * @param jpaEventHandlers * Event handlers. */ @SuppressWarnings("rawtypes") - public SimpleEventDispatcher(@NotNull final EventHandler... eventHandlers) { - this(Arrays.asList(eventHandlers)); + public SimpleJpaEventDispatcher(@NotNull final JpaEventHandler... jpaEventHandlers) { + this(Arrays.asList(jpaEventHandlers)); } /** * Constructor with list of event handlers. - * - * @param eventHandlers + * + * @param jpaEventHandlers * Event handlers. */ @SuppressWarnings("rawtypes") - public SimpleEventDispatcher(@NotNull final List eventHandlers) { + public SimpleJpaEventDispatcher(@NotNull final List jpaEventHandlers) { super(); - Contract.requireArgNotNull("eventHandlers", eventHandlers); - if (eventHandlers.isEmpty()) { + Contract.requireArgNotNull("eventHandlers", jpaEventHandlers); + if (jpaEventHandlers.isEmpty()) { throw new IllegalArgumentException("The argument 'eventHandlers' cannot be an empty list"); } this.eventHandlers = new HashMap<>(); - for (final EventHandler eventHandler : eventHandlers) { - List handlers = this.eventHandlers.get(eventHandler.getEventType()); + for (final JpaEventHandler jpaEventHandler : jpaEventHandlers) { + List handlers = this.eventHandlers.get(jpaEventHandler.getEventType()); if (handlers == null) { handlers = new ArrayList<>(); - this.eventHandlers.put(eventHandler.getEventType(), handlers); + this.eventHandlers.put(jpaEventHandler.getEventType(), handlers); } - handlers.add(eventHandler); + handlers.add(jpaEventHandler); } } @@ -81,36 +82,36 @@ public final Set getAllTypes() { } @Override - public final void dispatchCommonEvents(@NotNull final List commonEvents) { + public final void dispatchCommonEvents(@NotNull EntityManager em, @NotNull final List commonEvents) { Contract.requireArgNotNull("commonEvents", commonEvents); for (final CommonEvent commonEvent : commonEvents) { final Event event = (Event) commonEvent.getData(); - dispatchEvent(event); + dispatchEvent(em, event); } } @Override - public final void dispatchEvents(@NotNull final List events) { + public final void dispatchEvents(@NotNull EntityManager em, @NotNull final List events) { Contract.requireArgNotNull("events", events); for (final Event event : events) { - dispatchEvent(event); + dispatchEvent(em, event); } } - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({"rawtypes", "unchecked"}) @Override - public final void dispatchEvent(@NotNull final Event event) { + public final void dispatchEvent(@NotNull EntityManager em, @NotNull final Event event) { Contract.requireArgNotNull("event", event); - final List handlers = eventHandlers.get(event.getEventType()); + final List handlers = eventHandlers.get(event.getEventType()); if (handlers != null) { - for (final EventHandler handler : handlers) { - handler.handle(event); + for (final JpaEventHandler handler : handlers) { + handler.handle(em, event); } } } diff --git a/esc/src/test/java/org/fuin/cqrs4j/esc/ArchitectureTest.java b/esc/src/test/java/org/fuin/cqrs4j/esc/ArchitectureTest.java new file mode 100644 index 0000000..99682da --- /dev/null +++ b/esc/src/test/java/org/fuin/cqrs4j/esc/ArchitectureTest.java @@ -0,0 +1,43 @@ +package org.fuin.cqrs4j.esc; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import org.fuin.cqrs4j.core.Command; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.library.DependencyRules.NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + +/** + * Tests architectural aspects. + */ +@AnalyzeClasses(packagesOf = ArchitectureTest.class, importOptions = ImportOption.DoNotIncludeTests.class) +public class ArchitectureTest { + + private static final String THIS_PACKAGE = ArchitectureTest.class.getPackageName(); + + private static final String CORE_PACKAGE = Command.class.getPackageName(); + + @ArchTest + static final ArchRule no_accesses_to_upper_package = NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + + @ArchTest + static final ArchRule access_only_to_defined_packages = classes() + .that() + .resideInAPackage(THIS_PACKAGE) + .should() + .onlyDependOnClassesThat() + .resideInAnyPackage(THIS_PACKAGE, CORE_PACKAGE, "java..", + "org.fuin.ddd4j.common..", + "org.fuin.ddd4j.core..", + "org.fuin.ddd4j.esc..", + "org.fuin.esc.api..", + "org.fuin.objects4j.common..", + "org.fuin.objects4j.core..", + "jakarta.validation.constraints..", + "org.slf4j..", + "javax.annotation.concurrent.." + ); + +} diff --git a/esc/src/test/java/org/fuin/cqrs4j/esc/BaseTest.java b/esc/src/test/java/org/fuin/cqrs4j/esc/BaseTest.java new file mode 100644 index 0000000..69bff94 --- /dev/null +++ b/esc/src/test/java/org/fuin/cqrs4j/esc/BaseTest.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2013 Future Invent Informationsmanagement GmbH. All rights + * reserved. + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ +package org.fuin.cqrs4j.esc; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import org.fuin.units4j.archunit.Units4JConditions; + +@AnalyzeClasses(packagesOf = BaseTest.class) +class BaseTest { + + @ArchTest + static final ArchRule all_classes_should_have_tests = Units4JConditions.ALL_CLASSES_SHOULD_HAVE_TESTS; + +} + diff --git a/src/test/java/org/fuin/cqrs4j/SimpleEventDispatcherTest.java b/esc/src/test/java/org/fuin/cqrs4j/esc/SimpleJpaEventDispatcherTest.java similarity index 66% rename from src/test/java/org/fuin/cqrs4j/SimpleEventDispatcherTest.java rename to esc/src/test/java/org/fuin/cqrs4j/esc/SimpleJpaEventDispatcherTest.java index c163931..bb6e935 100644 --- a/src/test/java/org/fuin/cqrs4j/SimpleEventDispatcherTest.java +++ b/esc/src/test/java/org/fuin/cqrs4j/esc/SimpleJpaEventDispatcherTest.java @@ -1,38 +1,41 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.esc; -import static org.assertj.core.api.Assertions.assertThat; - -import java.util.ArrayList; -import java.util.List; - -import org.fuin.ddd4j.ddd.AbstractEvent; -import org.fuin.ddd4j.ddd.Event; -import org.fuin.ddd4j.ddd.EventType; +import jakarta.persistence.EntityManager; +import org.fuin.cqrs4j.core.JpaEventHandler; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventType; +import org.fuin.ddd4j.jsonb.AbstractEvent; import org.fuin.esc.api.CommonEvent; import org.fuin.esc.api.EventId; import org.fuin.esc.api.SimpleCommonEvent; import org.fuin.esc.api.TypeName; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.Serial; +import java.util.ArrayList; +import java.util.List; -//CHECKSTYLE:OFF -public final class SimpleEventDispatcherTest { +import static org.assertj.core.api.Assertions.assertThat; + +public final class SimpleJpaEventDispatcherTest { private static final EventType EVENT_TYPE_A = new EventType("EventA"); @@ -42,9 +45,9 @@ public final class SimpleEventDispatcherTest { public final void testDispatchEvents() { // PREPARE - final CollectingEventHandler handlerA = new CollectingEventHandler<>(EVENT_TYPE_A); - final CollectingEventHandler handlerB = new CollectingEventHandler<>(EVENT_TYPE_B); - final EventDispatcher testee = new SimpleEventDispatcher(handlerA, handlerB); + final CollectingJpaEventHandler handlerA = new CollectingJpaEventHandler<>(EVENT_TYPE_A); + final CollectingJpaEventHandler handlerB = new CollectingJpaEventHandler<>(EVENT_TYPE_B); + final JpaEventDispatcher testee = new SimpleJpaEventDispatcher(handlerA, handlerB); final List events = new ArrayList<>(); final EventA a1 = new EventA(); @@ -58,8 +61,10 @@ public final void testDispatchEvents() { final EventB b2 = new EventB(); events.add(b2); + final EntityManager em = Mockito.mock(EntityManager.class); + // TEST - testee.dispatchEvents(events); + testee.dispatchEvents(em, events); // VERIFY assertThat(handlerA.getEvents()).containsExactly(a1, a2, a3); @@ -71,9 +76,9 @@ public final void testDispatchEvents() { public final void testDispatchCommonEvents() { // PREPARE - final CollectingEventHandler handlerA = new CollectingEventHandler<>(EVENT_TYPE_A); - final CollectingEventHandler handlerB = new CollectingEventHandler<>(EVENT_TYPE_B); - final EventDispatcher testee = new SimpleEventDispatcher(handlerA, handlerB); + final CollectingJpaEventHandler handlerA = new CollectingJpaEventHandler<>(EVENT_TYPE_A); + final CollectingJpaEventHandler handlerB = new CollectingJpaEventHandler<>(EVENT_TYPE_B); + final JpaEventDispatcher testee = new SimpleJpaEventDispatcher(handlerA, handlerB); final List events = new ArrayList<>(); final EventA a1 = new EventA(); @@ -87,8 +92,10 @@ public final void testDispatchCommonEvents() { final EventB b2 = new EventB(); events.add(asCommonEvent(b2)); + final EntityManager em = Mockito.mock(EntityManager.class); + // TEST - testee.dispatchCommonEvents(events); + testee.dispatchCommonEvents(em, events); // VERIFY assertThat(handlerA.getEvents()).containsExactly(a1, a2, a3); @@ -100,9 +107,9 @@ public final void testDispatchCommonEvents() { public final void testGetAllTypes() { // PREPARE - final CollectingEventHandler handlerA = new CollectingEventHandler<>(EVENT_TYPE_A); - final CollectingEventHandler handlerB = new CollectingEventHandler<>(EVENT_TYPE_B); - final EventDispatcher testee = new SimpleEventDispatcher(handlerA, handlerB); + final CollectingJpaEventHandler handlerA = new CollectingJpaEventHandler<>(EVENT_TYPE_A); + final CollectingJpaEventHandler handlerB = new CollectingJpaEventHandler<>(EVENT_TYPE_B); + final JpaEventDispatcher testee = new SimpleJpaEventDispatcher(handlerA, handlerB); final List typeList = new ArrayList<>(); typeList.add(handlerA.getEventType()); @@ -117,10 +124,10 @@ public final void testGetAllTypes() { public final void testMultipleEventHandlersForOneEvent() { // PREPARE - final CollectingEventHandler handlerA1 = new CollectingEventHandler<>(EVENT_TYPE_A); - final CollectingEventHandler handlerA2 = new CollectingEventHandler<>(EVENT_TYPE_A); - final CollectingEventHandler handlerB = new CollectingEventHandler<>(EVENT_TYPE_B); - final EventDispatcher testee = new SimpleEventDispatcher(handlerA1, handlerA2, handlerB); + final CollectingJpaEventHandler handlerA1 = new CollectingJpaEventHandler<>(EVENT_TYPE_A); + final CollectingJpaEventHandler handlerA2 = new CollectingJpaEventHandler<>(EVENT_TYPE_A); + final CollectingJpaEventHandler handlerB = new CollectingJpaEventHandler<>(EVENT_TYPE_B); + final JpaEventDispatcher testee = new SimpleJpaEventDispatcher(handlerA1, handlerA2, handlerB); final List events = new ArrayList<>(); final EventA a1 = new EventA(); @@ -134,8 +141,10 @@ public final void testMultipleEventHandlersForOneEvent() { final EventB b2 = new EventB(); events.add(b2); + final EntityManager em = Mockito.mock(EntityManager.class); + // TEST - testee.dispatchEvents(events); + testee.dispatchEvents(em, events); // VERIFY assertThat(handlerA1.getEvents()).containsExactly(a1, a2, a3); @@ -152,6 +161,7 @@ private static CommonEvent asCommonEvent(final Event event) { private static class EventA extends AbstractEvent { + @Serial private static final long serialVersionUID = 1L; @Override @@ -163,6 +173,7 @@ public EventType getEventType() { private static class EventB extends AbstractEvent { + @Serial private static final long serialVersionUID = 1L; @Override @@ -172,14 +183,14 @@ public EventType getEventType() { } - @SuppressWarnings({ "unused" }) - private static class CollectingEventHandler implements EventHandler { + @SuppressWarnings({"unused"}) + private static class CollectingJpaEventHandler implements JpaEventHandler { private EventType type; private List events; - public CollectingEventHandler(EventType type) { + public CollectingJpaEventHandler(EventType type) { super(); this.type = type; this.events = new ArrayList(); @@ -191,15 +202,10 @@ public EventType getEventType() { } @Override - public void handle(TYPE event) { + public void handle(EntityManager em, TYPE event) { events.add(event); } - @SuppressWarnings("unused") - public EventType getType() { - return type; - } - public List getEvents() { return events; } @@ -207,4 +213,3 @@ public List getEvents() { } } -// CHECKSTYLE:ON diff --git a/jackson/pom.xml b/jackson/pom.xml new file mode 100644 index 0000000..e07af4c --- /dev/null +++ b/jackson/pom.xml @@ -0,0 +1,238 @@ + + + + 4.0.0 + + + org.fuin.cqrs4j + cqrs-4-java + 0.6.0-SNAPSHOT + ../pom.xml + + + cqrs-4-java-jackson + jar + ${description} (JACKSON) + + + + + + + org.fuin.cqrs4j + cqrs-4-java-core + + + + org.fuin.ddd4j + ddd-4-java-core + + + + org.fuin.ddd4j + ddd-4-java-jackson + + + + org.fuin + utils4j + + + + org.fuin.objects4j + objects4j-common + + + + org.fuin.objects4j + objects4j-jackson + + + + org.fuin.objects4j + objects4j-ui + + + + jakarta.validation + jakarta.validation-api + + + + jakarta.annotation + jakarta.annotation-api + + + + com.fasterxml.jackson.core + jackson-annotations + + + + com.fasterxml.jackson.core + jackson-databind + + + + com.fasterxml.jackson.core + jackson-core + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + + + + + + org.junit.jupiter + junit-jupiter + test + + + + org.fuin + units4j + test + + + + org.fuin.esc + esc-api + test + + + + org.hibernate.validator + hibernate-validator + test + + + + org.glassfish.expressly + expressly + test + + + + org.assertj + assertj-core + test + + + + nl.jqno.equalsverifier + equalsverifier + test + + + + com.tngtech.archunit + archunit + test + + + + com.tngtech.archunit + archunit-junit5 + test + + + + net.javacrumbs.json-unit + json-unit-fluent + test + + + + com.google.code.gson + gson + test + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.apache.maven.plugins + maven-source-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + org.apache.maven.plugins + maven-jar-plugin + + + **/* + + + + org.fuin.cqrs4j.jackson + + + + + + + org.apache.maven.plugins + maven-jdeps-plugin + + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + + + + io.smallrye + jandex-maven-plugin + + + + org.apache.maven.plugins + maven-dependency-plugin + + + jakarta.el:jakarta.el-api + org.glassfish.expressly:expressly + org.hibernate.validator:hibernate-validator + com.tngtech.archunit:archunit-junit5 + org.junit.jupiter:junit-jupiter + com.google.code.gson:gson + + + com.tngtech.archunit:archunit-junit5-api + org.junit.jupiter:junit-jupiter-api + + + + + + + + + diff --git a/jackson/src/main/java/org/fuin/cqrs4j/jackson/AbstractAggregateCommand.java b/jackson/src/main/java/org/fuin/cqrs4j/jackson/AbstractAggregateCommand.java new file mode 100644 index 0000000..27ce6f1 --- /dev/null +++ b/jackson/src/main/java/org/fuin/cqrs4j/jackson/AbstractAggregateCommand.java @@ -0,0 +1,260 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import org.fuin.cqrs4j.core.AggregateCommand; +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.AggregateVersion; +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityIdPath; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventId; +import org.fuin.objects4j.common.Contract; + +import java.io.Serial; + +/** + * Base class for all commands that are directed to an existing aggregate. + * + * @param + * Type of the aggregate root identifier. + * @param + * Type of the identifier (the last one in the path). + */ +public abstract class AbstractAggregateCommand extends AbstractCommand + implements AggregateCommand { + + @Serial + private static final long serialVersionUID = 1000L; + + @NotNull + @JsonProperty("entity-id-path") + private EntityIdPath entityIdPath; + + @Nullable + @JsonProperty("aggregate-version") + private AggregateVersion aggregateVersion; + + /** + * Default constructor for JAXB. + */ + protected AbstractAggregateCommand() { // NOSONAR Ignore uninitialized fields + super(); + } + + /** + * Constructor with aggregate root id and version. + * + * @param aggregateRootId + * Aggregate root identifier. + * @param aggregateVersion + * Expected aggregate version. + */ + public AbstractAggregateCommand(@NotNull final AggregateRootId aggregateRootId, @Nullable final AggregateVersion aggregateVersion) { + this(new EntityIdPath(aggregateRootId), aggregateVersion); + } + + /** + * Constructor with entitiy id path and version. + * + * @param entityIdPath + * Path from root aggregate to target entity. + * @param aggregateVersion + * Expected aggregate version. + */ + public AbstractAggregateCommand(@NotNull final EntityIdPath entityIdPath, @Nullable final AggregateVersion aggregateVersion) { + super(); + Contract.requireArgNotNull("entityIdPath", entityIdPath); + this.entityIdPath = entityIdPath; + this.aggregateVersion = aggregateVersion; + } + + /** + * Constructor with event this one responds to. Convenience method to set the correlation and causation identifiers correctly. + * + * @param entityIdPath + * Path from root aggregate to target entity. + * @param aggregateVersion + * Expected aggregate version. + * @param respondTo + * Causing event. + */ + public AbstractAggregateCommand(@NotNull final EntityIdPath entityIdPath, @Nullable final AggregateVersion aggregateVersion, + @NotNull final Event respondTo) { + super(respondTo); + Contract.requireArgNotNull("entityIdPath", entityIdPath); + this.entityIdPath = entityIdPath; + this.aggregateVersion = aggregateVersion; + } + + /** + * Constructor with optional data. + * + * @param entityIdPath + * Path from root aggregate to target entity. + * @param aggregateVersion + * Expected aggregate version. + * @param correlationId + * Correlation ID. + * @param causationId + * ID of the event that caused this one. + */ + public AbstractAggregateCommand(@NotNull final EntityIdPath entityIdPath, @Nullable final AggregateVersion aggregateVersion, + @Nullable final EventId correlationId, @Nullable final EventId causationId) { + super(correlationId, causationId); + Contract.requireArgNotNull("entityIdPath", entityIdPath); + this.entityIdPath = entityIdPath; + this.aggregateVersion = aggregateVersion; + } + + @Override + @NotNull + @JsonIgnore + public final EntityIdPath getEntityIdPath() { + return entityIdPath; + } + + @Override + @NotNull + @JsonIgnore + public final ENTITY_ID getEntityId() { + return entityIdPath.last(); + } + + @Override + @NotNull + @JsonIgnore + public final ROOT_ID getAggregateRootId() { + return entityIdPath.first(); + } + + @Override + @Nullable + @JsonIgnore + public final AggregateVersion getAggregateVersion() { + return aggregateVersion; + } + + @Override + @Nullable + @JsonIgnore + public final Integer getAggregateVersionInteger() { + if (aggregateVersion == null) { + return null; + } + return aggregateVersion.asBaseType(); + } + + /** + * Base class for event builders. + * + * @param + * Type of the aggregate identifier. + * @param + * Type of the entity identifier. + * @param + * Type of the event. + * @param + * Type of the builder. + */ + protected abstract static class Builder, BUILDER extends AbstractCommand.Builder> + extends AbstractCommand.Builder { + + private AbstractAggregateCommand delegate; + + /** + * Constructor with event. + * + * @param delegate + * Event to populate with data. + */ + public Builder(final TYPE delegate) { + super(delegate); + this.delegate = delegate; + } + + /** + * Sets the identifier path from aggregate root to the entity that emitted the event. + * + * @param entityIdPath + * Path of entity identifiers. + * + * @return This builder. + */ + @SuppressWarnings("unchecked") + public final BUILDER entityIdPath(@NotNull final EntityIdPath entityIdPath) { + Contract.requireArgNotNull("entityIdPath", entityIdPath); + delegate.entityIdPath = entityIdPath; + return (BUILDER) this; + } + + /** + * Convenience method to set the entity identifier path if the path has only the aggregate root identifier. + * + * @param id + * Aggregate root identifier that will be used to create the entity id path. + * + * @return This builder. + */ + @SuppressWarnings("unchecked") + public final BUILDER entityIdPath(@NotNull AggregateRootId id) { + Contract.requireArgNotNull("id", id); + delegate.entityIdPath = new EntityIdPath(id); + return (BUILDER) this; + } + + /** + * Sets the expected aggregate version. + * + * @param aggregateVersion + * Expected aggregate version. + * + * @return This builder. + */ + @SuppressWarnings("unchecked") + public final BUILDER aggregateVersion(@Nullable final AggregateVersion aggregateVersion) { + delegate.aggregateVersion = aggregateVersion; + return (BUILDER) this; + } + + /** + * Ensures that everything is set up for building the object or throws a runtime exception otherwise. + */ + protected final void ensureBuildableAbstractAggregateCommand() { + ensureBuildableAbstractCommand(); + ensureNotNull("entityIdPath", delegate.entityIdPath); + } + + /** + * Sets the internal instance to a new one. This must be called within the build method. + * + * @param delegate + * Delegate to use. + */ + protected final void resetAbstractAggregateCommand(final TYPE delegate) { + resetAbstractCommand(delegate); + this.delegate = delegate; + } + + } + +} diff --git a/src/main/java/org/fuin/cqrs4j/AbstractCommand.java b/jackson/src/main/java/org/fuin/cqrs4j/jackson/AbstractCommand.java similarity index 83% rename from src/main/java/org/fuin/cqrs4j/AbstractCommand.java rename to jackson/src/main/java/org/fuin/cqrs4j/jackson/AbstractCommand.java index ff3aca8..f42ec86 100644 --- a/src/main/java/org/fuin/cqrs4j/AbstractCommand.java +++ b/jackson/src/main/java/org/fuin/cqrs4j/jackson/AbstractCommand.java @@ -1,35 +1,38 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.jackson; +import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; +import org.fuin.cqrs4j.core.Command; +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventId; +import org.fuin.ddd4j.jackson.AbstractEvent; -import org.fuin.ddd4j.ddd.AbstractEvent; -import org.fuin.ddd4j.ddd.EntityId; -import org.fuin.ddd4j.ddd.Event; -import org.fuin.ddd4j.ddd.EventId; -import org.fuin.objects4j.common.Nullable; +import java.io.Serial; /** * Base class for all commands. */ public abstract class AbstractCommand extends AbstractEvent implements Command { + @Serial private static final long serialVersionUID = 1000L; /** @@ -41,7 +44,7 @@ public AbstractCommand() { /** * Constructor with event this one responds to. Convenience method to set the correlation and causation identifiers correctly. - * + * * @param respondTo * Causing event. */ @@ -51,7 +54,7 @@ public AbstractCommand(@NotNull final Event respondTo) { /** * Constructor with optional data. - * + * * @param correlationId * Correlation ID. * @param causationId @@ -63,7 +66,7 @@ public AbstractCommand(@Nullable final EventId correlationId, @Nullable final Ev /** * Base class for event builders. - * + * * @param * Type of the entity identifier. * @param @@ -74,21 +77,18 @@ public AbstractCommand(@Nullable final EventId correlationId, @Nullable final Ev protected abstract static class Builder> extends AbstractEvent.Builder { - private AbstractCommand delegate; - /** * Constructor with event. - * + * * @param delegate * Event to populate with data. */ public Builder(final TYPE delegate) { super(delegate); - this.delegate = delegate; } /** - * Ensures that everything is setup for building the object or throws a runtime exception otherwise. + * Ensures that everything is set up for building the object or throws a runtime exception otherwise. */ protected final void ensureBuildableAbstractCommand() { ensureBuildableAbstractEvent(); @@ -96,13 +96,12 @@ protected final void ensureBuildableAbstractCommand() { /** * Sets the internal instance to a new one. This must be called within the build method. - * + * * @param delegate * Delegate to use. */ protected final void resetAbstractCommand(final TYPE delegate) { resetAbstractEvent(delegate); - this.delegate = delegate; } } diff --git a/jackson/src/main/java/org/fuin/cqrs4j/jackson/AbstractResult.java b/jackson/src/main/java/org/fuin/cqrs4j/jackson/AbstractResult.java new file mode 100644 index 0000000..2fb4477 --- /dev/null +++ b/jackson/src/main/java/org/fuin/cqrs4j/jackson/AbstractResult.java @@ -0,0 +1,149 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import org.fuin.cqrs4j.core.Result; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.objects4j.common.Contract; +import org.fuin.objects4j.common.ExceptionShortIdentifable; +import org.fuin.objects4j.ui.Label; +import org.fuin.objects4j.ui.Prompt; +import org.fuin.objects4j.ui.ShortLabel; +import org.fuin.objects4j.ui.Tooltip; + +import java.io.Serial; +import java.io.Serializable; + +/** + * Result of a request. The type signals if the execution was successful or not. In case the result is not {@link ResultType#OK}, the + * fields code and message should contain unique information to help the user identifying the cause of the problem. A result may carry some + * optional data. + * + * @param + * Type of data returned. + */ +public abstract class AbstractResult implements Result, Serializable { + + @Serial + private static final long serialVersionUID = 1000L; + + static final String TYPE_PROPERTY = "type"; + + static final String CODE_PROPERTY = "code"; + + static final String MESSAGE_PROPERTY = "message"; + + @Label("Result Type") + @ShortLabel("TYPE") + @Tooltip("Type of the result") + @Prompt("ERROR") + @NotNull + @JsonProperty(TYPE_PROPERTY) + private ResultType type; + + @Label("Result Code") + @ShortLabel("CODE") + @Tooltip("Code that uniquely identifies the result. Mostly used in case of warnings or errors.") + @Prompt("E00001") + @Nullable + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonProperty(CODE_PROPERTY) + private String code; + + @Label("Result Message") + @ShortLabel("MSG") + @Tooltip("Message that describes the result. Mostly used in case of warnings or errors.") + @Prompt("The field 'Xyz' is mandatory") + @Nullable + @JsonInclude(JsonInclude.Include.NON_NULL) + @JsonProperty(MESSAGE_PROPERTY) + private String message; + + /** + * Protected default constructor for de-serialization. + */ + protected AbstractResult() { // NOSONAR Ignore uninitialized fields + super(); + } + + /** + * Constructor with all data. + * + * @param type + * Type. + * @param code + * Code. + * @param message + * Message. + */ + public AbstractResult(@NotNull final ResultType type, @Nullable final String code, @Nullable final String message) { + Contract.requireArgNotNull("type", type); + this.type = type; + this.code = code; + this.message = message; + } + + /** + * Constructor with exception. An exception of type {@link ExceptionShortIdentifable} will be used to fill the code field + * with the identifier value. If it's not a {@link ExceptionShortIdentifable} the code field will be set using the full + * qualified class name of the exception. + * + * @param exception + * The message for the result is equal to the exception message or the simple name of the exception class if the exception + * message is null. + */ + public AbstractResult(@NotNull final Exception exception) { + super(); + Contract.requireArgNotNull("exception", exception); + this.type = ResultType.ERROR; + if (exception instanceof ExceptionShortIdentifable) { + this.code = ((ExceptionShortIdentifable) exception).getShortId(); + } else { + this.code = exception.getClass().getName(); + } + if (exception.getMessage() == null) { + this.message = ""; + } else { + this.message = exception.getMessage(); + } + } + + @Override + @JsonIgnore + public final ResultType getType() { + return type; + } + + @Override + @JsonIgnore + public final String getCode() { + return code; + } + + @Override + @JsonIgnore + public final String getMessage() { + return message; + } + +} diff --git a/jackson/src/main/java/org/fuin/cqrs4j/jackson/Cqrs4JacksonModule.java b/jackson/src/main/java/org/fuin/cqrs4j/jackson/Cqrs4JacksonModule.java new file mode 100644 index 0000000..b8fbfe7 --- /dev/null +++ b/jackson/src/main/java/org/fuin/cqrs4j/jackson/Cqrs4JacksonModule.java @@ -0,0 +1,46 @@ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleDeserializers; +import com.fasterxml.jackson.databind.module.SimpleSerializers; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.fuin.utils4j.TestOmitted; + +import java.util.List; + +/** + * Module that registers the adapters for the package. + */ +@TestOmitted("Tested with other tests") +public class Cqrs4JacksonModule extends Module { + + @Override + public String getModuleName() { + return "Cqrs4JModule"; + } + + @Override + public Iterable getDependencies() { + return List.of(new JavaTimeModule()); + } + + @Override + public void setupModule(SetupContext context) { + final SimpleSerializers serializers = new SimpleSerializers(); + serializers.addSerializer(new DataResultJacksonSerializer()); + context.addSerializers(serializers); + + final SimpleDeserializers deserializers = new SimpleDeserializers(); + deserializers.addDeserializer(DataResult.class, new DataResultJacksonDeserializer()); + context.addDeserializers(deserializers); + } + + @Override + public Version version() { + // Don't forget to change from release to SNAPSHOT and back! + return new Version(0, 6, 0, "SNAPSHOT", + "org.fuin.cqrs4j", "cqrs-4-java-jackson"); + } + +} \ No newline at end of file diff --git a/jackson/src/main/java/org/fuin/cqrs4j/jackson/DataResult.java b/jackson/src/main/java/org/fuin/cqrs4j/jackson/DataResult.java new file mode 100644 index 0000000..0727aa2 --- /dev/null +++ b/jackson/src/main/java/org/fuin/cqrs4j/jackson/DataResult.java @@ -0,0 +1,281 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.annotation.Nullable; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.ddd4j.core.ExceptionData; +import org.fuin.objects4j.common.Contract; +import org.fuin.objects4j.common.MarshalInformation; +import org.fuin.objects4j.ui.Label; +import org.fuin.objects4j.ui.Prompt; +import org.fuin.objects4j.ui.ShortLabel; +import org.fuin.objects4j.ui.Tooltip; + +import java.io.Serial; + +/** + * Result of a request that contains data in addition to the standard result fields. The type signals if the execution was successful or + * not. In case the the result is not {@link ResultType#OK}, the fields code and message should contain unique information to help the user + * identifying the cause of the problem. + * + * @param Type of data returned in case of success (type = {@link ResultType#OK}). + */ +public final class DataResult extends AbstractResult { + + @Serial + private static final long serialVersionUID = 1000L; + + static final String DATA_CLASS_PROPERTY = "data-class"; + + static final String DATA_ELEMENT_PROPERTY = "data-element"; + + @JsonProperty(DATA_CLASS_PROPERTY) + private String dataClass; + + @JsonProperty(DATA_ELEMENT_PROPERTY) + private String dataElement; + + @Label("Data") + @ShortLabel("DATA") + @Tooltip("Optional result data") + @Prompt("Optional Data") + @Valid + @SuppressWarnings("java:S1948") // We assume the unknown data is serializable + private Object data; + + /** + * Protected default constructor for de-serialization. + */ + protected DataResult() { // NOSONAR Ignore uninitialized fields + super(); + } + + /** + * Constructor without data element name. + * + * @param type Type. + * @param code Code. + * @param message Message. + * @param data Optional result data. + */ + public DataResult(@NotNull final ResultType type, @Nullable final String code, @Nullable final String message, + @Nullable final DATA data) { + super(type, code, message); + if (data instanceof MarshalInformation) { + final MarshalInformation mui = (MarshalInformation) data; + this.data = mui.getData(); + this.dataClass = mui.getDataClass().getName(); + this.dataElement = mui.getDataElement(); + } else { + this.data = data; + this.dataClass = null; + this.dataElement = null; + } + } + + /** + * Constructor with all data. + * + * @param type Type. + * @param code Code. + * @param message Message. + * @param data Optional result data. + * @param dataElement Optional name of the data element. + */ + public DataResult(@NotNull final ResultType type, @Nullable final String code, @Nullable final String message, @Nullable final DATA data, + final String dataElement) { + super(type, code, message); + this.data = data; + if (data == null) { + this.dataClass = null; + this.dataElement = null; + } else { + this.dataClass = data.getClass().getName(); + this.dataElement = dataElement; + } + } + + /** + * Constructor with exception data. + * + * @param exceptionData . + */ + public DataResult(@NotNull final ExceptionData exceptionData) { + super(exceptionData.toException()); + this.data = exceptionData; + this.dataClass = exceptionData.getClass().getName(); + this.dataElement = exceptionData.getDataElement(); + } + + /** + * Returns the name of the class contained in the data element. + * + * @return Full qualified class name. + */ + @JsonIgnore + public final String getDataClass() { + return dataClass; + } + + /** + * Returns the name of the data attribute. + * + * @return Data element name. + */ + @JsonIgnore + public final String getDataElement() { + return dataElement; + } + + /** + * Returns the result data. + * + * @return Response data. + */ + @SuppressWarnings("unchecked") + @Nullable + @JsonIgnore + public final DATA getData() { + return (DATA) data; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((getCode() == null) ? 0 : getCode().hashCode()); + result = prime * result + ((getMessage() == null) ? 0 : getMessage().hashCode()); + result = prime * result + getType().hashCode(); + result = prime * result + ((dataClass == null) ? 0 : dataClass.hashCode()); + result = prime * result + ((dataElement == null) ? 0 : dataElement.hashCode()); + result = prime * result + ((data == null) ? 0 : data.hashCode()); + return result; + } + + @Override + public final boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DataResult other = (DataResult) obj; + if (getCode() == null) { + if (other.getCode() != null) { + return false; + } + } else if (!getCode().equals(other.getCode())) { + return false; + } + if (getMessage() == null) { + if (other.getMessage() != null) { + return false; + } + } else if (!getMessage().equals(other.getMessage())) { + return false; + } + if (getType() != other.getType()) { + return false; + } + if (dataClass == null) { + if (other.dataClass != null) { + return false; + } + } else if (!dataClass.equals(other.dataClass)) { + return false; + } + if (dataElement == null) { + if (other.dataElement != null) { + return false; + } + } else if (!dataElement.equals(other.dataElement)) { + return false; + } + if (data == null) { + if (other.data != null) { + return false; + } + } else if (!data.equals(other.data)) { + return false; + } + return true; + } + + + @Override + public final String toString() { + return "Result [type=" + getType() + ", code=" + getCode() + ", message=" + getMessage() + ", dataClass=" + dataClass + + ", dataElement=" + dataElement + "]"; + } + + /** + * Create a success result without any data. + * + * @return Result with type {@link ResultType#OK}. + */ + public static DataResult ok() { + return new DataResult<>(ResultType.OK, null, null, null); + } + + /** + * Create a success result with some data. + * + * @param data Optional data. + * @param Type of data. + * @return Result with type {@link ResultType#OK}. + */ + public static DataResult ok(@Nullable final T data) { + return new DataResult<>(ResultType.OK, null, null, data); + } + + /** + * Create a success result with some data. + * + * @param data Optional data. + * @param dataElement Optional name of the data element. + * @param Type of data. + * @return Result with type {@link ResultType#OK}. + */ + public static DataResult ok(@Nullable final T data, final String dataElement) { + return new DataResult<>(ResultType.OK, null, null, data, dataElement); + } + + /** + * Create an error result without any data. + * + * @param code Code. + * @param message Message. + * @param Not used. + * @return Error result with. + */ + public static DataResult error(@NotNull final String code, @NotNull final String message) { + Contract.requireArgNotNull("code", code); + Contract.requireArgNotNull("message", message); + return new DataResult<>(ResultType.ERROR, code, message, null); + } + +} diff --git a/jackson/src/main/java/org/fuin/cqrs4j/jackson/DataResultJacksonDeserializer.java b/jackson/src/main/java/org/fuin/cqrs4j/jackson/DataResultJacksonDeserializer.java new file mode 100644 index 0000000..7f4f885 --- /dev/null +++ b/jackson/src/main/java/org/fuin/cqrs4j/jackson/DataResultJacksonDeserializer.java @@ -0,0 +1,75 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.objects4j.jackson.Objects4JacksonUtils; + +import java.io.IOException; + +/** + * Converts an {@link DataResult} from/to JSON. + */ +@SuppressWarnings("rawtypes") +public final class DataResultJacksonDeserializer extends StdDeserializer { + + /** + * Default constructor. + */ + public DataResultJacksonDeserializer() { + super(DataResult.class); + } + + @Override + public DataResult deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException { + + final JsonNode node = jp.getCodec().readTree(jp); + + final ResultType type = ResultType.valueOf(node.get(AbstractResult.TYPE_PROPERTY).asText()); + final String code; + if (node.has(AbstractResult.CODE_PROPERTY)) { + code = node.get(AbstractResult.CODE_PROPERTY).asText(); + } else { + code = null; + } + final String message; + if (node.has(AbstractResult.MESSAGE_PROPERTY)) { + message = node.get(AbstractResult.MESSAGE_PROPERTY).asText(); + } else { + message = null; + } + if (node.has(DataResult.DATA_CLASS_PROPERTY)) { + if (!node.has(DataResult.DATA_ELEMENT_PROPERTY)) { + throw new IllegalStateException( + "The '" + DataResult.DATA_ELEMENT_PROPERTY + "' was not found, but is required for deserialization: " + node); + } + final String dataClassName = node.get(DataResult.DATA_CLASS_PROPERTY).asText(); + final String dataElement = node.get(DataResult.DATA_ELEMENT_PROPERTY).asText(); + final JsonNode dataNode = node.get(dataElement); + final Object data = Objects4JacksonUtils.deserialize(jp, ctxt, dataClassName, dataNode); + return new DataResult<>(type, code, message, data, dataElement); + } + return new DataResult<>(type, code, message, null); + } + +} diff --git a/jackson/src/main/java/org/fuin/cqrs4j/jackson/DataResultJacksonSerializer.java b/jackson/src/main/java/org/fuin/cqrs4j/jackson/DataResultJacksonSerializer.java new file mode 100644 index 0000000..d885ba5 --- /dev/null +++ b/jackson/src/main/java/org/fuin/cqrs4j/jackson/DataResultJacksonSerializer.java @@ -0,0 +1,65 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; +import org.fuin.utils4j.TestOmitted; + +import java.io.IOException; + +/** + * Converts an {@link DataResult} from/to JSON. + */ +@SuppressWarnings("rawtypes") +@TestOmitted("Tested with other tests") +public final class DataResultJacksonSerializer extends StdSerializer { + + /** + * Default constructor. + */ + public DataResultJacksonSerializer() { + super(DataResult.class); + } + + @Override + public void serialize(DataResult result, JsonGenerator generator, + SerializerProvider provider) throws IOException { + + generator.writeStartObject(); + generator.writeStringField(AbstractResult.TYPE_PROPERTY, result.getType().name()); + if (result.getCode() != null) { + generator.writeStringField(AbstractResult.CODE_PROPERTY, result.getCode()); + } + if (result.getMessage() != null) { + generator.writeStringField(AbstractResult.MESSAGE_PROPERTY, result.getMessage()); + } + if (result.getData() != null) { + generator.writeStringField(DataResult.DATA_CLASS_PROPERTY, result.getData().getClass().getName()); + final String elName = result.getDataElement(); + if (elName == null) { + throw new IllegalStateException("The 'dataElementName' was empty, but is required for serialization: " + result); + } + generator.writeStringField(DataResult.DATA_ELEMENT_PROPERTY, result.getDataElement()); + generator.writeObjectField(elName, result.getData()); + } + generator.writeEndObject(); + } + +} diff --git a/src/main/java/org/fuin/cqrs4j/SimpleResult.java b/jackson/src/main/java/org/fuin/cqrs4j/jackson/SimpleResult.java similarity index 89% rename from src/main/java/org/fuin/cqrs4j/SimpleResult.java rename to jackson/src/main/java/org/fuin/cqrs4j/jackson/SimpleResult.java index 3260699..aa47c4e 100644 --- a/src/main/java/org/fuin/cqrs4j/SimpleResult.java +++ b/jackson/src/main/java/org/fuin/cqrs4j/jackson/SimpleResult.java @@ -15,23 +15,25 @@ * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.jackson; +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; -import jakarta.xml.bind.annotation.XmlRootElement; - +import org.fuin.cqrs4j.core.ResultType; import org.fuin.objects4j.common.Contract; import org.fuin.objects4j.common.ExceptionShortIdentifable; -import org.fuin.objects4j.common.Nullable; + +import java.io.Serial; /** - * Result of a request. The type signals if the execution was successful or not. In case the the result is not {@link ResultType#OK}, the + * Result of a request. The type signals if the execution was successful or not. In case the result is not {@link ResultType#OK}, the * fields code and message should contain unique information to help the user identifying the cause of the problem. A simple result does not * carry any additional data. */ -@XmlRootElement(name = "result") public final class SimpleResult extends AbstractResult { + @Serial private static final long serialVersionUID = 1000L; /** @@ -64,25 +66,23 @@ public SimpleResult(@NotNull final ResultType type, @Nullable final String code, * The message for the result is equal to the exception message or the simple name of the exception class if the exception * message is null. */ - // CHECKSTYLE:OFF:AvoidInlineConditionals public SimpleResult(@NotNull final Exception exception) { - // CHECKSTYLE:ON - super(exception); + super(exception); } @Override + @JsonIgnore public Void getData() { return null; } @Override - // CHECKSTYLE:OFF Generated code public final int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((getCode() == null) ? 0 : getCode().hashCode()); result = prime * result + ((getMessage() == null) ? 0 : getMessage().hashCode()); - result = prime * result + ((getType() == null) ? 0 : getType().hashCode()); + result = prime * result + getType().hashCode(); return result; } @@ -112,14 +112,10 @@ public final boolean equals(final Object obj) { } else if (!getMessage().equals(other.getMessage())) { return false; } - if (getType() != other.getType()) { - return false; - } - return true; + return getType() == other.getType(); } - // CHECKSTYLE:ON - + @Override public final String toString() { return "Result [type=" + getType() + ", code=" + getCode() + ", message=" + getMessage() + "]"; diff --git a/jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module b/jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module new file mode 100644 index 0000000..ea456e8 --- /dev/null +++ b/jackson/src/main/resources/META-INF/services/com.fasterxml.jackson.databind.Module @@ -0,0 +1 @@ +org.fuin.cqrs4j.jackson.Cqrs4JacksonModule \ No newline at end of file diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/ACreatedEvent.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/ACreatedEvent.java new file mode 100644 index 0000000..6c428d8 --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/ACreatedEvent.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jackson; + +import org.fuin.ddd4j.core.EntityIdPath; +import org.fuin.ddd4j.core.EventType; +import org.fuin.ddd4j.jackson.AbstractDomainEvent; +import org.fuin.esc.api.HasSerializedDataTypeConstant; +import org.fuin.esc.api.SerializedDataType; +import org.fuin.esc.api.TypeName; +import org.fuin.utils4j.TestOmitted; + +import java.io.Serial; + +@TestOmitted("This is only a test class") +@HasSerializedDataTypeConstant +public class ACreatedEvent extends AbstractDomainEvent { + + @Serial + private static final long serialVersionUID = 1L; + + /** Unique name of the event. */ + public static final TypeName TYPE = new TypeName("ACreatedEvent"); + + /** Unique name of the serialized event. */ + public static final SerializedDataType SER_TYPE = new SerializedDataType(TYPE.asBaseType()); + + private static final EventType EVENT_TYPE = new EventType(TYPE.asBaseType()); + + private AId id; + + public ACreatedEvent(final AId id) { + super(new EntityIdPath(id)); + this.id = id; + } + + public AId getId() { + return id; + } + + @Override + public EventType getEventType() { + return EVENT_TYPE; + } + +} diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/AId.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/AId.java new file mode 100644 index 0000000..9d3f546 --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/AId.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jackson; + +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.utils4j.TestOmitted; + +import java.io.Serial; + +@TestOmitted("This is only a test class") +public class AId implements AggregateRootId { + + @Serial + private static final long serialVersionUID = 1L; + + public static final EntityType TYPE = new StringBasedEntityType("A"); + + private final long id; + + public AId(final long id) { + this.id = id; + } + + @Override + public EntityType getType() { + return TYPE; + } + + @Override + public String asString() { + return "" + id; + } + + @Override + public String asTypedString() { + return getType() + " " + asString(); + } + + @Override + public String toString() { + return "AId [id=" + id + "]"; + } + +} diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/AbstractAggregateCommandTest.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/AbstractAggregateCommandTest.java new file mode 100644 index 0000000..5b937eb --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/AbstractAggregateCommandTest.java @@ -0,0 +1,430 @@ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.validation.Validation; +import org.fuin.ddd4j.core.AggregateVersion; +import org.fuin.ddd4j.core.EntityIdPath; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventId; +import org.fuin.ddd4j.core.EventType; +import org.fuin.ddd4j.jackson.AbstractEvent; +import org.fuin.objects4j.common.Contract; +import org.junit.jupiter.api.Test; + +import java.io.Serial; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.fuin.utils4j.Utils4J.deserialize; +import static org.fuin.utils4j.Utils4J.serialize; + +public class AbstractAggregateCommandTest { + + private static final EventType MY_EVENT_TYPE = new EventType("MyEvent"); + + private static final EventType MY_COMMAND_TYPE = new EventType("MyCommandt"); + + @Test + public final void testConstructor() { + + // PREPARE + final AId aid = new AId(123L); + final EntityIdPath entityIdPath = new EntityIdPath(aid); + final AggregateVersion version = new AggregateVersion(1); + + // TEST + final AbstractAggregateCommand testee = new MyCommand(entityIdPath, version); + + // VERIFY + assertThat(testee.getAggregateRootId()).isEqualTo(aid); + assertThat(testee.getEntityId()).isEqualTo(aid); + assertThat(testee.getAggregateVersion()).isEqualTo(version); + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isNull(); + assertThat(testee.getCorrelationId()).isNull(); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testConstructor2() { + + // PREPARE + final AId aid = new AId(123L); + final BId bid = new BId(1L); + final EntityIdPath entityIdPath = new EntityIdPath(aid, bid); + final AggregateVersion version = new AggregateVersion(1); + + // TEST + final AbstractAggregateCommand testee = new MyCommand2(entityIdPath, version); + + // VERIFY + assertThat(testee.getAggregateRootId()).isEqualTo(aid); + assertThat(testee.getEntityId()).isEqualTo(bid); + assertThat(testee.getAggregateVersion()).isEqualTo(version); + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isNull(); + assertThat(testee.getCorrelationId()).isNull(); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testConstructor3() { + + // PREPARE + final AId aid = new AId(123L); + final BId bid = new BId(1L); + final CId cid = new CId(2L); + final EntityIdPath entityIdPath = new EntityIdPath(aid, bid, cid); + final AggregateVersion version = new AggregateVersion(1); + + // TEST + final AbstractAggregateCommand testee = new MyCommand3(entityIdPath, version); + + // VERIFY + assertThat(testee.getAggregateRootId()).isEqualTo(aid); + assertThat(testee.getEntityId()).isEqualTo(cid); + assertThat(testee.getAggregateVersion()).isEqualTo(version); + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isNull(); + assertThat(testee.getCorrelationId()).isNull(); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testConstructorEvent() { + + // PREPARE + final AId aid = new AId(123L); + final EntityIdPath entityIdPath = new EntityIdPath(aid); + final AggregateVersion version = new AggregateVersion(1); + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyEvent event = new MyEvent(correlationId, causationId); + + // TEST + final AbstractAggregateCommand testee = new MyCommand(entityIdPath, version, event); + + // VERIFY + assertThat(testee.getAggregateRootId()).isEqualTo(aid); + assertThat(testee.getEntityId()).isEqualTo(aid); + assertThat(testee.getAggregateVersion()).isEqualTo(version); + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isEqualTo(event.getEventId()); + assertThat(testee.getCorrelationId()).isEqualTo(correlationId); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testConstructorCorrelationCausation() { + + // PREPARE + final AId aid = new AId(123L); + final EntityIdPath entityIdPath = new EntityIdPath(aid); + final AggregateVersion version = new AggregateVersion(1); + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + + // TEST + final AbstractAggregateCommand testee = new MyCommand(entityIdPath, version, correlationId, causationId); + + // VERIFY + assertThat(testee.getAggregateRootId()).isEqualTo(aid); + assertThat(testee.getEntityId()).isEqualTo(aid); + assertThat(testee.getAggregateVersion()).isEqualTo(version); + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isEqualTo(causationId); + assertThat(testee.getCorrelationId()).isEqualTo(correlationId); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testBuilder() { + + // PREPARE + final EventId eventId = new EventId(); + final ZonedDateTime timestamp = ZonedDateTime.now(); + final AId aid = new AId(123L); + final EntityIdPath entityIdPath = new EntityIdPath(aid); + final AggregateVersion version = new AggregateVersion(1); + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyCommand.Builder testee = new MyCommand.Builder(); + + // TEST + final MyCommand cmd = testee + .eventId(eventId) + .timestamp(timestamp) + .entityIdPath(entityIdPath) + .aggregateVersion(version) + .correlationId(correlationId) + .causationId(causationId).build(); + + // VERIFY + assertThat(cmd.getEventId()).isEqualTo(eventId); + assertThat(cmd.getEventTimestamp()).isEqualTo(timestamp); + assertThat(cmd.getAggregateRootId()).isEqualTo(aid); + assertThat(cmd.getEntityId()).isEqualTo(aid); + assertThat(cmd.getAggregateVersion()).isEqualTo(version); + assertThat(cmd.getEventId()).isNotNull(); + assertThat(cmd.getEventTimestamp()).isNotNull(); + assertThat(cmd.getCausationId()).isEqualTo(causationId); + assertThat(cmd.getCorrelationId()).isEqualTo(correlationId); + assertThat(cmd.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testSerializeDeserialize() { + + // PREPARE + final AId aid = new AId(123L); + final EntityIdPath entityIdPath = new EntityIdPath(aid); + final AggregateVersion version = new AggregateVersion(1); + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyCommand original = new MyCommand(entityIdPath, version, correlationId, causationId); + + // TEST + final MyCommand copy = deserialize(serialize(original)); + + // VERIFY + assertThat(copy).isEqualTo(original); + assertThat(copy.getEntityIdPath()).isEqualTo(new EntityIdPath(aid)); + assertThat(copy.getAggregateVersion()).isEqualTo(version); + assertThat(copy.getCausationId()).isEqualTo(original.getCausationId()); + assertThat(copy.getCorrelationId()).isEqualTo(original.getCorrelationId()); + assertThat(copy.getEventId()).isEqualTo(original.getEventId()); + assertThat(copy.getEventType()).isEqualTo(original.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(original.getEventTimestamp()); + + } + + @Test + public final void testMarshalUnmarshal() throws Exception { + + final ObjectMapper objectMapper = TestUtils.objectMapper(); + + // PREPARE + final AId aid = new AId(123L); + final EntityIdPath entityIdPath = new EntityIdPath(aid); + final AggregateVersion version = new AggregateVersion(1); + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyCommand original = new MyCommand(entityIdPath, version, correlationId, causationId); + + // TEST + final String json = objectMapper.writeValueAsString(original); + final MyCommand copy = objectMapper.readValue(json, MyCommand.class); + + // VERIFY + assertThat(copy).isEqualTo(original); + assertThat(copy.getEntityIdPath()).isEqualTo(new EntityIdPath(aid)); + assertThat(copy.getAggregateVersion()).isEqualTo(version); + assertThat(copy.getCausationId()).isEqualTo(original.getCausationId()); + assertThat(copy.getCorrelationId()).isEqualTo(original.getCorrelationId()); + assertThat(copy.getEventId()).isEqualTo(original.getEventId()); + assertThat(copy.getEventType()).isEqualTo(original.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(original.getEventTimestamp()); + + } + + @Test + public final void testUnmarshal() throws Exception { + + final ObjectMapper objectMapper = TestUtils.objectMapper(); + + // PREPARE + final String json = """ + { + "entity-id-path" : "A 1/B 2/C 3", + "aggregate-version" : 1, + "event-id" : "f910c6d7-debc-46e1-ae02-9ca6f4658cf5", + "event-timestamp" : "2016-09-18T10:38:08.0+02:00[Europe/Berlin]", + "correlation-id" : "2a5893a9-00da-4003-b280-98324eccdef1", + "causation-id" : "f13d3481-51b7-423f-8fe7-5c342f7d7c46" + } + """; + + // TEST + final MyCommand copy = objectMapper.readValue(json, MyCommand.class); + + // VERIFY + Contract.requireValid(Validation.buildDefaultValidatorFactory().getValidator(), copy); + assertThat(copy.getEntityIdPath()).isEqualTo(new EntityIdPath(new AId(1L), new BId(2L), new CId(3L))); + assertThat(copy.getAggregateVersion()).isEqualTo(new AggregateVersion(1)); + assertThat(copy.getCausationId()).isEqualTo(new EventId(UUID.fromString("f13d3481-51b7-423f-8fe7-5c342f7d7c46"))); + assertThat(copy.getCorrelationId()).isEqualTo(new EventId(UUID.fromString("2a5893a9-00da-4003-b280-98324eccdef1"))); + assertThat(copy.getEventId()).isEqualTo(new EventId(UUID.fromString("f910c6d7-debc-46e1-ae02-9ca6f4658cf5"))); + assertThat(copy.getEventType()).isEqualTo(copy.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(ZonedDateTime.of(2016, 9, 18, 10, 38, 8, 0, ZoneId.of("Europe/Berlin"))); + + } + + @Test + public final void testUnmarshalNullVersion() throws Exception { + + final ObjectMapper objectMapper = TestUtils.objectMapper(); + + // PREPARE + final String json = """ + { + "entity-id-path" : "A 1/B 2/C 3", + "event-id" : "f910c6d7-debc-46e1-ae02-9ca6f4658cf5", + "event-timestamp" : "2016-09-18T10:38:08.0+02:00[Europe/Berlin]", + "correlation-id" : "2a5893a9-00da-4003-b280-98324eccdef1", + "causation-id" : "f13d3481-51b7-423f-8fe7-5c342f7d7c46" + } + """; + + // TEST + final MyCommand copy = objectMapper.readValue(json, MyCommand.class); + + // VERIFY + Contract.requireValid(Validation.buildDefaultValidatorFactory().getValidator(), copy); + assertThat(copy.getEntityIdPath()).isEqualTo(new EntityIdPath(new AId(1L), new BId(2L), new CId(3L))); + assertThat(copy.getAggregateVersion()).isNull(); + assertThat(copy.getCausationId()).isEqualTo(new EventId(UUID.fromString("f13d3481-51b7-423f-8fe7-5c342f7d7c46"))); + assertThat(copy.getCorrelationId()).isEqualTo(new EventId(UUID.fromString("2a5893a9-00da-4003-b280-98324eccdef1"))); + assertThat(copy.getEventId()).isEqualTo(new EventId(UUID.fromString("f910c6d7-debc-46e1-ae02-9ca6f4658cf5"))); + assertThat(copy.getEventType()).isEqualTo(copy.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(ZonedDateTime.of(2016, 9, 18, 10, 38, 8, 0, ZoneId.of("Europe/Berlin"))); + + } + + public static class MyCommand extends AbstractAggregateCommand { + + @Serial + private static final long serialVersionUID = 1L; + + public MyCommand() { + super(); + } + + public MyCommand(EntityIdPath entityIdPath, AggregateVersion aggregateVersion) { + super(entityIdPath, aggregateVersion); + } + + public MyCommand(EntityIdPath entityIdPath, AggregateVersion aggregateVersion, Event respondTo) { + super(entityIdPath, aggregateVersion, respondTo); + } + + public MyCommand(EntityIdPath entityIdPath, AggregateVersion aggregateVersion, EventId correlationId, EventId causationId) { + super(entityIdPath, aggregateVersion, correlationId, causationId); + } + + @Override + @JsonIgnore + public EventType getEventType() { + return MY_COMMAND_TYPE; + } + + public static class Builder extends AbstractAggregateCommand.Builder { + + private MyCommand delegate; + + public Builder() { + super(new MyCommand()); + delegate = delegate(); + } + + public MyCommand build() { + ensureBuildableAbstractAggregateCommand(); + final MyCommand result = delegate; + delegate = new MyCommand(); + resetAbstractAggregateCommand(delegate); + return result; + } + + } + + } + + public static class MyCommand2 extends AbstractAggregateCommand { + + @Serial + private static final long serialVersionUID = 1L; + + public MyCommand2() { + super(); + } + + public MyCommand2(EntityIdPath entityIdPath, AggregateVersion aggregateVersion) { + super(entityIdPath, aggregateVersion); + } + + public MyCommand2(EntityIdPath entityIdPath, AggregateVersion aggregateVersion, Event respondTo) { + super(entityIdPath, aggregateVersion, respondTo); + } + + public MyCommand2(EntityIdPath entityIdPath, AggregateVersion aggregateVersion, EventId correlationId, EventId causationId) { + super(entityIdPath, aggregateVersion, correlationId, causationId); + } + + @Override + @JsonIgnore + public EventType getEventType() { + return MY_COMMAND_TYPE; + } + + } + + public static class MyCommand3 extends AbstractAggregateCommand { + + @Serial + private static final long serialVersionUID = 1L; + + public MyCommand3() { + super(); + } + + public MyCommand3(EntityIdPath entityIdPath, AggregateVersion aggregateVersion) { + super(entityIdPath, aggregateVersion); + } + + public MyCommand3(EntityIdPath entityIdPath, AggregateVersion aggregateVersion, Event respondTo) { + super(entityIdPath, aggregateVersion, respondTo); + } + + public MyCommand3(EntityIdPath entityIdPath, AggregateVersion aggregateVersion, EventId correlationId, EventId causationId) { + super(entityIdPath, aggregateVersion, correlationId, causationId); + } + + @Override + @JsonIgnore + public EventType getEventType() { + return MY_COMMAND_TYPE; + } + + } + + public static class MyEvent extends AbstractEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public MyEvent(EventId correlationId, EventId causationId) { + super(correlationId, causationId); + } + + @Override + @JsonIgnore + public EventType getEventType() { + return MY_EVENT_TYPE; + } + + } + +} diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/AbstractCommandTest.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/AbstractCommandTest.java new file mode 100644 index 0000000..eb04add --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/AbstractCommandTest.java @@ -0,0 +1,175 @@ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventId; +import org.fuin.ddd4j.core.EventType; +import org.fuin.ddd4j.jackson.AbstractEvent; +import org.junit.jupiter.api.Test; + +import java.io.Serial; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.fuin.utils4j.Utils4J.deserialize; +import static org.fuin.utils4j.Utils4J.serialize; + +public class AbstractCommandTest { + + private static final EventType MY_EVENT_TYPE = new EventType("MyEvent"); + + private static final EventType MY_COMMAND_TYPE = new EventType("MyCommand"); + + @Test + public final void testConstructorDefault() { + + // TEST + final AbstractCommand testee = new MyCommand(); + + // VERIFY + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isNull(); + assertThat(testee.getCorrelationId()).isNull(); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testConstructorEvent() { + + // PREPARE + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyEvent event = new MyEvent(correlationId, causationId); + + // TEST + final AbstractCommand testee = new MyCommand(event); + + // VERIFY + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isEqualTo(event.getEventId()); + assertThat(testee.getCorrelationId()).isEqualTo(correlationId); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testSerializeDeserialize() { + + // PREPARE + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyCommand original = new MyCommand(correlationId, causationId); + + // TEST + final MyCommand copy = deserialize(serialize(original)); + + // VERIFY + assertThat(copy).isEqualTo(original); + assertThat(copy.getCausationId()).isEqualTo(original.getCausationId()); + assertThat(copy.getCorrelationId()).isEqualTo(original.getCorrelationId()); + assertThat(copy.getEventId()).isEqualTo(original.getEventId()); + assertThat(copy.getEventType()).isEqualTo(original.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(original.getEventTimestamp()); + + } + + @Test + public final void testMarshalUnmarshal() throws Exception { + + final ObjectMapper objectMapper = TestUtils.objectMapper(); + + // PREPARE + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyCommand original = new MyCommand(correlationId, causationId); + + // TEST + final String json = objectMapper.writeValueAsString(original); + final MyCommand copy = objectMapper.readValue(json, MyCommand.class); + + // VERIFY + assertThat(copy).isEqualTo(original); + assertThat(copy.getCausationId()).isEqualTo(original.getCausationId()); + assertThat(copy.getCorrelationId()).isEqualTo(original.getCorrelationId()); + assertThat(copy.getEventId()).isEqualTo(original.getEventId()); + assertThat(copy.getEventType()).isEqualTo(original.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(original.getEventTimestamp()); + + } + + @Test + public final void testUnmarshal() throws Exception { + + final ObjectMapper objectMapper = TestUtils.objectMapper(); + + // PREPARE + final String originalJson = """ + { + "event-id" : "f910c6d7-debc-46e1-ae02-9ca6f4658cf5", + "event-timestamp" : "2016-09-18T10:38:08.0+02:00[Europe/Berlin]", + "correlation-id" : "2a5893a9-00da-4003-b280-98324eccdef1", + "causation-id" : "f13d3481-51b7-423f-8fe7-5c342f7d7c46" + } + """; + + // TEST + final MyCommand copy = objectMapper.readValue(originalJson, MyCommand.class); + + // VERIFY + assertThat(copy.getCausationId()).isEqualTo(new EventId(UUID.fromString("f13d3481-51b7-423f-8fe7-5c342f7d7c46"))); + assertThat(copy.getCorrelationId()).isEqualTo(new EventId(UUID.fromString("2a5893a9-00da-4003-b280-98324eccdef1"))); + assertThat(copy.getEventId()).isEqualTo(new EventId(UUID.fromString("f910c6d7-debc-46e1-ae02-9ca6f4658cf5"))); + assertThat(copy.getEventType()).isEqualTo(copy.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(ZonedDateTime.of(2016, 9, 18, 10, 38, 8, 0, ZoneId.of("Europe/Berlin"))); + + } + + public static class MyCommand extends AbstractCommand { + + @Serial + private static final long serialVersionUID = 1L; + + public MyCommand() { + super(); + } + + public MyCommand(Event respondTo) { + super(respondTo); + } + + public MyCommand(EventId correlationId, EventId causationId) { + super(correlationId, causationId); + } + + @Override + @JsonIgnore + public EventType getEventType() { + return MY_COMMAND_TYPE; + } + + } + + public static class MyEvent extends AbstractEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public MyEvent(EventId correlationId, EventId causationId) { + super(correlationId, causationId); + } + + @Override + @JsonIgnore + public EventType getEventType() { + return MY_EVENT_TYPE; + } + + } + +} diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/ArchitectureTest.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/ArchitectureTest.java new file mode 100644 index 0000000..6947cb1 --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/ArchitectureTest.java @@ -0,0 +1,47 @@ +package org.fuin.cqrs4j.jackson; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import org.fuin.cqrs4j.core.Command; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.library.DependencyRules.NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + +/** + * Tests architectural aspects. + */ +@AnalyzeClasses(packagesOf = ArchitectureTest.class, importOptions = ImportOption.DoNotIncludeTests.class) +public class ArchitectureTest { + + private static final String THIS_PACKAGE = ArchitectureTest.class.getPackageName(); + + private static final String CORE_PACKAGE = Command.class.getPackageName(); + + @ArchTest + static final ArchRule no_accesses_to_upper_package = NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + + @ArchTest + static final ArchRule access_only_to_defined_packages = classes() + .that() + .resideInAPackage(THIS_PACKAGE) + .should() + .onlyDependOnClassesThat() + .resideInAnyPackage(THIS_PACKAGE, CORE_PACKAGE, "java..", + "org.fuin.ddd4j.common..", + "org.fuin.ddd4j.core..", + "org.fuin.ddd4j.jackson..", + "org.fuin.objects4j.ui..", + "org.fuin.objects4j.common..", + "org.fuin.objects4j.core..", + "org.fuin.objects4j.jackson..", + "org.fuin.utils4j..", + "jakarta.validation..", + "jakarta.annotation..", + "com.fasterxml.jackson..", + "org.slf4j..", + "javax.annotation.concurrent.." + ); + +} diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/BId.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/BId.java new file mode 100644 index 0000000..ec16326 --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/BId.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jackson; + +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.utils4j.TestOmitted; + +import java.io.Serial; + +@TestOmitted("This is only a test class") +public class BId implements EntityId { + + @Serial + private static final long serialVersionUID = 1L; + + public static final EntityType TYPE = new StringBasedEntityType("B"); + + private final long id; + + public BId(final long id) { + this.id = id; + } + + @Override + public EntityType getType() { + return TYPE; + } + + @Override + public String asString() { + return "" + id; + } + + @Override + public String asTypedString() { + return getType() + " " + asString(); + } + + @Override + public String toString() { + return "BId [id=" + id + "]"; + } + +} diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/BaseTest.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/BaseTest.java new file mode 100644 index 0000000..b3982a2 --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/BaseTest.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2013 Future Invent Informationsmanagement GmbH. All rights + * reserved. + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ +package org.fuin.cqrs4j.jackson; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import org.fuin.units4j.archunit.Units4JConditions; + +@AnalyzeClasses(packagesOf = BaseTest.class) +class BaseTest { + + @ArchTest + static final ArchRule all_classes_should_have_tests = Units4JConditions.ALL_CLASSES_SHOULD_HAVE_TESTS; + +} + diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/CId.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/CId.java new file mode 100644 index 0000000..99e08bb --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/CId.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jackson; + +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.utils4j.TestOmitted; + +import java.io.Serial; + +@TestOmitted("This is only a test class") +public class CId implements EntityId { + + @Serial + private static final long serialVersionUID = 1L; + + public static final EntityType TYPE = new StringBasedEntityType("C"); + + private final long id; + + public CId(final long id) { + this.id = id; + } + + @Override + public EntityType getType() { + return TYPE; + } + + @Override + public String asString() { + return "" + id; + } + + @Override + public String asTypedString() { + return getType() + " " + asString(); + } + + @Override + public String toString() { + return "CId [id=" + id + "]"; + } + +} diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/DataResultJacksonDeserializerTest.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/DataResultJacksonDeserializerTest.java new file mode 100644 index 0000000..d659a90 --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/DataResultJacksonDeserializerTest.java @@ -0,0 +1,349 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleDeserializers; +import com.fasterxml.jackson.databind.module.SimpleSerializers; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.ddd4j.jackson.AggregateNotFoundExceptionData; +import org.fuin.objects4j.common.AsStringCapable; +import org.fuin.objects4j.jackson.ValueObjectStringJacksonDeserializer; +import org.fuin.objects4j.jackson.ValueObjectStringJacksonSerializer; +import org.junit.jupiter.api.Test; + +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the {@link DataResultJacksonDeserializer} class. + */ +public class DataResultJacksonDeserializerTest { + + @Test + public final void testToFromJson() throws Exception { + + // PREPARE + final ObjectMapper objectMapper = createObjectMapper(); + final DataResult original = DataResult.ok(new MyData(1, "one"), "my-data"); + + // TEST + final String json = objectMapper.writeValueAsString(original); + final DataResult copy = objectMapper.readValue(json, DataResult.class); + + // VERIFY + assertThat(copy).isEqualTo(original); + + } + + @Test + public final void testFromToJsonVoidResult() throws IOException { + + // PREPARE + final ObjectMapper objectMapper = createObjectMapper(); + final String jsonOriginal = """ + { + "type": "OK" + } + """; + + // TEST + final DataResult original = objectMapper.readValue(jsonOriginal, DataResult.class); + + // VERIFY + assertThat(original.getType()).isEqualTo(ResultType.OK); + assertThat(original.getCode()).isNull(); + assertThat(original.getMessage()).isNull(); + assertThat(original.getData()).isNull(); + assertThat(original.getDataClass()).isNull(); + assertThat(original.getDataElement()).isNull(); + + // TEST + final String jsonCopy = objectMapper.writeValueAsString(original); + final DataResult copy = objectMapper.readValue(jsonCopy, DataResult.class); + assertThat(copy).isEqualTo(original); + + } + + @Test + public final void testFromToJsonSimpleResultOK() throws IOException { + + // PREPARE + final ObjectMapper objectMapper = createObjectMapper(); + final String jsonOriginal = """ + { + "type": "OK" + } + """; + + // TEST + final SimpleResult original = objectMapper.readValue(jsonOriginal, SimpleResult.class); + + // VERIFY + assertThat(original.getType()).isEqualTo(ResultType.OK); + assertThat(original.getCode()).isNull(); + assertThat(original.getMessage()).isNull(); + assertThat(original.getData()).isNull(); + + // TEST + final String jsonCopy = objectMapper.writeValueAsString(original); + final SimpleResult copy = objectMapper.readValue(jsonCopy, SimpleResult.class); + assertThat(copy).isEqualTo(original); + + } + + @Test + public final void testFromToJsonSimpleResultException() throws IOException { + + // PREPARE + final ObjectMapper objectMapper = createObjectMapper(); + final String jsonOriginal = """ + { + "type": "ERROR", + "code": "DDD4J-AGGREGATE_NOT_FOUND", + "message": "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found" + } + """; + + // TEST + final SimpleResult original = objectMapper.readValue(jsonOriginal, SimpleResult.class); + + // VERIFY + assertThat(original.getType()).isEqualTo(ResultType.ERROR); + assertThat(original.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(original.getMessage()).isEqualTo("Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found"); + assertThat(original.getData()).isNull(); + + // TEST + final String jsonCopy = objectMapper.writeValueAsString(original); + final SimpleResult copy = objectMapper.readValue(jsonCopy, SimpleResult.class); + assertThat(copy).isEqualTo(original); + + } + + @Test + public final void testFromToJsonResultData() throws IOException { + + // PREPARE + final ObjectMapper objectMapper = createObjectMapper(); + final String jsonOriginal = """ + { + "type": "OK", + "data-class": "org.fuin.cqrs4j.jackson.Invoice", + "data-element": "invoice", + "invoice": { + "id" : "I-0123456" + } + } + """; + + // TEST + final DataResult original = objectMapper.readValue(jsonOriginal, DataResult.class); + + // VERIFY + assertThat(original.getType()).isEqualTo(ResultType.OK); + assertThat(original.getCode()).isNull(); + assertThat(original.getMessage()).isNull(); + assertThat(original.getDataClass()).isEqualTo(Invoice.class.getName()); + assertThat(original.getDataElement()).isEqualTo("invoice"); + assertThat(original.getData()).isInstanceOf(Invoice.class); + assertThat(original.getData().getId()).isEqualTo("I-0123456"); + + // TEST + final String jsonCopy = objectMapper.writeValueAsString(original); + final DataResult copy = objectMapper.readValue(jsonCopy, DataResult.class); + assertThat(copy).isEqualTo(original); + + } + + @Test + public final void testFromToJsonResultException() throws IOException { + + // PREPARE + final ObjectMapper objectMapper = createObjectMapper(); + final String jsonOriginal = """ + { + "type": "ERROR", + "code": "DDD4J-AGGREGATE_NOT_FOUND", + "message": "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found", + "data-class": "org.fuin.ddd4j.jackson.AggregateNotFoundExceptionData", + "data-element": "aggregate-not-found-exception", + "aggregate-not-found-exception": { + "msg": "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found", + "sid": "DDD4J-AGGREGATE_NOT_FOUND", + "aggregate-type": "Invoice", + "aggregate-id": "4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119" + } + } + """; + + // TEST + final DataResult original = objectMapper.readValue(jsonOriginal, DataResult.class); + + // VERIFY + assertThat(original.getType()).isEqualTo(ResultType.ERROR); + assertThat(original.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(original.getMessage()).isEqualTo("Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found"); + assertThat(original.getDataClass()).isEqualTo("org.fuin.ddd4j.jackson.AggregateNotFoundExceptionData"); + assertThat(original.getDataElement()).isEqualTo("aggregate-not-found-exception"); + assertThat(original.getData()).isInstanceOf(AggregateNotFoundExceptionData.class); + + // TEST + final String jsonCopy = objectMapper.writeValueAsString(original); + final DataResult copy = objectMapper.readValue(jsonCopy, DataResult.class); + assertThat(copy).isEqualTo(original); + + } + + public static ObjectMapper createObjectMapper() { + return TestUtils.objectMapper() + .registerModule(new TestAdapterModule()); + } + + + public static final class InvoiceId implements AsStringCapable { + + private String id; + + public InvoiceId(final String id) { + super(); + this.id = id; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + InvoiceId other = (InvoiceId) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + + @Override + public String toString() { + return id; + } + + @Override + public String asString() { + return id; + } + + } + + public static class TestAdapterModule extends Module { + + @Override + public String getModuleName() { + return "TestModule"; + } + + @Override + public void setupModule(SetupContext context) { + + final SimpleSerializers serializers = new SimpleSerializers(); + serializers.addSerializer(new ValueObjectStringJacksonSerializer<>(InvoiceId.class)); + context.addSerializers(serializers); + + final SimpleDeserializers deserializers = new SimpleDeserializers(); + deserializers.addDeserializer(InvoiceId.class, new ValueObjectStringJacksonDeserializer<>(InvoiceId.class, InvoiceId::new)); + context.addDeserializers(deserializers); + } + + @Override + public Version version() { + return new Version(1, 0, 0, null, + "foo", "bar"); + } + + } + + public static final class MyData { + + @JsonProperty("id") + private int id; + + @JsonProperty("name") + private String name; + + protected MyData() { + super(); + } + + public MyData(final int id, final String name) { + super(); + this.id = id; + this.name = name; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + id; + result = prime * result + ((name == null) ? 0 : name.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + MyData other = (MyData) obj; + if (id != other.id) + return false; + if (name == null) { + if (other.name != null) + return false; + } else if (!name.equals(other.name)) + return false; + return true; + } + + @Override + public String toString() { + return "MyData [id=" + id + ", name=" + name + "]"; + } + + } + +} diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/DataResultTest.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/DataResultTest.java new file mode 100644 index 0000000..bf6f3d2 --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/DataResultTest.java @@ -0,0 +1,240 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.ddd4j.core.AggregateNotFoundException; +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.ddd4j.jackson.AggregateNotFoundExceptionData; +import org.junit.jupiter.api.Test; + +import java.io.Serial; +import java.util.UUID; + +import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +public final class DataResultTest { + + private static final EntityType TEST_TYPE = new StringBasedEntityType("Test"); + + @Test + public final void testEqualsHashCode() { + EqualsVerifier.simple().forClass(DataResult.class).verify(); + } + + @Test + public final void testConstructorAll() { + + // PREPARE + final String data = "Whatever"; + + // TEST + final DataResult testee = new DataResult<>(ResultType.WARNING, "X1", "Yes!", data); + + // VERIFY + assertThat(testee.getType()).isEqualTo(ResultType.WARNING); + assertThat(testee.getCode()).isEqualTo("X1"); + assertThat(testee.getMessage()).isEqualTo("Yes!"); + assertThat(testee.getData()).isEqualTo(data); + + } + + @Test + public final void testConstructorException() { + + // PREPARE + final TestId id = new TestId(); + final AggregateNotFoundException ex = new AggregateNotFoundException(TEST_TYPE, id); + final AggregateNotFoundExceptionData exData = new AggregateNotFoundExceptionData(ex); + + // TEST + final DataResult testee = new DataResult<>(exData); + + // VERIFY + assertThat(testee.getType()).isEqualTo(ResultType.ERROR); + assertThat(testee.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(testee.getMessage()).isEqualTo(TEST_TYPE + " with id " + id.asString() + " not found"); + assertThat(testee.getData()).isInstanceOf(AggregateNotFoundExceptionData.class); + + } + + @Test + public final void testUnmarshalMarshalVoidResult() throws Exception { + + final ObjectMapper objectMapper = TestUtils.objectMapper(); + + // PREPARE + final String originalJson = """ + { + "type": "OK" + } + """; + + // TEST + final DataResult copy = objectMapper.readValue(originalJson, DataResult.class); + + // VERIFY + assertThat(copy.getType()).isEqualTo(ResultType.OK); + + // TEST + final String copyJson = objectMapper.writeValueAsString(copy); + + // VERIFY + assertThatJson(copyJson).isEqualTo(originalJson); + + } + + @Test + public final void testUnmarshalMarshalDataResult() throws Exception { + + final ObjectMapper objectMapper = TestUtils.objectMapper(); + + // PREPARE + final String originalJson = """ + { + "type": "OK", + "data-class": "org.fuin.cqrs4j.jackson.Invoice", + "data-element": "invoice", + "invoice": { + "id" : "I-0123456" + } + } + """; + + // TEST + final DataResult copy = objectMapper.readValue(originalJson, DataResult.class); + + // VERIFY + assertThat(copy.getType()).isEqualTo(ResultType.OK); + assertThat(copy.getCode()).isNull(); + assertThat(copy.getMessage()).isNull(); + assertThat(copy.getData()).isInstanceOf(Invoice.class); + assertThat(copy.getData().getId()).isEqualTo("I-0123456"); + + // TEST + final String copyJson = objectMapper.writeValueAsString(copy); + + // VERIFY + assertThatJson(copyJson).isEqualTo(originalJson); + + } + + @Test + public final void testUnmarshalExceptionResult() throws Exception { + + final ObjectMapper objectMapper = TestUtils.objectMapper(); + + // PREPARE + final String originalJson = """ + { + "type": "ERROR", + "code": "DDD4J-AGGREGATE_NOT_FOUND", + "message": "Vendor with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found", + "data-class": "org.fuin.ddd4j.jackson.AggregateNotFoundExceptionData", + "data-element": "aggregate-not-found-exception", + "aggregate-not-found-exception" : { + "msg" : "Vendor with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found", + "sid" : "DDD4J-AGGREGATE_NOT_FOUND", + "aggregate-type" : "Vendor", + "aggregate-id" : "4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119" + } + } + """; + + // TEST + final DataResult copy = objectMapper.readValue(originalJson, DataResult.class); + + // VERIFY + final String msg = "Vendor with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found"; + assertThat(copy.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(copy.getType()).isEqualTo(ResultType.ERROR); + assertThat(copy.getMessage()).isEqualTo(msg); + assertThat(copy.getData()).isInstanceOf(AggregateNotFoundExceptionData.class); + final AggregateNotFoundException anfe = copy.getData().toException(); + assertThat(anfe.getMessage()).isEqualTo(msg); + assertThat(anfe.getType()).isEqualTo("Vendor"); + assertThat(anfe.getId()).isEqualTo("4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119"); + + // TEST + final String copyJson = objectMapper.writeValueAsString(copy); + + // VERIFY + assertThatJson(copyJson).isEqualTo(originalJson); + + } + + private static class TestId implements AggregateRootId { + + @Serial + private static final long serialVersionUID = 1L; + + private UUID id = UUID.randomUUID(); + + @Override + public EntityType getType() { + return TEST_TYPE; + } + + @Override + public String asTypedString() { + return TEST_TYPE + " " + id; + } + + @Override + public String asString() { + return id.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof TestId)) { + return false; + } + TestId other = (TestId) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + + } + +} diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/Invoice.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/Invoice.java new file mode 100644 index 0000000..f0726ef --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/Invoice.java @@ -0,0 +1,76 @@ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import org.fuin.objects4j.common.MarshalInformation; +import org.fuin.utils4j.TestOmitted; + +@TestOmitted("This is only a test class") +public class Invoice implements MarshalInformation { + + @JsonProperty("id") + private String id; + + protected Invoice() { + super(); + } + + public Invoice(String id) { + super(); + this.id = id; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Invoice other = (Invoice) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + + @Override + public String toString() { + return id; + } + + @Override + @JsonIgnore + public Class getDataClass() { + return Invoice.class; + } + + @Override + @JsonIgnore + public String getDataElement() { + return Invoice.class.getName(); + } + + @Override + @JsonIgnore + public Invoice getData() { + return this; + } + + @JsonIgnore + public String getId() { + return id; + } + +} diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/MyIdFactory.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/MyIdFactory.java new file mode 100644 index 0000000..18c51d9 --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/MyIdFactory.java @@ -0,0 +1,41 @@ +package org.fuin.cqrs4j.jackson; + +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityIdFactory; +import org.fuin.utils4j.TestOmitted; + +@TestOmitted("This is only a test class") +final class MyIdFactory implements EntityIdFactory { + @Override + public EntityId createEntityId(final String type, final String id) { + if (type.equals("A")) { + return new AId(Long.valueOf(id)); + } + if (type.equals("B")) { + return new BId(Long.valueOf(id)); + } + if (type.equals("C")) { + return new CId(Long.valueOf(id)); + } + throw new IllegalArgumentException("Unknown type: '" + type + "'"); + } + + @Override + public boolean containsType(final String type) { + if (type.equals("A")) { + return true; + } + if (type.equals("B")) { + return true; + } + if (type.equals("C")) { + return true; + } + return false; + } + + @Override + public boolean isValid(String type, String id) { + return true; + } +} diff --git a/src/test/java/org/fuin/cqrs4j/SimpleResultTest.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/SimpleResultTest.java similarity index 65% rename from src/test/java/org/fuin/cqrs4j/SimpleResultTest.java rename to jackson/src/test/java/org/fuin/cqrs4j/jackson/SimpleResultTest.java index 1e3dd19..e9958f2 100644 --- a/src/test/java/org/fuin/cqrs4j/SimpleResultTest.java +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/SimpleResultTest.java @@ -1,187 +1,200 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -package org.fuin.cqrs4j; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.fuin.utils4j.jaxb.JaxbUtils.marshal; -import static org.fuin.utils4j.jaxb.JaxbUtils.unmarshal; - -import java.io.IOException; -import java.util.UUID; - -import org.apache.commons.io.IOUtils; -import org.fuin.ddd4j.ddd.AggregateNotFoundException; -import org.fuin.ddd4j.ddd.AggregateRootId; -import org.fuin.ddd4j.ddd.EntityType; -import org.fuin.ddd4j.ddd.StringBasedEntityType; -import org.junit.jupiter.api.Test; -import org.xmlunit.builder.DiffBuilder; -import org.xmlunit.diff.Diff; - -// CHECKSTYLE:OFF -public final class SimpleResultTest { - - private static final EntityType TEST_TYPE = new StringBasedEntityType("Test"); - - @Test - public final void testConstructorAll() { - - // TEST - final SimpleResult testee = new SimpleResult(ResultType.WARNING, "X1", "Yes!"); - - // VERIFY - assertThat(testee.getType()).isEqualTo(ResultType.WARNING); - assertThat(testee.getCode()).isEqualTo("X1"); - assertThat(testee.getMessage()).isEqualTo("Yes!"); - - } - - @Test - public final void testConstructorException() { - - // PREPARE - final TestId id = new TestId(); - final AggregateNotFoundException ex = new AggregateNotFoundException(TEST_TYPE, id); - - // TEST - final SimpleResult testee = new SimpleResult(ex); - - // VERIFY - assertThat(testee.getType()).isEqualTo(ResultType.ERROR); - assertThat(testee.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); - assertThat(testee.getMessage()).isEqualTo(TEST_TYPE + " with id " + id.asString() + " not found"); - - } - - @Test - public final void testMarshalUnmarshal() { - - // PREPARE - final SimpleResult original = SimpleResult.ok(); - - // TEST - final String xml = marshal(original, SimpleResult.class); - final SimpleResult copy = unmarshal(xml, SimpleResult.class); - - // VERIFY - assertThat(original).isEqualTo(copy); - - } - - @Test - public final void testUnmarshalMarshalOkResult() throws IOException { - - // PREPARE - final String xml = IOUtils.toString(this.getClass().getResourceAsStream("/simple-result-ok.xml"), "utf-8"); - - // TEST - final SimpleResult copy = unmarshal(xml, SimpleResult.class); - - // VERIFY - assertThat(copy.getType()).isEqualTo(ResultType.OK); - - // TEST - final String copyXml = marshal(copy, SimpleResult.class); - - // VERIFY - final Diff documentDiff = DiffBuilder.compare(xml).withTest(copyXml).ignoreWhitespace().build(); - - assertThat(documentDiff.hasDifferences()).describedAs(documentDiff.toString()).isFalse(); - - } - - @Test - public final void testUnmarshalExceptionResult() throws IOException { - - // PREPARE - final String xml = IOUtils.toString(this.getClass().getResourceAsStream("/simple-result-exception.xml"), "utf-8"); - - // TEST - final SimpleResult copy = unmarshal(xml, SimpleResult.class); - - // VERIFY - final String msg = "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found"; - assertThat(copy.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); - assertThat(copy.getType()).isEqualTo(ResultType.ERROR); - assertThat(copy.getMessage()).isEqualTo(msg); - - // TEST - final String copyXml = marshal(copy, SimpleResult.class, AggregateNotFoundException.class); - - // VERIFY - final Diff documentDiff = DiffBuilder.compare(xml).withTest(copyXml).ignoreWhitespace().build(); - - assertThat(documentDiff.hasDifferences()).describedAs(documentDiff.toString()).isFalse(); - - } - - private static class TestId implements AggregateRootId { - - private static final long serialVersionUID = 1L; - - private UUID id = UUID.randomUUID(); - - @Override - public EntityType getType() { - return TEST_TYPE; - } - - @Override - public String asTypedString() { - return TEST_TYPE + " " + id; - } - - @Override - public String asString() { - return id.toString(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((id == null) ? 0 : id.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof TestId)) { - return false; - } - TestId other = (TestId) obj; - if (id == null) { - if (other.id != null) { - return false; - } - } else if (!id.equals(other.id)) { - return false; - } - return true; - } - - } - -} -// CHECKSTYLE:ON +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.databind.ObjectMapper; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.ddd4j.core.AggregateNotFoundException; +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.junit.jupiter.api.Test; + +import java.io.Serial; +import java.util.UUID; + +import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +public final class SimpleResultTest { + + private static final EntityType TEST_TYPE = new StringBasedEntityType("Test"); + + @Test + public final void testEqualsHashCode() { + EqualsVerifier.simple().forClass(SimpleResult.class).verify(); + } + + @Test + public final void testConstructorAll() { + + // TEST + final SimpleResult testee = new SimpleResult(ResultType.WARNING, "X1", "Yes!"); + + // VERIFY + assertThat(testee.getType()).isEqualTo(ResultType.WARNING); + assertThat(testee.getCode()).isEqualTo("X1"); + assertThat(testee.getMessage()).isEqualTo("Yes!"); + + } + + @Test + public final void testConstructorException() { + + // PREPARE + final TestId id = new TestId(); + final AggregateNotFoundException ex = new AggregateNotFoundException(TEST_TYPE, id); + + // TEST + final SimpleResult testee = new SimpleResult(ex); + + // VERIFY + assertThat(testee.getType()).isEqualTo(ResultType.ERROR); + assertThat(testee.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(testee.getMessage()).isEqualTo(TEST_TYPE + " with id " + id.asString() + " not found"); + + } + + @Test + public final void testMarshalUnmarshal() throws Exception { + + final ObjectMapper objectMapper = TestUtils.objectMapper(); + + // PREPARE + final SimpleResult original = SimpleResult.ok(); + + // TEST + final String json = objectMapper.writeValueAsString(original); + final SimpleResult copy = objectMapper.readValue(json, SimpleResult.class); + + // VERIFY + assertThat(original).isEqualTo(copy); + + } + + @Test + public final void testUnmarshalMarshalOkResult() throws Exception { + + final ObjectMapper objectMapper = TestUtils.objectMapper(); + + // PREPARE + final String originalJson = """ + { "type": "OK" } + """; + + // TEST + final SimpleResult copy = objectMapper.readValue(originalJson, SimpleResult.class); + + // VERIFY + assertThat(copy.getType()).isEqualTo(ResultType.OK); + + // TEST + final String copyJson = objectMapper.writeValueAsString(copy); + + // VERIFY + assertThatJson(copyJson).isEqualTo(originalJson); + + } + + @Test + public final void testUnmarshalExceptionResult() throws Exception { + + final ObjectMapper objectMapper = TestUtils.objectMapper(); + + // PREPARE + final String originalJson = """ + { + "type": "ERROR", + "code": "DDD4J-AGGREGATE_NOT_FOUND", + "message": "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found" + } + """; + + // TEST + final SimpleResult copy = objectMapper.readValue(originalJson, SimpleResult.class); + + // VERIFY + final String msg = "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found"; + assertThat(copy.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(copy.getType()).isEqualTo(ResultType.ERROR); + assertThat(copy.getMessage()).isEqualTo(msg); + + // TEST + final String copyJson = objectMapper.writeValueAsString(copy); + + // VERIFY + assertThatJson(copyJson).isEqualTo(originalJson); + + } + + private static class TestId implements AggregateRootId { + + @Serial + private static final long serialVersionUID = 1L; + + private final UUID id = UUID.randomUUID(); + + @Override + public EntityType getType() { + return TEST_TYPE; + } + + @Override + public String asTypedString() { + return TEST_TYPE + " " + id; + } + + @Override + public String asString() { + return id.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof TestId)) { + return false; + } + TestId other = (TestId) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + + } + +} diff --git a/jackson/src/test/java/org/fuin/cqrs4j/jackson/TestUtils.java b/jackson/src/test/java/org/fuin/cqrs4j/jackson/TestUtils.java new file mode 100644 index 0000000..45dd456 --- /dev/null +++ b/jackson/src/test/java/org/fuin/cqrs4j/jackson/TestUtils.java @@ -0,0 +1,34 @@ +package org.fuin.cqrs4j.jackson; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.fuin.ddd4j.core.EntityIdFactory; +import org.fuin.ddd4j.jackson.Ddd4JacksonModule; +import org.fuin.objects4j.jackson.Objects4JJacksonModule; +import org.fuin.utils4j.TestOmitted; + +/** + * Utils for the package. + */ +@TestOmitted("This is only a test class") +final class TestUtils { + + private static final EntityIdFactory ENTITY_ID_FACTORY = new MyIdFactory(); + + private TestUtils() { + } + + /** + * Creates an instance with the configured values. + * + * @return New instance. + */ + public static ObjectMapper objectMapper() { + return new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .registerModule(new Cqrs4JacksonModule()) + .registerModule(new Objects4JJacksonModule()) + .registerModule(new Ddd4JacksonModule(ENTITY_ID_FACTORY)); + } + +} diff --git a/jacoco/pom.xml b/jacoco/pom.xml new file mode 100644 index 0000000..a55941c --- /dev/null +++ b/jacoco/pom.xml @@ -0,0 +1,86 @@ + + + + 4.0.0 + + + org.fuin.cqrs4j + cqrs-4-java + 0.6.0-SNAPSHOT + ../pom.xml + + + cqrs-4-java-jacoco + ${description} (JACOCO) + + + + + org.fuin.cqrs4j + cqrs-4-java-core + + + + org.fuin.cqrs4j + cqrs-4-java-esc + + + + org.fuin.cqrs4j + cqrs-4-java-jaxb + + + + org.fuin.cqrs4j + cqrs-4-java-jsonb + + + + org.fuin.cqrs4j + cqrs-4-java-jackson + + + + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.1 + + true + + + + + org.jacoco + jacoco-maven-plugin + + + report-aggregate + verify + + report-aggregate + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + true + + + + + + + + diff --git a/jacoco/src/main/java/org/fuin/cqrs4j/jacoco/Dummy.java b/jacoco/src/main/java/org/fuin/cqrs4j/jacoco/Dummy.java new file mode 100644 index 0000000..3d7f7b6 --- /dev/null +++ b/jacoco/src/main/java/org/fuin/cqrs4j/jacoco/Dummy.java @@ -0,0 +1,8 @@ +package org.fuin.cqrs4j.jacoco; + +@SuppressWarnings("java:S2094") // Empty by intention +public class Dummy { + + // Required to have a valid Java project for the JaCoCo module + +} diff --git a/jaxb/pom.xml b/jaxb/pom.xml new file mode 100644 index 0000000..c769ba3 --- /dev/null +++ b/jaxb/pom.xml @@ -0,0 +1,221 @@ + + + + 4.0.0 + + + org.fuin.cqrs4j + cqrs-4-java + 0.6.0-SNAPSHOT + ../pom.xml + + + cqrs-4-java-jaxb + jar + ${description} (JAXB) + + + + + + + org.fuin.cqrs4j + cqrs-4-java-core + + + + org.fuin.ddd4j + ddd-4-java-core + + + + org.fuin.ddd4j + ddd-4-java-jaxb + + + + org.fuin.objects4j + objects4j-common + + + + org.fuin.objects4j + objects4j-ui + + + + jakarta.validation + jakarta.validation-api + + + + jakarta.annotation + jakarta.annotation-api + + + + jakarta.xml.bind + jakarta.xml.bind-api + + + + org.fuin + utils4j + + + + + + org.junit.jupiter + junit-jupiter + test + + + + org.fuin + units4j + test + + + + org.hibernate.validator + hibernate-validator + test + + + + org.glassfish.expressly + expressly + test + + + + org.glassfish.jaxb + jaxb-runtime + test + + + + org.fuin.esc + esc-api + test + + + + org.assertj + assertj-core + test + + + + org.xmlunit + xmlunit-core + test + + + + nl.jqno.equalsverifier + equalsverifier + test + + + + com.tngtech.archunit + archunit + test + + + + com.tngtech.archunit + archunit-junit5 + test + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.apache.maven.plugins + maven-source-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + org.apache.maven.plugins + maven-jar-plugin + + + **/* + + + + org.fuin.cqrs4j.jaxb + + + + + + + org.apache.maven.plugins + maven-jdeps-plugin + + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + + + + io.smallrye + jandex-maven-plugin + + + + org.apache.maven.plugins + maven-dependency-plugin + + + jakarta.el:jakarta.el-api + org.glassfish.expressly:expressly + org.hibernate.validator:hibernate-validator + org.glassfish.jaxb:jaxb-runtime + com.tngtech.archunit:archunit-junit5 + org.junit.jupiter:junit-jupiter + + + com.tngtech.archunit:archunit-junit5-api + org.junit.jupiter:junit-jupiter-api + + + org.fuin:utils4j + + + + + + + + + diff --git a/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/AbstractAggregateCommand.java b/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/AbstractAggregateCommand.java new file mode 100644 index 0000000..9e7b9e7 --- /dev/null +++ b/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/AbstractAggregateCommand.java @@ -0,0 +1,234 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jaxb; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import org.fuin.cqrs4j.core.AggregateCommand; +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.AggregateVersion; +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityIdPath; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventId; +import org.fuin.ddd4j.jaxb.AggregateVersionXmlAdapter; +import org.fuin.ddd4j.jaxb.EntityIdPathXmlAdapter; +import org.fuin.objects4j.common.Contract; + +import java.io.Serial; + +/** + * Base class for all commands that are directed to an existing aggregate. + * + * @param Type of the aggregate root identifier. + * @param Type of the identifier (the last one in the path). + */ +public abstract class AbstractAggregateCommand extends AbstractCommand + implements AggregateCommand { + + @Serial + private static final long serialVersionUID = 1000L; + + @NotNull + @XmlJavaTypeAdapter(EntityIdPathXmlAdapter.class) + @XmlElement(name = "entity-id-path") + private EntityIdPath entityIdPath; + + @Nullable + @XmlJavaTypeAdapter(AggregateVersionXmlAdapter.class) + @XmlElement(name = "aggregate-version") + private AggregateVersion aggregateVersion; + + /** + * Default constructor for JAXB. + */ + protected AbstractAggregateCommand() { // NOSONAR Ignore uninitialized fields + super(); + } + + /** + * Constructor with aggregate root id and version. + * + * @param aggregateRootId Aggregate root identifier. + * @param aggregateVersion Expected aggregate version. + */ + public AbstractAggregateCommand(@NotNull final AggregateRootId aggregateRootId, @Nullable final AggregateVersion aggregateVersion) { + this(new EntityIdPath(aggregateRootId), aggregateVersion); + } + + /** + * Constructor with entitiy id path and version. + * + * @param entityIdPath Path from root aggregate to target entity. + * @param aggregateVersion Expected aggregate version. + */ + public AbstractAggregateCommand(@NotNull final EntityIdPath entityIdPath, @Nullable final AggregateVersion aggregateVersion) { + super(); + Contract.requireArgNotNull("entityIdPath", entityIdPath); + this.entityIdPath = entityIdPath; + this.aggregateVersion = aggregateVersion; + } + + /** + * Constructor with event this one responds to. Convenience method to set the correlation and causation identifiers correctly. + * + * @param entityIdPath Path from root aggregate to target entity. + * @param aggregateVersion Expected aggregate version. + * @param respondTo Causing event. + */ + public AbstractAggregateCommand(@NotNull final EntityIdPath entityIdPath, @Nullable final AggregateVersion aggregateVersion, + @NotNull final Event respondTo) { + super(respondTo); + Contract.requireArgNotNull("entityIdPath", entityIdPath); + this.entityIdPath = entityIdPath; + this.aggregateVersion = aggregateVersion; + } + + /** + * Constructor with optional data. + * + * @param entityIdPath Path from root aggregate to target entity. + * @param aggregateVersion Expected aggregate version. + * @param correlationId Correlation ID. + * @param causationId ID of the event that caused this one. + */ + public AbstractAggregateCommand(@NotNull final EntityIdPath entityIdPath, @Nullable final AggregateVersion aggregateVersion, + @Nullable final EventId correlationId, @Nullable final EventId causationId) { + super(correlationId, causationId); + Contract.requireArgNotNull("entityIdPath", entityIdPath); + this.entityIdPath = entityIdPath; + this.aggregateVersion = aggregateVersion; + } + + @Override + @NotNull + public final EntityIdPath getEntityIdPath() { + return entityIdPath; + } + + @Override + @NotNull + public final ENTITY_ID getEntityId() { + return entityIdPath.last(); + } + + @Override + @NotNull + public final ROOT_ID getAggregateRootId() { + return entityIdPath.first(); + } + + @Override + @Nullable + public final AggregateVersion getAggregateVersion() { + return aggregateVersion; + } + + @Override + @Nullable + public final Integer getAggregateVersionInteger() { + if (aggregateVersion == null) { + return null; + } + return aggregateVersion.asBaseType(); + } + + /** + * Base class for event builders. + * + * @param Type of the aggregate identifier. + * @param Type of the entity identifier. + * @param Type of the event. + * @param Type of the builder. + */ + protected abstract static class Builder, BUILDER extends AbstractCommand.Builder> + extends AbstractCommand.Builder { + + private AbstractAggregateCommand delegate; + + /** + * Constructor with event. + * + * @param delegate Event to populate with data. + */ + public Builder(final TYPE delegate) { + super(delegate); + this.delegate = delegate; + } + + /** + * Sets the identifier path from aggregate root to the entity that emitted the event. + * + * @param entityIdPath Path of entity identifiers. + * @return This builder. + */ + @SuppressWarnings("unchecked") + public final BUILDER entityIdPath(@NotNull final EntityIdPath entityIdPath) { + Contract.requireArgNotNull("entityIdPath", entityIdPath); + delegate.entityIdPath = entityIdPath; + return (BUILDER) this; + } + + /** + * Convenience method to set the entity identifier path if the path has only the aggregate root identifier. + * + * @param id Aggregate root identifier that will be used to create the entity id path. + * @return This builder. + */ + @SuppressWarnings("unchecked") + public final BUILDER entityIdPath(@NotNull AggregateRootId id) { + Contract.requireArgNotNull("id", id); + delegate.entityIdPath = new EntityIdPath(id); + return (BUILDER) this; + } + + /** + * Sets the expected aggregate version. + * + * @param aggregateVersion Expected aggregate version. + * @return This builder. + */ + @SuppressWarnings("unchecked") + public final BUILDER aggregateVersion(@Nullable final AggregateVersion aggregateVersion) { + delegate.aggregateVersion = aggregateVersion; + return (BUILDER) this; + } + + /** + * Ensures that everything is set up for building the object or throws a runtime exception otherwise. + */ + protected final void ensureBuildableAbstractAggregateCommand() { + ensureBuildableAbstractCommand(); + ensureNotNull("entityIdPath", delegate.entityIdPath); + } + + /** + * Sets the internal instance to a new one. This must be called within the build method. + * + * @param delegate Delegate to use. + */ + protected final void resetAbstractAggregateCommand(final TYPE delegate) { + resetAbstractCommand(delegate); + this.delegate = delegate; + } + + } + +} diff --git a/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/AbstractCommand.java b/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/AbstractCommand.java new file mode 100644 index 0000000..2c3a147 --- /dev/null +++ b/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/AbstractCommand.java @@ -0,0 +1,109 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jaxb; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import org.fuin.cqrs4j.core.Command; +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventId; +import org.fuin.ddd4j.jaxb.AbstractEvent; + +import java.io.Serial; + +/** + * Base class for all commands. + */ +public abstract class AbstractCommand extends AbstractEvent implements Command { + + @Serial + private static final long serialVersionUID = 1000L; + + /** + * Default constructor. + */ + public AbstractCommand() { + super(); + } + + /** + * Constructor with event this one responds to. Convenience method to set the correlation and causation identifiers correctly. + * + * @param respondTo + * Causing event. + */ + public AbstractCommand(@NotNull final Event respondTo) { + super(respondTo); + } + + /** + * Constructor with optional data. + * + * @param correlationId + * Correlation ID. + * @param causationId + * ID of the event that caused this one. + */ + public AbstractCommand(@Nullable final EventId correlationId, @Nullable final EventId causationId) { + super(correlationId, causationId); + } + + /** + * Base class for event builders. + * + * @param + * Type of the entity identifier. + * @param + * Type of the event. + * @param + * Type of the builder. + */ + protected abstract static class Builder> + extends AbstractEvent.Builder { + + /** + * Constructor with event. + * + * @param delegate + * Event to populate with data. + */ + public Builder(final TYPE delegate) { + super(delegate); + } + + /** + * Ensures that everything is set up for building the object or throws a runtime exception otherwise. + */ + protected final void ensureBuildableAbstractCommand() { + ensureBuildableAbstractEvent(); + } + + /** + * Sets the internal instance to a new one. This must be called within the build method. + * + * @param delegate + * Delegate to use. + */ + protected final void resetAbstractCommand(final TYPE delegate) { + resetAbstractEvent(delegate); + } + + } + +} diff --git a/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/AbstractResult.java b/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/AbstractResult.java new file mode 100644 index 0000000..ab90ef9 --- /dev/null +++ b/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/AbstractResult.java @@ -0,0 +1,137 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jaxb; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import jakarta.xml.bind.annotation.XmlElement; +import org.fuin.cqrs4j.core.Result; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.objects4j.common.Contract; +import org.fuin.objects4j.common.ExceptionShortIdentifable; +import org.fuin.objects4j.ui.Label; +import org.fuin.objects4j.ui.Prompt; +import org.fuin.objects4j.ui.ShortLabel; +import org.fuin.objects4j.ui.Tooltip; + +import java.io.Serial; +import java.io.Serializable; + +/** + * Result of a request. The type signals if the execution was successful or not. In case the result is not {@link ResultType#OK}, the + * fields code and message should contain unique information to help the user identifying the cause of the problem. A result may carry some + * optional data. + * + * @param Type of data returned. + */ +public abstract class AbstractResult implements Result, Serializable { + + @Serial + private static final long serialVersionUID = 1000L; + + static final String TYPE_PROPERTY = "type"; + + static final String CODE_PROPERTY = "code"; + + static final String MESSAGE_PROPERTY = "message"; + + @Label("Result Type") + @ShortLabel("TYPE") + @Tooltip("Type of the result") + @Prompt("ERROR") + @NotNull + @XmlElement(name = TYPE_PROPERTY) + private ResultType type; + + @Label("Result Code") + @ShortLabel("CODE") + @Tooltip("Code that uniquely identifies the result. Mostly used in case of warnings or errors.") + @Prompt("E00001") + @Nullable + @XmlElement(name = CODE_PROPERTY) + private String code; + + @Label("Result Message") + @ShortLabel("MSG") + @Tooltip("Message that describes the result. Mostly used in case of warnings or errors.") + @Prompt("The field 'Xyz' is mandatory") + @Nullable + @XmlElement(name = MESSAGE_PROPERTY) + private String message; + + /** + * Protected default constructor for de-serialization. + */ + protected AbstractResult() { // NOSONAR Ignore uninitialized fields + super(); + } + + /** + * Constructor with all data. + * + * @param type Type. + * @param code Code. + * @param message Message. + */ + public AbstractResult(@NotNull final ResultType type, @Nullable final String code, @Nullable final String message) { + Contract.requireArgNotNull("type", type); + this.type = type; + this.code = code; + this.message = message; + } + + /** + * Constructor with exception. An exception of type {@link ExceptionShortIdentifable} will be used to fill the code field + * with the identifier value. If it's not a {@link ExceptionShortIdentifable} the code field will be set using the full + * qualified class name of the exception. + * + * @param exception The message for the result is equal to the exception message or the simple name of the exception class if the exception + * message is null. + */ + public AbstractResult(@NotNull final Exception exception) { + super(); + Contract.requireArgNotNull("exception", exception); + this.type = ResultType.ERROR; + if (exception instanceof ExceptionShortIdentifable) { + this.code = ((ExceptionShortIdentifable) exception).getShortId(); + } else { + this.code = exception.getClass().getName(); + } + if (exception.getMessage() == null) { + this.message = ""; + } else { + this.message = exception.getMessage(); + } + } + + @Override + public final ResultType getType() { + return type; + } + + @Override + public final String getCode() { + return code; + } + + @Override + public final String getMessage() { + return message; + } + +} diff --git a/src/main/java/org/fuin/cqrs4j/DataResult.java b/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/DataResult.java similarity index 78% rename from src/main/java/org/fuin/cqrs4j/DataResult.java rename to jaxb/src/main/java/org/fuin/cqrs4j/jaxb/DataResult.java index b267bce..04043cc 100644 --- a/src/main/java/org/fuin/cqrs4j/DataResult.java +++ b/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/DataResult.java @@ -15,24 +15,25 @@ * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.jaxb; -import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.annotation.Nullable; import jakarta.validation.Valid; import jakarta.validation.constraints.NotNull; import jakarta.xml.bind.annotation.XmlAnyElement; import jakarta.xml.bind.annotation.XmlRootElement; import jakarta.xml.bind.annotation.XmlTransient; - +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.ddd4j.core.ExceptionData; import org.fuin.objects4j.common.Contract; -import org.fuin.objects4j.common.ExceptionShortIdentifable; import org.fuin.objects4j.common.MarshalInformation; -import org.fuin.objects4j.common.Nullable; import org.fuin.objects4j.ui.Label; import org.fuin.objects4j.ui.Prompt; import org.fuin.objects4j.ui.ShortLabel; import org.fuin.objects4j.ui.Tooltip; +import java.io.Serial; + /** * Result of a request that contains data in addition to the standard result fields. The type signals if the execution was successful or * not. In case the the result is not {@link ResultType#OK}, the fields code and message should contain unique information to help the user @@ -44,17 +45,16 @@ @XmlRootElement(name = "result") public final class DataResult extends AbstractResult { + @Serial private static final long serialVersionUID = 1000L; static final String DATA_CLASS_PROPERTY = "data-class"; static final String DATA_ELEMENT_PROPERTY = "data-element"; - @JsonbProperty(DATA_CLASS_PROPERTY) @XmlTransient private String dataClass; - @JsonbProperty(DATA_ELEMENT_PROPERTY) @XmlTransient private String dataElement; @@ -64,6 +64,7 @@ public final class DataResult extends AbstractResult { @Prompt("Optional Data") @Valid @XmlAnyElement(lax = true) + @SuppressWarnings("java:S1948") // We assume the unknown data is serializable private Object data; /** @@ -86,7 +87,7 @@ protected DataResult() { // NOSONAR Ignore uninitialized fields * Optional result data. */ public DataResult(@NotNull final ResultType type, @Nullable final String code, @Nullable final String message, - @Nullable final DATA data) { + @Nullable final DATA data) { super(type, code, message); if (data instanceof MarshalInformation) { final MarshalInformation mui = (MarshalInformation) data; @@ -128,34 +129,20 @@ public DataResult(@NotNull final ResultType type, @Nullable final String code, @ } /** - * Constructor with exception. If the exception is type {@link MarshalInformation} then it will be used as data - * field, if not data will be null. An exception of type {@link ExceptionShortIdentifable} will be used to fill the - * code field with the identifier value. If it's not a {@link ExceptionShortIdentifable} the code field will - * be set using the full qualified class name of the exception. - * - * @param exception - * The message for the result is equal to the exception message or the simple name of the exception class if the exception - * message is null. + * Constructor with exception data. + * + * @param exceptionData . */ - // CHECKSTYLE:OFF:AvoidInlineConditionals - public DataResult(@NotNull final Exception exception) { - // CHECKSTYLE:ON - super(exception); - if (exception instanceof MarshalInformation) { - final MarshalInformation mui = (MarshalInformation) exception; - this.data = mui.getData(); - this.dataClass = mui.getDataClass().getName(); - this.dataElement = mui.getDataElement(); - } else { - this.data = null; - this.dataClass = null; - this.dataElement = null; - } + public DataResult(@NotNull final ExceptionData exceptionData) { + super(exceptionData.toException()); + this.data = exceptionData; + this.dataClass = exceptionData.getClass().getName(); + this.dataElement = exceptionData.getDataElement(); } /** * Returns the name of the class contained in the data element. - * + * * @return Full qualified class name. */ public final String getDataClass() { @@ -164,7 +151,7 @@ public final String getDataClass() { /** * Returns the name of the data attribute. - * + * * @return Data element name. */ public final String getDataElement() { @@ -173,7 +160,7 @@ public final String getDataElement() { /** * Returns the result data. - * + * * @return Response data. */ @SuppressWarnings("unchecked") @@ -183,13 +170,12 @@ public final DATA getData() { } @Override - // CHECKSTYLE:OFF Generated code public final int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((getCode() == null) ? 0 : getCode().hashCode()); result = prime * result + ((getMessage() == null) ? 0 : getMessage().hashCode()); - result = prime * result + ((getType() == null) ? 0 : getType().hashCode()); + result = prime * result + getType().hashCode(); result = prime * result + ((dataClass == null) ? 0 : dataClass.hashCode()); result = prime * result + ((dataElement == null) ? 0 : dataElement.hashCode()); result = prime * result + ((data == null) ? 0 : data.hashCode()); @@ -249,7 +235,6 @@ public final boolean equals(final Object obj) { return true; } - // CHECKSTYLE:ON @Override public final String toString() { @@ -259,7 +244,7 @@ public final String toString() { /** * Create a success result without any data. - * + * * @return Result with type {@link ResultType#OK}. */ public static DataResult ok() { @@ -268,14 +253,10 @@ public static DataResult ok() { /** * Create a success result with some data. - * - * @param data - * Optional data. - * + * + * @param data Optional data. + * @param Type of data. * @return Result with type {@link ResultType#OK}. - * - * @param - * Type of data. */ public static DataResult ok(@Nullable final T data) { return new DataResult<>(ResultType.OK, null, null, data); @@ -283,16 +264,11 @@ public static DataResult ok(@Nullable final T data) { /** * Create a success result with some data. - * - * @param data - * Optional data. - * @param dataElement - * Optional name of the data element. - * + * + * @param data Optional data. + * @param dataElement Optional name of the data element. + * @param Type of data. * @return Result with type {@link ResultType#OK}. - * - * @param - * Type of data. */ public static DataResult ok(@Nullable final T data, final String dataElement) { return new DataResult<>(ResultType.OK, null, null, data, dataElement); @@ -300,16 +276,11 @@ public static DataResult ok(@Nullable final T data, final String dataElem /** * Create an error result without any data. - * - * @param code - * Code. - * @param message - * Message. - * + * + * @param code Code. + * @param message Message. + * @param Not used. * @return Error result with. - * - * @param - * Not used. */ public static DataResult error(@NotNull final String code, @NotNull final String message) { Contract.requireArgNotNull("code", code); diff --git a/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/SimpleResult.java b/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/SimpleResult.java new file mode 100644 index 0000000..cbd2505 --- /dev/null +++ b/jaxb/src/main/java/org/fuin/cqrs4j/jaxb/SimpleResult.java @@ -0,0 +1,165 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jaxb; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import jakarta.xml.bind.annotation.XmlRootElement; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.objects4j.common.Contract; +import org.fuin.objects4j.common.ExceptionShortIdentifable; + +import java.io.Serial; + +/** + * Result of a request. The type signals if the execution was successful or not. In case the result is not {@link ResultType#OK}, the + * fields code and message should contain unique information to help the user identifying the cause of the problem. A simple result does not + * carry any additional data. + */ +@XmlRootElement(name = "result") +public final class SimpleResult extends AbstractResult { + + @Serial + private static final long serialVersionUID = 1000L; + + /** + * Protected default constructor for de-serialization. + */ + protected SimpleResult() { // NOSONAR Ignore uninitialized fields + super(); + } + + /** + * Constructor with all data. + * + * @param type + * Type. + * @param code + * Code. + * @param message + * Message. + */ + public SimpleResult(@NotNull final ResultType type, @Nullable final String code, @Nullable final String message) { + super(type, code, message); + } + + /** + * Constructor with exception. An exception of type {@link ExceptionShortIdentifable} will be used to fill the code field + * with the identifier value. If it's not a {@link ExceptionShortIdentifable} the code field will be set using the full + * qualified class name of the exception. + * + * @param exception + * The message for the result is equal to the exception message or the simple name of the exception class if the exception + * message is null. + */ + public SimpleResult(@NotNull final Exception exception) { + super(exception); + } + + @Override + public Void getData() { + return null; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((getCode() == null) ? 0 : getCode().hashCode()); + result = prime * result + ((getMessage() == null) ? 0 : getMessage().hashCode()); + result = prime * result + getType().hashCode(); + return result; + } + + @Override + public final boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SimpleResult other = (SimpleResult) obj; + if (getCode() == null) { + if (other.getCode() != null) { + return false; + } + } else if (!getCode().equals(other.getCode())) { + return false; + } + if (getMessage() == null) { + if (other.getMessage() != null) { + return false; + } + } else if (!getMessage().equals(other.getMessage())) { + return false; + } + return getType() == other.getType(); + } + + + @Override + public final String toString() { + return "Result [type=" + getType() + ", code=" + getCode() + ", message=" + getMessage() + "]"; + } + + /** + * Create a success result. + * + * @return Result with type {@link ResultType#OK}. + */ + public static SimpleResult ok() { + return new SimpleResult(ResultType.OK, null, null); + } + + /** + * Create a warning result. + * + * @param code + * Code. + * @param message + * Message. + * + * @return Result with type {@link ResultType#WARNING}. + */ + public static SimpleResult warning(@NotNull final String code, @NotNull final String message) { + Contract.requireArgNotNull("code", code); + Contract.requireArgNotNull("message", message); + return new SimpleResult(ResultType.WARNING, code, message); + } + + /** + * Create an error result. + * + * @param code + * Code. + * @param message + * Message. + * + * @return Result with type {@link ResultType#ERROR}. + */ + public static SimpleResult error(@NotNull final String code, @NotNull final String message) { + Contract.requireArgNotNull("code", code); + Contract.requireArgNotNull("message", message); + return new SimpleResult(ResultType.ERROR, code, message); + } + +} diff --git a/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/ACreatedEvent.java b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/ACreatedEvent.java new file mode 100644 index 0000000..9526395 --- /dev/null +++ b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/ACreatedEvent.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jaxb; + +import org.fuin.ddd4j.core.EntityIdPath; +import org.fuin.ddd4j.core.EventType; +import org.fuin.ddd4j.jaxb.AbstractDomainEvent; +import org.fuin.esc.api.HasSerializedDataTypeConstant; +import org.fuin.esc.api.SerializedDataType; +import org.fuin.esc.api.TypeName; +import org.fuin.utils4j.TestOmitted; + +import java.io.Serial; + +@TestOmitted("This is only a test class") +@HasSerializedDataTypeConstant +public class ACreatedEvent extends AbstractDomainEvent { + + @Serial + private static final long serialVersionUID = 1L; + + /** Unique name of the event. */ + public static final TypeName TYPE = new TypeName("ACreatedEvent"); + + /** Unique name of the serialized event. */ + public static final SerializedDataType SER_TYPE = new SerializedDataType(TYPE.asBaseType()); + + private static final EventType EVENT_TYPE = new EventType(TYPE.asBaseType()); + + private AId id; + + public ACreatedEvent(final AId id) { + super(new EntityIdPath(id)); + this.id = id; + } + + public AId getId() { + return id; + } + + @Override + public EventType getEventType() { + return EVENT_TYPE; + } + +} diff --git a/src/test/java/org/fuin/cqrs4j/AId.java b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/AId.java similarity index 83% rename from src/test/java/org/fuin/cqrs4j/AId.java rename to jaxb/src/test/java/org/fuin/cqrs4j/jaxb/AId.java index a197249..e64b926 100644 --- a/src/test/java/org/fuin/cqrs4j/AId.java +++ b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/AId.java @@ -1,58 +1,61 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -package org.fuin.cqrs4j; - -import org.fuin.ddd4j.ddd.AggregateRootId; -import org.fuin.ddd4j.ddd.EntityType; -import org.fuin.ddd4j.ddd.StringBasedEntityType; - -//CHECKSTYLE:OFF -public class AId implements AggregateRootId { - - private static final long serialVersionUID = 1L; - - public static final EntityType TYPE = new StringBasedEntityType("A"); - - private final long id; - - public AId(final long id) { - this.id = id; - } - - @Override - public EntityType getType() { - return TYPE; - } - - @Override - public String asString() { - return "" + id; - } - - @Override - public String asTypedString() { - return getType() + " " + asString(); - } - - @Override - public String toString() { - return "AId [id=" + id + "]"; - } - -} -// CHECKSTYLE:ON +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jaxb; + +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.utils4j.TestOmitted; + +import java.io.Serial; + +@TestOmitted("This is only a test class") +public class AId implements AggregateRootId { + + @Serial + private static final long serialVersionUID = 1L; + + public static final EntityType TYPE = new StringBasedEntityType("A"); + + private final long id; + + public AId(final long id) { + this.id = id; + } + + @Override + public EntityType getType() { + return TYPE; + } + + @Override + public String asString() { + return "" + id; + } + + @Override + public String asTypedString() { + return getType() + " " + asString(); + } + + @Override + public String toString() { + return "AId [id=" + id + "]"; + } + +} diff --git a/src/test/java/org/fuin/cqrs4j/AbstractAggregateCommandTest.java b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/AbstractAggregateCommandTest.java similarity index 89% rename from src/test/java/org/fuin/cqrs4j/AbstractAggregateCommandTest.java rename to jaxb/src/test/java/org/fuin/cqrs4j/jaxb/AbstractAggregateCommandTest.java index f31aef1..761e921 100644 --- a/src/test/java/org/fuin/cqrs4j/AbstractAggregateCommandTest.java +++ b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/AbstractAggregateCommandTest.java @@ -1,31 +1,33 @@ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.jaxb; -import static org.assertj.core.api.Assertions.assertThat; -import static org.fuin.utils4j.jaxb.JaxbUtils.marshal; -import static org.fuin.utils4j.jaxb.JaxbUtils.unmarshal; -import static org.fuin.utils4j.Utils4J.deserialize; -import static org.fuin.utils4j.Utils4J.serialize; +import jakarta.validation.Validation; +import jakarta.xml.bind.Marshaller; +import jakarta.xml.bind.Unmarshaller; +import jakarta.xml.bind.annotation.XmlRootElement; +import jakarta.xml.bind.annotation.adapters.XmlAdapter; +import org.fuin.ddd4j.core.AggregateVersion; +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityIdFactory; +import org.fuin.ddd4j.core.EntityIdPath; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventId; +import org.fuin.ddd4j.core.EventType; +import org.fuin.ddd4j.jaxb.AbstractEvent; +import org.fuin.ddd4j.jaxb.EntityIdPathXmlAdapter; +import org.fuin.objects4j.common.Contract; +import org.fuin.utils4j.Utils4J; +import org.fuin.utils4j.jaxb.JaxbUtils; +import org.fuin.utils4j.jaxb.MarshallerBuilder; +import org.fuin.utils4j.jaxb.UnmarshallerBuilder; +import org.junit.jupiter.api.Test; +import java.io.Serial; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.UUID; -import jakarta.validation.Validation; -import jakarta.xml.bind.annotation.XmlRootElement; -import jakarta.xml.bind.annotation.adapters.XmlAdapter; - -import org.fuin.ddd4j.ddd.AbstractEvent; -import org.fuin.ddd4j.ddd.AggregateVersion; -import org.fuin.ddd4j.ddd.EntityId; -import org.fuin.ddd4j.ddd.EntityIdFactory; -import org.fuin.ddd4j.ddd.EntityIdPath; -import org.fuin.ddd4j.ddd.EntityIdPathConverter; -import org.fuin.ddd4j.ddd.Event; -import org.fuin.ddd4j.ddd.EventId; -import org.fuin.ddd4j.ddd.EventType; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; -//CHECKSTYLE:OFF Test code public class AbstractAggregateCommandTest { private static final EventType MY_EVENT_TYPE = new EventType("MyEvent"); @@ -203,7 +205,7 @@ public final void testSerializeDeserialize() { final MyCommand original = new MyCommand(entityIdPath, version, correlationId, causationId); // TEST - final MyCommand copy = deserialize(serialize(original)); + final MyCommand copy = Utils4J.deserialize(Utils4J.serialize(original)); // VERIFY assertThat(copy).isEqualTo(original); @@ -229,8 +231,10 @@ public final void testMarshalUnmarshal() { final MyCommand original = new MyCommand(entityIdPath, version, correlationId, causationId); // TEST - final String xml = marshal(original, createXmlAdapter(), MyCommand.class); - final MyCommand copy = unmarshal(xml, createXmlAdapter(), MyCommand.class); + final Marshaller marshaller = new MarshallerBuilder().addClassesToBeBound(MyCommand.class).addAdapters(createXmlAdapter()).build(); + final Unmarshaller unmarshaller = new UnmarshallerBuilder().addClassesToBeBound(MyCommand.class).addAdapters(createXmlAdapter()).build(); + final String xml = JaxbUtils.marshal(marshaller, original); + final MyCommand copy = JaxbUtils.unmarshal(unmarshaller, xml); // VERIFY assertThat(copy).isEqualTo(original); @@ -254,11 +258,13 @@ public final void testUnmarshal() { + "2a5893a9-00da-4003-b280-98324eccdef1" + "f13d3481-51b7-423f-8fe7-5c342f7d7c46"; + final Unmarshaller unmarshaller = new UnmarshallerBuilder().addClassesToBeBound(MyCommand.class).addAdapters(createXmlAdapter()).build(); + // TEST - final MyCommand copy = unmarshal(xml, createXmlAdapter(), MyCommand.class); + final MyCommand copy = JaxbUtils.unmarshal(unmarshaller, xml); // VERIFY - Cqrs4JUtils.verifyPrecondition(Validation.buildDefaultValidatorFactory().getValidator(), copy); + Contract.requireValid(Validation.buildDefaultValidatorFactory().getValidator(), copy); assertThat(copy.getEntityIdPath()).isEqualTo(new EntityIdPath(new AId(1L), new BId(2L), new CId(3L))); assertThat(copy.getAggregateVersion()).isEqualTo(new AggregateVersion(1)); assertThat(copy.getCausationId()).isEqualTo(new EventId(UUID.fromString("f13d3481-51b7-423f-8fe7-5c342f7d7c46"))); @@ -279,11 +285,13 @@ public final void testUnmarshalNullVersion() { + "2a5893a9-00da-4003-b280-98324eccdef1" + "f13d3481-51b7-423f-8fe7-5c342f7d7c46"; + final Unmarshaller unmarshaller = new UnmarshallerBuilder().addClassesToBeBound(MyCommand.class).addAdapters(createXmlAdapter()).build(); + // TEST - final MyCommand copy = unmarshal(xml, createXmlAdapter(), MyCommand.class); + final MyCommand copy = JaxbUtils.unmarshal(unmarshaller, xml); // VERIFY - Cqrs4JUtils.verifyPrecondition(Validation.buildDefaultValidatorFactory().getValidator(), copy); + Contract.requireValid(Validation.buildDefaultValidatorFactory().getValidator(), copy); assertThat(copy.getEntityIdPath()).isEqualTo(new EntityIdPath(new AId(1L), new BId(2L), new CId(3L))); assertThat(copy.getAggregateVersion()).isNull(); assertThat(copy.getCausationId()).isEqualTo(new EventId(UUID.fromString("f13d3481-51b7-423f-8fe7-5c342f7d7c46"))); @@ -297,6 +305,7 @@ public final void testUnmarshalNullVersion() { @XmlRootElement(name = "my-command") public static class MyCommand extends AbstractAggregateCommand { + @Serial private static final long serialVersionUID = 1L; public MyCommand() { @@ -344,6 +353,7 @@ public MyCommand build() { @XmlRootElement(name = "my-command-2") public static class MyCommand2 extends AbstractAggregateCommand { + @Serial private static final long serialVersionUID = 1L; public MyCommand2() { @@ -372,6 +382,7 @@ public EventType getEventType() { @XmlRootElement(name = "my-command-3") public static class MyCommand3 extends AbstractAggregateCommand { + @Serial private static final long serialVersionUID = 1L; public MyCommand3() { @@ -399,6 +410,7 @@ public EventType getEventType() { public static class MyEvent extends AbstractEvent { + @Serial private static final long serialVersionUID = 1L; public MyEvent(EventId correlationId, EventId causationId) { @@ -449,8 +461,7 @@ public boolean isValid(String type, String id) { @SuppressWarnings("rawtypes") private static XmlAdapter[] createXmlAdapter() { - return new XmlAdapter[] { new EntityIdPathConverter(new MyIdFactory()) }; + return new XmlAdapter[] { new EntityIdPathXmlAdapter(new MyIdFactory()) }; } } -// CHECKSTYLE:ON diff --git a/src/test/java/org/fuin/cqrs4j/AbstractCommandTest.java b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/AbstractCommandTest.java similarity index 81% rename from src/test/java/org/fuin/cqrs4j/AbstractCommandTest.java rename to jaxb/src/test/java/org/fuin/cqrs4j/jaxb/AbstractCommandTest.java index a661217..661f4f6 100644 --- a/src/test/java/org/fuin/cqrs4j/AbstractCommandTest.java +++ b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/AbstractCommandTest.java @@ -1,24 +1,25 @@ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.jaxb; -import static org.assertj.core.api.Assertions.assertThat; -import static org.fuin.utils4j.jaxb.JaxbUtils.marshal; -import static org.fuin.utils4j.jaxb.JaxbUtils.unmarshal; -import static org.fuin.utils4j.Utils4J.deserialize; -import static org.fuin.utils4j.Utils4J.serialize; +import jakarta.xml.bind.Marshaller; +import jakarta.xml.bind.Unmarshaller; +import jakarta.xml.bind.annotation.XmlRootElement; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventId; +import org.fuin.ddd4j.core.EventType; +import org.fuin.ddd4j.jaxb.AbstractEvent; +import org.fuin.utils4j.Utils4J; +import org.fuin.utils4j.jaxb.JaxbUtils; +import org.fuin.utils4j.jaxb.MarshallerBuilder; +import org.fuin.utils4j.jaxb.UnmarshallerBuilder; +import org.junit.jupiter.api.Test; +import java.io.Serial; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.UUID; -import jakarta.xml.bind.annotation.XmlRootElement; - -import org.fuin.ddd4j.ddd.AbstractEvent; -import org.fuin.ddd4j.ddd.Event; -import org.fuin.ddd4j.ddd.EventId; -import org.fuin.ddd4j.ddd.EventType; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; -//CHECKSTYLE:OFF Test code public class AbstractCommandTest { private static final EventType MY_EVENT_TYPE = new EventType("MyEvent"); @@ -69,7 +70,7 @@ public final void testSerializeDeserialize() { final MyCommand original = new MyCommand(correlationId, causationId); // TEST - final MyCommand copy = deserialize(serialize(original)); + final MyCommand copy = Utils4J.deserialize(Utils4J.serialize(original)); // VERIFY assertThat(copy).isEqualTo(original); @@ -90,8 +91,10 @@ public final void testMarshalUnmarshal() { final MyCommand original = new MyCommand(correlationId, causationId); // TEST - final String xml = marshal(original, MyCommand.class); - final MyCommand copy = unmarshal(xml, MyCommand.class); + final Marshaller marshaller = new MarshallerBuilder().addClassesToBeBound(MyCommand.class).build(); + final Unmarshaller unmarshaller = new UnmarshallerBuilder().addClassesToBeBound(MyCommand.class).build(); + final String xml = JaxbUtils.marshal(marshaller, original); + final MyCommand copy = JaxbUtils.unmarshal(unmarshaller, xml); // VERIFY assertThat(copy).isEqualTo(original); @@ -112,8 +115,10 @@ public final void testUnmarshal() { + "2a5893a9-00da-4003-b280-98324eccdef1" + "f13d3481-51b7-423f-8fe7-5c342f7d7c46"; + final Unmarshaller unmarshaller = new UnmarshallerBuilder().addClassesToBeBound(MyCommand.class).build(); + // TEST - final MyCommand copy = unmarshal(xml, MyCommand.class); + final MyCommand copy = JaxbUtils.unmarshal(unmarshaller, xml); // VERIFY assertThat(copy.getCausationId()).isEqualTo(new EventId(UUID.fromString("f13d3481-51b7-423f-8fe7-5c342f7d7c46"))); @@ -127,6 +132,7 @@ public final void testUnmarshal() { @XmlRootElement(name = "my-command") public static class MyCommand extends AbstractCommand { + @Serial private static final long serialVersionUID = 1L; public MyCommand() { @@ -150,6 +156,7 @@ public EventType getEventType() { public static class MyEvent extends AbstractEvent { + @Serial private static final long serialVersionUID = 1L; public MyEvent(EventId correlationId, EventId causationId) { @@ -164,4 +171,3 @@ public EventType getEventType() { } } -// CHECKSTYLE:ON diff --git a/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/ArchitectureTest.java b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/ArchitectureTest.java new file mode 100644 index 0000000..4a8613c --- /dev/null +++ b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/ArchitectureTest.java @@ -0,0 +1,45 @@ +package org.fuin.cqrs4j.jaxb; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import org.fuin.cqrs4j.core.Command; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.library.DependencyRules.NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + +/** + * Tests architectural aspects. + */ +@AnalyzeClasses(packagesOf = ArchitectureTest.class, importOptions = ImportOption.DoNotIncludeTests.class) +public class ArchitectureTest { + + private static final String THIS_PACKAGE = ArchitectureTest.class.getPackageName(); + + private static final String CORE_PACKAGE = Command.class.getPackageName(); + + @ArchTest + static final ArchRule no_accesses_to_upper_package = NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + + @ArchTest + static final ArchRule access_only_to_defined_packages = classes() + .that() + .resideInAPackage(THIS_PACKAGE) + .should() + .onlyDependOnClassesThat() + .resideInAnyPackage(THIS_PACKAGE, CORE_PACKAGE, "java..", + "org.fuin.ddd4j.common..", + "org.fuin.ddd4j.core..", + "org.fuin.ddd4j.jaxb..", + "org.fuin.objects4j.ui..", + "org.fuin.objects4j.common..", + "org.fuin.objects4j.core..", + "jakarta.validation..", + "jakarta.annotation..", + "jakarta.xml.bind..", + "org.slf4j..", + "javax.annotation.concurrent.." + ); + +} diff --git a/src/test/java/org/fuin/cqrs4j/BId.java b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/BId.java similarity index 83% rename from src/test/java/org/fuin/cqrs4j/BId.java rename to jaxb/src/test/java/org/fuin/cqrs4j/jaxb/BId.java index a7a270f..5d98d2b 100644 --- a/src/test/java/org/fuin/cqrs4j/BId.java +++ b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/BId.java @@ -1,58 +1,61 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -package org.fuin.cqrs4j; - -import org.fuin.ddd4j.ddd.EntityId; -import org.fuin.ddd4j.ddd.EntityType; -import org.fuin.ddd4j.ddd.StringBasedEntityType; - -//CHECKSTYLE:OFF -public class BId implements EntityId { - - private static final long serialVersionUID = 1L; - - public static final EntityType TYPE = new StringBasedEntityType("B"); - - private final long id; - - public BId(final long id) { - this.id = id; - } - - @Override - public EntityType getType() { - return TYPE; - } - - @Override - public String asString() { - return "" + id; - } - - @Override - public String asTypedString() { - return getType() + " " + asString(); - } - - @Override - public String toString() { - return "BId [id=" + id + "]"; - } - -} -// CHECKSTYLE:ON +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jaxb; + +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.utils4j.TestOmitted; + +import java.io.Serial; + +@TestOmitted("This is only a test class") +public class BId implements EntityId { + + @Serial + private static final long serialVersionUID = 1L; + + public static final EntityType TYPE = new StringBasedEntityType("B"); + + private final long id; + + public BId(final long id) { + this.id = id; + } + + @Override + public EntityType getType() { + return TYPE; + } + + @Override + public String asString() { + return "" + id; + } + + @Override + public String asTypedString() { + return getType() + " " + asString(); + } + + @Override + public String toString() { + return "BId [id=" + id + "]"; + } + +} diff --git a/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/BaseTest.java b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/BaseTest.java new file mode 100644 index 0000000..909f341 --- /dev/null +++ b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/BaseTest.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2013 Future Invent Informationsmanagement GmbH. All rights + * reserved. + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ +package org.fuin.cqrs4j.jaxb; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import org.fuin.units4j.archunit.Units4JConditions; + +@AnalyzeClasses(packagesOf = BaseTest.class) +class BaseTest { + + @ArchTest + static final ArchRule all_classes_should_have_tests = Units4JConditions.ALL_CLASSES_SHOULD_HAVE_TESTS; + +} + diff --git a/src/test/java/org/fuin/cqrs4j/CId.java b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/CId.java similarity index 83% rename from src/test/java/org/fuin/cqrs4j/CId.java rename to jaxb/src/test/java/org/fuin/cqrs4j/jaxb/CId.java index d34f1bd..f7b7f33 100644 --- a/src/test/java/org/fuin/cqrs4j/CId.java +++ b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/CId.java @@ -1,58 +1,61 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -package org.fuin.cqrs4j; - -import org.fuin.ddd4j.ddd.EntityId; -import org.fuin.ddd4j.ddd.EntityType; -import org.fuin.ddd4j.ddd.StringBasedEntityType; - -//CHECKSTYLE:OFF -public class CId implements EntityId { - - private static final long serialVersionUID = 1L; - - public static final EntityType TYPE = new StringBasedEntityType("C"); - - private final long id; - - public CId(final long id) { - this.id = id; - } - - @Override - public EntityType getType() { - return TYPE; - } - - @Override - public String asString() { - return "" + id; - } - - @Override - public String asTypedString() { - return getType() + " " + asString(); - } - - @Override - public String toString() { - return "CId [id=" + id + "]"; - } - -} -// CHECKSTYLE:ON +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jaxb; + +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.utils4j.TestOmitted; + +import java.io.Serial; + +@TestOmitted("This is only a test class") +public class CId implements EntityId { + + @Serial + private static final long serialVersionUID = 1L; + + public static final EntityType TYPE = new StringBasedEntityType("C"); + + private final long id; + + public CId(final long id) { + this.id = id; + } + + @Override + public EntityType getType() { + return TYPE; + } + + @Override + public String asString() { + return "" + id; + } + + @Override + public String asTypedString() { + return getType() + " " + asString(); + } + + @Override + public String toString() { + return "CId [id=" + id + "]"; + } + +} diff --git a/src/test/java/org/fuin/cqrs4j/DataResultTest.java b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/DataResultTest.java similarity index 65% rename from src/test/java/org/fuin/cqrs4j/DataResultTest.java rename to jaxb/src/test/java/org/fuin/cqrs4j/jaxb/DataResultTest.java index 6309d95..b62dea4 100644 --- a/src/test/java/org/fuin/cqrs4j/DataResultTest.java +++ b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/DataResultTest.java @@ -1,283 +1,325 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -package org.fuin.cqrs4j; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.fuin.utils4j.jaxb.JaxbUtils.marshal; -import static org.fuin.utils4j.jaxb.JaxbUtils.unmarshal; - -import java.io.IOException; -import java.util.UUID; - -import jakarta.json.bind.annotation.JsonbProperty; -import jakarta.xml.bind.annotation.XmlAccessType; -import jakarta.xml.bind.annotation.XmlAccessorType; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.XmlRootElement; - -import org.apache.commons.io.IOUtils; -import org.fuin.ddd4j.ddd.AggregateNotFoundException; -import org.fuin.ddd4j.ddd.AggregateRootId; -import org.fuin.ddd4j.ddd.EntityType; -import org.fuin.ddd4j.ddd.StringBasedEntityType; -import org.fuin.objects4j.common.MarshalInformation; -import org.junit.jupiter.api.Test; -import org.xmlunit.builder.DiffBuilder; -import org.xmlunit.diff.Diff; - -// CHECKSTYLE:OFF -public final class DataResultTest { - - private static final EntityType TEST_TYPE = new StringBasedEntityType("Test"); - - @Test - public final void testConstructorAll() { - - // PREPARE - final String data = "Whatever"; - - // TEST - final DataResult testee = new DataResult<>(ResultType.WARNING, "X1", "Yes!", data); - - // VERIFY - assertThat(testee.getType()).isEqualTo(ResultType.WARNING); - assertThat(testee.getCode()).isEqualTo("X1"); - assertThat(testee.getMessage()).isEqualTo("Yes!"); - assertThat(testee.getData()).isEqualTo(data); - - } - - @Test - public final void testConstructorException() { - - // PREPARE - final TestId id = new TestId(); - final AggregateNotFoundException ex = new AggregateNotFoundException(TEST_TYPE, id); - - // TEST - final DataResult testee = new DataResult<>(ex); - - // VERIFY - assertThat(testee.getType()).isEqualTo(ResultType.ERROR); - assertThat(testee.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); - assertThat(testee.getMessage()).isEqualTo(TEST_TYPE + " with id " + id.asString() + " not found"); - assertThat(testee.getData()).isInstanceOf(AggregateNotFoundException.Data.class); - - } - - @Test - public final void testUnmarshalMarshalVoidResult() throws IOException { - - // PREPARE - final String xml = IOUtils.toString(this.getClass().getResourceAsStream("/data-result-void.xml"), "utf-8"); - - // TEST - final DataResult copy = unmarshal(xml, DataResult.class); - - // VERIFY - assertThat(copy.getType()).isEqualTo(ResultType.OK); - - // TEST - final String copyXml = marshal(copy, DataResult.class); - - // VERIFY - final Diff documentDiff = DiffBuilder.compare(xml).withTest(copyXml).ignoreWhitespace().build(); - - assertThat(documentDiff.hasDifferences()).describedAs(documentDiff.toString()).isFalse(); - - } - - @Test - public final void testUnmarshalMarshalDataResult() throws IOException { - - // PREPARE - final String xml = IOUtils.toString(this.getClass().getResourceAsStream("/data-result-data.xml"), "utf-8"); - - // TEST - final DataResult copy = unmarshal(xml, DataResult.class, Invoice.class); - - // VERIFY - assertThat(copy.getType()).isEqualTo(ResultType.OK); - assertThat(copy.getCode()).isNull(); - assertThat(copy.getMessage()).isNull(); - assertThat(copy.getData()).isInstanceOf(Invoice.class); - assertThat(((Invoice) copy.getData()).getId()).isEqualTo("I-0123456"); - - // TEST - final String copyXml = marshal(copy, DataResult.class, Invoice.class); - - // VERIFY - final Diff documentDiff = DiffBuilder.compare(xml).withTest(copyXml).ignoreWhitespace().build(); - - assertThat(documentDiff.hasDifferences()).describedAs(documentDiff.toString()).isFalse(); - - } - - @Test - public final void testUnmarshalExceptionResult() throws IOException { - - // PREPARE - final String xml = IOUtils.toString(this.getClass().getResourceAsStream("/data-result-exception.xml"), "utf-8"); - - // TEST - final DataResult copy = unmarshal(xml, DataResult.class, AggregateNotFoundException.Data.class); - - // VERIFY - final String msg = "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found"; - assertThat(copy.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); - assertThat(copy.getType()).isEqualTo(ResultType.ERROR); - assertThat(copy.getMessage()).isEqualTo(msg); - assertThat(copy.getData()).isInstanceOf(AggregateNotFoundException.Data.class); - final AggregateNotFoundException anfe = (AggregateNotFoundException) copy.getData().toException(); - assertThat(anfe.getMessage()).isEqualTo(msg); - assertThat(anfe.getAggregateType()).isEqualTo("Invoice"); - assertThat(anfe.getAggregateId()).isEqualTo("4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119"); - - // TEST - final String copyXml = marshal(copy, DataResult.class, AggregateNotFoundException.Data.class); - - // VERIFY - final Diff documentDiff = DiffBuilder.compare(xml).withTest(copyXml).ignoreWhitespace().build(); - - assertThat(documentDiff.hasDifferences()).describedAs(documentDiff.toString()).isFalse(); - - } - - private static class TestId implements AggregateRootId { - - private static final long serialVersionUID = 1L; - - private UUID id = UUID.randomUUID(); - - @Override - public EntityType getType() { - return TEST_TYPE; - } - - @Override - public String asTypedString() { - return TEST_TYPE + " " + id; - } - - @Override - public String asString() { - return id.toString(); - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((id == null) ? 0 : id.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - if (obj == null) { - return false; - } - if (!(obj instanceof TestId)) { - return false; - } - TestId other = (TestId) obj; - if (id == null) { - if (other.id != null) { - return false; - } - } else if (!id.equals(other.id)) { - return false; - } - return true; - } - - } - - @XmlRootElement(name = "invoice") - @XmlAccessorType(XmlAccessType.NONE) - public static class Invoice implements MarshalInformation { - - @JsonbProperty("id") - @XmlElement(name = "id") - private String id; - - protected Invoice() { - super(); - } - - public Invoice(String id) { - super(); - this.id = id; - } - - @Override - public int hashCode() { - final int prime = 31; - int result = 1; - result = prime * result + ((id == null) ? 0 : id.hashCode()); - return result; - } - - @Override - public boolean equals(Object obj) { - if (this == obj) - return true; - if (obj == null) - return false; - if (getClass() != obj.getClass()) - return false; - Invoice other = (Invoice) obj; - if (id == null) { - if (other.id != null) - return false; - } else if (!id.equals(other.id)) - return false; - return true; - } - - @Override - public String toString() { - return id; - } - - @Override - public Class getDataClass() { - return Invoice.class; - } - - @Override - public String getDataElement() { - return Invoice.class.getName(); - } - - @Override - public Invoice getData() { - return this; - } - - public String getId() { - return id; - } - - } - -} -// CHECKSTYLE:ON +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jaxb; + +import jakarta.xml.bind.Marshaller; +import jakarta.xml.bind.Unmarshaller; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.ddd4j.core.AggregateNotFoundException; +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.ddd4j.jaxb.AggregateNotFoundExceptionData; +import org.fuin.objects4j.common.MarshalInformation; +import org.fuin.utils4j.jaxb.JaxbUtils; +import org.fuin.utils4j.jaxb.MarshallerBuilder; +import org.fuin.utils4j.jaxb.UnmarshallerBuilder; +import org.junit.jupiter.api.Test; +import org.xmlunit.builder.DiffBuilder; +import org.xmlunit.diff.Diff; + +import java.io.IOException; +import java.io.Serial; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public final class DataResultTest { + + private static final EntityType TEST_TYPE = new StringBasedEntityType("Test"); + + @Test + public final void testEqualsHashCode() { + EqualsVerifier.simple().forClass(DataResult.class).verify(); + } + + @Test + public final void testConstructorAll() { + + // PREPARE + final String data = "Whatever"; + + // TEST + final DataResult testee = new DataResult<>(ResultType.WARNING, "X1", "Yes!", data); + + // VERIFY + assertThat(testee.getType()).isEqualTo(ResultType.WARNING); + assertThat(testee.getCode()).isEqualTo("X1"); + assertThat(testee.getMessage()).isEqualTo("Yes!"); + assertThat(testee.getData()).isEqualTo(data); + + } + + @Test + public final void testConstructorException() { + + // PREPARE + final TestId id = new TestId(); + final AggregateNotFoundException ex = new AggregateNotFoundException(TEST_TYPE, id); + final AggregateNotFoundExceptionData exData = new AggregateNotFoundExceptionData(ex); + + // TEST + final DataResult testee = new DataResult<>(exData); + + // VERIFY + assertThat(testee.getType()).isEqualTo(ResultType.ERROR); + assertThat(testee.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(testee.getMessage()).isEqualTo(TEST_TYPE + " with id " + id.asString() + " not found"); + + } + + @Test + public final void testUnmarshalMarshalVoidResult() throws IOException { + + // PREPARE + final String xml = """ + + + OK + + """; + + final Marshaller marshaller = new MarshallerBuilder().addClassesToBeBound(DataResult.class).build(); + final Unmarshaller unmarshaller = new UnmarshallerBuilder().addClassesToBeBound(DataResult.class).build(); + + // TEST + final DataResult copy = JaxbUtils.unmarshal(unmarshaller, xml); + + // VERIFY + assertThat(copy.getType()).isEqualTo(ResultType.OK); + + // TEST + final String copyXml = JaxbUtils.marshal(marshaller, copy); + + // VERIFY + final Diff documentDiff = DiffBuilder.compare(xml).withTest(copyXml).ignoreWhitespace().build(); + + assertThat(documentDiff.hasDifferences()).describedAs(documentDiff.toString()).isFalse(); + + } + + @Test + public final void testUnmarshalMarshalDataResult() throws IOException { + + // PREPARE + final String xml = """ + + + OK + + I-0123456 + + + """; + + final Marshaller marshaller = new MarshallerBuilder().addClassesToBeBound(DataResult.class, Invoice.class).build(); + final Unmarshaller unmarshaller = new UnmarshallerBuilder().addClassesToBeBound(DataResult.class, Invoice.class).build(); + + // TEST + final DataResult copy = JaxbUtils.unmarshal(unmarshaller, xml); + + // VERIFY + assertThat(copy.getType()).isEqualTo(ResultType.OK); + assertThat(copy.getCode()).isNull(); + assertThat(copy.getMessage()).isNull(); + assertThat(copy.getData()).isInstanceOf(Invoice.class); + assertThat(copy.getData().getId()).isEqualTo("I-0123456"); + + // TEST + final String copyXml = JaxbUtils.marshal(marshaller, copy); + + // VERIFY + final Diff documentDiff = DiffBuilder.compare(xml).withTest(copyXml).ignoreWhitespace().build(); + + assertThat(documentDiff.hasDifferences()).describedAs(documentDiff.toString()).isFalse(); + + } + + @Test + public final void testUnmarshalExceptionResult() throws IOException { + + // PREPARE + final String xml = """ + + + ERROR + DDD4J-AGGREGATE_NOT_FOUND + Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found + + Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found + Invoice + 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 + DDD4J-AGGREGATE_NOT_FOUND + + + """; + + final Marshaller marshaller = new MarshallerBuilder().addClassesToBeBound(DataResult.class, AggregateNotFoundExceptionData.class).build(); + final Unmarshaller unmarshaller = new UnmarshallerBuilder().addClassesToBeBound(DataResult.class, AggregateNotFoundExceptionData.class).build(); + + // TEST + final DataResult copy = JaxbUtils.unmarshal(unmarshaller, xml); + + // VERIFY + final String msg = "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found"; + assertThat(copy.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(copy.getType()).isEqualTo(ResultType.ERROR); + assertThat(copy.getMessage()).isEqualTo(msg); + assertThat(copy.getData()).isInstanceOf(AggregateNotFoundExceptionData.class); + final AggregateNotFoundException anfe = copy.getData().toException(); + assertThat(anfe.getMessage()).isEqualTo(msg); + assertThat(anfe.getType()).isEqualTo("Invoice"); + assertThat(anfe.getId()).isEqualTo("4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119"); + + // TEST + final String copyXml = JaxbUtils.marshal(marshaller, copy); + + // VERIFY + final Diff documentDiff = DiffBuilder.compare(xml).withTest(copyXml).ignoreWhitespace().build(); + + assertThat(documentDiff.hasDifferences()).describedAs(documentDiff.toString()).isFalse(); + + } + + private static class TestId implements AggregateRootId { + + @Serial + private static final long serialVersionUID = 1L; + + private UUID id = UUID.randomUUID(); + + @Override + public EntityType getType() { + return TEST_TYPE; + } + + @Override + public String asTypedString() { + return TEST_TYPE + " " + id; + } + + @Override + public String asString() { + return id.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof TestId)) { + return false; + } + TestId other = (TestId) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + + } + + @XmlRootElement(name = "invoice") + @XmlAccessorType(XmlAccessType.NONE) + public static class Invoice implements MarshalInformation { + + @XmlElement(name = "id") + private String id; + + protected Invoice() { + super(); + } + + public Invoice(String id) { + super(); + this.id = id; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Invoice other = (Invoice) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + + @Override + public String toString() { + return id; + } + + @Override + public Class getDataClass() { + return Invoice.class; + } + + @Override + public String getDataElement() { + return Invoice.class.getName(); + } + + @Override + public Invoice getData() { + return this; + } + + public String getId() { + return id; + } + + } + +} diff --git a/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/SimpleResultTest.java b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/SimpleResultTest.java new file mode 100644 index 0000000..a2708d0 --- /dev/null +++ b/jaxb/src/test/java/org/fuin/cqrs4j/jaxb/SimpleResultTest.java @@ -0,0 +1,216 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jaxb; + +import jakarta.xml.bind.Marshaller; +import jakarta.xml.bind.Unmarshaller; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.ddd4j.core.AggregateNotFoundException; +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.utils4j.jaxb.JaxbUtils; +import org.fuin.utils4j.jaxb.MarshallerBuilder; +import org.fuin.utils4j.jaxb.UnmarshallerBuilder; +import org.junit.jupiter.api.Test; +import org.xmlunit.builder.DiffBuilder; +import org.xmlunit.diff.Diff; + +import java.io.IOException; +import java.io.Serial; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +public final class SimpleResultTest { + + private static final EntityType TEST_TYPE = new StringBasedEntityType("Test"); + + @Test + public final void testEqualsHashCode() { + EqualsVerifier.simple().forClass(SimpleResult.class).verify(); + } + + @Test + public final void testConstructorAll() { + + // TEST + final SimpleResult testee = new SimpleResult(ResultType.WARNING, "X1", "Yes!"); + + // VERIFY + assertThat(testee.getType()).isEqualTo(ResultType.WARNING); + assertThat(testee.getCode()).isEqualTo("X1"); + assertThat(testee.getMessage()).isEqualTo("Yes!"); + + } + + @Test + public final void testConstructorException() { + + // PREPARE + final TestId id = new TestId(); + final AggregateNotFoundException ex = new AggregateNotFoundException(TEST_TYPE, id); + + // TEST + final SimpleResult testee = new SimpleResult(ex); + + // VERIFY + assertThat(testee.getType()).isEqualTo(ResultType.ERROR); + assertThat(testee.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(testee.getMessage()).isEqualTo(TEST_TYPE + " with id " + id.asString() + " not found"); + + } + + @Test + public final void testMarshalUnmarshal() { + + // PREPARE + final SimpleResult original = SimpleResult.ok(); + final Marshaller marshaller = new MarshallerBuilder().addClassesToBeBound(SimpleResult.class).build(); + final Unmarshaller unmarshaller = new UnmarshallerBuilder().addClassesToBeBound(SimpleResult.class).build(); + + // TEST + final String xml = JaxbUtils.marshal(marshaller, original); + final SimpleResult copy = JaxbUtils.unmarshal(unmarshaller, xml); + + // VERIFY + assertThat(original).isEqualTo(copy); + + } + + @Test + public final void testUnmarshalMarshalOkResult() throws IOException { + + // PREPARE + final String xml = """ + + + OK + + """; + + final Marshaller marshaller = new MarshallerBuilder().addClassesToBeBound(SimpleResult.class).build(); + final Unmarshaller unmarshaller = new UnmarshallerBuilder().addClassesToBeBound(SimpleResult.class).build(); + + // TEST + final SimpleResult copy = JaxbUtils.unmarshal(unmarshaller, xml); + + // VERIFY + assertThat(copy.getType()).isEqualTo(ResultType.OK); + + // TEST + final String copyXml = JaxbUtils.marshal(marshaller, copy); + + // VERIFY + final Diff documentDiff = DiffBuilder.compare(xml).withTest(copyXml).ignoreWhitespace().build(); + + assertThat(documentDiff.hasDifferences()).describedAs(documentDiff.toString()).isFalse(); + + } + + @Test + public final void testUnmarshalExceptionResult() throws IOException { + + // PREPARE + final String xml = """ + + + ERROR + DDD4J-AGGREGATE_NOT_FOUND + Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found + + """; + + final Marshaller marshaller = new MarshallerBuilder().addClassesToBeBound(SimpleResult.class).build(); + final Unmarshaller unmarshaller = new UnmarshallerBuilder().addClassesToBeBound(SimpleResult.class).build(); + + // TEST + final SimpleResult copy = JaxbUtils.unmarshal(unmarshaller, xml); + + // VERIFY + final String msg = "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found"; + assertThat(copy.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(copy.getType()).isEqualTo(ResultType.ERROR); + assertThat(copy.getMessage()).isEqualTo(msg); + + // TEST + final String copyXml = JaxbUtils.marshal(marshaller, copy); + + // VERIFY + final Diff documentDiff = DiffBuilder.compare(xml).withTest(copyXml).ignoreWhitespace().build(); + + assertThat(documentDiff.hasDifferences()).describedAs(documentDiff.toString()).isFalse(); + + } + + private static class TestId implements AggregateRootId { + + @Serial + private static final long serialVersionUID = 1L; + + private UUID id = UUID.randomUUID(); + + @Override + public EntityType getType() { + return TEST_TYPE; + } + + @Override + public String asTypedString() { + return TEST_TYPE + " " + id; + } + + @Override + public String asString() { + return id.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof TestId)) { + return false; + } + TestId other = (TestId) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + + } + +} diff --git a/jsonb/pom.xml b/jsonb/pom.xml new file mode 100644 index 0000000..e46a432 --- /dev/null +++ b/jsonb/pom.xml @@ -0,0 +1,270 @@ + + + + 4.0.0 + + + org.fuin.cqrs4j + cqrs-4-java + 0.6.0-SNAPSHOT + ../pom.xml + + + cqrs-4-java-jsonb + jar + ${description} (JSON-B) + + + + + + + org.fuin.cqrs4j + cqrs-4-java-core + + + + org.fuin.ddd4j + ddd-4-java-core + + + + org.fuin.ddd4j + ddd-4-java-jsonb + + + + org.fuin + utils4j + + + + org.fuin.objects4j + objects4j-common + + + + org.fuin.objects4j + objects4j-ui + + + + org.fuin.objects4j + objects4j-jsonb + + + + jakarta.validation + jakarta.validation-api + + + + jakarta.annotation + jakarta.annotation-api + + + + jakarta.json.bind + jakarta.json.bind-api + + + + jakarta.json + jakarta.json-api + + + + io.smallrye + jandex + + + + org.slf4j + slf4j-api + + + + + + org.junit.jupiter + junit-jupiter + test + + + + org.fuin + units4j + test + + + + org.fuin.esc + esc-api + + + + org.fuin.esc + esc-jsonb + test + + + + org.hibernate.validator + hibernate-validator + test + + + + org.glassfish.expressly + expressly + test + + + + org.glassfish + jakarta.json + test + + + + org.eclipse + yasson + test + + + + org.assertj + assertj-core + test + + + + net.javacrumbs.json-unit + json-unit-fluent + test + + + + com.google.code.gson + gson + test + + + + nl.jqno.equalsverifier + equalsverifier + test + + + + com.tngtech.archunit + archunit + test + + + + com.tngtech.archunit + archunit-junit5 + test + + + + org.mockito + mockito-core + test + + + + org.mockito + mockito-junit-jupiter + test + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.apache.maven.plugins + maven-source-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + org.apache.maven.plugins + maven-jar-plugin + + + **/* + + + + org.fuin.cqrs4j.jsonb + + + + + + + org.apache.maven.plugins + maven-jdeps-plugin + + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + + + + io.smallrye + jandex-maven-plugin + + + + org.apache.maven.plugins + maven-dependency-plugin + + + jakarta.el:jakarta.el-api + org.glassfish.expressly:expressly + org.hibernate.validator:hibernate-validator + org.fuin.esc:esc-jsonb + org.glassfish:jakarta.json + com.tngtech.archunit:archunit-junit5 + org.junit.jupiter:junit-jupiter + com.google.code.gson:gson + io.smallrye:jandex + + + com.tngtech.archunit:archunit-junit5-api + org.junit.jupiter:junit-jupiter-api + + + + + + + + + diff --git a/src/main/java/org/fuin/cqrs4j/AbstractAggregateCommand.java b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/AbstractAggregateCommand.java similarity index 85% rename from src/main/java/org/fuin/cqrs4j/AbstractAggregateCommand.java rename to jsonb/src/main/java/org/fuin/cqrs4j/jsonb/AbstractAggregateCommand.java index 789fb87..16b8b3b 100644 --- a/src/main/java/org/fuin/cqrs4j/AbstractAggregateCommand.java +++ b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/AbstractAggregateCommand.java @@ -1,42 +1,42 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; +package org.fuin.cqrs4j.jsonb; +import jakarta.annotation.Nullable; import jakarta.json.bind.annotation.JsonbProperty; import jakarta.json.bind.annotation.JsonbTypeAdapter; import jakarta.validation.constraints.NotNull; -import jakarta.xml.bind.annotation.XmlElement; -import jakarta.xml.bind.annotation.adapters.XmlJavaTypeAdapter; - -import org.fuin.ddd4j.ddd.AggregateRootId; -import org.fuin.ddd4j.ddd.AggregateVersion; -import org.fuin.ddd4j.ddd.AggregateVersionConverter; -import org.fuin.ddd4j.ddd.EntityId; -import org.fuin.ddd4j.ddd.EntityIdPath; -import org.fuin.ddd4j.ddd.EntityIdPathConverter; -import org.fuin.ddd4j.ddd.Event; -import org.fuin.ddd4j.ddd.EventId; +import org.fuin.cqrs4j.core.AggregateCommand; +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.AggregateVersion; +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityIdPath; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventId; +import org.fuin.ddd4j.jsonb.AggregateVersionJsonbAdapter; +import org.fuin.ddd4j.jsonb.EntityIdPathJsonbAdapter; import org.fuin.objects4j.common.Contract; -import org.fuin.objects4j.common.Nullable; + +import java.io.Serial; /** * Base class for all commands that are directed to an existing aggregate. - * + * * @param * Type of the aggregate root identifier. * @param @@ -45,20 +45,17 @@ public abstract class AbstractAggregateCommand extends AbstractCommand implements AggregateCommand { + @Serial private static final long serialVersionUID = 1000L; @NotNull - @JsonbTypeAdapter(EntityIdPathConverter.class) + @JsonbTypeAdapter(EntityIdPathJsonbAdapter.class) @JsonbProperty("entity-id-path") - @XmlJavaTypeAdapter(EntityIdPathConverter.class) - @XmlElement(name = "entity-id-path") private EntityIdPath entityIdPath; @Nullable - @JsonbTypeAdapter(AggregateVersionConverter.class) + @JsonbTypeAdapter(AggregateVersionJsonbAdapter.class) @JsonbProperty("aggregate-version") - @XmlJavaTypeAdapter(AggregateVersionConverter.class) - @XmlElement(name = "aggregate-version") private AggregateVersion aggregateVersion; /** @@ -70,7 +67,7 @@ protected AbstractAggregateCommand() { // NOSONAR Ignore uninitialized fields /** * Constructor with aggregate root id and version. - * + * * @param aggregateRootId * Aggregate root identifier. * @param aggregateVersion @@ -82,7 +79,7 @@ public AbstractAggregateCommand(@NotNull final AggregateRootId aggregateRootId, /** * Constructor with entitiy id path and version. - * + * * @param entityIdPath * Path from root aggregate to target entity. * @param aggregateVersion @@ -97,7 +94,7 @@ public AbstractAggregateCommand(@NotNull final EntityIdPath entityIdPath, @Nulla /** * Constructor with event this one responds to. Convenience method to set the correlation and causation identifiers correctly. - * + * * @param entityIdPath * Path from root aggregate to target entity. * @param aggregateVersion @@ -106,7 +103,7 @@ public AbstractAggregateCommand(@NotNull final EntityIdPath entityIdPath, @Nulla * Causing event. */ public AbstractAggregateCommand(@NotNull final EntityIdPath entityIdPath, @Nullable final AggregateVersion aggregateVersion, - @NotNull final Event respondTo) { + @NotNull final Event respondTo) { super(respondTo); Contract.requireArgNotNull("entityIdPath", entityIdPath); this.entityIdPath = entityIdPath; @@ -115,7 +112,7 @@ public AbstractAggregateCommand(@NotNull final EntityIdPath entityIdPath, @Nulla /** * Constructor with optional data. - * + * * @param entityIdPath * Path from root aggregate to target entity. * @param aggregateVersion @@ -126,7 +123,7 @@ public AbstractAggregateCommand(@NotNull final EntityIdPath entityIdPath, @Nulla * ID of the event that caused this one. */ public AbstractAggregateCommand(@NotNull final EntityIdPath entityIdPath, @Nullable final AggregateVersion aggregateVersion, - @Nullable final EventId correlationId, @Nullable final EventId causationId) { + @Nullable final EventId correlationId, @Nullable final EventId causationId) { super(correlationId, causationId); Contract.requireArgNotNull("entityIdPath", entityIdPath); this.entityIdPath = entityIdPath; @@ -146,7 +143,7 @@ public final ENTITY_ID getEntityId() { } @Override - @Nullable + @NotNull public final ROOT_ID getAggregateRootId() { return entityIdPath.first(); } @@ -168,7 +165,7 @@ public final Integer getAggregateVersionInteger() { /** * Base class for event builders. - * + * * @param * Type of the aggregate identifier. * @param @@ -185,7 +182,7 @@ protected abstract static class Builder + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jsonb; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import org.fuin.cqrs4j.core.Command; +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventId; +import org.fuin.ddd4j.jsonb.AbstractEvent; + +import java.io.Serial; + +/** + * Base class for all commands. + */ +public abstract class AbstractCommand extends AbstractEvent implements Command { + + @Serial + private static final long serialVersionUID = 1000L; + + /** + * Default constructor. + */ + public AbstractCommand() { + super(); + } + + /** + * Constructor with event this one responds to. Convenience method to set the correlation and causation identifiers correctly. + * + * @param respondTo + * Causing event. + */ + public AbstractCommand(@NotNull final Event respondTo) { + super(respondTo); + } + + /** + * Constructor with optional data. + * + * @param correlationId + * Correlation ID. + * @param causationId + * ID of the event that caused this one. + */ + public AbstractCommand(@Nullable final EventId correlationId, @Nullable final EventId causationId) { + super(correlationId, causationId); + } + + /** + * Base class for event builders. + * + * @param + * Type of the entity identifier. + * @param + * Type of the event. + * @param + * Type of the builder. + */ + protected abstract static class Builder> + extends AbstractEvent.Builder { + + /** + * Constructor with event. + * + * @param delegate + * Event to populate with data. + */ + public Builder(final TYPE delegate) { + super(delegate); + } + + /** + * Ensures that everything is set up for building the object or throws a runtime exception otherwise. + */ + protected final void ensureBuildableAbstractCommand() { + ensureBuildableAbstractEvent(); + } + + /** + * Sets the internal instance to a new one. This must be called within the build method. + * + * @param delegate + * Delegate to use. + */ + protected final void resetAbstractCommand(final TYPE delegate) { + resetAbstractEvent(delegate); + } + + } + +} diff --git a/src/main/java/org/fuin/cqrs4j/AbstractResult.java b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/AbstractResult.java similarity index 86% rename from src/main/java/org/fuin/cqrs4j/AbstractResult.java rename to jsonb/src/main/java/org/fuin/cqrs4j/jsonb/AbstractResult.java index 9c56658..8e17ce3 100644 --- a/src/main/java/org/fuin/cqrs4j/AbstractResult.java +++ b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/AbstractResult.java @@ -1,61 +1,62 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; - -import java.io.Serializable; +package org.fuin.cqrs4j.jsonb; +import jakarta.annotation.Nullable; import jakarta.json.bind.annotation.JsonbProperty; import jakarta.validation.constraints.NotNull; -import jakarta.xml.bind.annotation.XmlElement; - +import org.fuin.cqrs4j.core.Result; +import org.fuin.cqrs4j.core.ResultType; import org.fuin.objects4j.common.Contract; import org.fuin.objects4j.common.ExceptionShortIdentifable; -import org.fuin.objects4j.common.Nullable; import org.fuin.objects4j.ui.Label; import org.fuin.objects4j.ui.Prompt; import org.fuin.objects4j.ui.ShortLabel; import org.fuin.objects4j.ui.Tooltip; +import java.io.Serial; +import java.io.Serializable; + /** - * Result of a request. The type signals if the execution was successful or not. In case the the result is not {@link ResultType#OK}, the + * Result of a request. The type signals if the execution was successful or not. In case the result is not {@link ResultType#OK}, the * fields code and message should contain unique information to help the user identifying the cause of the problem. A result may carry some * optional data. - * + * * @param * Type of data returned. */ public abstract class AbstractResult implements Result, Serializable { + @Serial private static final long serialVersionUID = 1000L; - static final String TYPE_PROPERTY = "type"; - - static final String CODE_PROPERTY = "code"; - - static final String MESSAGE_PROPERTY = "message"; - + static final String TYPE_PROPERTY = "type"; + + static final String CODE_PROPERTY = "code"; + + static final String MESSAGE_PROPERTY = "message"; + @Label("Result Type") @ShortLabel("TYPE") @Tooltip("Type of the result") @Prompt("ERROR") @NotNull @JsonbProperty(TYPE_PROPERTY) - @XmlElement(name = TYPE_PROPERTY) private ResultType type; @Label("Result Code") @@ -64,7 +65,6 @@ public abstract class AbstractResult implements Result, Serializable @Prompt("E00001") @Nullable @JsonbProperty(CODE_PROPERTY) - @XmlElement(name = CODE_PROPERTY) private String code; @Label("Result Message") @@ -73,7 +73,6 @@ public abstract class AbstractResult implements Result, Serializable @Prompt("The field 'Xyz' is mandatory") @Nullable @JsonbProperty(MESSAGE_PROPERTY) - @XmlElement(name = MESSAGE_PROPERTY) private String message; /** @@ -85,7 +84,7 @@ protected AbstractResult() { // NOSONAR Ignore uninitialized fields /** * Constructor with all data. - * + * * @param type * Type. * @param code @@ -104,15 +103,13 @@ public AbstractResult(@NotNull final ResultType type, @Nullable final String cod * Constructor with exception. An exception of type {@link ExceptionShortIdentifable} will be used to fill the code field * with the identifier value. If it's not a {@link ExceptionShortIdentifable} the code field will be set using the full * qualified class name of the exception. - * + * * @param exception * The message for the result is equal to the exception message or the simple name of the exception class if the exception * message is null. */ - // CHECKSTYLE:OFF:AvoidInlineConditionals public AbstractResult(@NotNull final Exception exception) { - // CHECKSTYLE:ON - super(); + super(); Contract.requireArgNotNull("exception", exception); this.type = ResultType.ERROR; if (exception instanceof ExceptionShortIdentifable) { diff --git a/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/DataResult.java b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/DataResult.java new file mode 100644 index 0000000..a1425c1 --- /dev/null +++ b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/DataResult.java @@ -0,0 +1,277 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jsonb; + +import jakarta.annotation.Nullable; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.ddd4j.core.ExceptionData; +import org.fuin.objects4j.common.Contract; +import org.fuin.objects4j.common.MarshalInformation; +import org.fuin.objects4j.ui.Label; +import org.fuin.objects4j.ui.Prompt; +import org.fuin.objects4j.ui.ShortLabel; +import org.fuin.objects4j.ui.Tooltip; + +import java.io.Serial; + +/** + * Result of a request that contains data in addition to the standard result fields. The type signals if the execution was successful or + * not. In case the the result is not {@link ResultType#OK}, the fields code and message should contain unique information to help the user + * identifying the cause of the problem. + * + * @param Type of data returned in case of success (type = {@link ResultType#OK}). + */ +public final class DataResult extends AbstractResult { + + @Serial + private static final long serialVersionUID = 1000L; + + static final String DATA_CLASS_PROPERTY = "data-class"; + + static final String DATA_ELEMENT_PROPERTY = "data-element"; + + @JsonbProperty(DATA_CLASS_PROPERTY) + private String dataClass; + + @JsonbProperty(DATA_ELEMENT_PROPERTY) + private String dataElement; + + @Label("Data") + @ShortLabel("DATA") + @Tooltip("Optional result data") + @Prompt("Optional Data") + @Valid + @SuppressWarnings("java:S1948") // We assume the unknown data is serializable + private Object data; + + /** + * Protected default constructor for de-serialization. + */ + protected DataResult() { // NOSONAR Ignore uninitialized fields + super(); + } + + /** + * Constructor without data element name. + * + * @param type Type. + * @param code Code. + * @param message Message. + * @param data Optional result data. + */ + public DataResult(@NotNull final ResultType type, @Nullable final String code, @Nullable final String message, + @Nullable final DATA data) { + super(type, code, message); + if (data instanceof MarshalInformation) { + final MarshalInformation mui = (MarshalInformation) data; + this.data = mui.getData(); + this.dataClass = mui.getDataClass().getName(); + this.dataElement = mui.getDataElement(); + } else { + this.data = data; + this.dataClass = null; + this.dataElement = null; + } + } + + /** + * Constructor with all data. + * + * @param type Type. + * @param code Code. + * @param message Message. + * @param data Optional result data. + * @param dataElement Optional name of the data element. + */ + public DataResult(@NotNull final ResultType type, @Nullable final String code, @Nullable final String message, @Nullable final DATA data, + final String dataElement) { + super(type, code, message); + this.data = data; + if (data == null) { + this.dataClass = null; + this.dataElement = null; + } else { + this.dataClass = data.getClass().getName(); + this.dataElement = dataElement; + } + } + + /** + * Constructor with exception data. + * + * @param exceptionData . + */ + public DataResult(@NotNull final ExceptionData exceptionData) { + super(exceptionData.toException()); + this.data = exceptionData; + this.dataClass = exceptionData.getClass().getName(); + this.dataElement = exceptionData.getDataElement(); + } + + /** + * Returns the name of the class contained in the data element. + * + * @return Full qualified class name. + */ + public final String getDataClass() { + return dataClass; + } + + /** + * Returns the name of the data attribute. + * + * @return Data element name. + */ + public final String getDataElement() { + return dataElement; + } + + /** + * Returns the result data. + * + * @return Response data. + */ + @SuppressWarnings("unchecked") + @Nullable + public final DATA getData() { + return (DATA) data; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((getCode() == null) ? 0 : getCode().hashCode()); + result = prime * result + ((getMessage() == null) ? 0 : getMessage().hashCode()); + result = prime * result + getType().hashCode(); + result = prime * result + ((dataClass == null) ? 0 : dataClass.hashCode()); + result = prime * result + ((dataElement == null) ? 0 : dataElement.hashCode()); + result = prime * result + ((data == null) ? 0 : data.hashCode()); + return result; + } + + @Override + public final boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final DataResult other = (DataResult) obj; + if (getCode() == null) { + if (other.getCode() != null) { + return false; + } + } else if (!getCode().equals(other.getCode())) { + return false; + } + if (getMessage() == null) { + if (other.getMessage() != null) { + return false; + } + } else if (!getMessage().equals(other.getMessage())) { + return false; + } + if (getType() != other.getType()) { + return false; + } + if (dataClass == null) { + if (other.dataClass != null) { + return false; + } + } else if (!dataClass.equals(other.dataClass)) { + return false; + } + if (dataElement == null) { + if (other.dataElement != null) { + return false; + } + } else if (!dataElement.equals(other.dataElement)) { + return false; + } + if (data == null) { + if (other.data != null) { + return false; + } + } else if (!data.equals(other.data)) { + return false; + } + return true; + } + + + @Override + public final String toString() { + return "Result [type=" + getType() + ", code=" + getCode() + ", message=" + getMessage() + ", dataClass=" + dataClass + + ", dataElement=" + dataElement + "]"; + } + + /** + * Create a success result without any data. + * + * @return Result with type {@link ResultType#OK}. + */ + public static DataResult ok() { + return new DataResult<>(ResultType.OK, null, null, null); + } + + /** + * Create a success result with some data. + * + * @param data Optional data. + * @param Type of data. + * @return Result with type {@link ResultType#OK}. + */ + public static DataResult ok(@Nullable final T data) { + return new DataResult<>(ResultType.OK, null, null, data); + } + + /** + * Create a success result with some data. + * + * @param data Optional data. + * @param dataElement Optional name of the data element. + * @param Type of data. + * @return Result with type {@link ResultType#OK}. + */ + public static DataResult ok(@Nullable final T data, final String dataElement) { + return new DataResult<>(ResultType.OK, null, null, data, dataElement); + } + + /** + * Create an error result without any data. + * + * @param code Code. + * @param message Message. + * @param Not used. + * @return Error result with. + */ + public static DataResult error(@NotNull final String code, @NotNull final String message) { + Contract.requireArgNotNull("code", code); + Contract.requireArgNotNull("message", message); + return new DataResult<>(ResultType.ERROR, code, message, null); + } + +} diff --git a/src/main/java/org/fuin/cqrs4j/DataResultJsonbAdapter.java b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/DataResultJsonbAdapter.java similarity index 67% rename from src/main/java/org/fuin/cqrs4j/DataResultJsonbAdapter.java rename to jsonb/src/main/java/org/fuin/cqrs4j/jsonb/DataResultJsonbAdapter.java index 95c1acd..51bc11f 100644 --- a/src/main/java/org/fuin/cqrs4j/DataResultJsonbAdapter.java +++ b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/DataResultJsonbAdapter.java @@ -15,28 +15,22 @@ * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; - -import static org.fuin.cqrs4j.AbstractResult.CODE_PROPERTY; -import static org.fuin.cqrs4j.AbstractResult.MESSAGE_PROPERTY; -import static org.fuin.cqrs4j.AbstractResult.TYPE_PROPERTY; -import static org.fuin.cqrs4j.DataResult.DATA_CLASS_PROPERTY; -import static org.fuin.cqrs4j.DataResult.DATA_ELEMENT_PROPERTY; - -import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; +package org.fuin.cqrs4j.jsonb; import jakarta.json.Json; import jakarta.json.JsonObject; import jakarta.json.JsonObjectBuilder; import jakarta.json.JsonReader; import jakarta.json.JsonWriter; -import jakarta.json.bind.Jsonb; import jakarta.json.bind.adapter.JsonbAdapter; import jakarta.validation.constraints.NotNull; - +import org.fuin.cqrs4j.core.ResultType; import org.fuin.objects4j.common.Contract; +import org.fuin.objects4j.jsonb.JsonbProvider; + +import java.io.IOException; +import java.io.StringReader; +import java.io.StringWriter; /** * Converts an {@link DataResult} from/to JSON. @@ -44,38 +38,38 @@ @SuppressWarnings("rawtypes") public final class DataResultJsonbAdapter implements JsonbAdapter { - private final Jsonb jsonb; + private final JsonbProvider jsonbProvider; /** * Constructor with jsonb instance. * - * @param jsonb - * Jsonb instance used to marshal/unmarshal the data object. + * @param jsonbProvider + * Jsonb provider used to retrieve an instance of jsonb to marshal/unmarshal the data object. */ - public DataResultJsonbAdapter(@NotNull final Jsonb jsonb) { + public DataResultJsonbAdapter(@NotNull final JsonbProvider jsonbProvider) { super(); - Contract.requireArgNotNull("jsonb", jsonb); - this.jsonb = jsonb; + Contract.requireArgNotNull("jsonbProvider", jsonbProvider); + this.jsonbProvider = jsonbProvider; } @Override public JsonObject adaptToJson(final DataResult result) throws Exception { final JsonObjectBuilder builder = Json.createObjectBuilder(); - builder.add(TYPE_PROPERTY, result.getType().name()); + builder.add(AbstractResult.TYPE_PROPERTY, result.getType().name()); if (result.getCode() != null) { - builder.add(CODE_PROPERTY, result.getCode()); + builder.add(AbstractResult.CODE_PROPERTY, result.getCode()); } if (result.getMessage() != null) { - builder.add(MESSAGE_PROPERTY, result.getMessage()); + builder.add(AbstractResult.MESSAGE_PROPERTY, result.getMessage()); } if (result.getData() != null) { - builder.add(DATA_CLASS_PROPERTY, result.getData().getClass().getName()); - final String json = jsonb.toJson(result.getData()); + builder.add(DataResult.DATA_CLASS_PROPERTY, result.getData().getClass().getName()); + final String json = jsonbProvider.jsonb().toJson(result.getData()); final String elName = result.getDataElement(); if (elName == null) { throw new IllegalStateException("The 'dataElementName' was empty, but is required fro JSON-B: " + result); } - builder.add(DATA_ELEMENT_PROPERTY, result.getDataElement()); + builder.add(DataResult.DATA_ELEMENT_PROPERTY, result.getDataElement()); builder.add(elName, unmarshal(json)); } return builder.build(); @@ -83,19 +77,19 @@ public JsonObject adaptToJson(final DataResult result) throws Exception { @Override public DataResult adaptFromJson(final JsonObject jsonObj) throws Exception { - final ResultType type = ResultType.valueOf(jsonObj.getString(TYPE_PROPERTY)); - final String code = getString(jsonObj, CODE_PROPERTY); - final String message = getString(jsonObj, MESSAGE_PROPERTY); - if (jsonObj.containsKey(DATA_CLASS_PROPERTY)) { - if (!jsonObj.containsKey(DATA_ELEMENT_PROPERTY)) { + final ResultType type = ResultType.valueOf(jsonObj.getString(AbstractResult.TYPE_PROPERTY)); + final String code = getString(jsonObj, AbstractResult.CODE_PROPERTY); + final String message = getString(jsonObj, AbstractResult.MESSAGE_PROPERTY); + if (jsonObj.containsKey(DataResult.DATA_CLASS_PROPERTY)) { + if (!jsonObj.containsKey(DataResult.DATA_ELEMENT_PROPERTY)) { throw new IllegalStateException( - "The '" + DATA_ELEMENT_PROPERTY + "' was not found, but is required fro JSON-B: " + jsonObj); + "The '" + DataResult.DATA_ELEMENT_PROPERTY + "' was not found, but is required fro JSON-B: " + jsonObj); } - final Class dataClass = Class.forName(jsonObj.getString(DATA_CLASS_PROPERTY)); - final String dataElement = getString(jsonObj, DATA_ELEMENT_PROPERTY); + final Class dataClass = Class.forName(jsonObj.getString(DataResult.DATA_CLASS_PROPERTY)); + final String dataElement = getString(jsonObj, DataResult.DATA_ELEMENT_PROPERTY); final JsonObject data = jsonObj.getJsonObject(dataElement); final String json = marshal(data); - final Object obj = jsonb.fromJson(json, dataClass); + final Object obj = jsonbProvider.jsonb().fromJson(json, dataClass); return new DataResult<>(type, code, message, obj, dataElement); } return new DataResult<>(type, code, message, null); diff --git a/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/JandexJsonbRegistry.java b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/JandexJsonbRegistry.java new file mode 100644 index 0000000..751890d --- /dev/null +++ b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/JandexJsonbRegistry.java @@ -0,0 +1,175 @@ +package org.fuin.cqrs4j.jsonb; + +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.serializer.JsonbDeserializer; +import jakarta.json.bind.serializer.JsonbSerializer; +import org.fuin.ddd4j.core.EntityIdFactory; +import org.fuin.ddd4j.jsonb.EntityIdJsonbAdapter; +import org.fuin.esc.api.DeserializerRegistry; +import org.fuin.esc.api.SerializerRegistry; +import org.fuin.objects4j.jsonb.JsonbProvider; +import org.fuin.utils4j.jandex.JandexUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +/** + * Registry that is built up by scanning for classes that implement {@link jakarta.json.bind.adapter.JsonbAdapter}. + * It also cares about specialized {@link EntityIdJsonbAdapter} that require a {@link EntityIdFactory} as constructor argument. + */ +public final class JandexJsonbRegistry implements JsonbRegistry { + + private static final Logger LOG = LoggerFactory.getLogger(JandexJsonbRegistry.class); + + private final List> adapters; + + private final List> serializers; + + private final List> deserializers; + + /** + * Constructor without classes directory. Assumes that classes are in "target/classes". + * + * @param entityIdFactory Factory to use in case an adapter requires it for being constructed. + * @param serializerRegistry Serializer registry used to construct serializers/deserializers. + * @param deserializerRegistry Deserializer registry used to construct serializers/deserializers. + * @param jsonbProvider Provides an JSON-B instance. + */ + public JandexJsonbRegistry(final EntityIdFactory entityIdFactory, + final SerializerRegistry serializerRegistry, + final DeserializerRegistry deserializerRegistry, + final JsonbProvider jsonbProvider) { + this(entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, new File("target/classes")); + } + + /** + * Constructor with classes directories. Most likely only used in tests. + * + * @param entityIdFactory Factory to use in case an adapter requires it for being constructed. + * @param serializerRegistry Serializer registry used to construct serializers/deserializers. + * @param deserializerRegistry Deserializer registry used to construct serializers/deserializers. + * @param jsonbProvider Provides an JSON-B instance. + * @param classesDirs Directories with class files. + */ + public JandexJsonbRegistry(final EntityIdFactory entityIdFactory, + final SerializerRegistry serializerRegistry, + final DeserializerRegistry deserializerRegistry, + final JsonbProvider jsonbProvider, + final File... classesDirs) { + adapters = JandexUtils.findImplementors(JsonbAdapter.class, classesDirs).stream() + .peek(adapter -> LOG.info("Found {}: {}", JsonbAdapter.class.getSimpleName(), adapter)) + .map(clasz -> (JsonbAdapter) createInstance(entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, clasz)) + .filter(Objects::nonNull) + .toList(); + serializers = JandexUtils.findImplementors(JsonbSerializer.class, classesDirs).stream() + .peek(serializer -> LOG.info("Found {}: {}", JsonbSerializer.class.getSimpleName(), serializer)) + .map(clasz -> (JsonbSerializer) createInstance(entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, clasz)) + .filter(Objects::nonNull) + .toList(); + deserializers = JandexUtils.findImplementors(JsonbDeserializer.class, classesDirs).stream() + .peek(deserializer -> LOG.info("Found {}: {}", JsonbDeserializer.class.getSimpleName(), deserializer)) + .map(clasz -> (JsonbDeserializer) createInstance(entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, clasz)) + .filter(Objects::nonNull) + .toList(); + } + + @Override + public List> getAdapters() { + return adapters; + } + + @Override + public List> getSerializers() { + return serializers; + } + + @Override + public List> getDeserializers() { + return deserializers; + } + + @SuppressWarnings("unchecked") + static T createInstance(final EntityIdFactory entityIdFactory, + final SerializerRegistry serializerRegistry, + final DeserializerRegistry deserializerRegistry, + final JsonbProvider jsonbProvider, + final Class clasz) { + + final Constructor[] constructors = clasz.getConstructors(); + if (constructors.length == 0) { + LOG.warn("No public constructor found: {}", clasz.getName()); + } else { + for (final Constructor constructor : constructors) { + if (constructor.getParameterCount() == 0) { + return createInstance(clasz, () -> (T) constructor.newInstance()); + } else { + final Optional args = parameters(entityIdFactory, serializerRegistry, + deserializerRegistry, jsonbProvider, constructor); + if (args.isPresent()) { + return createInstance(clasz, () -> (T) constructor.newInstance(args.get())); + } + } + } + throw new IllegalArgumentException("Didn't find an appropriate constructor for: '" + clasz.getName()); + } + return null; + } + + static Optional parameters(final EntityIdFactory entityIdFactory, + final SerializerRegistry serializerRegistry, + final DeserializerRegistry deserializerRegistry, + final JsonbProvider jsonbProvider, + final Constructor constructor) { + final Object[] args = new Object[constructor.getParameterCount()]; + for (int i = 0; i < constructor.getParameterCount(); i++) { + final Optional param = parameter(entityIdFactory, serializerRegistry, deserializerRegistry, + jsonbProvider, constructor.getParameterTypes()[i]); + if (param.isPresent()) { + args[i] = param.get(); + } else { + return Optional.empty(); + } + } + return Optional.of(args); + } + + static Optional parameter(final EntityIdFactory entityIdFactory, + final SerializerRegistry serializerRegistry, + final DeserializerRegistry deserializerRegistry, + final JsonbProvider jsonbProvider, + Class parameterType) { + if (EntityIdFactory.class.isAssignableFrom(parameterType)) { + return Optional.of(entityIdFactory); + } + if (SerializerRegistry.class.isAssignableFrom(parameterType)) { + return Optional.of(serializerRegistry); + } + if (DeserializerRegistry.class.isAssignableFrom(parameterType)) { + return Optional.of(deserializerRegistry); + } + if (JsonbProvider.class.isAssignableFrom(parameterType)) { + return Optional.of(jsonbProvider); + } + return Optional.empty(); + } + + private static T createInstance(final Class clasz, final NewInstanceSupplier supplier) { + try { + return supplier.supply(); + } catch (final InstantiationException | IllegalAccessException | InvocationTargetException ex) { + LOG.error("Failed to instantiate {}", clasz.getName(), ex); + return null; + } + } + + private interface NewInstanceSupplier { + T supply() throws InstantiationException, IllegalAccessException, InvocationTargetException; + } + +} diff --git a/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/JsonbRegistry.java b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/JsonbRegistry.java new file mode 100644 index 0000000..0dee68c --- /dev/null +++ b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/JsonbRegistry.java @@ -0,0 +1,35 @@ +package org.fuin.cqrs4j.jsonb; + +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.serializer.JsonbDeserializer; +import jakarta.json.bind.serializer.JsonbSerializer; + +import java.util.List; + +/** + * Contains all known JSON-B adapters, serializers and deserializers. + */ +public interface JsonbRegistry { + + /** + * Returns a list of JSON-B adapter instances. + * + * @return Adapters. + */ + public List> getAdapters(); + + /** + * Returns a list of JSON-B serializer instances. + * + * @return Serializers. + */ + public List> getSerializers(); + + /** + * Returns a list of JSON-B deserializer instances. + * + * @return Deserializers. + */ + public List> getDeserializers(); + +} diff --git a/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/SimpleResult.java b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/SimpleResult.java new file mode 100644 index 0000000..1bf7272 --- /dev/null +++ b/jsonb/src/main/java/org/fuin/cqrs4j/jsonb/SimpleResult.java @@ -0,0 +1,163 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jsonb; + +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.objects4j.common.Contract; +import org.fuin.objects4j.common.ExceptionShortIdentifable; + +import java.io.Serial; + +/** + * Result of a request. The type signals if the execution was successful or not. In case the result is not {@link ResultType#OK}, the + * fields code and message should contain unique information to help the user identifying the cause of the problem. A simple result does not + * carry any additional data. + */ +public final class SimpleResult extends AbstractResult { + + @Serial + private static final long serialVersionUID = 1000L; + + /** + * Protected default constructor for de-serialization. + */ + protected SimpleResult() { // NOSONAR Ignore uninitialized fields + super(); + } + + /** + * Constructor with all data. + * + * @param type + * Type. + * @param code + * Code. + * @param message + * Message. + */ + public SimpleResult(@NotNull final ResultType type, @Nullable final String code, @Nullable final String message) { + super(type, code, message); + } + + /** + * Constructor with exception. An exception of type {@link ExceptionShortIdentifable} will be used to fill the code field + * with the identifier value. If it's not a {@link ExceptionShortIdentifable} the code field will be set using the full + * qualified class name of the exception. + * + * @param exception + * The message for the result is equal to the exception message or the simple name of the exception class if the exception + * message is null. + */ + public SimpleResult(@NotNull final Exception exception) { + super(exception); + } + + @Override + public Void getData() { + return null; + } + + @Override + public final int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((getCode() == null) ? 0 : getCode().hashCode()); + result = prime * result + ((getMessage() == null) ? 0 : getMessage().hashCode()); + result = prime * result + getType().hashCode(); + return result; + } + + @Override + public final boolean equals(final Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final SimpleResult other = (SimpleResult) obj; + if (getCode() == null) { + if (other.getCode() != null) { + return false; + } + } else if (!getCode().equals(other.getCode())) { + return false; + } + if (getMessage() == null) { + if (other.getMessage() != null) { + return false; + } + } else if (!getMessage().equals(other.getMessage())) { + return false; + } + return getType() == other.getType(); + } + + + @Override + public final String toString() { + return "Result [type=" + getType() + ", code=" + getCode() + ", message=" + getMessage() + "]"; + } + + /** + * Create a success result. + * + * @return Result with type {@link ResultType#OK}. + */ + public static SimpleResult ok() { + return new SimpleResult(ResultType.OK, null, null); + } + + /** + * Create a warning result. + * + * @param code + * Code. + * @param message + * Message. + * + * @return Result with type {@link ResultType#WARNING}. + */ + public static SimpleResult warning(@NotNull final String code, @NotNull final String message) { + Contract.requireArgNotNull("code", code); + Contract.requireArgNotNull("message", message); + return new SimpleResult(ResultType.WARNING, code, message); + } + + /** + * Create an error result. + * + * @param code + * Code. + * @param message + * Message. + * + * @return Result with type {@link ResultType#ERROR}. + */ + public static SimpleResult error(@NotNull final String code, @NotNull final String message) { + Contract.requireArgNotNull("code", code); + Contract.requireArgNotNull("message", message); + return new SimpleResult(ResultType.ERROR, code, message); + } + +} diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/ACreatedEvent.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/ACreatedEvent.java new file mode 100644 index 0000000..f409537 --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/ACreatedEvent.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jsonb; + +import org.fuin.ddd4j.core.EntityIdPath; +import org.fuin.ddd4j.core.EventType; +import org.fuin.ddd4j.jsonb.AbstractDomainEvent; +import org.fuin.esc.api.HasSerializedDataTypeConstant; +import org.fuin.esc.api.SerializedDataType; +import org.fuin.esc.api.TypeName; +import org.fuin.utils4j.TestOmitted; + +import java.io.Serial; + +@TestOmitted("This is only a test class") +@HasSerializedDataTypeConstant +public class ACreatedEvent extends AbstractDomainEvent { + + @Serial + private static final long serialVersionUID = 1L; + + /** Unique name of the event. */ + public static final TypeName TYPE = new TypeName("ACreatedEvent"); + + /** Unique name of the serialized event. */ + public static final SerializedDataType SER_TYPE = new SerializedDataType(TYPE.asBaseType()); + + private static final EventType EVENT_TYPE = new EventType(TYPE.asBaseType()); + + private AId id; + + public ACreatedEvent(final AId id) { + super(new EntityIdPath(id)); + this.id = id; + } + + public AId getId() { + return id; + } + + @Override + public EventType getEventType() { + return EVENT_TYPE; + } + +} diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/AId.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/AId.java new file mode 100644 index 0000000..d9c36f6 --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/AId.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jsonb; + +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.utils4j.TestOmitted; + +import java.io.Serial; + +@TestOmitted("This is only a test class") +public class AId implements AggregateRootId { + + @Serial + private static final long serialVersionUID = 1L; + + public static final EntityType TYPE = new StringBasedEntityType("A"); + + private final long id; + + public AId(final long id) { + this.id = id; + } + + @Override + public EntityType getType() { + return TYPE; + } + + @Override + public String asString() { + return "" + id; + } + + @Override + public String asTypedString() { + return getType() + " " + asString(); + } + + @Override + public String toString() { + return "AId [id=" + id + "]"; + } + +} diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/AbstractAggregateCommandTest.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/AbstractAggregateCommandTest.java new file mode 100644 index 0000000..397dcdd --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/AbstractAggregateCommandTest.java @@ -0,0 +1,432 @@ +package org.fuin.cqrs4j.jsonb; + +import jakarta.json.bind.Jsonb; +import jakarta.validation.Validation; +import org.fuin.ddd4j.core.AggregateVersion; +import org.fuin.ddd4j.core.EntityIdPath; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventId; +import org.fuin.ddd4j.core.EventType; +import org.fuin.ddd4j.jsonb.AbstractEvent; +import org.fuin.objects4j.common.Contract; +import org.junit.jupiter.api.Test; + +import java.io.Serial; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.fuin.cqrs4j.jsonb.TestUtils.jsonb; +import static org.fuin.utils4j.Utils4J.deserialize; +import static org.fuin.utils4j.Utils4J.serialize; + +public class AbstractAggregateCommandTest { + + private static final EventType MY_EVENT_TYPE = new EventType("MyEvent"); + + private static final EventType MY_COMMAND_TYPE = new EventType("MyCommandt"); + + @Test + public final void testConstructor() { + + // PREPARE + final AId aid = new AId(123L); + final EntityIdPath entityIdPath = new EntityIdPath(aid); + final AggregateVersion version = new AggregateVersion(1); + + // TEST + final AbstractAggregateCommand testee = new MyCommand(entityIdPath, version); + + // VERIFY + assertThat(testee.getAggregateRootId()).isEqualTo(aid); + assertThat(testee.getEntityId()).isEqualTo(aid); + assertThat(testee.getAggregateVersion()).isEqualTo(version); + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isNull(); + assertThat(testee.getCorrelationId()).isNull(); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testConstructor2() { + + // PREPARE + final AId aid = new AId(123L); + final BId bid = new BId(1L); + final EntityIdPath entityIdPath = new EntityIdPath(aid, bid); + final AggregateVersion version = new AggregateVersion(1); + + // TEST + final AbstractAggregateCommand testee = new MyCommand2(entityIdPath, version); + + // VERIFY + assertThat(testee.getAggregateRootId()).isEqualTo(aid); + assertThat(testee.getEntityId()).isEqualTo(bid); + assertThat(testee.getAggregateVersion()).isEqualTo(version); + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isNull(); + assertThat(testee.getCorrelationId()).isNull(); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testConstructor3() { + + // PREPARE + final AId aid = new AId(123L); + final BId bid = new BId(1L); + final CId cid = new CId(2L); + final EntityIdPath entityIdPath = new EntityIdPath(aid, bid, cid); + final AggregateVersion version = new AggregateVersion(1); + + // TEST + final AbstractAggregateCommand testee = new MyCommand3(entityIdPath, version); + + // VERIFY + assertThat(testee.getAggregateRootId()).isEqualTo(aid); + assertThat(testee.getEntityId()).isEqualTo(cid); + assertThat(testee.getAggregateVersion()).isEqualTo(version); + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isNull(); + assertThat(testee.getCorrelationId()).isNull(); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testConstructorEvent() { + + // PREPARE + final AId aid = new AId(123L); + final EntityIdPath entityIdPath = new EntityIdPath(aid); + final AggregateVersion version = new AggregateVersion(1); + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyEvent event = new MyEvent(correlationId, causationId); + + // TEST + final AbstractAggregateCommand testee = new MyCommand(entityIdPath, version, event); + + // VERIFY + assertThat(testee.getAggregateRootId()).isEqualTo(aid); + assertThat(testee.getEntityId()).isEqualTo(aid); + assertThat(testee.getAggregateVersion()).isEqualTo(version); + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isEqualTo(event.getEventId()); + assertThat(testee.getCorrelationId()).isEqualTo(correlationId); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testConstructorCorrelationCausation() { + + // PREPARE + final AId aid = new AId(123L); + final EntityIdPath entityIdPath = new EntityIdPath(aid); + final AggregateVersion version = new AggregateVersion(1); + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + + // TEST + final AbstractAggregateCommand testee = new MyCommand(entityIdPath, version, correlationId, causationId); + + // VERIFY + assertThat(testee.getAggregateRootId()).isEqualTo(aid); + assertThat(testee.getEntityId()).isEqualTo(aid); + assertThat(testee.getAggregateVersion()).isEqualTo(version); + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isEqualTo(causationId); + assertThat(testee.getCorrelationId()).isEqualTo(correlationId); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testBuilder() { + + // PREPARE + final EventId eventId = new EventId(); + final ZonedDateTime timestamp = ZonedDateTime.now(); + final AId aid = new AId(123L); + final EntityIdPath entityIdPath = new EntityIdPath(aid); + final AggregateVersion version = new AggregateVersion(1); + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyCommand.Builder testee = new MyCommand.Builder(); + + // TEST + final MyCommand cmd = testee + .eventId(eventId) + .timestamp(timestamp) + .entityIdPath(entityIdPath) + .aggregateVersion(version) + .correlationId(correlationId) + .causationId(causationId).build(); + + // VERIFY + assertThat(cmd.getEventId()).isEqualTo(eventId); + assertThat(cmd.getEventTimestamp()).isEqualTo(timestamp); + assertThat(cmd.getAggregateRootId()).isEqualTo(aid); + assertThat(cmd.getEntityId()).isEqualTo(aid); + assertThat(cmd.getAggregateVersion()).isEqualTo(version); + assertThat(cmd.getEventId()).isNotNull(); + assertThat(cmd.getEventTimestamp()).isNotNull(); + assertThat(cmd.getCausationId()).isEqualTo(causationId); + assertThat(cmd.getCorrelationId()).isEqualTo(correlationId); + assertThat(cmd.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testSerializeDeserialize() { + + // PREPARE + final AId aid = new AId(123L); + final EntityIdPath entityIdPath = new EntityIdPath(aid); + final AggregateVersion version = new AggregateVersion(1); + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyCommand original = new MyCommand(entityIdPath, version, correlationId, causationId); + + // TEST + final MyCommand copy = deserialize(serialize(original)); + + // VERIFY + assertThat(copy).isEqualTo(original); + assertThat(copy.getEntityIdPath()).isEqualTo(new EntityIdPath(aid)); + assertThat(copy.getAggregateVersion()).isEqualTo(version); + assertThat(copy.getCausationId()).isEqualTo(original.getCausationId()); + assertThat(copy.getCorrelationId()).isEqualTo(original.getCorrelationId()); + assertThat(copy.getEventId()).isEqualTo(original.getEventId()); + assertThat(copy.getEventType()).isEqualTo(original.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(original.getEventTimestamp()); + + } + + @Test + public final void testMarshalUnmarshal() throws Exception { + + try (final Jsonb jsonb = jsonb()) { + + // PREPARE + final AId aid = new AId(123L); + final EntityIdPath entityIdPath = new EntityIdPath(aid); + final AggregateVersion version = new AggregateVersion(1); + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyCommand original = new MyCommand(entityIdPath, version, correlationId, causationId); + + // TEST + final String json = jsonb.toJson(original); + final MyCommand copy = jsonb.fromJson(json, MyCommand.class); + + // VERIFY + assertThat(copy).isEqualTo(original); + assertThat(copy.getEntityIdPath()).isEqualTo(new EntityIdPath(aid)); + assertThat(copy.getAggregateVersion()).isEqualTo(version); + assertThat(copy.getCausationId()).isEqualTo(original.getCausationId()); + assertThat(copy.getCorrelationId()).isEqualTo(original.getCorrelationId()); + assertThat(copy.getEventId()).isEqualTo(original.getEventId()); + assertThat(copy.getEventType()).isEqualTo(original.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(original.getEventTimestamp()); + + } + + } + + @Test + public final void testUnmarshal() throws Exception { + + try (final Jsonb jsonb = jsonb()) { + + // PREPARE + final String json = """ + { + "entity-id-path" : "A 1/B 2/C 3", + "aggregate-version" : 1, + "event-id" : "f910c6d7-debc-46e1-ae02-9ca6f4658cf5", + "event-timestamp" : "2016-09-18T10:38:08.0+02:00[Europe/Berlin]", + "correlation-id" : "2a5893a9-00da-4003-b280-98324eccdef1", + "causation-id" : "f13d3481-51b7-423f-8fe7-5c342f7d7c46" + } + """; + + // TEST + final MyCommand copy = jsonb.fromJson(json, MyCommand.class); + + // VERIFY + Contract.requireValid(Validation.buildDefaultValidatorFactory().getValidator(), copy); + assertThat(copy.getEntityIdPath()).isEqualTo(new EntityIdPath(new AId(1L), new BId(2L), new CId(3L))); + assertThat(copy.getAggregateVersion()).isEqualTo(new AggregateVersion(1)); + assertThat(copy.getCausationId()).isEqualTo(new EventId(UUID.fromString("f13d3481-51b7-423f-8fe7-5c342f7d7c46"))); + assertThat(copy.getCorrelationId()).isEqualTo(new EventId(UUID.fromString("2a5893a9-00da-4003-b280-98324eccdef1"))); + assertThat(copy.getEventId()).isEqualTo(new EventId(UUID.fromString("f910c6d7-debc-46e1-ae02-9ca6f4658cf5"))); + assertThat(copy.getEventType()).isEqualTo(copy.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(ZonedDateTime.of(2016, 9, 18, 10, 38, 8, 0, ZoneId.of("Europe/Berlin"))); + + } + + } + + @Test + public final void testUnmarshalNullVersion() throws Exception { + + try (final Jsonb jsonb = jsonb()) { + + // PREPARE + final String json = """ + { + "entity-id-path" : "A 1/B 2/C 3", + "event-id" : "f910c6d7-debc-46e1-ae02-9ca6f4658cf5", + "event-timestamp" : "2016-09-18T10:38:08.0+02:00[Europe/Berlin]", + "correlation-id" : "2a5893a9-00da-4003-b280-98324eccdef1", + "causation-id" : "f13d3481-51b7-423f-8fe7-5c342f7d7c46" + } + """; + + // TEST + final MyCommand copy = jsonb.fromJson(json, MyCommand.class); + + // VERIFY + Contract.requireValid(Validation.buildDefaultValidatorFactory().getValidator(), copy); + assertThat(copy.getEntityIdPath()).isEqualTo(new EntityIdPath(new AId(1L), new BId(2L), new CId(3L))); + assertThat(copy.getAggregateVersion()).isNull(); + assertThat(copy.getCausationId()).isEqualTo(new EventId(UUID.fromString("f13d3481-51b7-423f-8fe7-5c342f7d7c46"))); + assertThat(copy.getCorrelationId()).isEqualTo(new EventId(UUID.fromString("2a5893a9-00da-4003-b280-98324eccdef1"))); + assertThat(copy.getEventId()).isEqualTo(new EventId(UUID.fromString("f910c6d7-debc-46e1-ae02-9ca6f4658cf5"))); + assertThat(copy.getEventType()).isEqualTo(copy.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(ZonedDateTime.of(2016, 9, 18, 10, 38, 8, 0, ZoneId.of("Europe/Berlin"))); + + } + + } + + public static class MyCommand extends AbstractAggregateCommand { + + @Serial + private static final long serialVersionUID = 1L; + + public MyCommand() { + super(); + } + + public MyCommand(EntityIdPath entityIdPath, AggregateVersion aggregateVersion) { + super(entityIdPath, aggregateVersion); + } + + public MyCommand(EntityIdPath entityIdPath, AggregateVersion aggregateVersion, Event respondTo) { + super(entityIdPath, aggregateVersion, respondTo); + } + + public MyCommand(EntityIdPath entityIdPath, AggregateVersion aggregateVersion, EventId correlationId, EventId causationId) { + super(entityIdPath, aggregateVersion, correlationId, causationId); + } + + @Override + public EventType getEventType() { + return MY_COMMAND_TYPE; + } + + public static class Builder extends AbstractAggregateCommand.Builder { + + private MyCommand delegate; + + public Builder() { + super(new MyCommand()); + delegate = delegate(); + } + + public MyCommand build() { + ensureBuildableAbstractAggregateCommand(); + final MyCommand result = delegate; + delegate = new MyCommand(); + resetAbstractAggregateCommand(delegate); + return result; + } + + } + + } + + public static class MyCommand2 extends AbstractAggregateCommand { + + @Serial + private static final long serialVersionUID = 1L; + + public MyCommand2() { + super(); + } + + public MyCommand2(EntityIdPath entityIdPath, AggregateVersion aggregateVersion) { + super(entityIdPath, aggregateVersion); + } + + public MyCommand2(EntityIdPath entityIdPath, AggregateVersion aggregateVersion, Event respondTo) { + super(entityIdPath, aggregateVersion, respondTo); + } + + public MyCommand2(EntityIdPath entityIdPath, AggregateVersion aggregateVersion, EventId correlationId, EventId causationId) { + super(entityIdPath, aggregateVersion, correlationId, causationId); + } + + @Override + public EventType getEventType() { + return MY_COMMAND_TYPE; + } + + } + + public static class MyCommand3 extends AbstractAggregateCommand { + + @Serial + private static final long serialVersionUID = 1L; + + public MyCommand3() { + super(); + } + + public MyCommand3(EntityIdPath entityIdPath, AggregateVersion aggregateVersion) { + super(entityIdPath, aggregateVersion); + } + + public MyCommand3(EntityIdPath entityIdPath, AggregateVersion aggregateVersion, Event respondTo) { + super(entityIdPath, aggregateVersion, respondTo); + } + + public MyCommand3(EntityIdPath entityIdPath, AggregateVersion aggregateVersion, EventId correlationId, EventId causationId) { + super(entityIdPath, aggregateVersion, correlationId, causationId); + } + + @Override + public EventType getEventType() { + return MY_COMMAND_TYPE; + } + + } + + public static class MyEvent extends AbstractEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public MyEvent(EventId correlationId, EventId causationId) { + super(correlationId, causationId); + } + + @Override + public EventType getEventType() { + return MY_EVENT_TYPE; + } + + } + +} diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/AbstractCommandTest.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/AbstractCommandTest.java new file mode 100644 index 0000000..0390e47 --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/AbstractCommandTest.java @@ -0,0 +1,177 @@ +package org.fuin.cqrs4j.jsonb; + +import jakarta.json.bind.Jsonb; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventId; +import org.fuin.ddd4j.core.EventType; +import org.fuin.ddd4j.jsonb.AbstractEvent; +import org.junit.jupiter.api.Test; + +import java.io.Serial; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.fuin.cqrs4j.jsonb.TestUtils.jsonb; +import static org.fuin.utils4j.Utils4J.deserialize; +import static org.fuin.utils4j.Utils4J.serialize; + +public class AbstractCommandTest { + + private static final EventType MY_EVENT_TYPE = new EventType("MyEvent"); + + private static final EventType MY_COMMAND_TYPE = new EventType("MyCommand"); + + @Test + public final void testConstructorDefault() { + + // TEST + final AbstractCommand testee = new MyCommand(); + + // VERIFY + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isNull(); + assertThat(testee.getCorrelationId()).isNull(); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testConstructorEvent() { + + // PREPARE + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyEvent event = new MyEvent(correlationId, causationId); + + // TEST + final AbstractCommand testee = new MyCommand(event); + + // VERIFY + assertThat(testee.getEventId()).isNotNull(); + assertThat(testee.getEventTimestamp()).isNotNull(); + assertThat(testee.getCausationId()).isEqualTo(event.getEventId()); + assertThat(testee.getCorrelationId()).isEqualTo(correlationId); + assertThat(testee.getEventType()).isEqualTo(MY_COMMAND_TYPE); + + } + + @Test + public final void testSerializeDeserialize() { + + // PREPARE + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyCommand original = new MyCommand(correlationId, causationId); + + // TEST + final MyCommand copy = deserialize(serialize(original)); + + // VERIFY + assertThat(copy).isEqualTo(original); + assertThat(copy.getCausationId()).isEqualTo(original.getCausationId()); + assertThat(copy.getCorrelationId()).isEqualTo(original.getCorrelationId()); + assertThat(copy.getEventId()).isEqualTo(original.getEventId()); + assertThat(copy.getEventType()).isEqualTo(original.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(original.getEventTimestamp()); + + } + + @Test + public final void testMarshalUnmarshal() throws Exception { + + try (final Jsonb jsonb = jsonb()) { + + // PREPARE + final EventId correlationId = new EventId(); + final EventId causationId = new EventId(); + final MyCommand original = new MyCommand(correlationId, causationId); + + // TEST + final String json = jsonb.toJson(original, MyCommand.class); + final MyCommand copy = jsonb.fromJson(json, MyCommand.class); + + // VERIFY + assertThat(copy).isEqualTo(original); + assertThat(copy.getCausationId()).isEqualTo(original.getCausationId()); + assertThat(copy.getCorrelationId()).isEqualTo(original.getCorrelationId()); + assertThat(copy.getEventId()).isEqualTo(original.getEventId()); + assertThat(copy.getEventType()).isEqualTo(original.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(original.getEventTimestamp()); + + } + + } + + @Test + public final void testUnmarshal() throws Exception { + + try (final Jsonb jsonb = jsonb()) { + + // PREPARE + final String originalJson = """ + { + "event-id" : "f910c6d7-debc-46e1-ae02-9ca6f4658cf5", + "event-timestamp" : "2016-09-18T10:38:08.0+02:00[Europe/Berlin]", + "correlation-id" : "2a5893a9-00da-4003-b280-98324eccdef1", + "causation-id" : "f13d3481-51b7-423f-8fe7-5c342f7d7c46" + } + """; + + // TEST + final MyCommand copy = jsonb.fromJson(originalJson, MyCommand.class); + + // VERIFY + assertThat(copy.getCausationId()).isEqualTo(new EventId(UUID.fromString("f13d3481-51b7-423f-8fe7-5c342f7d7c46"))); + assertThat(copy.getCorrelationId()).isEqualTo(new EventId(UUID.fromString("2a5893a9-00da-4003-b280-98324eccdef1"))); + assertThat(copy.getEventId()).isEqualTo(new EventId(UUID.fromString("f910c6d7-debc-46e1-ae02-9ca6f4658cf5"))); + assertThat(copy.getEventType()).isEqualTo(copy.getEventType()); + assertThat(copy.getEventTimestamp()).isEqualTo(ZonedDateTime.of(2016, 9, 18, 10, 38, 8, 0, ZoneId.of("Europe/Berlin"))); + + } + + } + + public static class MyCommand extends AbstractCommand { + + @Serial + private static final long serialVersionUID = 1L; + + public MyCommand() { + super(); + } + + public MyCommand(Event respondTo) { + super(respondTo); + } + + public MyCommand(EventId correlationId, EventId causationId) { + super(correlationId, causationId); + } + + @Override + public EventType getEventType() { + return MY_COMMAND_TYPE; + } + + } + + public static class MyEvent extends AbstractEvent { + + @Serial + private static final long serialVersionUID = 1L; + + public MyEvent(EventId correlationId, EventId causationId) { + super(correlationId, causationId); + } + + @Override + public EventType getEventType() { + return MY_EVENT_TYPE; + } + + } + +} diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/ArchitectureTest.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/ArchitectureTest.java new file mode 100644 index 0000000..7f222c0 --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/ArchitectureTest.java @@ -0,0 +1,45 @@ +package org.fuin.cqrs4j.jsonb; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import org.fuin.cqrs4j.core.Command; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.library.DependencyRules.NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + +/** + * Tests architectural aspects. + */ +@AnalyzeClasses(packagesOf = ArchitectureTest.class, importOptions = ImportOption.DoNotIncludeTests.class) +public class ArchitectureTest { + + private static final String THIS_PACKAGE = ArchitectureTest.class.getPackageName(); + + private static final String CORE_PACKAGE = Command.class.getPackageName(); + + @ArchTest + static final ArchRule no_accesses_to_upper_package = NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + + @ArchTest + static final ArchRule access_only_to_defined_packages = classes() + .that() + .resideInAPackage(THIS_PACKAGE) + .should() + .onlyDependOnClassesThat() + .resideInAnyPackage(THIS_PACKAGE, CORE_PACKAGE, "java..", + "org.fuin.ddd4j.common..", + "org.fuin.ddd4j.core..", + "org.fuin.ddd4j.jsonb..", + "org.fuin.objects4j.ui..", + "org.fuin.objects4j.common..", + "org.fuin.objects4j.core..", + "jakarta.validation..", + "jakarta.annotation..", + "jakarta.json..", + "org.slf4j..", + "javax.annotation.concurrent.." + ); + +} diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/BId.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/BId.java new file mode 100644 index 0000000..bb0ff81 --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/BId.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jsonb; + +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.utils4j.TestOmitted; + +import java.io.Serial; + +@TestOmitted("This is only a test class") +public class BId implements EntityId { + + @Serial + private static final long serialVersionUID = 1L; + + public static final EntityType TYPE = new StringBasedEntityType("B"); + + private final long id; + + public BId(final long id) { + this.id = id; + } + + @Override + public EntityType getType() { + return TYPE; + } + + @Override + public String asString() { + return "" + id; + } + + @Override + public String asTypedString() { + return getType() + " " + asString(); + } + + @Override + public String toString() { + return "BId [id=" + id + "]"; + } + +} diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/BaseTest.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/BaseTest.java new file mode 100644 index 0000000..530c205 --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/BaseTest.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2013 Future Invent Informationsmanagement GmbH. All rights + * reserved. + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ +package org.fuin.cqrs4j.jsonb; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import org.fuin.units4j.archunit.Units4JConditions; + +@AnalyzeClasses(packagesOf = BaseTest.class) +class BaseTest { + + @ArchTest + static final ArchRule all_classes_should_have_tests = Units4JConditions.ALL_CLASSES_SHOULD_HAVE_TESTS; + +} + diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/CId.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/CId.java new file mode 100644 index 0000000..e734b17 --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/CId.java @@ -0,0 +1,61 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jsonb; + +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.utils4j.TestOmitted; + +import java.io.Serial; + +@TestOmitted("This is only a test class") +public class CId implements EntityId { + + @Serial + private static final long serialVersionUID = 1L; + + public static final EntityType TYPE = new StringBasedEntityType("C"); + + private final long id; + + public CId(final long id) { + this.id = id; + } + + @Override + public EntityType getType() { + return TYPE; + } + + @Override + public String asString() { + return "" + id; + } + + @Override + public String asTypedString() { + return getType() + " " + asString(); + } + + @Override + public String toString() { + return "CId [id=" + id + "]"; + } + +} diff --git a/src/test/java/org/fuin/cqrs4j/DataResultJsonbAdapterTest.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/DataResultJsonbAdapterTest.java similarity index 70% rename from src/test/java/org/fuin/cqrs4j/DataResultJsonbAdapterTest.java rename to jsonb/src/test/java/org/fuin/cqrs4j/jsonb/DataResultJsonbAdapterTest.java index c388be6..f43c757 100644 --- a/src/test/java/org/fuin/cqrs4j/DataResultJsonbAdapterTest.java +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/DataResultJsonbAdapterTest.java @@ -1,48 +1,64 @@ /** - * Copyright (C) 2015 Michael Schnell. All rights reserved. + * Copyright (C) 2015 Michael Schnell. All rights reserved. * http://www.fuin.org/ - * + *

* This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. - * + *

* This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. - * + *

* You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see http://www.gnu.org/licenses/. */ -package org.fuin.cqrs4j; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.io.IOException; +package org.fuin.cqrs4j.jsonb; import jakarta.json.bind.Jsonb; import jakarta.json.bind.JsonbBuilder; import jakarta.json.bind.JsonbConfig; import jakarta.json.bind.adapter.JsonbAdapter; import jakarta.json.bind.annotation.JsonbProperty; - -import org.apache.commons.io.IOUtils; import org.eclipse.yasson.FieldAccessStrategy; -import org.fuin.cqrs4j.DataResultTest.Invoice; -import org.fuin.ddd4j.ddd.AggregateNotFoundException; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.ddd4j.jsonb.AggregateNotFoundExceptionData; +import org.fuin.objects4j.jsonb.JsonbProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.io.IOException; + +import static org.assertj.core.api.Assertions.assertThat; + /** * Test for the {@link DataResultJsonbAdapter} class. */ public class DataResultJsonbAdapterTest { + private JsonbProvider jsonbProvider; + + private Jsonb jsonb; + + @BeforeEach + public void setUp() { + jsonbProvider = jsonbProvider(); + jsonb = jsonb(jsonbProvider); + } + + @AfterEach + public void tearDown() { + jsonb = null; + jsonbProvider = null; + } + @Test public final void testToFromJson() throws Exception { // PREPARE - final Jsonb jsonb = jsonbb(); final DataResult original = DataResult.ok(new MyData(1, "one"), "my-data"); // TEST @@ -58,8 +74,11 @@ public final void testToFromJson() throws Exception { public final void testFromToJsonVoidResult() throws IOException { // PREPARE - final Jsonb jsonb = jsonbb(); - final String jsonOriginal = IOUtils.toString(this.getClass().getResourceAsStream("/data-result-void.json"), "utf-8"); + final String jsonOriginal = """ + { + "type": "OK" + } + """; // TEST final DataResult original = jsonb.fromJson(jsonOriginal, DataResult.class); @@ -83,8 +102,11 @@ public final void testFromToJsonVoidResult() throws IOException { public final void testFromToJsonSimpleResultOK() throws IOException { // PREPARE - final Jsonb jsonb = jsonbb(); - final String jsonOriginal = IOUtils.toString(this.getClass().getResourceAsStream("/simple-result-ok.json"), "utf-8"); + final String jsonOriginal = """ + { + "type": "OK" + } + """; // TEST final SimpleResult original = jsonb.fromJson(jsonOriginal, SimpleResult.class); @@ -106,8 +128,13 @@ public final void testFromToJsonSimpleResultOK() throws IOException { public final void testFromToJsonSimpleResultException() throws IOException { // PREPARE - final Jsonb jsonb = jsonbb(); - final String jsonOriginal = IOUtils.toString(this.getClass().getResourceAsStream("/simple-result-exception.json"), "utf-8"); + final String jsonOriginal = """ + { + "type": "ERROR", + "code": "DDD4J-AGGREGATE_NOT_FOUND", + "message": "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found" + } + """; // TEST final SimpleResult original = jsonb.fromJson(jsonOriginal, SimpleResult.class); @@ -129,11 +156,19 @@ public final void testFromToJsonSimpleResultException() throws IOException { public final void testFromToJsonResultData() throws IOException { // PREPARE - final Jsonb jsonb = jsonbb(); - final String jsonOriginal = IOUtils.toString(this.getClass().getResourceAsStream("/data-result-data.json"), "utf-8"); + final String jsonOriginal = """ + { + "type": "OK", + "data-class": "org.fuin.cqrs4j.jsonb.DataResultTest$Invoice", + "data-element": "invoice", + "invoice": { + "id" : "I-0123456" + } + } + """; // TEST - final DataResult original = jsonb.fromJson(jsonOriginal, DataResult.class); + final DataResult original = jsonb.fromJson(jsonOriginal, DataResult.class); // VERIFY assertThat(original.getType()).isEqualTo(ResultType.OK); @@ -141,12 +176,12 @@ public final void testFromToJsonResultData() throws IOException { assertThat(original.getMessage()).isNull(); assertThat(original.getDataClass()).isEqualTo(DataResultTest.Invoice.class.getName()); assertThat(original.getDataElement()).isEqualTo("invoice"); - assertThat(original.getData()).isInstanceOf(Invoice.class); - assertThat(((Invoice) original.getData()).getId()).isEqualTo("I-0123456"); + assertThat(original.getData()).isInstanceOf(DataResultTest.Invoice.class); + assertThat(original.getData().getId()).isEqualTo("I-0123456"); // TEST final String jsonCopy = jsonb.toJson(original); - final DataResult copy = jsonb.fromJson(jsonCopy, DataResult.class); + final DataResult copy = jsonb.fromJson(jsonCopy, DataResult.class); assertThat(copy).isEqualTo(original); } @@ -155,43 +190,54 @@ public final void testFromToJsonResultData() throws IOException { public final void testFromToJsonResultException() throws IOException { // PREPARE - final Jsonb jsonb = jsonbb(); - final String jsonOriginal = IOUtils.toString(this.getClass().getResourceAsStream("/data-result-exception.json"), "utf-8"); + final String jsonOriginal = """ + { + "type": "ERROR", + "code": "DDD4J-AGGREGATE_NOT_FOUND", + "message": "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found", + "data-class": "org.fuin.ddd4j.jsonb.AggregateNotFoundExceptionData", + "data-element": "aggregate-not-found-exception", + "aggregate-not-found-exception": { + "msg": "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found", + "sid": "DDD4J-AGGREGATE_NOT_FOUND", + "aggregate-type": "Invoice", + "aggregate-id": "4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119" + } + } + """; // TEST - final DataResult original = jsonb.fromJson(jsonOriginal, DataResult.class); + final DataResult original = jsonb.fromJson(jsonOriginal, DataResult.class); // VERIFY assertThat(original.getType()).isEqualTo(ResultType.ERROR); assertThat(original.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); assertThat(original.getMessage()).isEqualTo("Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found"); - assertThat(original.getDataClass()).isEqualTo("org.fuin.ddd4j.ddd.AggregateNotFoundException$Data"); + assertThat(original.getDataClass()).isEqualTo("org.fuin.ddd4j.jsonb.AggregateNotFoundExceptionData"); assertThat(original.getDataElement()).isEqualTo("aggregate-not-found-exception"); - assertThat(original.getData()).isInstanceOf(AggregateNotFoundException.Data.class); + assertThat(original.getData()).isInstanceOf(AggregateNotFoundExceptionData.class); // TEST final String jsonCopy = jsonb.toJson(original); - final DataResult copy = jsonb.fromJson(jsonCopy, DataResult.class); + final DataResult copy = jsonb.fromJson(jsonCopy, DataResult.class); assertThat(copy).isEqualTo(original); } - private static Jsonb jsonb() { - final JsonbConfig config = new JsonbConfig().withPropertyVisibilityStrategy(new FieldAccessStrategy()) + private static JsonbProvider jsonbProvider() { + final JsonbConfig config = new JsonbConfig() + .withPropertyVisibilityStrategy(new FieldAccessStrategy()) .withAdapters(new InvoiceIdAdapter()); - return JsonbBuilder.create(config); + return new JsonbProvider(config); } - private static Jsonb jsonb(final Jsonb jsonb) { - final JsonbConfig config = new JsonbConfig().withPropertyVisibilityStrategy(new FieldAccessStrategy()) - .withAdapters(new DataResultJsonbAdapter(jsonb), new InvoiceIdAdapter()); + private static Jsonb jsonb(final JsonbProvider jsonbProvider) { + final JsonbConfig config = new JsonbConfig() + .withPropertyVisibilityStrategy(new FieldAccessStrategy()) + .withAdapters(new DataResultJsonbAdapter(jsonbProvider), new InvoiceIdAdapter()); return JsonbBuilder.create(config); } - private static Jsonb jsonbb() { - return jsonb(jsonb()); - } - public static final class InvoiceId { private String id; diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/DataResultTest.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/DataResultTest.java new file mode 100644 index 0000000..5e86bd7 --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/DataResultTest.java @@ -0,0 +1,335 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jsonb; + +import jakarta.json.bind.Jsonb; +import jakarta.json.bind.JsonbBuilder; +import jakarta.json.bind.JsonbConfig; +import jakarta.json.bind.annotation.JsonbProperty; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.eclipse.yasson.FieldAccessStrategy; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.ddd4j.core.AggregateNotFoundException; +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.ddd4j.jsonb.AggregateNotFoundExceptionData; +import org.fuin.objects4j.common.MarshalInformation; +import org.fuin.objects4j.jsonb.JsonbProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.Serial; +import java.util.UUID; + +import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; + +public final class DataResultTest { + + private static final EntityType TEST_TYPE = new StringBasedEntityType("Test"); + + private JsonbProvider jsonbProvider; + + private Jsonb jsonb; + + @BeforeEach + public void setUp() { + jsonbProvider = jsonbProvider(); + jsonb = jsonb(jsonbProvider); + } + + @AfterEach + public void tearDown() { + jsonb = null; + jsonbProvider = null; + } + + @Test + public final void testEqualsHashCode() { + EqualsVerifier.simple().forClass(DataResult.class).verify(); + } + + @Test + public final void testConstructorAll() { + + // PREPARE + final String data = "Whatever"; + + // TEST + final DataResult testee = new DataResult<>(ResultType.WARNING, "X1", "Yes!", data); + + // VERIFY + assertThat(testee.getType()).isEqualTo(ResultType.WARNING); + assertThat(testee.getCode()).isEqualTo("X1"); + assertThat(testee.getMessage()).isEqualTo("Yes!"); + assertThat(testee.getData()).isEqualTo(data); + + } + + @Test + public final void testConstructorException() { + + // PREPARE + final TestId id = new TestId(); + final AggregateNotFoundException ex = new AggregateNotFoundException(TEST_TYPE, id); + final AggregateNotFoundExceptionData exData = new AggregateNotFoundExceptionData(ex); + + // TEST + final DataResult testee = new DataResult<>(exData); + + // VERIFY + assertThat(testee.getType()).isEqualTo(ResultType.ERROR); + assertThat(testee.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(testee.getMessage()).isEqualTo(TEST_TYPE + " with id " + id.asString() + " not found"); + assertThat(testee.getData()).isInstanceOf(AggregateNotFoundExceptionData.class); + + } + + @Test + public final void testUnmarshalMarshalVoidResult() throws Exception { + + // PREPARE + final String originalJson = """ + { + "type": "OK" + } + """; + + // TEST + final DataResult copy = jsonb.fromJson(originalJson, DataResult.class); + + // VERIFY + assertThat(copy.getType()).isEqualTo(ResultType.OK); + + // TEST + final String copyJson = jsonb.toJson(copy, DataResult.class); + + // VERIFY + assertThatJson(copyJson).isEqualTo(originalJson); + + } + + @Test + public final void testUnmarshalMarshalDataResult() throws Exception { + + // PREPARE + final String originalJson = """ + { + "type": "OK", + "data-class": "org.fuin.cqrs4j.jsonb.DataResultTest$Invoice", + "data-element": "invoice", + "invoice": { + "id" : "I-0123456" + } + } + """; + + // TEST + final DataResult copy = jsonb.fromJson(originalJson, DataResult.class); + + // VERIFY + assertThat(copy.getType()).isEqualTo(ResultType.OK); + assertThat(copy.getCode()).isNull(); + assertThat(copy.getMessage()).isNull(); + assertThat(copy.getData()).isInstanceOf(Invoice.class); + assertThat(copy.getData().getId()).isEqualTo("I-0123456"); + + // TEST + final String copyJson = jsonb.toJson(copy, DataResult.class); + + // VERIFY + assertThatJson(copyJson).isEqualTo(originalJson); + + } + + @Test + public final void testUnmarshalExceptionResult() throws Exception { + + // PREPARE + final String originalJson = """ + { + "type": "ERROR", + "code": "DDD4J-AGGREGATE_NOT_FOUND", + "message": "Vendor with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found", + "data-class": "org.fuin.ddd4j.jsonb.AggregateNotFoundExceptionData", + "data-element": "aggregate-not-found-exception", + "aggregate-not-found-exception" : { + "msg" : "Vendor with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found", + "sid" : "DDD4J-AGGREGATE_NOT_FOUND", + "aggregate-type" : "Vendor", + "aggregate-id" : "4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119" + } + } + """; + + // TEST + final DataResult copy = jsonb.fromJson(originalJson, DataResult.class); + + // VERIFY + final String msg = "Vendor with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found"; + assertThat(copy.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(copy.getType()).isEqualTo(ResultType.ERROR); + assertThat(copy.getMessage()).isEqualTo(msg); + assertThat(copy.getData()).isInstanceOf(AggregateNotFoundExceptionData.class); + final AggregateNotFoundException anfe = copy.getData().toException(); + assertThat(anfe.getMessage()).isEqualTo(msg); + assertThat(anfe.getType()).isEqualTo("Vendor"); + assertThat(anfe.getId()).isEqualTo("4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119"); + + // TEST + final String copyJson = jsonb.toJson(copy, DataResult.class); + + // VERIFY + assertThatJson(copyJson).isEqualTo(originalJson); + + } + + private static JsonbProvider jsonbProvider() { + final JsonbConfig config = new JsonbConfig().withPropertyVisibilityStrategy(new FieldAccessStrategy()) + .withAdapters(new DataResultJsonbAdapterTest.InvoiceIdAdapter()); + return new JsonbProvider(config); + } + + private static Jsonb jsonb(final JsonbProvider jsonbProvider) { + final JsonbConfig config = new JsonbConfig().withPropertyVisibilityStrategy(new FieldAccessStrategy()) + .withAdapters(new DataResultJsonbAdapter(jsonbProvider), new DataResultJsonbAdapterTest.InvoiceIdAdapter()); + return JsonbBuilder.create(config); + } + + private static class TestId implements AggregateRootId { + + @Serial + private static final long serialVersionUID = 1L; + + private UUID id = UUID.randomUUID(); + + @Override + public EntityType getType() { + return TEST_TYPE; + } + + @Override + public String asTypedString() { + return TEST_TYPE + " " + id; + } + + @Override + public String asString() { + return id.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof TestId)) { + return false; + } + TestId other = (TestId) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + + } + + public static class Invoice implements MarshalInformation { + + @JsonbProperty("id") + private String id; + + protected Invoice() { + super(); + } + + public Invoice(String id) { + super(); + this.id = id; + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Invoice other = (Invoice) obj; + if (id == null) { + if (other.id != null) + return false; + } else if (!id.equals(other.id)) + return false; + return true; + } + + @Override + public String toString() { + return id; + } + + @Override + public Class getDataClass() { + return Invoice.class; + } + + @Override + public String getDataElement() { + return Invoice.class.getName(); + } + + @Override + public Invoice getData() { + return this; + } + + public String getId() { + return id; + } + + } + +} diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/JandexJsonbRegistryTest.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/JandexJsonbRegistryTest.java new file mode 100644 index 0000000..a17ce88 --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/JandexJsonbRegistryTest.java @@ -0,0 +1,190 @@ +package org.fuin.cqrs4j.jsonb; + +import org.fuin.ddd4j.core.EntityIdFactory; +import org.fuin.esc.api.DeserializerRegistry; +import org.fuin.esc.api.SerializerRegistry; +import org.fuin.objects4j.jsonb.JsonbProvider; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Constructor; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the {@link JandexJsonbRegistry} class. + */ +@ExtendWith(MockitoExtension.class) +class JandexJsonbRegistryTest { + + @Mock + private EntityIdFactory entityIdFactory; + + @Mock + private SerializerRegistry serializerRegistry; + + @Mock + private DeserializerRegistry deserializerRegistry; + + @Mock + private JsonbProvider jsonbProvider; + + @Test + void getAllLists() { + + final JandexJsonbRegistry testee = new JandexJsonbRegistry(entityIdFactory, serializerRegistry, + deserializerRegistry, jsonbProvider); + + assertThat(testee.getAdapters()).isNotEmpty(); + assertThat(testee.getSerializers()).isNotEmpty(); + assertThat(testee.getDeserializers()).isNotEmpty(); + + } + + @Test + void testParameters4() throws NoSuchMethodException { + + final Constructor constructor = Foo4.class.getConstructor(EntityIdFactory.class, + SerializerRegistry.class, DeserializerRegistry.class, JsonbProvider.class); + final Optional parameters = JandexJsonbRegistry.parameters(entityIdFactory, + serializerRegistry, + deserializerRegistry, jsonbProvider, constructor); + assertThat(parameters).isPresent(); + assertThat(parameters.get()).isEqualTo(new Object[]{entityIdFactory, serializerRegistry, + deserializerRegistry, jsonbProvider}); + + } + + @Test + void testParameters3() throws NoSuchMethodException { + + final Constructor constructor = Foo3.class.getConstructor(JsonbProvider.class, + DeserializerRegistry.class, SerializerRegistry.class); + final Optional parameters = JandexJsonbRegistry.parameters(entityIdFactory, + serializerRegistry, deserializerRegistry, jsonbProvider, constructor); + assertThat(parameters).isPresent(); + assertThat(parameters.get()).isEqualTo(new Object[]{jsonbProvider, + deserializerRegistry, serializerRegistry}); + + } + + @Test + void testParameters2() throws NoSuchMethodException { + + final Constructor constructor = Foo2.class.getConstructor( + DeserializerRegistry.class, SerializerRegistry.class); + final Optional parameters = JandexJsonbRegistry.parameters(entityIdFactory, + serializerRegistry, deserializerRegistry, jsonbProvider, constructor); + assertThat(parameters).isPresent(); + assertThat(parameters.get()).isEqualTo(new Object[]{ + deserializerRegistry, serializerRegistry}); + + } + + @Test + void testParameters1() throws NoSuchMethodException { + + final Constructor constructor = Foo1.class.getConstructor(JsonbProvider.class); + final Optional parameters = JandexJsonbRegistry.parameters(entityIdFactory, + serializerRegistry, deserializerRegistry, jsonbProvider, constructor); + assertThat(parameters).isPresent(); + assertThat(parameters.get()).isEqualTo(new Object[]{jsonbProvider}); + + } + + @Test + void testParameters0() throws NoSuchMethodException { + + final Constructor constructor = Foo0.class.getConstructor(); + final Optional parameters = JandexJsonbRegistry.parameters(entityIdFactory, + serializerRegistry, deserializerRegistry, jsonbProvider, constructor); + assertThat(parameters).isPresent(); + assertThat(parameters.get()).isEqualTo(new Object[]{}); + + } + + @Test + void testCreateInstance4() throws NoSuchMethodException { + assertThat((Object) JandexJsonbRegistry.createInstance( + entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, Foo4.class)) + .isInstanceOf(Foo4.class); + } + + @Test + void testCreateInstance3() throws NoSuchMethodException { + assertThat((Object) JandexJsonbRegistry.createInstance( + entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, Foo3.class)) + .isInstanceOf(Foo3.class); + } + + @Test + void testCreateInstance2() throws NoSuchMethodException { + assertThat((Object) JandexJsonbRegistry.createInstance( + entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, Foo2.class)) + .isInstanceOf(Foo2.class); + } + + @Test + void testCreateInstance1() throws NoSuchMethodException { + assertThat((Object) JandexJsonbRegistry.createInstance( + entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, Foo1.class)) + .isInstanceOf(Foo1.class); + } + + @Test + void testCreateInstance0() throws NoSuchMethodException { + assertThat((Object) JandexJsonbRegistry.createInstance( + entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, Foo0.class)) + .isInstanceOf(Foo0.class); + } + + @Test + void testParameter() { + + assertThat(JandexJsonbRegistry.parameter(entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, + EntityIdFactory.class)).hasValue(entityIdFactory); + assertThat(JandexJsonbRegistry.parameter(entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, + SerializerRegistry.class)).hasValue(serializerRegistry); + assertThat(JandexJsonbRegistry.parameter(entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, + DeserializerRegistry.class)).hasValue(deserializerRegistry); + assertThat(JandexJsonbRegistry.parameter(entityIdFactory, serializerRegistry, deserializerRegistry, jsonbProvider, + Integer.class)).isEmpty(); + + } + + public static class Foo4 { + public Foo4(EntityIdFactory entityIdFactory, + SerializerRegistry serializerRegistry, + DeserializerRegistry deserializerRegistry, + JsonbProvider jsonbProvider) { + } + } + + public static class Foo3 { + public Foo3(JsonbProvider jsonbProvider, + DeserializerRegistry deserializerRegistry, + SerializerRegistry serializerRegistry) { + } + } + + public static class Foo2 { + public Foo2(DeserializerRegistry deserializerRegistry, + SerializerRegistry serializerRegistry) { + } + } + + public static class Foo1 { + public Foo1(JsonbProvider jsonbProvidery) { + } + } + + public static class Foo0 { + public Foo0() { + // Test + } + } + +} \ No newline at end of file diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/MyIdFactory.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/MyIdFactory.java new file mode 100644 index 0000000..ee27196 --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/MyIdFactory.java @@ -0,0 +1,41 @@ +package org.fuin.cqrs4j.jsonb; + +import org.fuin.ddd4j.core.EntityId; +import org.fuin.ddd4j.core.EntityIdFactory; +import org.fuin.utils4j.TestOmitted; + +@TestOmitted("This is only a test class") +final class MyIdFactory implements EntityIdFactory { + @Override + public EntityId createEntityId(final String type, final String id) { + if (type.equals("A")) { + return new AId(Long.valueOf(id)); + } + if (type.equals("B")) { + return new BId(Long.valueOf(id)); + } + if (type.equals("C")) { + return new CId(Long.valueOf(id)); + } + throw new IllegalArgumentException("Unknown type: '" + type + "'"); + } + + @Override + public boolean containsType(final String type) { + if (type.equals("A")) { + return true; + } + if (type.equals("B")) { + return true; + } + if (type.equals("C")) { + return true; + } + return false; + } + + @Override + public boolean isValid(String type, String id) { + return true; + } +} diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/SimpleResultTest.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/SimpleResultTest.java new file mode 100644 index 0000000..4c26190 --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/SimpleResultTest.java @@ -0,0 +1,208 @@ +/** + * Copyright (C) 2015 Michael Schnell. All rights reserved. + * http://www.fuin.org/ + * + * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + * + * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see http://www.gnu.org/licenses/. + */ +package org.fuin.cqrs4j.jsonb; + +import jakarta.json.bind.Jsonb; +import nl.jqno.equalsverifier.EqualsVerifier; +import org.fuin.cqrs4j.core.ResultType; +import org.fuin.ddd4j.core.AggregateNotFoundException; +import org.fuin.ddd4j.core.AggregateRootId; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.junit.jupiter.api.Test; + +import java.io.Serial; +import java.util.UUID; + +import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.fuin.cqrs4j.jsonb.TestUtils.jsonb; + +public final class SimpleResultTest { + + private static final EntityType TEST_TYPE = new StringBasedEntityType("Test"); + + @Test + public final void testEqualsHashCode() { + EqualsVerifier.simple().forClass(SimpleResult.class).verify(); + } + + @Test + public final void testConstructorAll() { + + // TEST + final SimpleResult testee = new SimpleResult(ResultType.WARNING, "X1", "Yes!"); + + // VERIFY + assertThat(testee.getType()).isEqualTo(ResultType.WARNING); + assertThat(testee.getCode()).isEqualTo("X1"); + assertThat(testee.getMessage()).isEqualTo("Yes!"); + + } + + @Test + public final void testConstructorException() { + + // PREPARE + final TestId id = new TestId(); + final AggregateNotFoundException ex = new AggregateNotFoundException(TEST_TYPE, id); + + // TEST + final SimpleResult testee = new SimpleResult(ex); + + // VERIFY + assertThat(testee.getType()).isEqualTo(ResultType.ERROR); + assertThat(testee.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(testee.getMessage()).isEqualTo(TEST_TYPE + " with id " + id.asString() + " not found"); + + } + + @Test + public final void testMarshalUnmarshal() throws Exception { + + try (final Jsonb jsonb = jsonb()) { + + // PREPARE + final SimpleResult original = SimpleResult.ok(); + + // TEST + final String json = jsonb.toJson(original, SimpleResult.class); + final SimpleResult copy = jsonb.fromJson(json, SimpleResult.class); + + // VERIFY + assertThat(original).isEqualTo(copy); + + } + + } + + @Test + public final void testUnmarshalMarshalOkResult() throws Exception { + + try (final Jsonb jsonb = jsonb()) { + + // PREPARE + final String originalJson = """ + { "type": "OK" } + """; + + // TEST + final SimpleResult copy = jsonb.fromJson(originalJson, SimpleResult.class); + + // VERIFY + assertThat(copy.getType()).isEqualTo(ResultType.OK); + + // TEST + final String copyJson = jsonb.toJson(copy, SimpleResult.class); + + // VERIFY + assertThatJson(copyJson).isEqualTo(originalJson); + + } + + } + + @Test + public final void testUnmarshalExceptionResult() throws Exception { + + try (final Jsonb jsonb = jsonb()) { + + + // PREPARE + final String originalJson = """ + { + "type": "ERROR", + "code": "DDD4J-AGGREGATE_NOT_FOUND", + "message": "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found" + } + """; + + // TEST + final SimpleResult copy = jsonb.fromJson(originalJson, SimpleResult.class); + + // VERIFY + final String msg = "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found"; + assertThat(copy.getCode()).isEqualTo("DDD4J-AGGREGATE_NOT_FOUND"); + assertThat(copy.getType()).isEqualTo(ResultType.ERROR); + assertThat(copy.getMessage()).isEqualTo(msg); + + // TEST + final String copyJson = jsonb.toJson(copy); + + // VERIFY + assertThatJson(copyJson).isEqualTo(originalJson); + + } + + } + + private static class TestId implements AggregateRootId { + + @Serial + private static final long serialVersionUID = 1L; + + private final UUID id = UUID.randomUUID(); + + @Override + public EntityType getType() { + return TEST_TYPE; + } + + @Override + public String asTypedString() { + return TEST_TYPE + " " + id; + } + + @Override + public String asString() { + return id.toString(); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((id == null) ? 0 : id.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (!(obj instanceof TestId)) { + return false; + } + TestId other = (TestId) obj; + if (id == null) { + if (other.id != null) { + return false; + } + } else if (!id.equals(other.id)) { + return false; + } + return true; + } + + } + +} diff --git a/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/TestUtils.java b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/TestUtils.java new file mode 100644 index 0000000..aba63d2 --- /dev/null +++ b/jsonb/src/test/java/org/fuin/cqrs4j/jsonb/TestUtils.java @@ -0,0 +1,39 @@ +package org.fuin.cqrs4j.jsonb; + +import jakarta.json.bind.Jsonb; +import jakarta.json.bind.JsonbBuilder; +import jakarta.json.bind.JsonbConfig; +import jakarta.json.bind.config.BinaryDataStrategy; +import org.eclipse.yasson.FieldAccessStrategy; +import org.fuin.ddd4j.core.EntityIdFactory; +import org.fuin.ddd4j.jsonb.EntityIdPathJsonbAdapter; +import org.fuin.utils4j.TestOmitted; + +import java.nio.charset.StandardCharsets; + +/** + * Utils for the package. + */ +@TestOmitted("This is only a test class") +final class TestUtils { + + private TestUtils() { + } + + /** + * Creates an instance with the configured values. + * + * @return New instance. + */ + public static Jsonb jsonb() { + final EntityIdFactory factory = new MyIdFactory(); + return JsonbBuilder.create( + new JsonbConfig() + .withEncoding(StandardCharsets.UTF_8.name()) + .withPropertyVisibilityStrategy(new FieldAccessStrategy()) + .withAdapters(new EntityIdPathJsonbAdapter(factory)) + .withBinaryDataStrategy(BinaryDataStrategy.BASE_64) + ); + } + +} diff --git a/pom.xml b/pom.xml index de76cfd..c4bb043 100644 --- a/pom.xml +++ b/pom.xml @@ -8,12 +8,13 @@ org.fuin pom - 1.8.0 + 1.9.0 + org.fuin.cqrs4j cqrs-4-java - 0.5.0 - jar + 0.6.0-SNAPSHOT + pom Base classes for Command Query Responsibility Segregation (CQRS) with Java @@ -28,186 +29,466 @@ - 0.6.0 + Base classes for Command Query Responsibility Segregation (CQRS) with Java + 3.21.0 + 3.4.4 + 2.18.2 + 0.9.0-SNAPSHOT + 0.7.0-SNAPSHOT + 0.11.0-SNAPSHOT + 0.15.0-SNAPSHOT + 0.12.0 + 1.4.0 + ${project.basedir}/../jacoco/target/site/jacoco-aggregate/jacoco.xml - - - - - - org.fuin - ddd-4-java - 0.5.0 - - - - jakarta.validation - jakarta.validation-api - 3.0.2 - true - - - - org.apache.commons - commons-lang3 - 3.14.0 - - - - org.slf4j - slf4j-api - 2.0.9 - - - - jakarta.persistence - jakarta.persistence-api - 3.1.0 - true - - - - - - org.fuin.esc - esc-mem - ${esc.version} - test - - - - org.junit.jupiter - junit-jupiter - 5.10.1 - test - - - - org.assertj - assertj-core - 3.24.2 - test - - - - org.fuin - units4j - 0.11.0 - test - - - - org.hibernate.validator - hibernate-validator - 8.0.1.Final - test - - - - jakarta.el - jakarta.el-api - 5.0.1 - test - - - - org.glassfish - jakarta.el - 4.0.2 - test - - - - com.sun.mail - jakarta.mail - 2.0.1 - test - - - - org.xmlunit - xmlunit-core - 2.9.1 - test - - - - nl.jqno.equalsverifier - equalsverifier - 3.15.5 - test - - - - commons-io - commons-io - 2.15.1 - test - - - - org.glassfish.jaxb - jaxb-runtime - 4.0.4 - test - - - - org.eclipse - yasson - 3.0.3 - test - - - + + + + + + + + org.fuin.cqrs4j + cqrs-4-java-core + ${project.version} + + + + org.fuin.cqrs4j + cqrs-4-java-esc + ${project.version} + + + + org.fuin.cqrs4j + cqrs-4-java-jsonb + ${project.version} + + + + org.fuin.cqrs4j + cqrs-4-java-jackson + ${project.version} + + + + org.fuin.cqrs4j + cqrs-4-java-jaxb + ${project.version} + + + + org.fuin.cqrs4j + cqrs-4-java-springboot + ${project.version} + + + + org.fuin.cqrs4j + cqrs-4-java-quarkus + ${project.version} + + + + + + org.fuin.ddd4j + ddd-4-java-core + ${ddd4j.version} + + + + org.fuin.ddd4j + ddd-4-java-codegen-api + ${ddd4j.version} + + + + org.fuin.ddd4j + ddd-4-java-jaxb + ${ddd4j.version} + + + + org.fuin.ddd4j + ddd-4-java-jsonb + ${ddd4j.version} + + + + org.fuin.ddd4j + ddd-4-java-jackson + ${ddd4j.version} + + + + org.fuin.ddd4j + ddd-4-java-esc + ${ddd4j.version} + + + + org.fuin.esc + esc-api + ${esc.version} + + + + org.fuin.objects4j + objects4j-common + ${objects4j.version} + + + + org.fuin.objects4j + objects4j-core + ${objects4j.version} + + + + org.fuin.objects4j + objects4j-jaxb + ${objects4j.version} + + + + org.fuin.objects4j + objects4j-jackson + ${objects4j.version} + + + + org.fuin.objects4j + objects4j-jpa + ${objects4j.version} + + + + org.fuin.objects4j + objects4j-jsonb + ${objects4j.version} + + + + org.fuin.objects4j + objects4j-ui + ${objects4j.version} + + + + jakarta.annotation + jakarta.annotation-api + 2.1.1 + + + + jakarta.validation + jakarta.validation-api + 3.0.2 + + + + org.apache.commons + commons-lang3 + 3.17.0 + + + + org.slf4j + slf4j-api + 2.0.6 + + + + io.github.threeten-jaxb + threeten-jaxb-core + 2.2.0 + + + + jakarta.json + jakarta.json-api + 2.1.3 + + + + jakarta.json.bind + jakarta.json.bind-api + 3.0.1 + + + + io.smallrye + jandex + 3.2.7 + + + + jakarta.persistence + jakarta.persistence-api + 3.1.0 + + + + org.fuin.esc + esc-mem + ${esc.version} + + + + org.fuin.esc + esc-esgrpc + ${esc.version} + + + + org.fuin.esc + esc-client + ${esc.version} + + + + org.fuin.esc + esc-jsonb + ${esc.version} + + + + org.fuin.esc + esc-jackson + ${esc.version} + + + + org.junit.jupiter + junit-jupiter + 5.10.5 + + + + org.assertj + assertj-core + 3.26.3 + + + + org.fuin + utils4j + ${utils4j.version} + + + + org.fuin + units4j + ${units4j.version} + + + + org.hibernate.validator + hibernate-validator + 8.0.2.Final + + + + jakarta.el + jakarta.el-api + 5.0.1 + + + + org.glassfish.expressly + expressly + 5.0.0 + + + + com.sun.mail + jakarta.mail + 2.0.1 + + + + org.xmlunit + xmlunit-core + 2.10.0 + + + + nl.jqno.equalsverifier + equalsverifier + 3.19.2 + + + + commons-io + commons-io + 2.18.0 + + + + org.glassfish.jaxb + jaxb-runtime + 4.0.5 + + + + org.glassfish + jakarta.json + 2.0.1 + + + + org.eclipse + yasson + 3.0.4 + + + + net.javacrumbs.json-unit + json-unit-fluent + 4.1.0 + + + + com.google.code.gson + gson + 2.11.0 + + + + com.tngtech.archunit + archunit + ${archunit.version} + + + + com.tngtech.archunit + archunit-junit5 + ${archunit.version} + + + + com.google.code.findbugs + jsr305 + 3.0.2 + + + + jakarta.xml.bind + jakarta.xml.bind-api + 4.0.2 + + + + org.mockito + mockito-core + 5.14.2 + + + + org.mockito + mockito-junit-jupiter + 5.14.2 + + + + io.kurrent + kurrentdb-client + 1.0.0 + + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + + + - - - - org.apache.maven.plugins - maven-compiler-plugin - - - - org.apache.maven.plugins - maven-source-plugin - - - - org.apache.maven.plugins - maven-javadoc-plugin - - - - org.apache.maven.plugins - maven-jar-plugin - - - **/* - - - - org.fuin.cqrs4j - - - - - - - org.apache.maven.plugins - maven-jdeps-plugin - - - - org.jacoco - jacoco-maven-plugin - - - + + + + + + org.apache.maven.plugins + maven-dependency-plugin + 3.8.1 + + + analyze + + analyze-only + + + true + + + + + + + io.smallrye + jandex-maven-plugin + 3.2.7 + + + make-index + + jandex + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.5.3 + + -Duser.country=US -Duser.language=en --add-opens java.base/java.util=ALL-UNNAMED ${argLine} + + + + + + + + core + esc + jaxb + jsonb + jackson + jacoco + springboot + quarkus + test + + + diff --git a/quarkus/pom.xml b/quarkus/pom.xml new file mode 100644 index 0000000..d888cdf --- /dev/null +++ b/quarkus/pom.xml @@ -0,0 +1,270 @@ + + + 4.0.0 + + + org.fuin.cqrs4j + cqrs-4-java + 0.6.0-SNAPSHOT + ../pom.xml + + + cqrs-4-java-quarkus + jar + ${description} (QUARKUS) + + + + + io.quarkus + quarkus-bom + ${quarkus.version} + pom + import + + + + + + + + + + org.fuin.cqrs4j + cqrs-4-java-esc + + + + org.slf4j + slf4j-api + + + + org.fuin.esc + esc-api + + + + io.quarkus + quarkus-scheduler + + + + io.quarkus + quarkus-hibernate-orm + + + + org.fuin.ddd4j + ddd-4-java-core + + + + org.fuin.cqrs4j + cqrs-4-java-core + + + + io.quarkus + quarkus-scheduler-api + + + + org.fuin.objects4j + objects4j-common + + + + jakarta.persistence + jakarta.persistence-api + + + + io.quarkus + quarkus-core + + + + org.fuin + utils4j + + + + jakarta.inject + jakarta.inject-api + + + + io.quarkus.arc + arc + + + + io.quarkus + quarkus-narayana-jta + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + + + + jakarta.validation + jakarta.validation-api + + + + org.eclipse.microprofile.config + microprofile-config-api + + + + + + io.quarkus + quarkus-junit5 + test + + + + io.quarkus + quarkus-junit5-mockito + test + + + + org.mockito + mockito-core + test + + + + io.quarkus + quarkus-test-common + test + + + + org.assertj + assertj-core + test + + + + com.tngtech.archunit + archunit + test + + + + com.tngtech.archunit + archunit-junit5 + test + + + + org.fuin + units4j + test + + + + org.eclipse + yasson + test + + + + org.hibernate.validator + hibernate-validator + test + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.apache.maven.plugins + maven-source-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + org.apache.maven.plugins + maven-jar-plugin + + + **/* + + + + org.fuin.cqrs4j.quarkus.core + + + + + + + org.apache.maven.plugins + maven-jdeps-plugin + + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + + + + io.smallrye + jandex-maven-plugin + + + + org.apache.maven.plugins + maven-dependency-plugin + + + jakarta.el:jakarta.el-api + com.tngtech.archunit:archunit-junit5 + org.junit.jupiter:junit-jupiter + io.quarkus:quarkus-scheduler + io.quarkus:quarkus-hibernate-orm + io.quarkus:quarkus-junit5-mockito + org.eclipse:yasson + org.hibernate.validator:hibernate-validator + + + com.tngtech.archunit:archunit-junit5-api + org.junit.jupiter:junit-jupiter-api + + + + + + + + + diff --git a/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/base/EventstoreConfig.java b/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/base/EventstoreConfig.java new file mode 100644 index 0000000..4eb5324 --- /dev/null +++ b/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/base/EventstoreConfig.java @@ -0,0 +1,84 @@ +package org.fuin.cqrs4j.quarkus.base; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; +import org.eclipse.microprofile.config.inject.ConfigProperty; + +/** + * Configuration for the eventstore. + */ +@ApplicationScoped +public class EventstoreConfig { + + static final String PREFIX = "org.fuin.cqrs4j.eventstore"; + + /** + * Key for the eventstore TLS property. + */ + public static final String KEY_TLS = PREFIX + ".tls"; + + /** + * Key for the eventstore host property. + */ + public static final String KEY_HOST = PREFIX + ".host"; + + /** + * Key for the eventstore port property. + */ + public static final String KEY_PORT = PREFIX + ".port"; + + private final boolean tls; + + @Size(min = 1, max = 235) + private final String host; + + @Min(1024) + @Max(65535) + private final int port; + + /** + * Constructor with all data. + * + * @param tls Use TlS (https) or not (http). + * @param host Host name. + * @param port Port number. + */ + public EventstoreConfig(@ConfigProperty(name = KEY_TLS, defaultValue = "false") final Boolean tls, + @ConfigProperty(name = KEY_HOST, defaultValue = "localhost") final String host, + @ConfigProperty(name = KEY_PORT, defaultValue = "2113") final Integer port) { + super(); + this.tls = tls != null && tls; + this.host = host == null ? "localhost" : host; + this.port = port == null ? 2113 : port; + } + + /** + * Returns if TLS should be used to communicate with the event store. + * + * @return {@literal true} use TLS (https) or {@literal false} (http). + */ + public boolean isTls() { + return tls; + } + + /** + * Returns the host name of the event store. + * + * @return Host name. + */ + public String getHost() { + return host; + } + + /** + * Returns the HTTP/HTTPS port of the event store. + * + * @return Port number. + */ + public int getPort() { + return port; + } + +} diff --git a/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/view/QryProjectionPosition.java b/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/view/QryProjectionPosition.java new file mode 100644 index 0000000..7780566 --- /dev/null +++ b/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/view/QryProjectionPosition.java @@ -0,0 +1,87 @@ +package org.fuin.cqrs4j.quarkus.view; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import org.fuin.esc.api.SimpleStreamId; +import org.fuin.esc.api.StreamId; +import org.fuin.objects4j.common.Contract; + +/** + * Stores the next position to read from the projection in the event store. + */ +@Entity +@Table(name = "QUARKUS_QRY_PROJECTION_POS") +public class QryProjectionPosition { + + @Id + @Column(name = "STREAM_ID", nullable = false, length = 250, updatable = false) + @NotNull + private String streamId; + + @Column(name = "NEXT_POS", nullable = false, updatable = true) + @NotNull + private Long nextPos; + + /** + * JPA constructor. + */ + protected QryProjectionPosition() { + super(); + } + + /** + * Constructor with mandatory data. + * + * @param streamId + * Unique stream identifier. + * @param nextPos + * Next position from the stream to read. + */ + public QryProjectionPosition(@NotNull final StreamId streamId, @NotNull final Long nextPos) { + super(); + Contract.requireArgNotNull("streamId", streamId); + Contract.requireArgNotNull("nextPos", nextPos); + this.streamId = streamId.asString(); + this.nextPos = nextPos; + } + + /** + * Returns the unique stream identifier. + * + * @return Stream ID. + */ + @NotNull + public StreamId getStreamId() { + return new SimpleStreamId(streamId); + } + + /** + * Returns the next position read from the stream. + * + * @return Position to read next time. + */ + @NotNull + public Long getNextPos() { + return nextPos; + } + + /** + * Sets the next position read from the stream. + * + * @param nextPos + * New position to set. + */ + public void setNextPosition(@NotNull final Long nextPos) { + Contract.requireArgNotNull("nextPos", nextPos); + this.nextPos = nextPos; + } + + @Override + public String toString() { + return "QryProjectionPosition [streamId=" + streamId + ", nextPos=" + nextPos + "]"; + } + +} diff --git a/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/view/QryProjectionPositionRepository.java b/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/view/QryProjectionPositionRepository.java new file mode 100644 index 0000000..68859e7 --- /dev/null +++ b/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/view/QryProjectionPositionRepository.java @@ -0,0 +1,53 @@ +package org.fuin.cqrs4j.quarkus.view; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.validation.constraints.NotNull; +import org.fuin.cqrs4j.esc.ProjectionService; +import org.fuin.esc.api.StreamId; +import org.fuin.objects4j.common.Contract; + +/** + * Repository that contains the position of the stream. + */ +@ApplicationScoped +public class QryProjectionPositionRepository implements ProjectionService { + + private static final String ARG_STREAM_ID = "streamId"; + @Inject + EntityManager em; + + @Override + public void resetProjectionPosition(@NotNull final StreamId streamId) { + Contract.requireArgNotNull(ARG_STREAM_ID, streamId); + final QryProjectionPosition pos = em.find(QryProjectionPosition.class, streamId.asString()); + if (pos != null) { + pos.setNextPosition(0L); + } + } + + @Override + public Long readProjectionPosition(@NotNull StreamId streamId) { + Contract.requireArgNotNull(ARG_STREAM_ID, streamId); + final QryProjectionPosition pos = em.find(QryProjectionPosition.class, streamId.asString()); + if (pos == null) { + return 0L; + } + return pos.getNextPos(); + } + + @Override + public void updateProjectionPosition(@NotNull StreamId streamId, @NotNull Long nextEventNumber) { + Contract.requireArgNotNull(ARG_STREAM_ID, streamId); + Contract.requireArgNotNull("nextEventNumber", nextEventNumber); + final QryProjectionPosition pos = em.find(QryProjectionPosition.class, streamId.asString()); + if (pos == null) { + em.persist(new QryProjectionPosition(streamId, nextEventNumber)); + } else { + pos.setNextPosition(nextEventNumber); + em.merge(pos); + } + } + +} diff --git a/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/view/QuarkusJpaViewManager.java b/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/view/QuarkusJpaViewManager.java new file mode 100644 index 0000000..d91c483 --- /dev/null +++ b/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/view/QuarkusJpaViewManager.java @@ -0,0 +1,190 @@ +package org.fuin.cqrs4j.quarkus.view; + +import io.quarkus.arc.All; +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.runtime.Shutdown; +import io.quarkus.runtime.Startup; +import io.quarkus.scheduler.Scheduler; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import org.fuin.cqrs4j.core.CqrsUtils; +import org.fuin.cqrs4j.core.JpaView; +import org.fuin.cqrs4j.core.View; +import org.fuin.cqrs4j.esc.ProjectionService; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventType; +import org.fuin.esc.api.CommonEvent; +import org.fuin.esc.api.EventStore; +import org.fuin.esc.api.ProjectionAdminEventStore; +import org.fuin.esc.api.ProjectionStreamId; +import org.fuin.esc.api.StreamEventsSlice; +import org.fuin.esc.api.TypeName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.Semaphore; + +import static org.fuin.utils4j.Utils4J.tryLocked; + +/** + * Creates scheduler update jobs for all classes implementing the {@link View} interface. + * Avoids boilerplate code: Instead of having a separated "Projector", "EventDispatcher" + * and a "ChunkHandler" class for each view, there is only one simplified "View" class now. + */ +@ApplicationScoped +public class QuarkusJpaViewManager { + + private static final Logger LOG = LoggerFactory.getLogger(QuarkusJpaViewManager.class); + + @Inject + Scheduler scheduler; + + @Inject + @All + List rawViews; + + @Inject + EventStore eventstore; + + @Inject + ProjectionAdminEventStore admin; + + @Inject + ProjectionService projectionService; + + @Inject + EntityManagerFactory entityManagerFactory; + + private List views; + + @Startup + void createViews() { + LOG.info("Create views..."); + views = rawViews.stream().map(ViewExt::new).toList(); + for (final ViewExt view : views) { + LOG.info("Create: {}", view.getName()); + scheduler.newJob(view.getName()) + .setCron(view.getCron()) + .setTask(executionContext -> updateView(view)) + .schedule(); + } + } + + @Shutdown + void shutdownViews() { + LOG.info("Shutdown views..."); + for (final ViewExt view : views) { + LOG.info("Shutdown: {}", view.getName()); + scheduler.unscheduleJob(view.getName()); + } + } + + private void updateView(final ViewExt view) { + tryLocked(view.getLock(), () -> new Thread(() -> { + try (final EntityManager em = entityManagerFactory.createEntityManager()) { + LOG.debug("updateView({})", view.getName()); + readStreamEvents(em, view); + } catch (final RuntimeException ex) { + LOG.error("Error reading events from stream", ex); + } + } + ).start()); + } + + private void readStreamEvents(final EntityManager em, final ViewExt view) { + + // Create an event store projection if it does not exist. + if (!admin.projectionExists(view.getProjectionStreamId())) { + final List typeNames = asTypeNames(view.getEventTypes()); + LOG.info("Create projection '{}' with events: {}", view.getProjectionStreamId(), typeNames); + admin.createProjection(view.getProjectionStreamId(), true, typeNames); + } + + // Read and dispatch events + final Long nextEventNumber = projectionService.readProjectionPosition(view.getProjectionStreamId()); + eventstore.readAllEventsForward(view.getProjectionStreamId(), nextEventNumber, view.getChunkSize(), + currentSlice -> handleChunk(em, view, currentSlice)); + + } + + private List asTypeNames(Set eventTypes) { + return eventTypes.stream().map(eventType -> new TypeName((eventType.asString()))).toList(); + } + + private void handleChunk(final EntityManager em, final ViewExt view, final StreamEventsSlice currentSlice) { + QuarkusTransaction.requiringNew() + .timeout(10) + .call(() -> { + LOG.debug("Handle chunk: {}", currentSlice); + view.handleEvents(em, asEvents(currentSlice.getEvents())); + projectionService.updateProjectionPosition(view.getProjectionStreamId(), currentSlice.getNextEventNumber()); + return 0; + }); + } + + private List asEvents(List events) { + return events.stream().map(event -> (Event) event.getData()).toList(); + } + + /** + * Extends the view with some necessary values used only by this class. + */ + private static class ViewExt implements JpaView { + + private final JpaView delegate; + + private final ProjectionStreamId projectionStreamId; + + private final Semaphore lock; + + public ViewExt(final JpaView delegate) { + this.delegate = delegate; + + final Set eventTypes = delegate.getEventTypes(); + final String name = delegate.getName() + "-" + CqrsUtils.calculateAdler32Checksum(eventTypes); + projectionStreamId = new ProjectionStreamId(name); + + this.lock = new Semaphore(1); + + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public String getCron() { + return delegate.getCron(); + } + + @Override + public Set getEventTypes() { + return delegate.getEventTypes(); + } + + @Override + public int getChunkSize() { + return delegate.getChunkSize(); + } + + @Override + public void handleEvents(final EntityManager em, List events) { + delegate.handleEvents(em, events); + } + + public ProjectionStreamId getProjectionStreamId() { + return projectionStreamId; + } + + public Semaphore getLock() { + return lock; + } + + } + +} diff --git a/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/ArchitectureTest.java b/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/ArchitectureTest.java new file mode 100644 index 0000000..1dde5a9 --- /dev/null +++ b/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/ArchitectureTest.java @@ -0,0 +1,46 @@ +package org.fuin.cqrs4j.quarkus; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.library.DependencyRules.NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + +/** + * Tests architectural aspects. + */ +@AnalyzeClasses(packagesOf = ArchitectureTest.class, importOptions = ImportOption.DoNotIncludeTests.class) +public class ArchitectureTest { + + private static final String THIS_PACKAGE = ArchitectureTest.class.getPackageName(); + + @ArchTest + static final ArchRule no_accesses_to_upper_package = NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + + @ArchTest + static final ArchRule access_only_to_defined_packages = classes() + .that() + .resideInAPackage(THIS_PACKAGE) + .should() + .onlyDependOnClassesThat() + .resideInAnyPackage(THIS_PACKAGE, "java..", + "jakarta.enterprise.context..", + "jakarta.inject..", + "jakarta.persistence..", + "jakarta.validation.constraints..", + "org.fuin.cqrs4j.core..", + "org.fuin.cqrs4j.esc..", + "org.fuin.ddd4j.core..", + "org.fuin.esc.api..", + "org.fuin.objects4j.common..", + "org.fuin.utils4j..", + "org.slf4j..", + "io.quarkus.arc..", + "io.quarkus.narayana.jta..", + "io.quarkus.runtime..", + "io.quarkus.scheduler.." + ); + +} diff --git a/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/BaseTest.java b/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/BaseTest.java new file mode 100644 index 0000000..4954459 --- /dev/null +++ b/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/BaseTest.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2013 Future Invent Informationsmanagement GmbH. All rights + * reserved. + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ +package org.fuin.cqrs4j.quarkus; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import org.fuin.units4j.archunit.Units4JConditions; + +@AnalyzeClasses(packagesOf = BaseTest.class) +class BaseTest { + + @ArchTest + static final ArchRule all_classes_should_have_tests = Units4JConditions.ALL_CLASSES_SHOULD_HAVE_TESTS; + +} + diff --git a/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/base/EventstoreConfigTest.java b/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/base/EventstoreConfigTest.java new file mode 100644 index 0000000..55d9ea7 --- /dev/null +++ b/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/base/EventstoreConfigTest.java @@ -0,0 +1,39 @@ +package org.fuin.cqrs4j.quarkus.base; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the {@link EventstoreConfig} class. + */ +class EventstoreConfigTest { + + private Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + @Test + void testConstructionNull() { + final EventstoreConfig testee = new EventstoreConfig(null, null, null); + assertThat(testee.isTls()).isFalse(); + assertThat(testee.getHost()).isEqualTo("localhost"); + assertThat(testee.getPort()).isEqualTo(2113); + assertThat(validator.validate(testee)).isEmpty(); + } + + @Test + void testHostError() { + assertThat(validator.validate(new EventstoreConfig(null, "", null))) + .allMatch(violation -> + violation.getMessage().contains("size must be between 1 and 235")); + } + + @Test + void testPortError() { + assertThat(validator.validate(new EventstoreConfig(null, null, 100))) + .allMatch(violation -> + violation.getMessage().contains("must be greater than or equal to 1024")); + } + +} \ No newline at end of file diff --git a/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/view/QryProjectionPositionRepositoryTest.java b/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/view/QryProjectionPositionRepositoryTest.java new file mode 100644 index 0000000..81b17b4 --- /dev/null +++ b/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/view/QryProjectionPositionRepositoryTest.java @@ -0,0 +1,29 @@ +package org.fuin.cqrs4j.quarkus.view; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test for the {@link QryProjectionPosition} class. + */ +@Disabled("TODO Implement!") +class QryProjectionPositionRepositoryTest { + + @Test + void resetProjectionPosition() { + fail("Not yet implemented"); + } + + @Test + void readProjectionPosition() { + fail("Not yet implemented"); + } + + @Test + void updateProjectionPosition() { + fail("Not yet implemented"); + } + +} \ No newline at end of file diff --git a/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/view/QryProjectionPositionTest.java b/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/view/QryProjectionPositionTest.java new file mode 100644 index 0000000..a37a810 --- /dev/null +++ b/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/view/QryProjectionPositionTest.java @@ -0,0 +1,29 @@ +package org.fuin.cqrs4j.quarkus.view; + +import org.fuin.esc.api.SimpleStreamId; +import org.fuin.esc.api.StreamId; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the {@link QryProjectionPosition} class. + */ +class QryProjectionPositionTest { + + @Test + void testCreateAndSetGet() { + + final StreamId streamId = new SimpleStreamId("streamId"); + long nextPos = 4711; + final QryProjectionPosition testee = new QryProjectionPosition(streamId, nextPos); + assertThat(testee.getStreamId()).isEqualTo(streamId); + assertThat(testee.getNextPos()).isEqualTo(nextPos); + + nextPos = nextPos + 1; + testee.setNextPosition(nextPos); + assertThat(testee.getNextPos()).isEqualTo(nextPos); + + } + +} \ No newline at end of file diff --git a/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/view/QuarkusJpaViewManagerTest.java b/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/view/QuarkusJpaViewManagerTest.java new file mode 100644 index 0000000..944a1ee --- /dev/null +++ b/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/view/QuarkusJpaViewManagerTest.java @@ -0,0 +1,87 @@ +package org.fuin.cqrs4j.quarkus.view; + +import io.quarkus.scheduler.ScheduledExecution; +import io.quarkus.scheduler.Scheduler; +import io.quarkus.test.InjectMock; +import io.quarkus.test.junit.QuarkusTest; +import io.quarkus.test.junit.QuarkusTestProfile; +import io.quarkus.test.junit.TestProfile; +import jakarta.inject.Inject; +import org.fuin.cqrs4j.core.JpaView; +import org.fuin.cqrs4j.esc.ProjectionService; +import org.fuin.esc.api.EventStore; +import org.fuin.esc.api.ProjectionAdminEventStore; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test for the {@link QuarkusJpaViewManager} class. + */ +@Disabled("TODO Implement!") +@QuarkusTest +@TestProfile(QuarkusJpaViewManagerTest.class) +class QuarkusJpaViewManagerTest implements QuarkusTestProfile { + + @Inject + QuarkusJpaViewManager testee; + + @InjectMock + Scheduler scheduler; + + @InjectMock + List rawViews; + + @InjectMock + EventStore eventstore; + + @InjectMock + ProjectionAdminEventStore admin; + + @InjectMock + ProjectionService projectionService; + + @Override + public Map getConfigOverrides() { + return Map.of("quarkus.hibernate-orm.default.active", "false"); + } + + @Test + void createViews() { + + // GIVEN + final JpaView view = mock(JpaView.class); + final List views = List.of(view); + when(rawViews.stream()).thenReturn(views.stream()); + + final Scheduler.JobDefinition jobDefinition = mock(Scheduler.JobDefinition.class); + when(scheduler.newJob(view.getName())).thenReturn(jobDefinition); + + // WHEN + testee.createViews(); + + // THEN + verify(jobDefinition, times(1)).setCron(view.getCron()); + verify(jobDefinition, times(1)).setTask((Consumer) any()); + verify(jobDefinition, times(1)).schedule(); + + + + } + + @Test + void shutdownViews() { + fail("Not yet implemented"); + } + +} \ No newline at end of file diff --git a/release-notes.md b/release-notes.md new file mode 100644 index 0000000..fd973ab --- /dev/null +++ b/release-notes.md @@ -0,0 +1,4 @@ +# Release Notes + +## 0.6.0 +- Added new Jackson module diff --git a/springboot/pom.xml b/springboot/pom.xml new file mode 100644 index 0000000..f277333 --- /dev/null +++ b/springboot/pom.xml @@ -0,0 +1,226 @@ + + + 4.0.0 + + + org.fuin.cqrs4j + cqrs-4-java + 0.6.0-SNAPSHOT + ../pom.xml + + + cqrs-4-java-springboot + jar + ${description} (SPRINGBOOT) + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + + + + org.fuin.cqrs4j + cqrs-4-java-esc + + + + org.fuin.esc + esc-api + + + + org.fuin + utils4j + + + + org.fuin.cqrs4j + cqrs-4-java-core + + + + org.fuin.ddd4j + ddd-4-java-core + + + + org.fuin.objects4j + objects4j-common + + + + jakarta.persistence + jakarta.persistence-api + + + + jakarta.validation + jakarta.validation-api + + + + org.springframework + spring-tx + + + + org.springframework + spring-core + + + + org.springframework + spring-context + + + + org.springframework.boot + spring-boot + + + + org.springframework + spring-orm + + + + org.slf4j + slf4j-api + + + + + + org.assertj + assertj-core + test + + + + com.tngtech.archunit + archunit + test + + + + com.tngtech.archunit + archunit-junit5 + test + + + + org.fuin + units4j + test + + + + org.glassfish.expressly + expressly + test + + + + org.hibernate.validator + hibernate-validator + test + + + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + true + + + + + org.apache.maven.plugins + maven-source-plugin + + + + org.apache.maven.plugins + maven-javadoc-plugin + + + + org.apache.maven.plugins + maven-jar-plugin + + + **/* + + + + org.fuin.cqrs4j.springboot.core + + + + + + + org.apache.maven.plugins + maven-jdeps-plugin + + + + org.jacoco + jacoco-maven-plugin + + + prepare-agent + + prepare-agent + + + + + + + io.smallrye + jandex-maven-plugin + + + + org.apache.maven.plugins + maven-dependency-plugin + + + jakarta.el:jakarta.el-api + org.glassfish.expressly:expressly + org.hibernate.validator:hibernate-validator + com.tngtech.archunit:archunit-junit5 + org.junit.jupiter:junit-jupiter + + + com.tngtech.archunit:archunit-junit5-api + org.junit.jupiter:junit-jupiter-api + + + + + + + + + diff --git a/springboot/src/main/java/org/fuin/cqrs4j/springboot/base/EventstoreConfig.java b/springboot/src/main/java/org/fuin/cqrs4j/springboot/base/EventstoreConfig.java new file mode 100644 index 0000000..5e77fad --- /dev/null +++ b/springboot/src/main/java/org/fuin/cqrs4j/springboot/base/EventstoreConfig.java @@ -0,0 +1,83 @@ +package org.fuin.cqrs4j.springboot.base; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.Size; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Configuration for the eventstore. + */ +@ConfigurationProperties(EventstoreConfig.PREFIX) +public class EventstoreConfig { + + static final String PREFIX = "org.fuin.cqrs4j.eventstore"; + + /** + * Key for the eventstore TLS property. + */ + public static final String KEY_TLS = PREFIX + ".tls"; + + /** + * Key for the eventstore host property. + */ + public static final String KEY_HOST = PREFIX + ".host"; + + /** + * Key for the eventstore port property. + */ + public static final String KEY_PORT = PREFIX + ".port"; + + private final boolean tls; + + @Size(min = 1, max = 235) + private final String host; + + @Min(1024) + @Max(65535) + private final int port; + + /** + * Constructor with all data. + * + * @param tls Use TlS (https) or not (http). + * @param host Host name. + * @param port Port number. + */ + public EventstoreConfig(final Boolean tls, + final String host, + final Integer port) { + super(); + this.tls = tls != null && tls; + this.host = host == null ? "localhost" : host; + this.port = port == null ? 2113 : port; + } + + /** + * Returns if TLS should be used to communicate with the event store. + * + * @return {@literal true} use TLS (https) or {@literal false} (http). + */ + public boolean isTls() { + return tls; + } + + /** + * Returns the host name of the event store. + * + * @return Host name. + */ + public String getHost() { + return host; + } + + /** + * Returns the HTTP/HTTPS port of the event store. + * + * @return Port number. + */ + public int getPort() { + return port; + } + +} diff --git a/springboot/src/main/java/org/fuin/cqrs4j/springboot/view/QryProjectionPosition.java b/springboot/src/main/java/org/fuin/cqrs4j/springboot/view/QryProjectionPosition.java new file mode 100644 index 0000000..1d57dd6 --- /dev/null +++ b/springboot/src/main/java/org/fuin/cqrs4j/springboot/view/QryProjectionPosition.java @@ -0,0 +1,87 @@ +package org.fuin.cqrs4j.springboot.view; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import org.fuin.esc.api.SimpleStreamId; +import org.fuin.esc.api.StreamId; +import org.fuin.objects4j.common.Contract; + +/** + * Stores the next position to read from a projection in the event store. + */ +@Entity +@Table(name = "SPRING_QRY_PROJECTION_POS") +public class QryProjectionPosition { + + @Id + @Column(name = "STREAM_ID", nullable = false, length = 250, updatable = false) + @NotNull + private String streamId; + + @Column(name = "NEXT_POS", nullable = false, updatable = true) + @NotNull + private Long nextPos; + + /** + * JPA constructor. + */ + protected QryProjectionPosition() { + super(); + } + + /** + * Constructor with mandatory data. + * + * @param streamId + * Unique stream identifier. + * @param nextPos + * Next position from the stream to read. + */ + public QryProjectionPosition(@NotNull final StreamId streamId, @NotNull final Long nextPos) { + super(); + Contract.requireArgNotNull("streamId", streamId); + Contract.requireArgNotNull("nextPos", nextPos); + this.streamId = streamId.asString(); + this.nextPos = nextPos; + } + + /** + * Returns the unique stream identifier. + * + * @return Stream ID. + */ + @NotNull + public StreamId getStreamId() { + return new SimpleStreamId(streamId); + } + + /** + * Returns the next position read from the stream. + * + * @return Position to read next time. + */ + @NotNull + public Long getNextPos() { + return nextPos; + } + + /** + * Sets the next position read from the stream. + * + * @param nextPos + * New position to set. + */ + public void setNextPosition(@NotNull final Long nextPos) { + Contract.requireArgNotNull("nextPos", nextPos); + this.nextPos = nextPos; + } + + @Override + public String toString() { + return "QryProjectionPosition [streamId=" + streamId + ", nextPos=" + nextPos + "]"; + } + +} diff --git a/springboot/src/main/java/org/fuin/cqrs4j/springboot/view/QryProjectionService.java b/springboot/src/main/java/org/fuin/cqrs4j/springboot/view/QryProjectionService.java new file mode 100644 index 0000000..b545abc --- /dev/null +++ b/springboot/src/main/java/org/fuin/cqrs4j/springboot/view/QryProjectionService.java @@ -0,0 +1,56 @@ +package org.fuin.cqrs4j.springboot.view; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.validation.constraints.NotNull; +import org.fuin.cqrs4j.esc.ProjectionService; +import org.fuin.esc.api.StreamId; +import org.fuin.objects4j.common.Contract; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +/** + * Service to read and persist the next position of a stream to read. + */ +@Repository +public class QryProjectionService implements ProjectionService { + + private static final String ARG_STREAM_ID = "streamId"; + + @PersistenceContext + private EntityManager em; + + @Override + public void resetProjectionPosition(@NotNull final StreamId streamId) { + Contract.requireArgNotNull(ARG_STREAM_ID, streamId); + final QryProjectionPosition pos = em.find(QryProjectionPosition.class, streamId.asString()); + if (pos != null) { + pos.setNextPosition(0L); + } + } + + @Override + @Transactional(readOnly = true) + public Long readProjectionPosition(@NotNull StreamId streamId) { + Contract.requireArgNotNull(ARG_STREAM_ID, streamId); + final QryProjectionPosition pos = em.find(QryProjectionPosition.class, streamId.asString()); + if (pos == null) { + return 0L; + } + return pos.getNextPos(); + } + + @Override + public void updateProjectionPosition(@NotNull StreamId streamId, @NotNull Long nextEventNumber) { + Contract.requireArgNotNull(ARG_STREAM_ID, streamId); + Contract.requireArgNotNull("nextEventNumber", nextEventNumber); + QryProjectionPosition pos = em.find(QryProjectionPosition.class, streamId.asString()); + if (pos == null) { + pos = new QryProjectionPosition(streamId, nextEventNumber); + em.persist(pos); + } else { + pos.setNextPosition(nextEventNumber); + } + } + +} diff --git a/springboot/src/main/java/org/fuin/cqrs4j/springboot/view/SpringJpaViewManager.java b/springboot/src/main/java/org/fuin/cqrs4j/springboot/view/SpringJpaViewManager.java new file mode 100644 index 0000000..339f8f0 --- /dev/null +++ b/springboot/src/main/java/org/fuin/cqrs4j/springboot/view/SpringJpaViewManager.java @@ -0,0 +1,264 @@ +package org.fuin.cqrs4j.springboot.view; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.EntityManagerFactory; +import org.fuin.cqrs4j.core.CqrsUtils; +import org.fuin.cqrs4j.core.JpaView; +import org.fuin.cqrs4j.core.View; +import org.fuin.cqrs4j.esc.ProjectionService; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventType; +import org.fuin.esc.api.CommonEvent; +import org.fuin.esc.api.EventStore; +import org.fuin.esc.api.ProjectionAdminEventStore; +import org.fuin.esc.api.ProjectionStreamId; +import org.fuin.esc.api.StreamAlreadyExistsException; +import org.fuin.esc.api.StreamEventsSlice; +import org.fuin.esc.api.TypeName; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationListener; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.core.annotation.Order; +import org.springframework.orm.jpa.EntityManagerFactoryUtils; +import org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor; +import org.springframework.scheduling.annotation.SchedulingConfigurer; +import org.springframework.scheduling.config.CronTask; +import org.springframework.scheduling.config.ScheduledTask; +import org.springframework.scheduling.config.ScheduledTaskRegistrar; +import org.springframework.stereotype.Component; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.support.TransactionCallbackWithoutResult; +import org.springframework.transaction.support.TransactionTemplate; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.Semaphore; + +import static org.fuin.utils4j.Utils4J.tryLocked; + +/** + * Creates scheduler update tasks for all classes implementing the {@link View} interface. + * Avoids boilerplate code: Instead of having a separated "Projector", "EventDispatcher" + * and a "ChunkHandler" class for each view, there is only one simplified "View" class now. + */ +@Component +@Order(0) +public class SpringJpaViewManager implements ApplicationListener, SchedulingConfigurer { + + private static final Logger LOG = LoggerFactory.getLogger(SpringJpaViewManager.class); + + private final ScheduledAnnotationBeanPostProcessor postProcessor; + + private final List rawViews; + + private final EventStore eventstore; + + private final ProjectionAdminEventStore admin; + + private final ProjectionService projectionService; + + private final TransactionTemplate requiresNewTransaction; + + private final EntityManagerFactory entityManagerFactory; + + private List views; + + /** + * Constructor with mandatory data. + * + * @param postProcessor Helps to cancel the scheduled jobs ob shutdown. + * @param rawViews User defined view list. + * @param eventstore Eventstore instance to use. + * @param admin Admin interface to eventstore. + * @param projectionService Service to manage projections. + * @param transactionManager Helps to open necessary transactions manually. + * @param entityManagerFactory Entity manager factory. + */ + public SpringJpaViewManager( + final ScheduledAnnotationBeanPostProcessor postProcessor, + final List rawViews, + final EventStore eventstore, + final ProjectionAdminEventStore admin, + final ProjectionService projectionService, + final PlatformTransactionManager transactionManager, + final EntityManagerFactory entityManagerFactory) { + this.postProcessor = Objects.requireNonNull(postProcessor, "postProcessor==null"); + this.rawViews = Objects.requireNonNull(rawViews, "rawViews==null"); + this.eventstore = Objects.requireNonNull(eventstore, "eventstore==null"); + this.admin = Objects.requireNonNull(admin, "admin==null"); + this.projectionService = Objects.requireNonNull(projectionService, "projectionService==null"); + Objects.requireNonNull(transactionManager, "transactionManager==null"); + this.entityManagerFactory = Objects.requireNonNull(entityManagerFactory, "entityManagerFactory==null"); + this.requiresNewTransaction = new TransactionTemplate(transactionManager); + this.requiresNewTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + this.requiresNewTransaction.setTimeout(10); + } + + @Override + public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { + createViews(taskRegistrar); + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + shutdownViews(); + } + + private void createViews(ScheduledTaskRegistrar taskRegistrar) { + LOG.info("Create {} views...", rawViews == null ? 0 : rawViews.size()); + if (rawViews != null && !rawViews.isEmpty()) { + views = rawViews.stream().map(ViewExt::new).toList(); + for (final ViewExt view : views) { + LOG.info("Create view: {}", view.getName()); + view.setCronTask(new CronTask(() -> updateView(view), view.getCron())); + taskRegistrar.addCronTask(view.getCronTask()); + } + } + } + + private void shutdownViews() { + LOG.info("Shutdown {} views...", views == null ? 0 : views.size()); + final Set scheduledTasks = postProcessor.getScheduledTasks(); + for (final ViewExt view : views) { + LOG.info("Shutdown view: {}", view.getName()); + scheduledTasks.stream() + .filter(scheduled -> scheduled.getTask() == view.getCronTask()) + .findFirst() + .ifPresent(ScheduledTask::cancel); + } + } + + + private void updateView(final ViewExt view) { + tryLocked(view.getLock(), () -> new Thread(() -> { + try { + LOG.debug("updateView({})", view.getName()); + readStreamEvents(view); + } catch (final RuntimeException ex) { + LOG.error("Error reading events from stream", ex); + } + } + ).start()); + } + + private void readStreamEvents(final ViewExt view) { + + // Create an event store projection if it does not exist. + if (!admin.projectionExists(view.getProjectionStreamId())) { + final List typeNames = asTypeNames(view.getEventTypes()); + LOG.info("Create projection '{}' with events: {}", view.getProjectionStreamId(), typeNames); + try { + admin.createProjection(view.getProjectionStreamId(), true, typeNames); + } catch (StreamAlreadyExistsException ex) { + LOG.info("Race condition: After projectionExists({}) create failed with 'already exists'", view.getProjectionStreamId()); + } + } + + // Read and dispatch events + final Long nextEventNumber = projectionService.readProjectionPosition(view.getProjectionStreamId()); + eventstore.readAllEventsForward(view.getProjectionStreamId(), nextEventNumber, view.getChunkSize(), + currentSlice -> handleChunk(view, currentSlice)); + + } + + private List asTypeNames(Set eventTypes) { + return eventTypes.stream().map(eventType -> new TypeName((eventType.asString()))).toList(); + } + + private void handleChunk(final ViewExt view, final StreamEventsSlice currentSlice) { + requiresNewTransaction.execute(new TransactionCallbackWithoutResult() { + public void doInTransactionWithoutResult(TransactionStatus status) { + LOG.debug("Handle chunk: {}", currentSlice); + final EntityManager em = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory); + view.handleEvents(em, asEvents(currentSlice.getEvents())); + projectionService.updateProjectionPosition(view.getProjectionStreamId(), currentSlice.getNextEventNumber()); + } + }); + } + + private static List asEvents(List events) { + return events.stream().map(event -> (Event) event.getData()).toList(); + } + + /** + * Extends the view with some necessary values used only by this class. + */ + private static class ViewExt implements JpaView { + + private final JpaView delegate; + + private final ProjectionStreamId projectionStreamId; + + private final Semaphore lock; + + private CronTask cronTask; + + public ViewExt(final JpaView delegate) { + this.delegate = delegate; + + final Set eventTypes = delegate.getEventTypes(); + final String name = delegate.getName() + "-" + CqrsUtils.calculateAdler32Checksum(eventTypes); + projectionStreamId = new ProjectionStreamId(name); + + this.lock = new Semaphore(1); + + } + + /** + * Returns the task used. + * + * @return Task. + */ + public CronTask getCronTask() { + return cronTask; + } + + /** + * Sets the task to use. + * + * @param cronTask Task. + */ + public void setCronTask(CronTask cronTask) { + this.cronTask = cronTask; + } + + @Override + public String getName() { + return delegate.getName(); + } + + @Override + public String getCron() { + return delegate.getCron(); + } + + @Override + public Set getEventTypes() { + return delegate.getEventTypes(); + } + + @Override + public int getChunkSize() { + return delegate.getChunkSize(); + } + + @Override + public void handleEvents(EntityManager em, List events) { + delegate.handleEvents(em, events); + } + + public ProjectionStreamId getProjectionStreamId() { + return projectionStreamId; + } + + public Semaphore getLock() { + return lock; + } + + } + +} diff --git a/springboot/src/test/java/org/fuin/cqrs4j/springboot/ArchitectureTest.java b/springboot/src/test/java/org/fuin/cqrs4j/springboot/ArchitectureTest.java new file mode 100644 index 0000000..18c3dd3 --- /dev/null +++ b/springboot/src/test/java/org/fuin/cqrs4j/springboot/ArchitectureTest.java @@ -0,0 +1,45 @@ +package org.fuin.cqrs4j.springboot; + +import com.tngtech.archunit.core.importer.ImportOption; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static com.tngtech.archunit.library.DependencyRules.NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + +/** + * Tests architectural aspects. + */ +@AnalyzeClasses(packagesOf = ArchitectureTest.class, importOptions = ImportOption.DoNotIncludeTests.class) +public class ArchitectureTest { + + private static final String THIS_PACKAGE = ArchitectureTest.class.getPackageName(); + + @ArchTest + static final ArchRule no_accesses_to_upper_package = NO_CLASSES_SHOULD_DEPEND_UPPER_PACKAGES; + + @ArchTest + static final ArchRule access_only_to_defined_packages = classes() + .that() + .resideInAPackage(THIS_PACKAGE) + .should() + .onlyDependOnClassesThat() + .resideInAnyPackage(THIS_PACKAGE, "java..", + "jakarta.persistence..", + "jakarta.validation.constraints..", + "org.fuin.cqrs4j.core..", + "org.fuin.cqrs4j.esc..", + "org.fuin.ddd4j.core..", + "org.fuin.esc.api..", + "org.fuin.objects4j.common..", + "org.fuin.utils4j..", + "org.slf4j..", + "org.springframework.context..", + "org.springframework.core..", + "org.springframework.scheduling..", + "org.springframework.stereotype..", + "org.springframework.transaction.." + ); + +} diff --git a/springboot/src/test/java/org/fuin/cqrs4j/springboot/BaseTest.java b/springboot/src/test/java/org/fuin/cqrs4j/springboot/BaseTest.java new file mode 100644 index 0000000..091a7a1 --- /dev/null +++ b/springboot/src/test/java/org/fuin/cqrs4j/springboot/BaseTest.java @@ -0,0 +1,32 @@ +/** + * Copyright (C) 2013 Future Invent Informationsmanagement GmbH. All rights + * reserved. + *

+ * This library is free software; you can redistribute it and/or modify it under + * the terms of the GNU Lesser General Public License as published by the Free + * Software Foundation; either version 3 of the License, or (at your option) any + * later version. + *

+ * This library is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more + * details. + *

+ * You should have received a copy of the GNU Lesser General Public License + * along with this library. If not, see . + */ +package org.fuin.cqrs4j.springboot; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import org.fuin.units4j.archunit.Units4JConditions; + +@AnalyzeClasses(packagesOf = BaseTest.class) +class BaseTest { + + @ArchTest + static final ArchRule all_classes_should_have_tests = Units4JConditions.ALL_CLASSES_SHOULD_HAVE_TESTS; + +} + diff --git a/springboot/src/test/java/org/fuin/cqrs4j/springboot/base/EventstoreConfigTest.java b/springboot/src/test/java/org/fuin/cqrs4j/springboot/base/EventstoreConfigTest.java new file mode 100644 index 0000000..7e7b400 --- /dev/null +++ b/springboot/src/test/java/org/fuin/cqrs4j/springboot/base/EventstoreConfigTest.java @@ -0,0 +1,39 @@ +package org.fuin.cqrs4j.springboot.base; + +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the {@link EventstoreConfig} class. + */ +class EventstoreConfigTest { + + private Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + @Test + void testConstructionNull() { + final EventstoreConfig testee = new EventstoreConfig(null, null, null); + assertThat(testee.isTls()).isFalse(); + assertThat(testee.getHost()).isEqualTo("localhost"); + assertThat(testee.getPort()).isEqualTo(2113); + assertThat(validator.validate(testee)).isEmpty(); + } + + @Test + void testHostError() { + assertThat(validator.validate(new EventstoreConfig(null, "", null))) + .allMatch(violation -> + violation.getMessage().contains("size must be between 1 and 235")); + } + + @Test + void testPortError() { + assertThat(validator.validate(new EventstoreConfig(null, null, 100))) + .allMatch(violation -> + violation.getMessage().contains("must be greater than or equal to 1024")); + } + +} \ No newline at end of file diff --git a/springboot/src/test/java/org/fuin/cqrs4j/springboot/view/QryProjectionPositionTest.java b/springboot/src/test/java/org/fuin/cqrs4j/springboot/view/QryProjectionPositionTest.java new file mode 100644 index 0000000..f3a0c3c --- /dev/null +++ b/springboot/src/test/java/org/fuin/cqrs4j/springboot/view/QryProjectionPositionTest.java @@ -0,0 +1,29 @@ +package org.fuin.cqrs4j.springboot.view; + +import org.fuin.esc.api.SimpleStreamId; +import org.fuin.esc.api.StreamId; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Test for the {@link QryProjectionPosition} class. + */ +class QryProjectionPositionTest { + + @Test + void testCreateAndSetGet() { + + final StreamId streamId = new SimpleStreamId("streamId"); + long nextPos = 4711; + final QryProjectionPosition testee = new QryProjectionPosition(streamId, nextPos); + assertThat(testee.getStreamId()).isEqualTo(streamId); + assertThat(testee.getNextPos()).isEqualTo(nextPos); + + nextPos = nextPos + 1; + testee.setNextPosition(nextPos); + assertThat(testee.getNextPos()).isEqualTo(nextPos); + + } + +} \ No newline at end of file diff --git a/springboot/src/test/java/org/fuin/cqrs4j/springboot/view/QryProjectionServiceTest.java b/springboot/src/test/java/org/fuin/cqrs4j/springboot/view/QryProjectionServiceTest.java new file mode 100644 index 0000000..fb66b66 --- /dev/null +++ b/springboot/src/test/java/org/fuin/cqrs4j/springboot/view/QryProjectionServiceTest.java @@ -0,0 +1,29 @@ +package org.fuin.cqrs4j.springboot.view; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test for the {@link QryProjectionPosition} class. + */ +@Disabled("TODO Implement!") +class QryProjectionServiceTest { + + @Test + void resetProjectionPosition() { + fail("Not yet implemented"); + } + + @Test + void readProjectionPosition() { + fail("Not yet implemented"); + } + + @Test + void updateProjectionPosition() { + fail("Not yet implemented"); + } + +} \ No newline at end of file diff --git a/springboot/src/test/java/org/fuin/cqrs4j/springboot/view/SpringJpaViewManagerTest.java b/springboot/src/test/java/org/fuin/cqrs4j/springboot/view/SpringJpaViewManagerTest.java new file mode 100644 index 0000000..91430d0 --- /dev/null +++ b/springboot/src/test/java/org/fuin/cqrs4j/springboot/view/SpringJpaViewManagerTest.java @@ -0,0 +1,24 @@ +package org.fuin.cqrs4j.springboot.view; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * Test for the {@link SpringJpaViewManager} class. + */ +@Disabled("TODO Implement!") +class SpringJpaViewManagerTest { + + @Test + void createViews() { + fail("Not yet implemented"); + } + + @Test + void shutdownViews() { + fail("Not yet implemented"); + } + +} \ No newline at end of file diff --git a/src/main/java/org/fuin/cqrs4j/CommandExecutor.java b/src/main/java/org/fuin/cqrs4j/CommandExecutor.java deleted file mode 100644 index 66461f2..0000000 --- a/src/main/java/org/fuin/cqrs4j/CommandExecutor.java +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -package org.fuin.cqrs4j; - -import java.util.Set; - -import jakarta.validation.constraints.NotNull; - -import org.fuin.ddd4j.ddd.AggregateAlreadyExistsException; -import org.fuin.ddd4j.ddd.AggregateDeletedException; -import org.fuin.ddd4j.ddd.AggregateNotFoundException; -import org.fuin.ddd4j.ddd.AggregateVersionConflictException; -import org.fuin.ddd4j.ddd.AggregateVersionNotFoundException; -import org.fuin.ddd4j.ddd.EventType; - -/** - * Executes one or more commands. - * - * @param - * Type of context for the command execution. - * @param - * Result of the command execution. - * @param - * Type of command to execute. - */ -public interface CommandExecutor { - - /** - * Returns a list of commands this executor can handle. - * - * @return List of unique command types. - */ - @NotNull - public Set getCommandTypes(); - - /** - * Executes the given command. Only the main aggregate related exceptions are modeled via throws. All other checked exceptions must be - * wrapped into a {@link CommandExecutionFailedException}. - * - * @param ctx - * Context of the execute. - * @param cmd - * Command to execute. - * - * @return Result. - * - * @throws AggregateVersionConflictException - * There is a conflict between an expected and an actual version for the aggregate targeted by the command. - * @throws AggregateNotFoundException - * The aggregate targeted by the command with a given type and identifier was not found in the repository. - * @throws AggregateVersionNotFoundException - * The requested version for the aggregate targeted by the command does not exist. - * @throws AggregateDeletedException - * The aggregate targeted by the command was deleted from the repository. - * @throws AggregateAlreadyExistsException - * The aggregate targeted by the command already exists when trying to create it. - * @throws CommandExecutionFailedException - * Other checked exceptions are wrapped into this one. - */ - public RESULT execute(@NotNull CONTEXT ctx, @NotNull CMD cmd) throws AggregateVersionConflictException, AggregateNotFoundException, - AggregateVersionNotFoundException, AggregateDeletedException, AggregateAlreadyExistsException, CommandExecutionFailedException; - -} diff --git a/src/main/java/org/fuin/cqrs4j/Cqrs4JUtils.java b/src/main/java/org/fuin/cqrs4j/Cqrs4JUtils.java deleted file mode 100644 index e5d7cd3..0000000 --- a/src/main/java/org/fuin/cqrs4j/Cqrs4JUtils.java +++ /dev/null @@ -1,174 +0,0 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -package org.fuin.cqrs4j; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.concurrent.Semaphore; - -import jakarta.validation.ConstraintViolation; -import jakarta.validation.Validator; -import jakarta.validation.constraints.NotNull; - -import org.fuin.ddd4j.ddd.EntityId; -import org.fuin.ddd4j.ddd.EntityIdPath; -import org.fuin.objects4j.common.ConstraintViolationException; -import org.fuin.objects4j.common.Contract; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * Provides some helper methods. - */ -public final class Cqrs4JUtils { - - /** Classes used for JAX-B serialization. */ - public static final List> JAXB_CLASSES = Collections - .unmodifiableList(Arrays.asList(DataResult.class, SimpleResult.class, ConstraintViolationException.class)); - - private static final Logger LOG = LoggerFactory.getLogger(Cqrs4JUtils.class); - - /** Some constraints were violated. */ - public static final String PRECONDITION_VIOLATED = "PRECONDITION_VIOLATED"; - - /** Result code for {@link #verifyParamEntityIdPathEqualsCmdEntityIdPath(AggregateCommand, EntityId...)} failures. */ - public static final String PARAM_ENTITY_PATH_NOT_EQUAL_CMD_ENTITY_PATH = "PARAM_ENTITY_PATH_NOT_EQUAL_CMD_ENTITY_PATH"; - - /** Prefix for unique short identifiers. */ - public static final String SHORT_ID_PREFIX = "CQRS4J"; - - /** - * Private by intention. - */ - private Cqrs4JUtils() { - throw new UnsupportedOperationException(); - } - - /** - * Tries to acquire a lock and runs the code. If no lock can be acquired, the method terminates immediately without executing anything. - * - * @param lock - * Semaphore to use. - * @param code - * Code to run. - */ - public static void tryLocked(@NotNull final Semaphore lock, @NotNull final Runnable code) { - Contract.requireArgNotNull("lock", lock); - Contract.requireArgNotNull("code", code); - if (lock.tryAcquire()) { - try { - code.run(); - } finally { - lock.release(); - } - } - } - - /** - * Waits until a lock is available and executes the code after it was acquired. - * - * @param lock - * Semaphore to use. - * @param code - * Code to run. - */ - public static void runLocked(@NotNull final Semaphore lock, @NotNull final Runnable code) { - Contract.requireArgNotNull("lock", lock); - Contract.requireArgNotNull("code", code); - try { - lock.acquire(); - try { - code.run(); - } finally { - lock.release(); - } - } catch (final InterruptedException ex) { // NOSONAR - LOG.warn("Couldn't clear view", ex); - } - } - - /** - * Verifies a precondition. In case of constraint violations, the error is logged and an error result is returned. - * - * @param validator - * Validator to use. - * @param obj - * Object to validate. A {@literal null} value is considered to be an error. - * - * @return Error result or {@literal null} if validation was successful. - */ - public static Result verifyPrecondition(@NotNull final Validator validator, @NotNull final Object obj) { - - Contract.requireArgNotNull("validator", validator); - Contract.requireArgNotNull("obj", obj); - - final Set> violations = validator.validate(obj); - if (violations.isEmpty()) { - return null; - } - final String errors = Contract.asString(violations, ", "); - LOG.error(errors); - return SimpleResult.error(PRECONDITION_VIOLATED, errors); - } - - /** - * Verifies that an aggregate identifier from a parameter is equal to the aggregate identifier from the command. This is helpful, if for - * example the URL contains the name of the aggregate, followed by an aggregate identifier.
- * Example: POST /customer/f832a5a4-dd80-49df-856a-7274de82cd6b/create
- * The ID from the URL must match the aggregate ID that is passed via the command in the body. - * - * @param cmd - * Command that has the entity identifier path to compare with. - * @param entityIds - * Identifiers to compare with. - * - * @return Error result or {@literal null} if validation was successful. - * - * @param - * Type of the aggregate root identifier. - */ - @SafeVarargs - public static Result verifyParamEntityIdPathEqualsCmdEntityIdPath(@NotNull final AggregateCommand cmd, - @NotNull final ID... entityIds) { - - Contract.requireArgNotNull("cmd", cmd); - Contract.requireArgNotNull("entityIds", entityIds); - - final EntityIdPath paramPath = new EntityIdPath(entityIds); - if (paramPath.equals(cmd.getEntityIdPath())) { - return null; - } - - final StringBuilder sb = new StringBuilder(); - for (final ID entityId : entityIds) { - if (sb.length() > 0) { - sb.append(", "); - } - sb.append(entityId.asTypedString()); - } - - final String msg = "Entity path constructred from URL parameters " + sb + " is not the same as command's entityPath: '" - + cmd.getEntityIdPath() + "'"; - LOG.error(msg + " [" + PARAM_ENTITY_PATH_NOT_EQUAL_CMD_ENTITY_PATH + "]"); - return SimpleResult.error(PARAM_ENTITY_PATH_NOT_EQUAL_CMD_ENTITY_PATH, msg); - - } - -} diff --git a/src/main/resources/META-INF/beans.xml b/src/main/resources/META-INF/beans.xml deleted file mode 100644 index 4ca8195..0000000 --- a/src/main/resources/META-INF/beans.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/src/test/java/org/fuin/cqrs4j/Cqrs4JUtilsTest.java b/src/test/java/org/fuin/cqrs4j/Cqrs4JUtilsTest.java deleted file mode 100644 index 2d3f51e..0000000 --- a/src/test/java/org/fuin/cqrs4j/Cqrs4JUtilsTest.java +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -package org.fuin.cqrs4j; - -import static org.assertj.core.api.Assertions.assertThat; - -import jakarta.validation.constraints.NotNull; - -import org.fuin.ddd4j.ddd.AggregateVersion; -import org.fuin.ddd4j.ddd.EntityIdPath; -import org.fuin.ddd4j.ddd.EventType; -import org.fuin.objects4j.common.Contract; -import org.fuin.objects4j.vo.EmailAddressStr; -import org.junit.jupiter.api.Test; - -// CHECKSTYLE:OFF -public final class Cqrs4JUtilsTest { - - @Test - public void testVerifyPrecondition() { - - final MyClass myObj1 = new MyClass(); - assertThat(Cqrs4JUtils.verifyPrecondition(Contract.getValidator(), myObj1)) - .isEqualTo(new SimpleResult(ResultType.ERROR, Cqrs4JUtils.PRECONDITION_VIOLATED, "MyClass.email must not be null")); - - final MyClass myObj2 = new MyClass(); - myObj2.email = "test@fuin.org"; - assertThat(Cqrs4JUtils.verifyPrecondition(Contract.getValidator(), myObj2)).isNull(); - - } - - @Test - public void testVerifyParamIdEqualsCmdAggregateId() { - - // PREPARE - AId aid1 = new AId(1L); - AggregateVersion version = new AggregateVersion(0); - BId bid123 = new BId(123L); - AId aid2 = new AId(2L); - final AggregateCommand cmd = new MyCommand(aid1, version, bid123); - - // TEST & VERIFY - assertThat(Cqrs4JUtils.verifyParamEntityIdPathEqualsCmdEntityIdPath(cmd, aid1, bid123)).isNull(); - assertThat(Cqrs4JUtils.verifyParamEntityIdPathEqualsCmdEntityIdPath(cmd, aid1)) - .isEqualTo(new SimpleResult(ResultType.ERROR, Cqrs4JUtils.PARAM_ENTITY_PATH_NOT_EQUAL_CMD_ENTITY_PATH, - "Entity path constructred from URL parameters A 1 is not the same as command's entityPath: 'A 1/B 123'")); - assertThat(Cqrs4JUtils.verifyParamEntityIdPathEqualsCmdEntityIdPath(cmd, aid2, bid123)) - .isEqualTo(new SimpleResult(ResultType.ERROR, Cqrs4JUtils.PARAM_ENTITY_PATH_NOT_EQUAL_CMD_ENTITY_PATH, - "Entity path constructred from URL parameters A 2, B 123 is not the same as command's entityPath: 'A 1/B 123'")); - - } - - private static class MyClass { - - @NotNull - @EmailAddressStr - private String email; - - } - - private static class MyCommand extends AbstractAggregateCommand { - - private static final long serialVersionUID = 1L; - - public MyCommand(AId id, AggregateVersion aggregateVersion, final BId bid) { - super(new EntityIdPath(id, bid), aggregateVersion); - } - - @Override - @NotNull - public EventType getEventType() { - return new EventType("MyCommand"); - } - - } - -} -// CHECKSTYLE:ON diff --git a/src/test/java/org/fuin/cqrs4j/GeneralTest.java b/src/test/java/org/fuin/cqrs4j/GeneralTest.java deleted file mode 100644 index 5b2d928..0000000 --- a/src/test/java/org/fuin/cqrs4j/GeneralTest.java +++ /dev/null @@ -1,45 +0,0 @@ -/** - * Copyright (C) 2015 Michael Schnell. All rights reserved. - * http://www.fuin.org/ - * - * This library is free software; you can redistribute it and/or modify it under - * the terms of the GNU Lesser General Public License as published by the Free - * Software Foundation; either version 3 of the License, or (at your option) any - * later version. - * - * This library is distributed in the hope that it will be useful, but WITHOUT - * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS - * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more - * details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this library. If not, see http://www.gnu.org/licenses/. - */ -package org.fuin.cqrs4j; - -import static org.fuin.units4j.AssertCoverage.assertEveryClassHasATest; - -import java.io.File; - -import org.fuin.units4j.AssertCoverage.ClassFilter; -import org.junit.jupiter.api.Test; - -/** - * General tests for the project. - */ -public class GeneralTest { - - /** - * Verifies the test coverage of the project. - */ - @Test - public final void testEveryClassHasATest() { - assertEveryClassHasATest(new File("src/main/java"), new ClassFilter() { - @Override - public boolean isIncludeClass(final Class clasz) { - return !clasz.getName().endsWith("Exception"); - } - }); - } - -} diff --git a/src/test/resources/META-INF/beans.xml b/src/test/resources/META-INF/beans.xml deleted file mode 100644 index 4ca8195..0000000 --- a/src/test/resources/META-INF/beans.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - diff --git a/src/test/resources/META-INF/persistence.xml b/src/test/resources/META-INF/persistence.xml deleted file mode 100644 index a300cdf..0000000 --- a/src/test/resources/META-INF/persistence.xml +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - org.hibernate.ejb.HibernatePersistence - - - - true - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/src/test/resources/data-result-data.json b/src/test/resources/data-result-data.json deleted file mode 100644 index 5667c01..0000000 --- a/src/test/resources/data-result-data.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "type": "OK", - "data-class": "org.fuin.cqrs4j.DataResultTest$Invoice", - "data-element": "invoice", - "invoice": { - "id" : "I-0123456" - } -} diff --git a/src/test/resources/data-result-data.xml b/src/test/resources/data-result-data.xml deleted file mode 100644 index 137156e..0000000 --- a/src/test/resources/data-result-data.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - OK - - I-0123456 - - diff --git a/src/test/resources/data-result-exception.json b/src/test/resources/data-result-exception.json deleted file mode 100644 index 2e3e6de..0000000 --- a/src/test/resources/data-result-exception.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "type": "ERROR", - "code": "DDD4J-AGGREGATE_NOT_FOUND", - "message": "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found", - "data-class": "org.fuin.ddd4j.ddd.AggregateNotFoundException$Data", - "data-element": "aggregate-not-found-exception", - "aggregate-not-found-exception": { - "msg": "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found", - "sid": "DDD4J-AGGREGATE_NOT_FOUND", - "aggregate-type": "Invoice", - "aggregate-id": "4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119" - } -} diff --git a/src/test/resources/data-result-exception.xml b/src/test/resources/data-result-exception.xml deleted file mode 100644 index 1286359..0000000 --- a/src/test/resources/data-result-exception.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - ERROR - DDD4J-AGGREGATE_NOT_FOUND - Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found - - Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found - DDD4J-AGGREGATE_NOT_FOUND - Invoice - 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 - - diff --git a/src/test/resources/data-result-void.json b/src/test/resources/data-result-void.json deleted file mode 100644 index 090a05a..0000000 --- a/src/test/resources/data-result-void.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "OK" -} diff --git a/src/test/resources/data-result-void.xml b/src/test/resources/data-result-void.xml deleted file mode 100644 index 49fa44e..0000000 --- a/src/test/resources/data-result-void.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - OK - diff --git a/src/test/resources/log4j.properties b/src/test/resources/log4j.properties deleted file mode 100644 index a94f36e..0000000 --- a/src/test/resources/log4j.properties +++ /dev/null @@ -1,4 +0,0 @@ -log4j.rootLogger=INFO, CONSOLE -log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender -log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout -log4j.appender.CONSOLE.layout.ConversionPattern=%d{HH:mm:ss,SSS} [%-20t] %-5p %-40c{1} - %m%n diff --git a/src/test/resources/simple-result-exception.json b/src/test/resources/simple-result-exception.json deleted file mode 100644 index c7f67fa..0000000 --- a/src/test/resources/simple-result-exception.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "ERROR", - "code": "DDD4J-AGGREGATE_NOT_FOUND", - "message": "Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found" -} diff --git a/src/test/resources/simple-result-exception.xml b/src/test/resources/simple-result-exception.xml deleted file mode 100644 index 5b81d27..0000000 --- a/src/test/resources/simple-result-exception.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - ERROR - DDD4J-AGGREGATE_NOT_FOUND - Invoice with id 4dcf4c2c-10e1-4db9-ba9e-d1e644e9d119 not found - diff --git a/src/test/resources/simple-result-ok.json b/src/test/resources/simple-result-ok.json deleted file mode 100644 index 090a05a..0000000 --- a/src/test/resources/simple-result-ok.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "OK" -} diff --git a/src/test/resources/simple-result-ok.xml b/src/test/resources/simple-result-ok.xml deleted file mode 100644 index 49fa44e..0000000 --- a/src/test/resources/simple-result-ok.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - OK - diff --git a/test/helper/pom.xml b/test/helper/pom.xml new file mode 100644 index 0000000..541eb9d --- /dev/null +++ b/test/helper/pom.xml @@ -0,0 +1,74 @@ + + + + 4.0.0 + + + org.fuin.cqrs4j + cqrs-4-java-test + 0.6.0-SNAPSHOT + ../pom.xml + + + cqrs-4-java-test-helper + jar + ${description} (TEST-HELPER) + + + 1.20.6 + + + + + + + + org.testcontainers + junit-jupiter + ${testcontainers.version} + + + + org.testcontainers + mariadb + ${testcontainers.version} + + + + + + + + + + + + org.apache.maven.plugins + maven-jar-plugin + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + + org.jacoco + jacoco-maven-plugin + + + none + + + + + + + + + diff --git a/test/helper/src/main/java/org/fuin/cqrs4j/test/helper/TestHelper.java b/test/helper/src/main/java/org/fuin/cqrs4j/test/helper/TestHelper.java new file mode 100644 index 0000000..91e716e --- /dev/null +++ b/test/helper/src/main/java/org/fuin/cqrs4j/test/helper/TestHelper.java @@ -0,0 +1,57 @@ +package org.fuin.cqrs4j.test.helper; + +import com.sun.security.auth.module.UnixSystem; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.containers.wait.strategy.HttpWaitStrategy; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; +import java.util.Map; + +/** + * Helper functions for the test modules. + */ +public final class TestHelper { + + private TestHelper() { + throw new UnsupportedOperationException("Cannot instantiate utility class"); + } + + /** + * Creates a preconfigured eventstore container. + * + * @param version Docker image version of the eventstore image to use. + * @return Container. + */ + @SuppressWarnings({"java:S2095"}) + public static GenericContainer createEventstoreContainer(String version) { + return new GenericContainer<>("eventstore/eventstore:" + version) + .withCreateContainerCmdModifier(cmd -> + cmd.withUser(new UnixSystem().getUid() + ":" + new UnixSystem().getGid())) + .withNetworkMode("bridge") + .withExposedPorts(2113) + .withEnv(Map.of("EVENTSTORE_MEM_DB", "TRUE", + "EVENTSTORE_RUN_PROJECTIONS", "All", + "EVENTSTORE_INSECURE", "true", + "EVENTSTORE_LOG", "/tmp/log-eventstore")) + .waitingFor(new HttpWaitStrategy().withMethod("GET") + .forPath("/web/index.html#/") + .withReadTimeout(Duration.of(20, ChronoUnit.SECONDS))); + } + + /** + * Creates a preconfigured MariaDB container. + * + * @param version Docker image version of the MariaDB image to use. + * @return Container. + */ + @SuppressWarnings("java:S2095") // Resource will be closed after using unit test + public static MariaDBContainer createMariaDBContainer(String version) { + return new MariaDBContainer<>("mariadb:" + version) + .withDatabaseName("testdb") + .withUsername("mary") + .withPassword("abc"); + } + +} diff --git a/test/pom.xml b/test/pom.xml new file mode 100644 index 0000000..e531735 --- /dev/null +++ b/test/pom.xml @@ -0,0 +1,40 @@ + + + + 4.0.0 + + + org.fuin.cqrs4j + cqrs-4-java + 0.6.0-SNAPSHOT + ../pom.xml + + + cqrs-4-java-test + pom + ${description} (TEST) + + + + + + + org.fuin.cqrs4j + cqrs-4-java-test-helper + ${project.version} + + + + + + + + + helper + springboot + quarkus + + + diff --git a/test/quarkus/pom.xml b/test/quarkus/pom.xml new file mode 100644 index 0000000..10700e5 --- /dev/null +++ b/test/quarkus/pom.xml @@ -0,0 +1,300 @@ + + + 4.0.0 + + + org.fuin.cqrs4j + cqrs-4-java-test + 0.6.0-SNAPSHOT + ../pom.xml + + + cqrs-4-java-test-quarkus + jar + ${description} (TEST-QUARKUS) + + + 3.21.0 + + + + + + io.quarkus + quarkus-bom + ${quarkus.version} + pom + import + + + + + + + + + + org.fuin.cqrs4j + cqrs-4-java-core + + + + org.fuin.cqrs4j + cqrs-4-java-quarkus + + + + org.fuin.cqrs4j + cqrs-4-java-jsonb + + + + org.fuin.cqrs4j + cqrs-4-java-test-helper + + + + org.fuin.objects4j + objects4j-common + + + + org.fuin.objects4j + objects4j-core + + + + org.fuin.objects4j + objects4j-jaxb + + + + org.fuin.objects4j + objects4j-jpa + + + + org.fuin.objects4j + objects4j-jsonb + + + + org.fuin.esc + esc-client + + + + org.fuin.esc + esc-jsonb + + + + org.fuin.esc + esc-esgrpc + + + + io.quarkus + quarkus-resteasy + + + + io.quarkus + quarkus-resteasy-jsonb + + + + io.quarkus + quarkus-resteasy-jaxb + + + + io.quarkus + quarkus-hibernate-orm + + + + io.quarkus + quarkus-jdbc-mariadb + + + + io.quarkus + quarkus-scheduler + + + + org.jboss.slf4j + slf4j-jboss-logmanager + + + + org.eclipse + yasson + + + + org.hibernate.validator + hibernate-validator + + + + org.fuin.ddd4j + ddd-4-java-codegen-api + + + + + + io.quarkus + quarkus-junit5 + test + + + + io.rest-assured + rest-assured + test + + + + org.assertj + assertj-core + test + + + + org.awaitility + awaitility + test + + + + org.testcontainers + mariadb + test + + + + net.javacrumbs.json-unit + json-unit-fluent + test + + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + add-source + generate-sources + + add-source + + + + ${project.basedir}/src-gen/main/java + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${project.basedir}/src-gen/main/java + + + org.fuin.ddd4j + ddd-4-java-codegen-processor + ${ddd4j.version} + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.1 + + true + + + + + org.jacoco + jacoco-maven-plugin + + + none + + + + + + io.quarkus + quarkus-maven-plugin + ${quarkus.version} + + + + build + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + + + org.jboss.logmanager.LogManager + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.2.5 + + + + integration-test + verify + + + + org.jboss.logmanager.LogManager + + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + true + + + + + + + + diff --git a/test/quarkus/src-gen/main/java/org/fuin/cqrs4j/quarkus/test/model/PersonCreatedEvent.java b/test/quarkus/src-gen/main/java/org/fuin/cqrs4j/quarkus/test/model/PersonCreatedEvent.java new file mode 100644 index 0000000..b359cb8 --- /dev/null +++ b/test/quarkus/src-gen/main/java/org/fuin/cqrs4j/quarkus/test/model/PersonCreatedEvent.java @@ -0,0 +1,152 @@ +package org.fuin.cqrs4j.quarkus.test.model; + +import jakarta.validation.constraints.NotNull; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbTypeAdapter; +import javax.annotation.Nullable; +import org.fuin.ddd4j.core.EntityIdPath; +import org.fuin.ddd4j.core.EventType; +import org.fuin.ddd4j.jsonb.AbstractDomainEvent; +import org.fuin.esc.api.HasSerializedDataTypeConstant; +import org.fuin.esc.api.SerializedDataType; +import org.fuin.objects4j.common.Contract; + +import java.io.Serial; + +import javax.annotation.concurrent.Immutable; + + +/** + * A person was created. + */ +@Immutable +@HasSerializedDataTypeConstant +public final class PersonCreatedEvent extends AbstractDomainEvent { + + @Serial + private static final long serialVersionUID = 1000L; + + /** Unique name of the event used to store it - Should never change. */ + public static final EventType TYPE = new EventType(PersonCreatedEvent.class.getSimpleName()); + + /** Unique name of the serialized event. */ + public static final SerializedDataType SER_TYPE = new SerializedDataType(TYPE.asBaseType()); + + @JsonbProperty("id") + private PersonId id; + + @JsonbProperty("name") + private PersonName name; + + + /** + * Protected default constructor for deserialization. + */ + protected PersonCreatedEvent() { // NOSONAR Default constructor + super(); + } + + /** + * Constructor with event data. + * + * @param id TODO Add '@Label' annotation. TODO Add '@Tooltip' annotation. + * @param name TODO Add '@Label' annotation. TODO Add '@Tooltip' annotation. + */ + protected PersonCreatedEvent( + final PersonId id, + final PersonName name + ) { + super(new EntityIdPath(id)); + this.id = id; + this.name = name; + } + + @Override + public EventType getEventType() { + return TYPE; + } + + /** + * Returns: TODO Add '@Label' annotation. + * + * @return TODO Add '@Label' annotation. TODO Add '@Tooltip' annotation. + */ + public PersonId getId() { + return id; + } + /** + * Returns: TODO Add '@Label' annotation. + * + * @return TODO Add '@Label' annotation. TODO Add '@Tooltip' annotation. + */ + public PersonName getName() { + return name; + } + + @Override + public String toString() { + return "MyEvent happened"; + } + + /** + * Creates a new builder instance. + * + * @return New builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builds an instance of the outer class. + */ + public static final class Builder extends AbstractDomainEvent.Builder { + + private PersonCreatedEvent delegate; + + private Builder() { + super(new PersonCreatedEvent()); + delegate = delegate(); + } + + /** + * Sets: TODO Add '@Label' annotation. + * + * @param id TODO Add '@Label' annotation. + * @return This builder. + */ + @SuppressWarnings("unchecked") + public final Builder id(@NotNull final PersonId id) { + Contract.requireArgNotNull("id", id); + delegate.id = id; + return this; + } + /** + * Sets: TODO Add '@Label' annotation. + * + * @param name TODO Add '@Label' annotation. + * @return This builder. + */ + @SuppressWarnings("unchecked") + public final Builder name(@NotNull final PersonName name) { + Contract.requireArgNotNull("name", name); + delegate.name = name; + return this; + } + + /** + * Creates the event and clears the builder. + * + * @return New instance. + */ + public PersonCreatedEvent build() { + ensureBuildableAbstractDomainEvent(); + final PersonCreatedEvent result = delegate; + delegate = new PersonCreatedEvent(); + resetAbstractDomainEvent(delegate); + return result; + } + + } + +} diff --git a/test/quarkus/src-gen/main/java/org/fuin/cqrs4j/quarkus/test/model/PersonId.java b/test/quarkus/src-gen/main/java/org/fuin/cqrs4j/quarkus/test/model/PersonId.java new file mode 100644 index 0000000..56d245e --- /dev/null +++ b/test/quarkus/src-gen/main/java/org/fuin/cqrs4j/quarkus/test/model/PersonId.java @@ -0,0 +1,188 @@ +package org.fuin.cqrs4j.quarkus.test.model; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.UUID; + +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.persistence.AttributeConverter; + +import jakarta.annotation.Generated; +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; +import jakarta.validation.constraints.NotNull; + +import org.fuin.ddd4j.core.AggregateRootUuid; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.HasEntityTypeConstant; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.objects4j.common.ConstraintViolationException; +import org.fuin.objects4j.common.HasPublicStaticValueOfMethod; + +import javax.annotation.concurrent.Immutable; + +/** + * Unique identifier of a person. + */ +@Generated("Generated class - Manual changes will be overwritten") +@Immutable +@HasPublicStaticValueOfMethod +@HasEntityTypeConstant + +public final class PersonId extends AggregateRootUuid { + + private static final long serialVersionUID = 1000L; + + /** Unique name of the aggregate this identifier refers to. */ + public static final EntityType TYPE = new StringBasedEntityType("PERSON"); + + /** + * Default constructor that generates a random UUID. + */ + public PersonId() { + super(TYPE); + } + + /** + * Constructor with mandatory data. + * + * @param value + * Value. + */ + public PersonId(final UUID value) { + super(TYPE, value); + } + + /** + * Parses a given string and returns a new instance of this type. + * + * @param value + * String with valid UUID to convert. A null value returns null. + * + * @return Converted value. + */ + public static PersonId valueOf(final String value) { + if (value == null) { + return null; + } + requireArgValid("value", value); + return new PersonId(UUID.fromString(value)); + } + + /** + * Verifies that a given string can be converted into the type. + * + * @param value + * Value to validate. + * + * @return Returns true if it's a valid type else false. + */ + public static boolean isValid(final String value) { + if (value == null) { + return true; + } + return AggregateRootUuid.isValid(value); + } + + /** + * Verifies if the argument is valid and throws an exception if this is not the case. + * + * @param name + * Name of the value for a possible error message. + * @param value + * Value to check. + * + * @throws ConstraintViolationException + * The value was not valid. + */ + public static void requireArgValid(@NotNull final String name, @NotNull final String value) throws ConstraintViolationException { + if (!isValid(value)) { + throw new ConstraintViolationException("The argument '" + name + "' is not valid: '" + value + "'"); + } + } + + /** + * Ensures that the string can be converted into the type. + */ + @Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Constraint(validatedBy = { Validator.class }) + @Documented + public static @interface PersonIdStr { + + String message() + + default "{org.fuin.cqrs4j.quarkus.test.model.PersonId.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + } + + /** + * Validates if a string is compliant with the type. + */ + public static final class Validator implements ConstraintValidator { + + @Override + public final void initialize(final PersonIdStr annotation) { + // Not used + } + + @Override + public final boolean isValid(final String value, final ConstraintValidatorContext context) { + return PersonId.isValid(value); + } + + } + + /** + * Converts the value object from/to string. + */ + public static final class Converter implements JsonbAdapter, AttributeConverter { + + private PersonId toVO(final UUID value) { + if (value == null) { + return null; + } + return new PersonId(value); + } + + private UUID fromVO(final PersonId value) { + if (value == null) { + return null; + } + return value.asBaseType(); + } + // JSONB Adapter + + @Override + public final UUID adaptToJson(final PersonId obj) throws Exception { + return fromVO(obj); + } + + @Override + public final PersonId adaptFromJson(final UUID str) throws Exception { + return toVO(str); + } + + // JPA + + @Override + public final UUID convertToDatabaseColumn(final PersonId value) { + return fromVO(value); + } + + @Override + public final PersonId convertToEntityAttribute(final UUID value) { + return toVO(value); + } + } + +} diff --git a/test/quarkus/src-gen/main/java/org/fuin/cqrs4j/quarkus/test/model/PersonName.java b/test/quarkus/src-gen/main/java/org/fuin/cqrs4j/quarkus/test/model/PersonName.java new file mode 100644 index 0000000..40ccd44 --- /dev/null +++ b/test/quarkus/src-gen/main/java/org/fuin/cqrs4j/quarkus/test/model/PersonName.java @@ -0,0 +1,239 @@ +package org.fuin.cqrs4j.quarkus.test.model; + +import java.io.Serializable; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Objects; + +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.persistence.AttributeConverter; +import jakarta.annotation.Generated; +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; +import jakarta.validation.constraints.NotNull; + +import org.fuin.objects4j.common.ConstraintViolationException; +import org.fuin.objects4j.common.AsStringCapable; +import org.fuin.objects4j.common.ValueObjectWithBaseType; + +import javax.annotation.concurrent.Immutable; + +/** + * The name of the person. + * + * CAUTION: Instances of this type may contain invalid values by deserializing it. + * This means if you create it from JSON, XML or database (JPA) it may not have a correct length or pattern. + */ +@Generated("Generated class - Manual changes will be overwritten") +@Immutable + +public final class PersonName implements ValueObjectWithBaseType, Comparable, Serializable, AsStringCapable { + + private static final long serialVersionUID = 1L; + + + /** Minimal length of a valid value. */ + public static final int MIN_LENGTH = 1; + + /** Maximum length of a valid value. */ + public static final int MAX_LENGTH = 200; + + @NotNull + @PersonNameStr + private String value; + + /** + * Protected default constructor for deserialization. + */ + protected PersonName() { + super(); + } + + /** + * Constructor with mandatory data. + * + * @param value + * Value. + */ + public PersonName(final String value) { + this(value, true); + } + + private PersonName(final String value, final boolean strict) { + super(); + if (strict) { + PersonName.requireArgValid("value", value); + } + this.value = value; + } + + @Override + public final String asBaseType() { + return value; + } + + @Override + public final String toString() { + return value; + } + + @Override + public final String asString() { + return value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final PersonName other = (PersonName) obj; + return Objects.equals(value, other.value); + } + + @Override + public final int compareTo(final PersonName other) { + return value.compareTo(other.value); + } + + @Override + @NotNull + public final Class getBaseType() { + return String.class; + } + + /** + * Verifies that a given string can be converted into the type. + * + * @param value + * Value to validate. + * + * @return Returns true if it's a valid type else false. + */ + public static boolean isValid(final String value) { + if (value == null) { + return true; + } + if (value.length() < MIN_LENGTH) { + return false; + } + final String trimmed = value.trim(); + if (trimmed.length() > MAX_LENGTH) { + return false; + } + return true; + } + + /** + * Verifies if the argument is valid and throws an exception if this is not the case. + * + * @param name + * Name of the value for a possible error message. + * @param value + * Value to check. + * + * @throws ConstraintViolationException + * The value was not valid. + */ + public static void requireArgValid(@NotNull final String name, @NotNull final String value) throws ConstraintViolationException { + if (!isValid(value)) { + throw new ConstraintViolationException("The argument '" + name + "' is not valid: '" + value + "'"); + } + } + + /** + * Ensures that the string can be converted into the type. + */ + @Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Constraint(validatedBy = { Validator.class }) + @Documented + public static @interface PersonNameStr { + + String message() + + default "{org.fuin.cqrs4j.quarkus.test.model.PersonName.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + } + + /** + * Validates if a string is compliant with the type. + */ + public static final class Validator implements ConstraintValidator { + + @Override + public final void initialize(final PersonNameStr annotation) { + // Not used + } + + @Override + public final boolean isValid(final String value, final ConstraintValidatorContext context) { + return PersonName.isValid(value); + } + + } + + /** + * Converts the value object from/to string. + */ + public static final class Converter implements JsonbAdapter, AttributeConverter { + + private PersonName toVO(final String value) { + if (value == null) { + return null; + } + return new PersonName(value, false); + } + + private String fromVO(final PersonName value) { + if (value == null) { + return null; + } + return value.asBaseType(); + } + // JSONB Adapter + + @Override + public final String adaptToJson(final PersonName obj) throws Exception { + return fromVO(obj); + } + + @Override + public final PersonName adaptFromJson(final String str) throws Exception { + return toVO(str); + } + + // JPA + + @Override + public final String convertToDatabaseColumn(final PersonName value) { + return fromVO(value); + } + + @Override + public final PersonName convertToEntityAttribute(final String value) { + return toVO(value); + } + } + +} diff --git a/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/app/KurrentDBWrapper.java b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/app/KurrentDBWrapper.java new file mode 100644 index 0000000..f5f5eb6 --- /dev/null +++ b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/app/KurrentDBWrapper.java @@ -0,0 +1,69 @@ +package org.fuin.cqrs4j.quarkus.test.app; + +import io.kurrent.dbclient.KurrentDBClient; +import io.kurrent.dbclient.KurrentDBClientSettings; +import io.kurrent.dbclient.KurrentDBProjectionManagementClient; +import org.fuin.cqrs4j.quarkus.base.EventstoreConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Helper class that wraps the KurrentDB clients to be compliant with required CDI default constructor. + */ +public class KurrentDBWrapper { + + private static final Logger LOG = LoggerFactory.getLogger(KurrentDBWrapper.class); + + private KurrentDBClient client; + + private KurrentDBProjectionManagementClient projectionManagementClient; + + protected KurrentDBWrapper() { + } + + public KurrentDBWrapper(final EventstoreConfig config) { + client = kurrentDBClient(config); + projectionManagementClient = kurrentDBProjectionManagementClient(config); + } + + public KurrentDBClient getClient() { + return client; + } + + public KurrentDBProjectionManagementClient getProjectionManagementClient() { + return projectionManagementClient; + } + + public void shutdown() { + try { + client.shutdown(); + } catch (final RuntimeException ex) { + LOG.error("Failed to close client", ex); + } + try { + projectionManagementClient.shutdown(); + } catch (final RuntimeException ex) { + LOG.error("Failed to close projectionManagementClient", ex); + } + } + + private static KurrentDBClient kurrentDBClient(final EventstoreConfig config) { + final KurrentDBClientSettings setts = KurrentDBClientSettings.builder() + .addHost(config.getHost(), config.getPort()) + .defaultCredentials("admin", "changeit") // Just for test + .tls(false) + .buildConnectionSettings(); + return KurrentDBClient.create(setts); + } + + private static KurrentDBProjectionManagementClient kurrentDBProjectionManagementClient(final EventstoreConfig config) { + final KurrentDBClientSettings settings = KurrentDBClientSettings.builder() + .addHost(config.getHost(), config.getPort()) + .defaultCredentials("admin", "changeit") // Just for test + .tls(config.isTls()) + .buildConnectionSettings(); + return KurrentDBProjectionManagementClient.create(settings); + } + + +} diff --git a/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/app/PersonResource.java b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/app/PersonResource.java new file mode 100644 index 0000000..354b85f --- /dev/null +++ b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/app/PersonResource.java @@ -0,0 +1,36 @@ +package org.fuin.cqrs4j.quarkus.test.app; + +import jakarta.inject.Inject; +import jakarta.persistence.EntityManager; +import jakarta.transaction.Transactional; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.fuin.cqrs4j.quarkus.test.model.PersonEntity; +import org.fuin.cqrs4j.quarkus.test.model.PersonId; + +/** + * REST resource reading persons. + */ +@Path("/persons") +@Transactional +public class PersonResource { + + @Inject + EntityManager em; + + @GET + @Path("{id}") + @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) + public Response read(@PathParam("id") PersonId id) { + final PersonEntity person = em.find(PersonEntity.class, id.asBaseType()); + if (person == null) { + return Response.status(Response.Status.NOT_FOUND).build(); + } + return Response.ok(person).build(); + } + +} diff --git a/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/app/QuarkusApp.java b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/app/QuarkusApp.java new file mode 100644 index 0000000..091320e --- /dev/null +++ b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/app/QuarkusApp.java @@ -0,0 +1,21 @@ +package org.fuin.cqrs4j.quarkus.test.app; + +import io.quarkus.runtime.Quarkus; +import io.quarkus.runtime.annotations.QuarkusMain; + +/** + * Represents the (custom) entry point, most likely used to the Quarkus application in the IDE. + */ +@QuarkusMain +public class QuarkusApp { + + /** + * Main method to start the app. + * + * @param args Arguments from the command line. + */ + public static void main(String[] args) { + Quarkus.run(args); + } + +} \ No newline at end of file diff --git a/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/app/QuarkusFactory.java b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/app/QuarkusFactory.java new file mode 100644 index 0000000..1389ae8 --- /dev/null +++ b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/app/QuarkusFactory.java @@ -0,0 +1,178 @@ +package org.fuin.cqrs4j.quarkus.test.app; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Disposes; +import jakarta.enterprise.inject.Produces; +import jakarta.inject.Singleton; +import jakarta.json.bind.JsonbConfig; +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.json.bind.serializer.JsonbDeserializer; +import jakarta.json.bind.serializer.JsonbSerializer; +import org.fuin.cqrs4j.jsonb.JandexJsonbRegistry; +import org.fuin.cqrs4j.jsonb.JsonbRegistry; +import org.fuin.cqrs4j.quarkus.base.EventstoreConfig; +import org.fuin.cqrs4j.quarkus.test.view.PersonsView; +import org.fuin.ddd4j.core.EntityIdFactory; +import org.fuin.ddd4j.core.JandexEntityIdFactory; +import org.fuin.esc.api.EnhancedMimeType; +import org.fuin.esc.api.ProjectionAdminEventStore; +import org.fuin.esc.api.SerDeserializerRegistry; +import org.fuin.esc.api.SerializedDataTypeRegistry; +import org.fuin.esc.api.SimpleSerializerDeserializerRegistry; +import org.fuin.esc.client.JandexSerializedDataTypeRegistry; +import org.fuin.esc.esgrpc.ESGrpcEventStore; +import org.fuin.esc.esgrpc.GrpcProjectionAdminEventStore; +import org.fuin.esc.esgrpc.IESGrpcEventStore; +import org.fuin.esc.jsonb.BaseTypeFactory; +import org.fuin.esc.jsonb.EscJsonbUtils; +import org.fuin.esc.jsonb.JsonbSerDeserializer; +import org.fuin.objects4j.jsonb.FieldAccessStrategy; +import org.fuin.objects4j.jsonb.JsonbProvider; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.charset.StandardCharsets; + +/** + * CDI factory that creates necessary beans. + */ +@ApplicationScoped +public class QuarkusFactory { + + private static final Logger LOG = LoggerFactory.getLogger(QuarkusFactory.class); + + // TODO Re-enable after problem with event store connection is solved + //@Produces + public PersonsView personsView() { + return new PersonsView(); + } + + @Produces + @Singleton + public EntityIdFactory entityIdFactory() { + return new JandexEntityIdFactory(); + } + + @Produces + @Singleton + public JsonbConfig jsonbConfig() { + return new JsonbConfig() + .withPropertyVisibilityStrategy(new FieldAccessStrategy()) + .withEncoding(StandardCharsets.UTF_8.name()); + } + + @Produces + @Singleton + public JsonbProvider jsonbProvider(JsonbConfig jsonbConfig) { + return new JsonbProvider(jsonbConfig); + } + + @Produces + @Singleton + public JsonbSerDeserializer jsonbSerDeserializer(JsonbProvider jsonbProvider, + SerializedDataTypeRegistry typeRegistry) { + return new JsonbSerDeserializer(jsonbProvider, typeRegistry, StandardCharsets.UTF_8); + } + + @Produces + @Singleton + public SerializedDataTypeRegistry serializedDataTypeRegistry() { + return new JandexSerializedDataTypeRegistry(); + } + + @Produces + @Singleton + public SerDeserializerRegistry serDeserializerRegistry(JsonbConfig jsonbConfig, + JsonbProvider jsonbProvider, + EntityIdFactory entityIdFactory, + SerializedDataTypeRegistry typeRegistry, + JsonbSerDeserializer jsonbSerDeserializer) { + + final SimpleSerializerDeserializerRegistry.Builder builder = new SimpleSerializerDeserializerRegistry.Builder(EscJsonbUtils.MIME_TYPE); + for (final SerializedDataTypeRegistry.TypeClass tc : typeRegistry.findAll()) { + builder.add(tc.type(), jsonbSerDeserializer); + LOG.info("Registered type '{}' with serializer: {}", tc.type().asBaseType(), jsonbSerDeserializer.getClass().getSimpleName()); + } + + final SerDeserializerRegistry registry = builder.build(); + + EscJsonbUtils.addEscSerDeserializer(builder, jsonbSerDeserializer); + + final JsonbRegistry jsonbRegistry = new JandexJsonbRegistry(entityIdFactory, registry, registry, jsonbProvider); + jsonbConfig.withAdapters(jsonbRegistry.getAdapters().toArray(new JsonbAdapter[0])); + jsonbConfig.withSerializers(jsonbRegistry.getSerializers().toArray(new JsonbSerializer[0])); + jsonbConfig.withDeserializers(jsonbRegistry.getDeserializers().toArray(new JsonbDeserializer[0])); + + return registry; + + } + + @Produces + @Singleton + public KurrentDBWrapper kurrentDBWrapper(final EventstoreConfig config) { + return new KurrentDBWrapper(config); + } + + /** + * Creates an GRPC based event store.
+ *
+ * CAUTION: The returned event store instance is NOT thread safe. + * + * @param kurrentDBWrapper Shared client connection. + * @param registry Serialization registry. + * @return Application scope event store. + */ + @Produces + @Dependent + public IESGrpcEventStore createEventStore(final KurrentDBWrapper kurrentDBWrapper, + final SerDeserializerRegistry registry) { + + final IESGrpcEventStore eventstore = new ESGrpcEventStore.Builder() + .eventStore(kurrentDBWrapper.getClient()) + .serDesRegistry(registry) + .baseTypeFactory(new BaseTypeFactory()) + .targetContentType(EnhancedMimeType.create("application", "json", StandardCharsets.UTF_8)) + .build(); + + eventstore.open(); + return eventstore; + + } + + @Produces + @Dependent + @SuppressWarnings("java:S2095") // Resource will be closed with "disposes" method + public ProjectionAdminEventStore getProjectionAdminEventStore(final KurrentDBWrapper kurrentDBWrapper) { + return new GrpcProjectionAdminEventStore(kurrentDBWrapper.getProjectionManagementClient()).open(); + + } + + /** + * Shuts the wrapper with the clients inside down when the context is disposed. + * + * @param kurrentDBWrapper Wrapper to shut down. + */ + public void shutdownKurrentDBWrapper(@Disposes final KurrentDBWrapper kurrentDBWrapper) { + kurrentDBWrapper.shutdown(); + } + + /** + * Closes the GRPC based event store when the context is disposed. + * + * @param es Event store to close. + */ + public void closeEventStore(@Disposes final IESGrpcEventStore es) { + es.close(); + } + + /** + * Closes the projection admin event store when the context is disposed. + * + * @param es Event store to close. + */ + public void closeProjectionAdminEventStore(@Disposes final ProjectionAdminEventStore es) { + es.close(); + } + +} diff --git a/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/AbstractPersonsView.java b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/AbstractPersonsView.java new file mode 100644 index 0000000..72757ab --- /dev/null +++ b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/AbstractPersonsView.java @@ -0,0 +1,51 @@ +package org.fuin.cqrs4j.quarkus.test.model; + +import jakarta.persistence.EntityManager; +import org.fuin.cqrs4j.core.JpaView; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Set; + +/** + * Handles the events required to maintain the persons view. + */ +public abstract class AbstractPersonsView implements JpaView { + + private static final Logger LOG = LoggerFactory.getLogger(AbstractPersonsView.class); + + @Override + public String getName() { + return "persons-view"; + } + + @Override + public Set getEventTypes() { + return Set.of(PersonCreatedEvent.TYPE); + } + + @Override + public void handleEvents(final EntityManager em, final List events) { + for (final Event event : events) { + if (event instanceof PersonCreatedEvent ev) { + handlePersonCreatedEvent(em, ev); + } else { + throw new IllegalStateException("Cannot handle event: " + event); + } + } + } + + private void handlePersonCreatedEvent(final EntityManager em, final PersonCreatedEvent event) { + LOG.info("Handle {}: {}", event.getClass().getSimpleName(), event); + final PersonEntity entity = em.find(PersonEntity.class, event.getId().asBaseType()); + if (entity == null) { + em.persist(new PersonEntity(event.getId(), event.getName())); + } else { + LOG.info("Ignored {}} because entity already exists: {}", event.getClass().getSimpleName(), event); + } + } + +} \ No newline at end of file diff --git a/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/GEN_PersonCreatedEvent.java b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/GEN_PersonCreatedEvent.java new file mode 100644 index 0000000..8a9091e --- /dev/null +++ b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/GEN_PersonCreatedEvent.java @@ -0,0 +1,24 @@ +package org.fuin.cqrs4j.quarkus.test.model; + +import org.fuin.ddd4j.codegen.api.EventVO; + +/** + * Do not use this class. It's just for generating some code using APT. + */ +@EventVO(pkg="org.fuin.cqrs4j.quarkus.test.model", + name = "PersonCreatedEvent", + entityIdPathParams = "id", + description = "A person was created", + jsonb = true, + serialVersionUID = 1000L, + entityIdClass = "PersonId", + message = "MyEvent happened" +) +@SuppressWarnings("java:S1214") // Just a helper to generate code +public interface GEN_PersonCreatedEvent { + + PersonId id = null; + + PersonName name = null; + +} diff --git a/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/GEN_PersonId.java b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/GEN_PersonId.java new file mode 100644 index 0000000..98b4485 --- /dev/null +++ b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/GEN_PersonId.java @@ -0,0 +1,18 @@ +package org.fuin.cqrs4j.quarkus.test.model; + +import org.fuin.ddd4j.codegen.api.AggregateRootUuidVO; + +/** + * Do not use this class. It's just for generating some code using APT. + */ +@AggregateRootUuidVO( + pkg="org.fuin.cqrs4j.quarkus.test.model", + name = "PersonId", + entityType = "PERSON", + description = "Unique identifier of a person", + jsonb = true, jpa = true, + serialVersionUID = 1000L, + example = "b20d7373-1950-478a-ab61-d022cd44f507" +) +public interface GEN_PersonId { +} diff --git a/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/GEN_PersonName.java b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/GEN_PersonName.java new file mode 100644 index 0000000..08803df --- /dev/null +++ b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/GEN_PersonName.java @@ -0,0 +1,17 @@ +package org.fuin.cqrs4j.quarkus.test.model; + +import org.fuin.ddd4j.codegen.api.StringVO; + +/** + * Do not use this class. It's just for generating some code using APT. + */ +@StringVO( + pkg="org.fuin.cqrs4j.quarkus.test.model", + name = "PersonName", + description = "The name of the person", + jsonb = true, jpa = true, + minLength = 1, maxLength = 200 +) +public interface GEN_PersonName { + +} diff --git a/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/PersonEntity.java b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/PersonEntity.java new file mode 100644 index 0000000..412f9dd --- /dev/null +++ b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/model/PersonEntity.java @@ -0,0 +1,89 @@ +package org.fuin.cqrs4j.quarkus.test.model; + +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.json.bind.annotation.JsonbTypeAdapter; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.fuin.objects4j.jsonb.UUIDJsonbAdapter; + +import java.util.Objects; +import java.util.UUID; + +/** + * Represents a person. Equals/hashCode are based on the ID only and the entity is sorted by the name. + */ +@Entity(name = "PERSON") +public class PersonEntity implements Comparable { + + @Id + @Column(name = "ID", unique = true, nullable = false) + @JsonbProperty + @JsonbTypeAdapter(UUIDJsonbAdapter.class) + public UUID id; + + @Column(name = "NAME", nullable = false, length = PersonName.MAX_LENGTH) + @JsonbProperty + public String name; + + /** + * Constructor used by JPA & JSON-B. + */ + protected PersonEntity() { + } + + /** + * Constructor with mandatory data. + * + * @param id Unique identifier of the person. + * @param name Name of the person. + */ + public PersonEntity(PersonId id, PersonName name) { + this.id = Objects.requireNonNull(id, "id==null").asBaseType(); + this.name = Objects.requireNonNull(name, "name==null").asBaseType(); + } + + /** + * Returns the unique identifier of the person. + * + * @return Person ID. + */ + public PersonId getId() { + return new PersonId(id); + } + + /** + * Returns the name of the person. + * + * @return Person name. + */ + public PersonName getName() { + return new PersonName(name); + } + + @Override + public int compareTo(PersonEntity o) { + return this.name.compareTo(o.name); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PersonEntity that = (PersonEntity) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + @Override + public String toString() { + return "PersonEntity{" + + "id=" + id + + ", name=" + name + + '}'; + } + +} diff --git a/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/view/PersonsView.java b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/view/PersonsView.java new file mode 100644 index 0000000..acb6092 --- /dev/null +++ b/test/quarkus/src/main/java/org/fuin/cqrs4j/quarkus/test/view/PersonsView.java @@ -0,0 +1,13 @@ +package org.fuin.cqrs4j.quarkus.test.view; + +import org.fuin.cqrs4j.quarkus.test.model.AbstractPersonsView; + +public class PersonsView extends AbstractPersonsView { + + @Override + public String getCron() { + // Every second + return "* * * * * ?"; + } + +} diff --git a/test/quarkus/src/main/resources/application.properties b/test/quarkus/src/main/resources/application.properties new file mode 100644 index 0000000..6c7cf25 --- /dev/null +++ b/test/quarkus/src/main/resources/application.properties @@ -0,0 +1,13 @@ +quarkus.datasource.db-kind=mariadb +quarkus.datasource.username=mary +quarkus.datasource.password=abc +quarkus.datasource.jdbc.url=jdbc:mariadb://localhost:3306/testdb + +quarkus.hibernate-orm.database.generation=drop-and-create + +quarkus.scheduler.enabled=true +quarkus.scheduler.start-mode=forced + +quarkus.log.console.format=[%c{2.}] (%t) %s%e%n +quarkus.log.level=INFO +quarkus.log.category."org.fuin.cqrs4j".level=DEBUG diff --git a/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/EventstoreResource.java b/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/EventstoreResource.java new file mode 100644 index 0000000..823c4a8 --- /dev/null +++ b/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/EventstoreResource.java @@ -0,0 +1,26 @@ +package org.fuin.cqrs4j.quarkus.test; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import org.fuin.cqrs4j.quarkus.base.EventstoreConfig; +import org.testcontainers.containers.GenericContainer; + +import java.util.Map; + +import static org.fuin.cqrs4j.test.helper.TestHelper.createEventstoreContainer; + +public class EventstoreResource implements QuarkusTestResourceLifecycleManager { + + static GenericContainer es = createEventstoreContainer("24.10"); + + @Override + public Map start() { + es.start(); + return Map.of(EventstoreConfig.KEY_PORT, "" + es.getFirstMappedPort()); + } + + @Override + public void stop() { + es.stop(); + } + +} diff --git a/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/MariaDbResource.java b/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/MariaDbResource.java new file mode 100644 index 0000000..f152111 --- /dev/null +++ b/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/MariaDbResource.java @@ -0,0 +1,25 @@ +package org.fuin.cqrs4j.quarkus.test; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; +import org.testcontainers.containers.MariaDBContainer; + +import java.util.Map; + +import static org.fuin.cqrs4j.test.helper.TestHelper.createMariaDBContainer; + +public class MariaDbResource implements QuarkusTestResourceLifecycleManager { + + static MariaDBContainer db = createMariaDBContainer("11"); + + @Override + public Map start() { + db.start(); + return Map.of("quarkus.datasource.jdbc.url", db.getJdbcUrl()); + } + + @Override + public void stop() { + db.stop(); + } + +} diff --git a/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/QuarkusAppTest.java b/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/QuarkusAppTest.java new file mode 100644 index 0000000..51c9d5a --- /dev/null +++ b/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/QuarkusAppTest.java @@ -0,0 +1,126 @@ +package org.fuin.cqrs4j.quarkus.test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.fuin.cqrs4j.jsonb.JsonbRegistry; +import org.fuin.cqrs4j.quarkus.test.model.PersonEntity; +import org.fuin.cqrs4j.quarkus.test.model.PersonId; +import org.fuin.cqrs4j.quarkus.test.model.PersonName; +import org.fuin.esc.api.CommonEvent; +import org.fuin.esc.api.EventStore; +import org.fuin.esc.api.ExpectedVersion; +import org.fuin.esc.api.SimpleStreamId; +import org.fuin.esc.api.StreamId; +import org.fuin.objects4j.jsonb.JsonbProvider; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.function.Supplier; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.fuin.cqrs4j.quarkus.test.QuarkusTestHelper.commonEvent; +import static org.fuin.cqrs4j.quarkus.test.QuarkusTestHelper.personCreatedEvent; + +/** + * Tests the JSON-B, JAX-B and JPA adapters. + *

+ * Unfortunately Rest Assured cannot be used because it's not on "jakarta" namespace yet. + * rest-assured issue #1651 + * java.lang.NoClassDefFoundError: javax/json/bind/Jsonb + */ +@Disabled("Find out why connection to Eventstore hangs (See TODO below)...") +@QuarkusTest +@QuarkusTestResource(MariaDbResource.class) +@QuarkusTestResource(EventstoreResource.class) +class QuarkusAppTest { + + private static final Logger LOG = LoggerFactory.getLogger(QuarkusAppTest.class); + + private static final HttpClient CLIENT = HttpClient.newHttpClient(); + + @Inject + EventStore eventStore; + + @Inject + JsonbProvider jsonbProvider; + + @ConfigProperty(name = "quarkus.http.port") + Integer port; + + private String getBaseUrl() { + return "http://localhost:" + port + "/persons"; + } + + @Test + void createAndWaitForView() { + + final PersonId id = new PersonId(); + final PersonName name = new PersonName("Peter Parker"); + + // Add a created event to the aggregate stream - Should update the view + final StreamId streamId = new SimpleStreamId(PersonId.TYPE.asString() + "-" + id.asString()); + final CommonEvent commonEvent = commonEvent(personCreatedEvent(id, name)); + + LOG.info("Append event to stream: {}", jsonbProvider.jsonb().toJson(commonEvent)); + // TODO Investigate why Quarkus hangs here! + eventStore.appendToStream(streamId, ExpectedVersion.NO_OR_EMPTY_STREAM.getNo(), commonEvent); + + // Read via HTTP + final Supplier> getPerson = () -> send(newBuilder(getBaseUrl() + "/" + id).GET().build()); + + LOG.info("Waiting for GET person..."); + await().atMost(5, SECONDS).until(() -> getPerson.get().statusCode() == Response.Status.OK.getStatusCode()); + LOG.info("GET Response received..."); + + final HttpResponse read = getPerson.get(); + assertThat(read.statusCode()).isEqualTo(Response.Status.OK.getStatusCode()); + final PersonEntity copy = fromJson(read.body()); + assertThat(copy.getId()).isEqualTo(id); + assertThat(copy.getName()).isEqualTo(name); + + } + + private static HttpRequest.Builder newBuilder(final String uri) { + return HttpRequest.newBuilder() + .uri(asURI(uri)) + .headers("Content-Type", MediaType.APPLICATION_JSON + ";charset=" + StandardCharsets.UTF_8.name(), + "Accept", MediaType.APPLICATION_JSON + ";charset=" + StandardCharsets.UTF_8.name()); + } + + private PersonEntity fromJson(final String json) { + LOG.info("Received json: {}", json); + return jsonbProvider.jsonb().fromJson(json, PersonEntity.class); + } + + private static HttpResponse send(final HttpRequest request) { + try { + return CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (final IOException | InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + private static URI asURI(String uri) { + try { + return new URI(uri); + } catch (final URISyntaxException ex) { + throw new RuntimeException(ex); + } + } + +} \ No newline at end of file diff --git a/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/QuarkusFactoryTest.java b/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/QuarkusFactoryTest.java new file mode 100644 index 0000000..29ac8ec --- /dev/null +++ b/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/QuarkusFactoryTest.java @@ -0,0 +1,72 @@ +package org.fuin.cqrs4j.quarkus.test; + +import jakarta.json.bind.JsonbConfig; +import org.fuin.cqrs4j.jsonb.JsonbRegistry; +import org.fuin.cqrs4j.quarkus.test.app.QuarkusFactory; +import org.fuin.cqrs4j.quarkus.test.model.PersonCreatedEvent; +import org.fuin.cqrs4j.quarkus.test.model.PersonId; +import org.fuin.cqrs4j.quarkus.test.model.PersonName; +import org.fuin.ddd4j.core.EntityIdFactory; +import org.fuin.esc.api.CommonEvent; +import org.fuin.esc.api.SerDeserializerRegistry; +import org.fuin.esc.api.SerializedDataTypeRegistry; +import org.fuin.esc.jsonb.JsonbSerDeserializer; +import org.fuin.objects4j.jsonb.JsonbProvider; +import org.fuin.utils4j.Utils4J; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.fuin.cqrs4j.quarkus.test.QuarkusTestHelper.commonEvent; +import static org.fuin.cqrs4j.quarkus.test.QuarkusTestHelper.personCreatedEvent; + +/** + * Test for the {@link QuarkusFactory} class. + */ +public class QuarkusFactoryTest { + + @Test + public void testToJson() { + + final QuarkusFactory testee = new QuarkusFactory(); + final EntityIdFactory entityIdFactory = testee.entityIdFactory(); + final JsonbConfig jsonbConfig = testee.jsonbConfig(); + final JsonbProvider jsonbProvider = testee.jsonbProvider(jsonbConfig); + final SerializedDataTypeRegistry typeRegistry = testee.serializedDataTypeRegistry(); + final JsonbSerDeserializer jsonbSerDeserializer = testee.jsonbSerDeserializer(jsonbProvider, typeRegistry); + final SerDeserializerRegistry serDeserializerRegistry = testee.serDeserializerRegistry(jsonbConfig, jsonbProvider, entityIdFactory, typeRegistry, jsonbSerDeserializer); + // We don't need the "serDeserializerRegistry" in this test, + // but it also adds the JSON-B adapters/serializers/deserializers + assertThat(serDeserializerRegistry).isNotNull(); + + final PersonId id = new PersonId(); + final PersonName name = new PersonName("Peter Parker"); + final PersonCreatedEvent event = personCreatedEvent(id, name); + final CommonEvent commonEvent = commonEvent(event); + + final String expectedJson = Utils4J.replaceVars(""" + { + "data": { + "event-id": "${event-id}", + "event-timestamp": "${event-timestamp}", + "aggregate-version": 0, + "entity-id-path": "PERSON ${person-id}", + "id": "${person-id}", + "name": "Peter Parker" + }, + "dataType": "PersonCreatedEvent", + "id": "${event-id}" + } + """, Map.of( + "event-timestamp", event.getEventTimestamp().toString(), + "event-id", event.getEventId().toString(), + "person-id", event.getId().toString() + )); + final String actualJson = jsonbProvider.jsonb().toJson(commonEvent); + assertThatJson(actualJson).isEqualTo(expectedJson); + + } + +} diff --git a/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/QuarkusTestHelper.java b/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/QuarkusTestHelper.java new file mode 100644 index 0000000..7dc14cd --- /dev/null +++ b/test/quarkus/src/test/java/org/fuin/cqrs4j/quarkus/test/QuarkusTestHelper.java @@ -0,0 +1,53 @@ +package org.fuin.cqrs4j.quarkus.test; + +import org.fuin.cqrs4j.quarkus.test.model.PersonCreatedEvent; +import org.fuin.cqrs4j.quarkus.test.model.PersonId; +import org.fuin.cqrs4j.quarkus.test.model.PersonName; +import org.fuin.ddd4j.core.AggregateVersion; +import org.fuin.esc.api.CommonEvent; +import org.fuin.esc.api.SimpleCommonEvent; +import org.fuin.esc.api.TypeName; + +import java.time.ZonedDateTime; + +/** + * Helper functions for the test modules. + */ +public final class QuarkusTestHelper { + + private QuarkusTestHelper() { + throw new UnsupportedOperationException("Cannot instantiate utility class"); + } + + /** + * Creates a {@link PersonCreatedEvent}. + * + * @param id Unique person identifier. + * @param name Name of the person. + * @return Event to store. + */ + public static PersonCreatedEvent personCreatedEvent(PersonId id, PersonName name) { + return PersonCreatedEvent.builder() + .aggregateVersion(AggregateVersion.valueOf(0)) + .entityIdPath(id) + .eventId(new org.fuin.ddd4j.core.EventId()) + .id(id) + .name(name) + .timestamp(ZonedDateTime.now()) + .build(); + } + + /** + * Creates a {@link CommonEvent} with a {@link PersonCreatedEvent} inside. + * + * @param event Event. + * @return Event to store. + */ + public static CommonEvent commonEvent(PersonCreatedEvent event) { + return new SimpleCommonEvent( + new org.fuin.esc.api.EventId(event.getEventId().asString()), + new TypeName(PersonCreatedEvent.TYPE.asBaseType()), + event); + } + +} diff --git a/test/quarkus/src/test/resources/application-test.properties b/test/quarkus/src/test/resources/application-test.properties new file mode 100644 index 0000000..9537dee --- /dev/null +++ b/test/quarkus/src/test/resources/application-test.properties @@ -0,0 +1 @@ +quarkus.http.port=0 diff --git a/test/springboot/pom.xml b/test/springboot/pom.xml new file mode 100644 index 0000000..6e10442 --- /dev/null +++ b/test/springboot/pom.xml @@ -0,0 +1,275 @@ + + + 4.0.0 + + + org.fuin.cqrs4j + cqrs-4-java-test + 0.6.0-SNAPSHOT + ../pom.xml + + + cqrs-4-java-test-springboot + jar + ${description} (TEST-SPRINGBOOT) + + + 3.4.4 + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring.boot.version} + pom + import + + + + + + + + + + org.fuin.cqrs4j + cqrs-4-java-core + + + + org.fuin.cqrs4j + cqrs-4-java-springboot + + + + org.fuin.cqrs4j + cqrs-4-java-jackson + + + + org.fuin.esc + esc-client + + + + org.fuin.esc + esc-jackson + + + + org.fuin.esc + esc-esgrpc + + + + org.fuin.objects4j + objects4j-common + + + + org.fuin.objects4j + objects4j-core + + + + org.fuin.objects4j + objects4j-jackson + + + + org.fuin.objects4j + objects4j-jpa + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.mariadb.jdbc + mariadb-java-client + + + + io.kurrent + kurrentdb-client + + + + org.fuin.cqrs4j + cqrs-4-java-test-helper + + + + org.fuin + utils4j + + + + org.eclipse + yasson + + + + org.hibernate.validator + hibernate-validator + + + + org.fuin.ddd4j + ddd-4-java-codegen-api + + + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.springframework.boot + spring-boot-testcontainers + test + + + + org.testcontainers + junit-jupiter + test + + + + io.rest-assured + rest-assured + test + + + + org.assertj + assertj-core + test + + + + org.awaitility + awaitility + test + + + + org.testcontainers + mariadb + test + + + + + + + + + + org.codehaus.mojo + build-helper-maven-plugin + 3.6.0 + + + add-source + generate-sources + + add-source + + + + ${project.basedir}/src-gen/main/java + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${project.basedir}/src-gen/main/java + + + org.fuin.ddd4j + ddd-4-java-codegen-processor + ${ddd4j.version} + + + + + + + org.apache.maven.plugins + maven-deploy-plugin + 3.1.1 + + true + + + + + org.jacoco + jacoco-maven-plugin + + + none + + + + + + org.springframework.boot + spring-boot-maven-plugin + 3.2.2 + + + + org.apache.maven.plugins + maven-surefire-plugin + + + + org.apache.maven.plugins + maven-failsafe-plugin + 3.2.5 + + + + integration-test + verify + + + + + + + org.apache.maven.plugins + maven-dependency-plugin + + true + + + + + + + + diff --git a/test/springboot/src-gen/main/java/org/fuin/cqrs4j/springboot/test/model/PersonCreatedEvent.java b/test/springboot/src-gen/main/java/org/fuin/cqrs4j/springboot/test/model/PersonCreatedEvent.java new file mode 100644 index 0000000..b1b4d39 --- /dev/null +++ b/test/springboot/src-gen/main/java/org/fuin/cqrs4j/springboot/test/model/PersonCreatedEvent.java @@ -0,0 +1,153 @@ +package org.fuin.cqrs4j.springboot.test.model; + +import jakarta.validation.constraints.NotNull; +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import javax.annotation.Nullable; +import org.fuin.ddd4j.core.EntityIdPath; +import org.fuin.ddd4j.core.EventType; +import org.fuin.ddd4j.jackson.AbstractDomainEvent; +import org.fuin.esc.api.HasSerializedDataTypeConstant; +import org.fuin.esc.api.SerializedDataType; +import org.fuin.objects4j.common.Contract; + +import java.io.Serial; + +import javax.annotation.concurrent.Immutable; + + +/** + * A person was created. + */ +@Immutable +@HasSerializedDataTypeConstant +public final class PersonCreatedEvent extends AbstractDomainEvent { + + @Serial + private static final long serialVersionUID = 1000L; + + /** Unique name of the event used to store it - Should never change. */ + public static final EventType TYPE = new EventType(PersonCreatedEvent.class.getSimpleName()); + + /** Unique name of the serialized event. */ + public static final SerializedDataType SER_TYPE = new SerializedDataType(TYPE.asBaseType()); + + @JsonProperty("id") + private PersonId id; + + @JsonProperty("name") + private PersonName name; + + + /** + * Protected default constructor for deserialization. + */ + protected PersonCreatedEvent() { // NOSONAR Default constructor + super(); + } + + /** + * Constructor with event data. + * + * @param id TODO Add '@Label' annotation. TODO Add '@Tooltip' annotation. + * @param name TODO Add '@Label' annotation. TODO Add '@Tooltip' annotation. + */ + protected PersonCreatedEvent( + final PersonId id, + final PersonName name + ) { + super(new EntityIdPath(id)); + this.id = id; + this.name = name; + } + + @Override + @JsonIgnore + public EventType getEventType() { + return TYPE; + } + + /** + * Returns: TODO Add '@Label' annotation. + * + * @return TODO Add '@Label' annotation. TODO Add '@Tooltip' annotation. + */ + public PersonId getId() { + return id; + } + /** + * Returns: TODO Add '@Label' annotation. + * + * @return TODO Add '@Label' annotation. TODO Add '@Tooltip' annotation. + */ + public PersonName getName() { + return name; + } + + @Override + public String toString() { + return "MyEvent happened"; + } + + /** + * Creates a new builder instance. + * + * @return New builder instance. + */ + public static Builder builder() { + return new Builder(); + } + + /** + * Builds an instance of the outer class. + */ + public static final class Builder extends AbstractDomainEvent.Builder { + + private PersonCreatedEvent delegate; + + private Builder() { + super(new PersonCreatedEvent()); + delegate = delegate(); + } + + /** + * Sets: TODO Add '@Label' annotation. + * + * @param id TODO Add '@Label' annotation. + * @return This builder. + */ + @SuppressWarnings("unchecked") + public final Builder id(@NotNull final PersonId id) { + Contract.requireArgNotNull("id", id); + delegate.id = id; + return this; + } + /** + * Sets: TODO Add '@Label' annotation. + * + * @param name TODO Add '@Label' annotation. + * @return This builder. + */ + @SuppressWarnings("unchecked") + public final Builder name(@NotNull final PersonName name) { + Contract.requireArgNotNull("name", name); + delegate.name = name; + return this; + } + + /** + * Creates the event and clears the builder. + * + * @return New instance. + */ + public PersonCreatedEvent build() { + ensureBuildableAbstractDomainEvent(); + final PersonCreatedEvent result = delegate; + delegate = new PersonCreatedEvent(); + resetAbstractDomainEvent(delegate); + return result; + } + + } + +} diff --git a/test/springboot/src-gen/main/java/org/fuin/cqrs4j/springboot/test/model/PersonId.java b/test/springboot/src-gen/main/java/org/fuin/cqrs4j/springboot/test/model/PersonId.java new file mode 100644 index 0000000..4b6f2d4 --- /dev/null +++ b/test/springboot/src-gen/main/java/org/fuin/cqrs4j/springboot/test/model/PersonId.java @@ -0,0 +1,188 @@ +package org.fuin.cqrs4j.springboot.test.model; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.UUID; + +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.persistence.AttributeConverter; + +import jakarta.annotation.Generated; +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; +import jakarta.validation.constraints.NotNull; + +import org.fuin.ddd4j.core.AggregateRootUuid; +import org.fuin.ddd4j.core.EntityType; +import org.fuin.ddd4j.core.HasEntityTypeConstant; +import org.fuin.ddd4j.core.StringBasedEntityType; +import org.fuin.objects4j.common.ConstraintViolationException; +import org.fuin.objects4j.common.HasPublicStaticValueOfMethod; + +import javax.annotation.concurrent.Immutable; + +/** + * Unique identifier of a person. + */ +@Generated("Generated class - Manual changes will be overwritten") +@Immutable +@HasPublicStaticValueOfMethod +@HasEntityTypeConstant + +public final class PersonId extends AggregateRootUuid { + + private static final long serialVersionUID = 1000L; + + /** Unique name of the aggregate this identifier refers to. */ + public static final EntityType TYPE = new StringBasedEntityType("PERSON"); + + /** + * Default constructor that generates a random UUID. + */ + public PersonId() { + super(TYPE); + } + + /** + * Constructor with mandatory data. + * + * @param value + * Value. + */ + public PersonId(final UUID value) { + super(TYPE, value); + } + + /** + * Parses a given string and returns a new instance of this type. + * + * @param value + * String with valid UUID to convert. A null value returns null. + * + * @return Converted value. + */ + public static PersonId valueOf(final String value) { + if (value == null) { + return null; + } + requireArgValid("value", value); + return new PersonId(UUID.fromString(value)); + } + + /** + * Verifies that a given string can be converted into the type. + * + * @param value + * Value to validate. + * + * @return Returns true if it's a valid type else false. + */ + public static boolean isValid(final String value) { + if (value == null) { + return true; + } + return AggregateRootUuid.isValid(value); + } + + /** + * Verifies if the argument is valid and throws an exception if this is not the case. + * + * @param name + * Name of the value for a possible error message. + * @param value + * Value to check. + * + * @throws ConstraintViolationException + * The value was not valid. + */ + public static void requireArgValid(@NotNull final String name, @NotNull final String value) throws ConstraintViolationException { + if (!isValid(value)) { + throw new ConstraintViolationException("The argument '" + name + "' is not valid: '" + value + "'"); + } + } + + /** + * Ensures that the string can be converted into the type. + */ + @Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Constraint(validatedBy = { Validator.class }) + @Documented + public static @interface PersonIdStr { + + String message() + + default "{org.fuin.cqrs4j.springboot.test.model.PersonId.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + } + + /** + * Validates if a string is compliant with the type. + */ + public static final class Validator implements ConstraintValidator { + + @Override + public final void initialize(final PersonIdStr annotation) { + // Not used + } + + @Override + public final boolean isValid(final String value, final ConstraintValidatorContext context) { + return PersonId.isValid(value); + } + + } + + /** + * Converts the value object from/to string. + */ + public static final class Converter implements JsonbAdapter, AttributeConverter { + + private PersonId toVO(final UUID value) { + if (value == null) { + return null; + } + return new PersonId(value); + } + + private UUID fromVO(final PersonId value) { + if (value == null) { + return null; + } + return value.asBaseType(); + } + // JSONB Adapter + + @Override + public final UUID adaptToJson(final PersonId obj) throws Exception { + return fromVO(obj); + } + + @Override + public final PersonId adaptFromJson(final UUID str) throws Exception { + return toVO(str); + } + + // JPA + + @Override + public final UUID convertToDatabaseColumn(final PersonId value) { + return fromVO(value); + } + + @Override + public final PersonId convertToEntityAttribute(final UUID value) { + return toVO(value); + } + } + +} diff --git a/test/springboot/src-gen/main/java/org/fuin/cqrs4j/springboot/test/model/PersonName.java b/test/springboot/src-gen/main/java/org/fuin/cqrs4j/springboot/test/model/PersonName.java new file mode 100644 index 0000000..4b7712e --- /dev/null +++ b/test/springboot/src-gen/main/java/org/fuin/cqrs4j/springboot/test/model/PersonName.java @@ -0,0 +1,239 @@ +package org.fuin.cqrs4j.springboot.test.model; + +import java.io.Serializable; +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.util.Objects; + +import jakarta.json.bind.adapter.JsonbAdapter; +import jakarta.persistence.AttributeConverter; +import jakarta.annotation.Generated; +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; +import jakarta.validation.constraints.NotNull; + +import org.fuin.objects4j.common.ConstraintViolationException; +import org.fuin.objects4j.common.AsStringCapable; +import org.fuin.objects4j.common.ValueObjectWithBaseType; + +import javax.annotation.concurrent.Immutable; + +/** + * The name of the person. + * + * CAUTION: Instances of this type may contain invalid values by deserializing it. + * This means if you create it from JSON, XML or database (JPA) it may not have a correct length or pattern. + */ +@Generated("Generated class - Manual changes will be overwritten") +@Immutable + +public final class PersonName implements ValueObjectWithBaseType, Comparable, Serializable, AsStringCapable { + + private static final long serialVersionUID = 1L; + + + /** Minimal length of a valid value. */ + public static final int MIN_LENGTH = 1; + + /** Maximum length of a valid value. */ + public static final int MAX_LENGTH = 200; + + @NotNull + @PersonNameStr + private String value; + + /** + * Protected default constructor for deserialization. + */ + protected PersonName() { + super(); + } + + /** + * Constructor with mandatory data. + * + * @param value + * Value. + */ + public PersonName(final String value) { + this(value, true); + } + + private PersonName(final String value, final boolean strict) { + super(); + if (strict) { + PersonName.requireArgValid("value", value); + } + this.value = value; + } + + @Override + public final String asBaseType() { + return value; + } + + @Override + public final String toString() { + return value; + } + + @Override + public final String asString() { + return value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + final PersonName other = (PersonName) obj; + return Objects.equals(value, other.value); + } + + @Override + public final int compareTo(final PersonName other) { + return value.compareTo(other.value); + } + + @Override + @NotNull + public final Class getBaseType() { + return String.class; + } + + /** + * Verifies that a given string can be converted into the type. + * + * @param value + * Value to validate. + * + * @return Returns true if it's a valid type else false. + */ + public static boolean isValid(final String value) { + if (value == null) { + return true; + } + if (value.length() < MIN_LENGTH) { + return false; + } + final String trimmed = value.trim(); + if (trimmed.length() > MAX_LENGTH) { + return false; + } + return true; + } + + /** + * Verifies if the argument is valid and throws an exception if this is not the case. + * + * @param name + * Name of the value for a possible error message. + * @param value + * Value to check. + * + * @throws ConstraintViolationException + * The value was not valid. + */ + public static void requireArgValid(@NotNull final String name, @NotNull final String value) throws ConstraintViolationException { + if (!isValid(value)) { + throw new ConstraintViolationException("The argument '" + name + "' is not valid: '" + value + "'"); + } + } + + /** + * Ensures that the string can be converted into the type. + */ + @Target({ ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE }) + @Retention(RetentionPolicy.RUNTIME) + @Constraint(validatedBy = { Validator.class }) + @Documented + public static @interface PersonNameStr { + + String message() + + default "{org.fuin.cqrs4j.springboot.test.model.PersonName.message}"; + + Class[] groups() default {}; + + Class[] payload() default {}; + + } + + /** + * Validates if a string is compliant with the type. + */ + public static final class Validator implements ConstraintValidator { + + @Override + public final void initialize(final PersonNameStr annotation) { + // Not used + } + + @Override + public final boolean isValid(final String value, final ConstraintValidatorContext context) { + return PersonName.isValid(value); + } + + } + + /** + * Converts the value object from/to string. + */ + public static final class Converter implements JsonbAdapter, AttributeConverter { + + private PersonName toVO(final String value) { + if (value == null) { + return null; + } + return new PersonName(value, false); + } + + private String fromVO(final PersonName value) { + if (value == null) { + return null; + } + return value.asBaseType(); + } + // JSONB Adapter + + @Override + public final String adaptToJson(final PersonName obj) throws Exception { + return fromVO(obj); + } + + @Override + public final PersonName adaptFromJson(final String str) throws Exception { + return toVO(str); + } + + // JPA + + @Override + public final String convertToDatabaseColumn(final PersonName value) { + return fromVO(value); + } + + @Override + public final PersonName convertToEntityAttribute(final String value) { + return toVO(value); + } + } + +} diff --git a/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/app/PersonResource.java b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/app/PersonResource.java new file mode 100644 index 0000000..154425d --- /dev/null +++ b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/app/PersonResource.java @@ -0,0 +1,46 @@ +package org.fuin.cqrs4j.springboot.test.app; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.transaction.Transactional; +import org.fuin.cqrs4j.springboot.test.model.PersonEntity; +import org.fuin.cqrs4j.springboot.test.model.PersonId; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * REST resource providing the persons. + */ +@RestController +@Transactional +@RequestMapping(value = "/persons", + consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE}, + produces = {MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE} +) +public class PersonResource { + + private static final Logger LOG = LoggerFactory.getLogger(PersonResource.class); + + @PersistenceContext + EntityManager em; + + @GetMapping("/{id}") + public ResponseEntity read(@PathVariable("id") PersonId id) { + LOG.info("read({}) / em={}", id, em); + final PersonEntity person = em.find(PersonEntity.class, id.asBaseType()); + if (person == null) { + LOG.info("result: NOT_FOUND"); + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + LOG.info("result: {}", person); + return new ResponseEntity<>(person, HttpStatus.OK); + } + +} diff --git a/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/app/SpringBootApp.java b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/app/SpringBootApp.java new file mode 100644 index 0000000..e2905c8 --- /dev/null +++ b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/app/SpringBootApp.java @@ -0,0 +1,36 @@ +package org.fuin.cqrs4j.springboot.test.app; + +import org.fuin.cqrs4j.springboot.base.EventstoreConfig; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * Represents the (custom) entry point, most likely used to the Quarkus application in the IDE. + */ +@SpringBootApplication(scanBasePackages = { + "org.fuin.cqrs4j.springboot.view", + "org.fuin.cqrs4j.springboot.test.view", + "org.fuin.cqrs4j.springboot.test.app" +}) +@EnableConfigurationProperties(EventstoreConfig.class) +@EntityScan(basePackages = { + "org.fuin.cqrs4j.springboot.view", + "org.fuin.cqrs4j.springboot.test.view", + "org.fuin.cqrs4j.springboot.test.model" +}) +@EnableScheduling +public class SpringBootApp { + + /** + * Main method to start the app. + * + * @param args Arguments from the command line. + */ + public static void main(String[] args) { + SpringApplication.run(SpringBootApp.class, args); + } + +} \ No newline at end of file diff --git a/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/app/SpringBootConfig.java b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/app/SpringBootConfig.java new file mode 100644 index 0000000..50fe4a4 --- /dev/null +++ b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/app/SpringBootConfig.java @@ -0,0 +1,179 @@ +package org.fuin.cqrs4j.springboot.test.app; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import io.kurrent.dbclient.KurrentDBClient; +import io.kurrent.dbclient.KurrentDBClientSettings; +import io.kurrent.dbclient.KurrentDBProjectionManagementClient; +import org.fuin.cqrs4j.jackson.Cqrs4JacksonModule; +import org.fuin.cqrs4j.springboot.base.EventstoreConfig; +import org.fuin.cqrs4j.springboot.test.view.PersonsView; +import org.fuin.ddd4j.core.EntityIdFactory; +import org.fuin.ddd4j.core.JandexEntityIdFactory; +import org.fuin.ddd4j.jackson.Ddd4JacksonModule; +import org.fuin.esc.api.EnhancedMimeType; +import org.fuin.esc.api.ProjectionAdminEventStore; +import org.fuin.esc.api.SerDeserializerRegistry; +import org.fuin.esc.api.SerializedDataTypeRegistry; +import org.fuin.esc.api.SimpleSerializerDeserializerRegistry; +import org.fuin.esc.client.JandexSerializedDataTypeRegistry; +import org.fuin.esc.esgrpc.ESGrpcEventStore; +import org.fuin.esc.esgrpc.GrpcProjectionAdminEventStore; +import org.fuin.esc.esgrpc.IESGrpcEventStore; +import org.fuin.esc.jackson.BaseTypeFactory; +import org.fuin.esc.jackson.EscJacksonModule; +import org.fuin.esc.jackson.EscJacksonUtils; +import org.fuin.esc.jackson.JacksonSerDeserializer; +import org.fuin.objects4j.jackson.ImmutableObjectMapper; +import org.fuin.objects4j.jackson.Objects4JJacksonModule; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.net.Authenticator; +import java.net.PasswordAuthentication; +import java.net.http.HttpClient; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.temporal.ChronoUnit; + +@Configuration +public class SpringBootConfig { + + private static final Logger LOG = LoggerFactory.getLogger(SpringBootConfig.class); + + @Bean + public PersonsView personsView() { + return new PersonsView(); + } + + @Bean + public EntityIdFactory entityIdFactory() { + return new JandexEntityIdFactory(); + } + + @Bean + public ImmutableObjectMapper.Builder immutableObjectMapperBuilder( + EntityIdFactory entityIdFactory) { + return new ImmutableObjectMapper.Builder(new ObjectMapper() + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + .enable(SerializationFeature.INDENT_OUTPUT) + .setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY) + .registerModule(new Cqrs4JacksonModule()) + .registerModule(new Objects4JJacksonModule()) + .registerModule(new Ddd4JacksonModule(entityIdFactory)) + .registerModule(new TestModelJacksonModule(entityIdFactory)) + ); + } + + @Bean + public ImmutableObjectMapper.Provider immutableObjectMapperProvider( + ImmutableObjectMapper.Builder mapperBuilder) { + return new ImmutableObjectMapper.Provider(mapperBuilder); + } + + @Bean + public JacksonSerDeserializer jacksonSerDeserializer( + final ImmutableObjectMapper.Provider mapperProvider, + final SerializedDataTypeRegistry typeRegistry) { + return new JacksonSerDeserializer.Builder() + .withObjectMapper(mapperProvider) + .withTypeRegistry(typeRegistry) + .withEncoding(StandardCharsets.UTF_8) + .build(); + } + + + @Bean + public SerializedDataTypeRegistry serializedDataTypeRegistry() { + return new JandexSerializedDataTypeRegistry(); + } + + @Bean + public SerDeserializerRegistry serDeserializerRegistry(SerializedDataTypeRegistry typeRegistry, + JacksonSerDeserializer jacksonSerDeserializer, + ImmutableObjectMapper.Builder mapperBuilder) { + + final SimpleSerializerDeserializerRegistry.Builder builder = new SimpleSerializerDeserializerRegistry.Builder(EscJacksonUtils.MIME_TYPE); + for (final SerializedDataTypeRegistry.TypeClass tc : typeRegistry.findAll()) { + builder.add(tc.type(), jacksonSerDeserializer); + LOG.info("Registered type '{}' with serializer: {}", tc.type().asBaseType(), jacksonSerDeserializer.getClass().getSimpleName()); + } + final SerDeserializerRegistry registry = builder.build(); + mapperBuilder.registerModule(new EscJacksonModule(registry, registry)); + return registry; + } + + @Bean(destroyMethod = "shutdown") + public KurrentDBClient createKurrentDBClient(final EventstoreConfig config) { + final KurrentDBClientSettings settings = KurrentDBClientSettings.builder() + .addHost(config.getHost(), config.getPort()) + .defaultCredentials("admin", "changeit") // Just for test + .tls(config.isTls()) + .buildConnectionSettings(); + return KurrentDBClient.create(settings); + } + + @Bean + public KurrentDBProjectionManagementClient createKurrentDBProjectionManagementClient(final EventstoreConfig config) { + final KurrentDBClientSettings settings = KurrentDBClientSettings.builder() + .addHost(config.getHost(), config.getPort()) + .defaultCredentials("admin", "changeit") // Just for test + .tls(config.isTls()) + .buildConnectionSettings(); + return KurrentDBProjectionManagementClient.create(settings); + } + + + /** + * Creates an event store connection. + * + * @param client Client to use. + * @return New event store instance. + */ + @SuppressWarnings("java:S2095") // Spring will correctly close it by calling "close()" on instance + @Bean(destroyMethod = "close") + public IESGrpcEventStore getESGrpcEventStore(final SerDeserializerRegistry registry, + final KurrentDBClient client) { + return new ESGrpcEventStore.Builder() + .eventStore(client) + .serDesRegistry(registry) + .baseTypeFactory(new BaseTypeFactory()) + .targetContentType(EnhancedMimeType.create("application", "json", StandardCharsets.UTF_8)) + .build() + .open(); + } + + /** + * Creates an GRPC based projection admin event store. + * + * @param client Client to use. + * @return New event store instance. + */ + @SuppressWarnings("java:S2095") // Spring will correctly close it by calling "close()" on instance + @Bean(destroyMethod = "close") + public ProjectionAdminEventStore getProjectionAdminEventStore(final KurrentDBProjectionManagementClient client) { + return new GrpcProjectionAdminEventStore(client).open(); + } + + @Bean + public HttpClient getHttpClient(final EventstoreConfig config) { + return HttpClient.newBuilder() + .authenticator(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + // Just for test + return new PasswordAuthentication( + "admin", + "changeit".toCharArray()); + } + }) + .connectTimeout(Duration.of(10, ChronoUnit.SECONDS)) + .build(); + } + +} diff --git a/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/app/TestModelJacksonModule.java b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/app/TestModelJacksonModule.java new file mode 100644 index 0000000..16b8af4 --- /dev/null +++ b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/app/TestModelJacksonModule.java @@ -0,0 +1,52 @@ +package org.fuin.cqrs4j.springboot.test.app; + +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.module.SimpleDeserializers; +import com.fasterxml.jackson.databind.module.SimpleSerializers; +import org.fuin.cqrs4j.springboot.test.model.PersonId; +import org.fuin.cqrs4j.springboot.test.model.PersonName; +import org.fuin.ddd4j.core.EntityIdFactory; +import org.fuin.ddd4j.jackson.EntityIdJacksonDeserializer; +import org.fuin.ddd4j.jackson.EntityIdJacksonSerializer; +import org.fuin.objects4j.jackson.ValueObjectStringJacksonDeserializer; +import org.fuin.objects4j.jackson.ValueObjectStringJacksonSerializer; + +import java.util.Objects; + +/** + * Jackson module that has the test classes. + */ +public final class TestModelJacksonModule extends Module { + + private final EntityIdFactory entityIdFactory; + + public TestModelJacksonModule(EntityIdFactory entityIdFactory) { + this.entityIdFactory = Objects.requireNonNull(entityIdFactory, "entityIdFactory==null"); + } + + public String getModuleName() { + return "Cqrs4JavaTest"; + } + + @Override + public void setupModule(Module.SetupContext context) { + + final SimpleSerializers serializers = new SimpleSerializers(); + serializers.addSerializer(new EntityIdJacksonSerializer<>(PersonId.class)); + serializers.addSerializer(PersonName.class, new ValueObjectStringJacksonSerializer<>(PersonName.class)); + context.addSerializers(serializers); + + final SimpleDeserializers deserializers = new SimpleDeserializers(); + deserializers.addDeserializer(PersonId.class, new EntityIdJacksonDeserializer<>(PersonId.class, entityIdFactory)); + deserializers.addDeserializer(PersonName.class, new ValueObjectStringJacksonDeserializer<>(PersonName.class, PersonName::new)); + context.addDeserializers(deserializers); + } + + public Version version() { + return new Version(0, 6, 0, "SNAPSHOT", + "org.fuin.cqrs4j", "cqrs-4-java-test" + ); + } + +} diff --git a/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/model/GEN_PersonCreatedEvent.java b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/model/GEN_PersonCreatedEvent.java new file mode 100644 index 0000000..818160d --- /dev/null +++ b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/model/GEN_PersonCreatedEvent.java @@ -0,0 +1,26 @@ +package org.fuin.cqrs4j.springboot.test.model; + +import org.fuin.cqrs4j.springboot.test.model.PersonId; +import org.fuin.cqrs4j.springboot.test.model.PersonName; +import org.fuin.ddd4j.codegen.api.EventVO; + +/** + * Do not use this class. It's just for generating some code using APT. + */ +@EventVO(pkg="org.fuin.cqrs4j.springboot.test.model", + name = "PersonCreatedEvent", + entityIdPathParams = "id", + description = "A person was created", + jackson = true, + serialVersionUID = 1000L, + entityIdClass = "PersonId", + message = "MyEvent happened" +) +@SuppressWarnings("java:S1214") // Just a helper to generate code +public interface GEN_PersonCreatedEvent { + + PersonId id = null; + + PersonName name = null; + +} diff --git a/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/model/GEN_PersonId.java b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/model/GEN_PersonId.java new file mode 100644 index 0000000..6a04a19 --- /dev/null +++ b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/model/GEN_PersonId.java @@ -0,0 +1,18 @@ +package org.fuin.cqrs4j.springboot.test.model; + +import org.fuin.ddd4j.codegen.api.AggregateRootUuidVO; + +/** + * Do not use this class. It's just for generating some code using APT. + */ +@AggregateRootUuidVO( + pkg="org.fuin.cqrs4j.springboot.test.model", + name = "PersonId", + entityType = "PERSON", + description = "Unique identifier of a person", + jsonb = true, jpa = true, + serialVersionUID = 1000L, + example = "b20d7373-1950-478a-ab61-d022cd44f507" +) +public interface GEN_PersonId { +} diff --git a/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/model/GEN_PersonName.java b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/model/GEN_PersonName.java new file mode 100644 index 0000000..b70382f --- /dev/null +++ b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/model/GEN_PersonName.java @@ -0,0 +1,17 @@ +package org.fuin.cqrs4j.springboot.test.model; + +import org.fuin.ddd4j.codegen.api.StringVO; + +/** + * Do not use this class. It's just for generating some code using APT. + */ +@StringVO( + pkg="org.fuin.cqrs4j.springboot.test.model", + name = "PersonName", + description = "The name of the person", + jsonb = true, jpa = true, + minLength = 1, maxLength = 200 +) +public interface GEN_PersonName { + +} diff --git a/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/model/PersonEntity.java b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/model/PersonEntity.java new file mode 100644 index 0000000..3b6ddf7 --- /dev/null +++ b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/model/PersonEntity.java @@ -0,0 +1,87 @@ +package org.fuin.cqrs4j.springboot.test.model; + +import com.fasterxml.jackson.annotation.JsonProperty; +import jakarta.json.bind.annotation.JsonbProperty; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import java.util.Objects; +import java.util.UUID; + +/** + * Represents a person. Equals/hashCode are based on the ID only and the entity is sorted by the name. + */ +@Entity(name = "PERSON") +public class PersonEntity implements Comparable { + + @Id + @Column(name = "ID", unique = true, nullable = false) + @JsonProperty + public UUID id; + + @Column(name = "NAME", nullable = false, length = PersonName.MAX_LENGTH) + @JsonbProperty + public String name; + + /** + * Constructor used by JPA & JSON-B. + */ + protected PersonEntity() { + } + + /** + * Constructor with mandatory data. + * + * @param id Unique identifier of the person. + * @param name Name of the person. + */ + public PersonEntity(PersonId id, PersonName name) { + this.id = Objects.requireNonNull(id, "id==null").asBaseType(); + this.name = Objects.requireNonNull(name, "name==null").asBaseType(); + } + + /** + * Returns the unique identifier of the person. + * + * @return Person ID. + */ + public UUID getId() { + return id; + } + + /** + * Returns the name of the person. + * + * @return Person name. + */ + public String getName() { + return name; + } + + @Override + public int compareTo(PersonEntity o) { + return this.name.compareTo(o.name); + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + PersonEntity that = (PersonEntity) o; + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return Objects.hashCode(id); + } + + @Override + public String toString() { + return "PersonEntity{" + + "id=" + id + + ", name=" + name + + '}'; + } + +} diff --git a/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/view/PersonsView.java b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/view/PersonsView.java new file mode 100644 index 0000000..9ddbb06 --- /dev/null +++ b/test/springboot/src/main/java/org/fuin/cqrs4j/springboot/test/view/PersonsView.java @@ -0,0 +1,56 @@ +package org.fuin.cqrs4j.springboot.test.view; + +import jakarta.persistence.EntityManager; +import org.fuin.cqrs4j.core.JpaView; +import org.fuin.cqrs4j.springboot.test.model.PersonCreatedEvent; +import org.fuin.cqrs4j.springboot.test.model.PersonEntity; +import org.fuin.ddd4j.core.Event; +import org.fuin.ddd4j.core.EventType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Set; + +public class PersonsView implements JpaView { + + private static final Logger LOG = LoggerFactory.getLogger(PersonsView.class); + + @Override + public String getName() { + return "persons-view"; + } + + @Override + public Set getEventTypes() { + return Set.of(PersonCreatedEvent.TYPE); + } + + @Override + public String getCron() { + // Every second + return "* * * * * *"; + } + + @Override + public void handleEvents(final EntityManager em, final List events) { + for (final Event event : events) { + if (event instanceof PersonCreatedEvent ev) { + handlePersonCreatedEvent(em, ev); + } else { + throw new IllegalStateException("Cannot handle event: " + event); + } + } + } + + private void handlePersonCreatedEvent(final EntityManager em, final PersonCreatedEvent event) { + LOG.info("Handle {}: {}", event.getClass().getSimpleName(), event); + final PersonEntity entity = em.find(PersonEntity.class, event.getId().asBaseType()); + if (entity == null) { + em.persist(new PersonEntity(event.getId(), event.getName())); + } else { + LOG.info("Ignored {}} because entity already exists: {}", event.getClass().getSimpleName(), event); + } + } + +} diff --git a/test/springboot/src/main/resources/application.properties b/test/springboot/src/main/resources/application.properties new file mode 100644 index 0000000..4efbb0d --- /dev/null +++ b/test/springboot/src/main/resources/application.properties @@ -0,0 +1,9 @@ +spring.datasource.url=jdbc:mariadb://localhost:3306/testdb +spring.datasource.username=mary +spring.datasource.password=abc +spring.datasource.driver-class-name=org.mariadb.jdbc.Driver +spring.jpa.hibernate.ddl-auto=create + +logging.level.root=info + + diff --git a/test/springboot/src/test/java/org/fuin/cqrs4j/springboot/test/app/SpringBootAppTest.java b/test/springboot/src/test/java/org/fuin/cqrs4j/springboot/test/app/SpringBootAppTest.java new file mode 100644 index 0000000..420f724 --- /dev/null +++ b/test/springboot/src/test/java/org/fuin/cqrs4j/springboot/test/app/SpringBootAppTest.java @@ -0,0 +1,176 @@ +package org.fuin.cqrs4j.springboot.test.app; + +import com.fasterxml.jackson.core.JsonProcessingException; +import org.fuin.cqrs4j.springboot.base.EventstoreConfig; +import org.fuin.cqrs4j.springboot.test.model.PersonEntity; +import org.fuin.cqrs4j.springboot.test.model.PersonId; +import org.fuin.cqrs4j.springboot.test.model.PersonName; +import org.fuin.esc.api.CommonEvent; +import org.fuin.esc.api.EventStore; +import org.fuin.esc.api.ExpectedVersion; +import org.fuin.esc.api.SimpleStreamId; +import org.fuin.esc.api.StreamId; +import org.fuin.objects4j.jackson.ImmutableObjectMapper; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.web.server.LocalServerPort; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.MariaDBContainer; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.util.function.Supplier; + +import static java.util.concurrent.TimeUnit.SECONDS; +import static org.assertj.core.api.Assertions.assertThat; +import static org.awaitility.Awaitility.await; +import static org.fuin.cqrs4j.springboot.test.app.SpringBootTestHelper.createPersonCreatedEvent; +import static org.fuin.cqrs4j.test.helper.TestHelper.createEventstoreContainer; +import static org.fuin.cqrs4j.test.helper.TestHelper.createMariaDBContainer; + +/** + * Tests the JSON-B, JAX-B and JPA adapters. + *

+ * Unfortunately Rest Assured cannot be used because it's not on "jakarta" namespace yet. + * rest-assured issue #1651 + * java.lang.NoClassDefFoundError: javax/json/bind/Jsonb + */ +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@Testcontainers +class SpringBootAppTest { + + private static final Logger LOG = LoggerFactory.getLogger(SpringBootAppTest.class); + + static GenericContainer es = createEventstoreContainer("24.10"); + + static MariaDBContainer db = createMariaDBContainer("11"); + + @DynamicPropertySource + static void elasticProperties(DynamicPropertyRegistry registry) { + LOG.info("Eventstore Port: {}", es.getFirstMappedPort()); + registry.add(EventstoreConfig.KEY_PORT, () -> "" + es.getFirstMappedPort()); + LOG.info("Database JDBC Url: {}", db.getJdbcUrl()); + registry.add("spring.datasource.url", () -> db.getJdbcUrl()); + } + + private static final HttpClient CLIENT = HttpClient.newHttpClient(); + + @BeforeAll + static void startContainers() { + es.start(); + db.start(); + } + + @AfterAll + static void stopContainers() { + try { + es.stop(); + } catch (final RuntimeException ex) { + LOG.error("Failed to stop eventstore", ex); + } + try { + db.stop(); + } catch (final RuntimeException ex) { + LOG.error("Failed to stop database", ex); + } + } + + @LocalServerPort + private int port; + + @Autowired + private EventStore eventStore; + + @Autowired + private ImmutableObjectMapper.Provider mapperProvider; + + private String getBaseUrl() { + return "http://localhost:" + port + "/persons"; + } + + @Test + void createAndWaitForView() { + + final PersonId id = new PersonId(); + final PersonName name = new PersonName("Peter Parker"); + + // Add a created event to the aggregate stream - Should update the view + final StreamId streamId = new SimpleStreamId(PersonId.TYPE.asString() + "-" + id.asString()); + final CommonEvent commonEvent = createPersonCreatedEvent(id, name); + LOG.info("Append event to stream: {}", writeValueAsString(commonEvent)); + eventStore.appendToStream(streamId, ExpectedVersion.NO_OR_EMPTY_STREAM.getNo(), commonEvent); + + // Read via HTTP + final Supplier> getPerson = () -> send(newBuilder(getBaseUrl() + "/" + id).GET().build()); + LOG.info("Waiting for GET person..."); + await().atMost(5, SECONDS).until(() -> getPerson.get().statusCode() == HttpStatus.OK.value()); + LOG.info("GET Response received..."); + + final HttpResponse read = getPerson.get(); + assertThat(read.statusCode()).isEqualTo(HttpStatus.OK.value()); + final PersonEntity copy = fromJson(read.body()); + assertThat(copy.getId()).isEqualTo(id.asBaseType()); + assertThat(copy.getName()).isEqualTo(name.asBaseType()); + + } + + private static HttpRequest.Builder newBuilder(final String uri) { + return HttpRequest.newBuilder() + .uri(asURI(uri)) + .headers("Content-Type", MediaType.APPLICATION_JSON_VALUE + ";charset=" + StandardCharsets.UTF_8.name(), + "Accept", MediaType.APPLICATION_JSON_VALUE + ";charset=" + StandardCharsets.UTF_8.name()); + } + + private PersonEntity fromJson(final String json) { + LOG.info("Received json: {}", json); + return readValue(json, PersonEntity.class); + } + + private static HttpResponse send(final HttpRequest request) { + try { + return CLIENT.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (final IOException | InterruptedException ex) { + throw new RuntimeException(ex); + } + } + + private T readValue(String json, Class type) { + try { + return mapperProvider.reader().readValue(json, type); + } catch (IOException ex) { + throw new RuntimeException("Failed to parse JSON for type " + type.getName() + ": " + json , ex); + } + } + + private String writeValueAsString(Object obj) { + try { + return mapperProvider.writer().writeValueAsString(obj); + } catch (JsonProcessingException ex) { + throw new RuntimeException("Failed to write as JSON: " + obj, ex); + } + } + + private static URI asURI(String uri) { + try { + return new URI(uri); + } catch (final URISyntaxException ex) { + throw new RuntimeException(ex); + } + } + +} \ No newline at end of file diff --git a/test/springboot/src/test/java/org/fuin/cqrs4j/springboot/test/app/SpringBootTestHelper.java b/test/springboot/src/test/java/org/fuin/cqrs4j/springboot/test/app/SpringBootTestHelper.java new file mode 100644 index 0000000..6678154 --- /dev/null +++ b/test/springboot/src/test/java/org/fuin/cqrs4j/springboot/test/app/SpringBootTestHelper.java @@ -0,0 +1,45 @@ +package org.fuin.cqrs4j.springboot.test.app; + +import org.fuin.cqrs4j.springboot.test.model.PersonCreatedEvent; +import org.fuin.cqrs4j.springboot.test.model.PersonId; +import org.fuin.cqrs4j.springboot.test.model.PersonName; +import org.fuin.ddd4j.core.AggregateVersion; +import org.fuin.esc.api.CommonEvent; +import org.fuin.esc.api.SimpleCommonEvent; +import org.fuin.esc.api.TypeName; + +import java.time.ZonedDateTime; + +/** + * Helper functions for the test modules. + */ +public final class SpringBootTestHelper { + + private SpringBootTestHelper() { + throw new UnsupportedOperationException("Cannot instantiate utility class"); + } + + /** + * Creates a {@link PersonCreatedEvent} packed into a {@link CommonEvent}. + * + * @param id Unique person identifier. + * @param name Name of the person. + * @return Event to store. + */ + public static CommonEvent createPersonCreatedEvent(PersonId id, PersonName name) { + final org.fuin.esc.api.EventId eventId = new org.fuin.esc.api.EventId(); + final PersonCreatedEvent event = PersonCreatedEvent.builder() + .aggregateVersion(AggregateVersion.valueOf(0)) + .entityIdPath(id) + .eventId(new org.fuin.ddd4j.core.EventId(eventId.asBaseType())) + .id(id) + .name(name) + .timestamp(ZonedDateTime.now()) + .build(); + return new SimpleCommonEvent( + eventId, + new TypeName(PersonCreatedEvent.TYPE.asBaseType()), + event); + } + +}