├── .github └── workflows │ └── maven.yml ├── .gitignore ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docker-compose.yml ├── eclipse-java-google-style.xml ├── intellij-java-google-style.xml ├── lombok.config ├── pom.xml ├── pom.xml.releaseBackup ├── release.properties └── src ├── main └── java │ └── events │ └── dewdrop │ ├── Dewdrop.java │ ├── aggregate │ ├── AggregateRoot.java │ ├── AggregateStateOrchestrator.java │ ├── EventRecorder.java │ ├── EventStateMachine.java │ └── annotation │ │ ├── Aggregate.java │ │ └── AggregateId.java │ ├── api │ ├── result │ │ └── Result.java │ └── validators │ │ ├── ValidationError.java │ │ ├── ValidationException.java │ │ └── ValidationResult.java │ ├── command │ ├── AbstractCommandHandlerMapper.java │ ├── CommandHandler.java │ ├── CommandHandlerMapper.java │ └── CommandMapper.java │ ├── config │ ├── DependencyInjectionAdapter.java │ ├── DewdropProperties.java │ ├── DewdropSettings.java │ └── ascii │ │ └── Ascii.java │ ├── read │ └── readmodel │ │ ├── DefaultAnnotationReadModelMapper.java │ │ ├── QueryStateOrchestrator.java │ │ ├── ReadModel.java │ │ ├── ReadModelConstructed.java │ │ ├── ReadModelFactory.java │ │ ├── ReadModelMapper.java │ │ ├── ReadModelWrapper.java │ │ ├── annotation │ │ ├── AggregateStream.java │ │ ├── AggregateStreams.java │ │ ├── CacheRootKey.java │ │ ├── CategoryStream.java │ │ ├── CategoryStreams.java │ │ ├── CreationEvent.java │ │ ├── DewdropCache.java │ │ ├── EventHandler.java │ │ ├── EventStream.java │ │ ├── EventStreams.java │ │ ├── ForeignCacheKey.java │ │ ├── OnEvent.java │ │ ├── PrimaryCacheKey.java │ │ ├── ReadModel.java │ │ └── StreamStartPosition.java │ │ ├── cache │ │ ├── ImprovedMapBackedInMemoryCacheProcessor.java │ │ ├── InMemoryCacheProcessor.java │ │ ├── MapBackedInMemoryCacheProcessor.java │ │ └── SingleItemInMemoryCache.java │ │ ├── query │ │ └── QueryHandler.java │ │ └── stream │ │ ├── NameAndPosition.java │ │ ├── Stream.java │ │ ├── StreamAnnotationDetails.java │ │ ├── StreamDetails.java │ │ ├── StreamFactory.java │ │ ├── StreamListener.java │ │ ├── StreamReader.java │ │ ├── StreamType.java │ │ ├── SubscriptionStartStrategy.java │ │ └── subscription │ │ └── Subscription.java │ ├── streamstore │ ├── eventstore │ │ ├── EventStore.java │ │ └── EventStoreUtils.java │ ├── process │ │ ├── AggregateRootLifecycle.java │ │ ├── AggregateStateCommandProcessor.java │ │ └── StandaloneAggregateProcessor.java │ ├── repository │ │ └── StreamStoreGetByIDRequest.java │ ├── serialize │ │ └── JsonSerializer.java │ ├── stream │ │ └── PrefixStreamNameGenerator.java │ └── write │ │ └── StreamWriter.java │ ├── structure │ ├── NoStreamException.java │ ├── StreamNameGenerator.java │ ├── api │ │ ├── AbstractMessage.java │ │ ├── Command.java │ │ ├── Event.java │ │ ├── Message.java │ │ ├── ValidationFunction.java │ │ └── validator │ │ │ └── DewdropValidator.java │ ├── datastore │ │ └── StreamStore.java │ ├── events │ │ ├── CorrelationCausation.java │ │ ├── ReadEventData.java │ │ ├── StreamReadResults.java │ │ └── WriteEventData.java │ ├── read │ │ ├── Direction.java │ │ ├── Handler.java │ │ └── ReadRequest.java │ ├── serialize │ │ └── EventSerializer.java │ ├── subscribe │ │ ├── EventProcessor.java │ │ └── SubscribeRequest.java │ └── write │ │ └── WriteRequest.java │ └── utils │ ├── AggregateIdUtils.java │ ├── AggregateUtils.java │ ├── AssignCorrelationAndCausation.java │ ├── CacheUtils.java │ ├── CommandHandlerUtils.java │ ├── DependencyInjectionUtils.java │ ├── DewdropAnnotationUtils.java │ ├── DewdropReflectionUtils.java │ ├── EventHandlerUtils.java │ ├── QueryHandlerUtils.java │ ├── ReadModelUtils.java │ ├── ReflectionsConfigUtils.java │ └── StreamUtils.java └── test ├── java └── events │ └── dewdrop │ ├── DewdropTest.java │ ├── UserLifecycleTest.java │ ├── UserSignupTest.java │ ├── aggregate │ ├── AggregateRootTest.java │ ├── AggregateStateOrchestratorTest.java │ ├── EventRecorderTest.java │ ├── EventStateMachineTest.java │ └── QueryStateOrchestratorTest.java │ ├── api │ ├── result │ │ └── ResultTest.java │ └── validators │ │ ├── ValidationExceptionTest.java │ │ └── ValidationResultTest.java │ ├── config │ ├── DewdropPropertiesTest.java │ └── DewdropSettingsTest.java │ ├── datastore │ └── eventstore │ │ └── EventStoreUtilsTest.java │ ├── fixture │ ├── automated │ │ ├── DewdropAccountAggregate.java │ │ ├── DewdropUserAggregate.java │ │ └── user │ │ │ └── UserAggregate.java │ ├── command │ │ ├── DewdropAccountCommand.java │ │ ├── DewdropAddFundsToAccountCommand.java │ │ ├── DewdropCreateAccountCommand.java │ │ ├── DewdropCreateUserCommand.java │ │ ├── DewdropDeactivateUserCommand.java │ │ ├── DewdropUserCommand.java │ │ └── user │ │ │ ├── CsrClaimUsernameCommand.java │ │ │ ├── UserClaimUsernameCommand.java │ │ │ ├── UserCommand.java │ │ │ └── UserSignupCommand.java │ ├── customized │ │ ├── DewdropAccountAggregateSubclass.java │ │ ├── DewdropCommandService.java │ │ └── DewdropStandaloneCommandService.java │ ├── events │ │ ├── DewdropAccountCreated.java │ │ ├── DewdropAccountEvent.java │ │ ├── DewdropFundsAddedToAccount.java │ │ ├── DewdropUserCreated.java │ │ ├── DewdropUserDeactivate.java │ │ ├── DewdropUserEvent.java │ │ ├── UserLoggedIn.java │ │ └── user │ │ │ ├── CsrClaimedUsername.java │ │ │ ├── UserClaimedUsername.java │ │ │ ├── UserEvent.java │ │ │ └── UserSignedUp.java │ └── readmodel │ │ ├── AccountCreatedService.java │ │ ├── accountdetails │ │ ├── details │ │ │ ├── DewdropAccountDetails.java │ │ │ ├── DewdropAccountDetailsReadModel.java │ │ │ └── DewdropGetAccountByIdQuery.java │ │ └── summary │ │ │ ├── DewdropAccountSummary.java │ │ │ ├── DewdropAccountSummaryQuery.java │ │ │ └── DewdropAccountSummaryReadModel.java │ │ └── users │ │ ├── DewdropGetUserByIdQuery.java │ │ ├── DewdropGetUserByIdQueryForAggregate.java │ │ ├── DewdropUser.java │ │ ├── DewdropUserAggregateReadModel.java │ │ ├── DewdropUsersReadModel.java │ │ ├── GetUserByIdQuery.java │ │ ├── User.java │ │ └── lifecycle │ │ └── UsersReadModel.java │ ├── read │ └── readmodel │ │ ├── DefaultAnnotationReadModelMapperTest.java │ │ ├── ReadModelFactoryTest.java │ │ ├── ReadModelTest.java │ │ ├── ReadModelWrapperTest.java │ │ ├── cache │ │ ├── ImprovedMapBackedInMemoryCacheProcessorTest.java │ │ └── MapBackedInMemoryCacheProcessorTest.java │ │ └── stream │ │ ├── NameAndPositionTest.java │ │ ├── StreamDetailsTest.java │ │ ├── StreamFactoryTest.java │ │ ├── StreamListenerTest.java │ │ ├── StreamReaderTest.java │ │ ├── StreamTest.java │ │ └── subscription │ │ └── SubscriptionTest.java │ ├── streamstore │ ├── eventstore │ │ └── EventStoreTest.java │ ├── process │ │ ├── AggregateRootLifecycleTest.java │ │ ├── AggregateStateCommandProcessorTest.java │ │ └── StandaloneAggregateProcessorTest.java │ ├── serialize │ │ └── JsonSerializerTest.java │ ├── stream │ │ └── PrefixStreamNameGeneratorTest.java │ └── write │ │ └── StreamWriterTest.java │ ├── structure │ └── api │ │ └── validator │ │ └── DewdropValidatorTest.java │ └── utils │ ├── AggregateIdUtilsTest.java │ ├── AggregateUtilsTest.java │ ├── AssignCorrelationAndCausationTest.java │ ├── CacheUtilsTest.java │ ├── CommandHandlerUtilsTest.java │ ├── DependencyInjectionUtilsTest.java │ ├── DewdropAnnotationUtilsTest.java │ ├── DewdropReflectionUtilsTest.java │ ├── EventHandlerUtilsTest.java │ ├── QueryHandlerUtilsTest.java │ ├── ReadModelUtilsTest.java │ ├── ReflectionsConfigUtilsTest.java │ └── StreamUtilsTest.java └── resources ├── dewdropper.yml └── log4j2.xml /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: Java CI with Maven 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v3 21 | with: 22 | java-version: '11' 23 | distribution: 'temurin' 24 | cache: maven 25 | - name: Start Docker 26 | run: docker compose up -d 27 | - name: Build with Maven 28 | run: mvn -B verify -P coverage --no-transfer-progress 29 | - uses: codecov/codecov-action@v3 30 | with: 31 | token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos 32 | files: ./target/site/jacoco-merged-test-coverage-report/jacoco.xml 33 | verbose: true # optional (default = false) 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.nar 17 | *.ear 18 | *.zip 19 | *.tar.gz 20 | *.rar 21 | *.iml 22 | 23 | .idea/ 24 | 25 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 26 | hs_err_pid* 27 | 28 | target/ 29 | target/* 30 | 31 | # mac 32 | .DS_Store 33 | 34 | .java-version 35 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "java.compile.nullAnalysis.mode": "automatic", 3 | "java.configuration.updateBuildConfiguration": "interactive" 4 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email (info@betterthanbrian.com), or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team at info@betterthanbrian.com. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ 93 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | 2 | services: 3 | eventstore.db: 4 | image: eventstore/eventstore:21.2.0-buster-slim 5 | environment: 6 | - EVENTSTORE_CLUSTER_SIZE=1 7 | - EVENTSTORE_RUN_PROJECTIONS=System 8 | - EVENTSTORE_START_STANDARD_PROJECTIONS=true 9 | - EVENTSTORE_EXT_TCP_PORT=1113 10 | - EVENTSTORE_EXT_HTTP_PORT=2113 11 | - EVENTSTORE_INSECURE=true 12 | - EVENTSTORE_ENABLE_EXTERNAL_TCP=true 13 | - EVENTSTORE_ENABLE_ATOM_PUB_OVER_HTTP=true 14 | # - EVENTSTORE_EXT_HOST_ADVERTISE_AS=host.docker.internal 15 | ports: 16 | - "1113:1113" 17 | - "2113:2113" 18 | volumes: 19 | - type: volume 20 | source: eventstore-volume-data 21 | target: /var/lib/eventstore 22 | - type: volume 23 | source: eventstore-volume-logs 24 | target: /var/log/eventstore 25 | volumes: 26 | eventstore-volume-data: 27 | eventstore-volume-logs: 28 | -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | lombok.addLombokGeneratedAnnotation = true -------------------------------------------------------------------------------- /release.properties: -------------------------------------------------------------------------------- 1 | #release configuration 2 | #Fri Apr 07 09:49:45 MDT 2023 3 | projectVersionPolicyId=default 4 | remoteTagging=true 5 | project.scm.events.dewdrop\:dewdrop.tag=HEAD 6 | scm.commentPrefix=[maven-release-plugin] 7 | completedPhase=end-release 8 | scm.url=scm\:git\:git@github.com\:matsientst/dewdrop.git 9 | project.scm.events.dewdrop\:dewdrop.url=https\://github.com/matsientst/dewdrop 10 | project.dev.events.dewdrop\:dewdrop=-SNAPSHOT 11 | scm.tagNameFormat=@{project.artifactId}-@{project.version} 12 | pushChanges=true 13 | project.scm.events.dewdrop\:dewdrop.developerConnection=scm\:git\:git@github.com\:matsientst/dewdrop.git 14 | scm.tag=dewdrop-1.0.4.16 15 | exec.snapshotReleasePluginAllowed=false 16 | project.rel.events.dewdrop\:dewdrop=1.0.4.16 17 | preparationGoals=clean verify 18 | project.scm.events.dewdrop\:dewdrop.connection=scm\:git\:git\://github.com/matsientst/dewdrop.git 19 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/Dewdrop.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop; 2 | 3 | import events.dewdrop.api.result.Result; 4 | import events.dewdrop.api.validators.ValidationException; 5 | import events.dewdrop.config.DewdropSettings; 6 | import events.dewdrop.structure.api.Command; 7 | 8 | /** 9 | * Dewdrop is a simple, fast, and powerful java based event sourcing framework. The idea of Dewdrop 10 | * is to make it easy to build an event driven system easily and quickly by pushing all the complex 11 | * reading, writing and marshalling down deep into the framework allowing your team to focus on 12 | * building out the business logic in terms of AggregateRoot behavior, Query logic, and ReadModel 13 | * composition. 14 | */ 15 | public class Dewdrop { 16 | private final DewdropSettings settings; 17 | 18 | public Dewdrop(DewdropSettings settings) { 19 | this.settings = settings; 20 | } 21 | 22 | /** 23 | * This is the main entry point for the Dewdrop framework. It is used to execute a command against 24 | * an AggregateRoot and return the Result. To have this work correctly, you'll need an AggregateRoot 25 | * class that is decorated with @AggregateRoot and a method on that class that is decorated 26 | * with @CommandHandler with your command class as the only parameter. 27 | * 28 | * The Command object needs to extend the Command class and can use JSR-303 validation annotations 29 | * to validate the command by calling DewdropValidator.validate(command). 30 | * 31 | * The event that is generated and returned can be of any type that extends the Event class. 32 | * Alternatively, it can return a List of Event objects. 33 | * 34 | *
35 |      * @CommandHandler
36 |      * public AccountCreated createAccount(CreateAccountCommand command) {
37 |      *     DewdropValidator.validate(command);
38 |      *     return new AccountCreated(command.getAccountId(), command.getUserId(), command.getBalance());
39 |      * }
40 |      * 
41 | * 42 | * @param The type of command that you are executing. 43 | * @param command The command to execute. 44 | * @return {@code Result} 45 | * @throws ValidationException If the command is invalid. 46 | */ 47 | public Result executeCommand(T command) throws ValidationException { 48 | return settings.getAggregateStateOrchestrator().executeCommand(command); 49 | } 50 | 51 | /** 52 | * This method is exactly the same as `executeCommand()` except it will generate the corresponding 53 | * correlation and causation ids for the command. These are used to correlate events and ascribe a 54 | * tracking to understand where the event came from. 55 | * 56 | * @param The type of command that you are executing. 57 | * @param command The command to execute. 58 | * @param previous The previous command that was executed. 59 | * @return {@code Result} 60 | * @throws ValidationException If the command is invalid. 61 | */ 62 | public Result executeSubsequentCommand(T command, Command previous) throws ValidationException { 63 | return settings.getAggregateStateOrchestrator().executeSubsequentCommand(command, previous); 64 | } 65 | 66 | /** 67 | * This is the query entry point for the Dewdrop framework. It is used to execute a query against a 68 | * ReadModel and return the Result. To have this work correctly, you'll need a ReadModel class that 69 | * is decorated with @ReadModel and a method on that class that is decorated with @QueryHandler with 70 | * your query class as the only parameter. 71 | * 72 | *
73 |      * @QueryHandler
74 |      * public AccountDetails getById(GetAccountByIdQuery query) {
75 |      *     return cache.get(query.getAccountId());
76 |      * }
77 |      * 
78 | * 79 | * @param The type of query that you are executing. 80 | * @param The expected result of the query that you are executing. 81 | * @param query The query to execute. 82 | * @return {@code Result} 83 | */ 84 | public Result executeQuery(T query) { 85 | return settings.getQueryStateOrchestrator().executeQuery(query); 86 | } 87 | 88 | /** 89 | * `getSettings()` returns the settings object for the current Dewdrop instance 90 | * 91 | * @return DewdropSettings 92 | */ 93 | public DewdropSettings getSettings() { 94 | return settings; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/aggregate/AggregateRoot.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.aggregate; 2 | 3 | import java.util.ArrayList; 4 | import java.util.List; 5 | import java.util.Optional; 6 | import java.util.UUID; 7 | 8 | import events.dewdrop.structure.api.Message; 9 | import events.dewdrop.structure.events.CorrelationCausation; 10 | import lombok.Getter; 11 | 12 | @Getter 13 | public class AggregateRoot extends EventStateMachine { 14 | private final List messages = new ArrayList<>(); 15 | private Object target = null; 16 | private String targetClassName = null; 17 | 18 | public AggregateRoot() { 19 | super(); 20 | this.target = this; 21 | this.targetClassName = this.getClass().getName(); 22 | } 23 | 24 | public AggregateRoot(Object target) { 25 | super(); 26 | this.target = target; 27 | this.targetClassName = target.getClass().getName(); 28 | } 29 | 30 | private UUID correlationId; 31 | private UUID causationId; 32 | 33 | public void setSource(CorrelationCausation command) { 34 | if (correlationId != null && recorder.hasRecordedEvents()) { throw new IllegalStateException("Cannot change source unless there are no recorded events, or current source is null"); } 35 | 36 | this.correlationId = command.getCorrelationId(); 37 | this.causationId = Optional.ofNullable(command.getCausationId()).orElse(command.getMessageId()); 38 | } 39 | 40 | @Override 41 | public boolean equals(Object o) { 42 | if (o instanceof AggregateRoot) { return getTarget().equals(((AggregateRoot) o).getTarget()); } 43 | return getTarget().equals(o); 44 | } 45 | 46 | @Override 47 | public int hashCode() { 48 | return getTarget().hashCode(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/aggregate/AggregateStateOrchestrator.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.aggregate; 2 | 3 | import events.dewdrop.api.result.Result; 4 | import events.dewdrop.api.validators.ValidationException; 5 | import events.dewdrop.command.CommandMapper; 6 | import events.dewdrop.streamstore.process.AggregateStateCommandProcessor; 7 | import events.dewdrop.utils.AssignCorrelationAndCausation; 8 | import java.lang.reflect.Method; 9 | import java.util.Optional; 10 | import lombok.extern.log4j.Log4j2; 11 | import events.dewdrop.structure.api.Command; 12 | import events.dewdrop.structure.events.CorrelationCausation; 13 | 14 | @Log4j2 15 | /** 16 | * This class is responsible for orchestrating the stateful processing of events for a single 17 | * aggregate 18 | */ 19 | public class AggregateStateOrchestrator { 20 | private CommandMapper commandMapper; 21 | private AggregateStateCommandProcessor aggregateStateCommandProcessor; 22 | 23 | public AggregateStateOrchestrator(CommandMapper commandMapper, AggregateStateCommandProcessor aggregateStateCommandProcessor) { 24 | this.commandMapper = commandMapper; 25 | this.aggregateStateCommandProcessor = aggregateStateCommandProcessor; 26 | } 27 | 28 | /** 29 | * Retrieves the appropriate command handler method for the given command and invokes it on the 30 | * AggregateRoot 31 | * 32 | * @param The type of the result of the command handler method 33 | * @param command The command to execute. 34 | * @return {@code Result} 35 | * @throws ValidationException If the command is invalid 36 | */ 37 | public Result executeCommand(T command) throws ValidationException { 38 | Optional commandHandlerMethod = commandMapper.getCommandHandlersThatSupportCommand(command); 39 | 40 | if (commandHandlerMethod.isEmpty()) { return Result.empty(); } 41 | 42 | return aggregateStateCommandProcessor.processCommand(command, commandHandlerMethod.get()); 43 | } 44 | 45 | /** 46 | * Retrieves the appropriate command handler method for the given command and invokes it on the 47 | * AggregateRoot If the command has a previous, related command that you want to keep the causation 48 | * and correlationIds for then this assigns the correlation and causation to the command and process 49 | * it. 50 | * 51 | * @param The type of the result of the command handler method 52 | * @param command The command to execute 53 | * @param previous The previous command that was executed. 54 | * @return {@code Result} 55 | * @throws ValidationException If the command is invalid 56 | */ 57 | public Result executeSubsequentCommand(T command, CorrelationCausation previous) throws ValidationException { 58 | Optional commandHandlerMethod = commandMapper.getCommandHandlersThatSupportCommand(command); 59 | 60 | if (commandHandlerMethod.isEmpty()) { return Result.empty(); } 61 | 62 | command = AssignCorrelationAndCausation.assignTo(previous, command); 63 | return aggregateStateCommandProcessor.processCommand(command, commandHandlerMethod.get()); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/aggregate/EventRecorder.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.aggregate; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import events.dewdrop.structure.api.Message; 6 | import java.io.Serializable; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public class EventRecorder implements Serializable { 11 | private List recorded; 12 | 13 | public EventRecorder() { 14 | recorded = new ArrayList<>(); 15 | } 16 | 17 | public boolean hasRecordedEvents() { 18 | return !recorded.isEmpty(); 19 | } 20 | 21 | public void recordEvent(Message message) { 22 | requireNonNull(message); 23 | 24 | recorded.add(message); 25 | } 26 | 27 | public void reset() { 28 | recorded.clear(); 29 | } 30 | 31 | 32 | public List recordedEvents() { 33 | return recorded; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/aggregate/EventStateMachine.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.aggregate; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import events.dewdrop.utils.EventHandlerUtils; 6 | import events.dewdrop.structure.api.Message; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import lombok.Data; 10 | import lombok.extern.log4j.Log4j2; 11 | 12 | @Data 13 | @Log4j2 14 | public abstract class EventStateMachine { 15 | private long version; 16 | protected EventRecorder recorder; 17 | 18 | public EventStateMachine() { 19 | this.version = -1; 20 | this.recorder = new EventRecorder(); 21 | } 22 | 23 | public void restoreFromEvents(List messages) { 24 | requireNonNull(messages); 25 | 26 | if (recorder.hasRecordedEvents()) { throw new IllegalStateException("Restoring from events is not possible when an instance has recorded events."); } 27 | 28 | messages.forEach(message -> { 29 | if (version < 0) { // new aggregates have an expected version of -1 or -2 30 | version = 0; // got first message (zero based) 31 | } else { 32 | version++; 33 | } 34 | callEventHandler(message); 35 | }); 36 | } 37 | 38 | 39 | public void updateWithEvents(List messages, long expectedVersion) { 40 | requireNonNull(messages); 41 | 42 | if (version < 0) { throw new IllegalArgumentException("Updating with events is not possible when an instance has no historical events"); } 43 | if (version != expectedVersion) { throw new IllegalArgumentException("Expected version mismatch when updating - actual version:" + version + ", expectedVersion:" + expectedVersion); } 44 | 45 | messages.forEach(message -> { 46 | version++; 47 | callEventHandler(message); 48 | }); 49 | } 50 | 51 | 52 | public List takeEvents() { 53 | takeEventStarted(); 54 | List records = new ArrayList<>(recorder.recordedEvents()); 55 | recorder.reset(); 56 | version += records.size(); 57 | takeEventsCompleted(); 58 | return records; 59 | } 60 | 61 | 62 | public void takeEventStarted() { 63 | // Not Implemented yet. 64 | } 65 | 66 | 67 | public void takeEventsCompleted() { 68 | // Not Implemented yet. 69 | } 70 | 71 | 72 | public void onEventRaised(Message message) { 73 | // Not Implemented yet. 74 | } 75 | 76 | public void raise(Message message) { 77 | onEventRaised(message); 78 | callEventHandler(message); 79 | recorder.recordEvent(message); 80 | } 81 | 82 | protected void callEventHandler(Message message) { 83 | EventHandlerUtils.callEventHandler(getTarget(), message); 84 | } 85 | 86 | protected abstract Object getTarget(); 87 | } 88 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/aggregate/annotation/Aggregate.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.aggregate.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ElementType.TYPE}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface Aggregate { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/aggregate/annotation/AggregateId.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.aggregate.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ElementType.FIELD}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface AggregateId { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/api/result/Result.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.api.result; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Consumer; 5 | 6 | public class Result { 7 | 8 | private static final Result EMPTY = new Result<>(); 9 | private final T value; 10 | private final Exception exception; 11 | 12 | private Result() { 13 | this.value = null; 14 | this.exception = null; 15 | } 16 | 17 | @SuppressWarnings("java:S112") 18 | public void rethrowRuntime() { 19 | if (exception != null) { throw new RuntimeException(exception); } 20 | } 21 | 22 | public void rethrow() { 23 | if (exception != null) { throw new RuntimeException(exception); } 24 | } 25 | 26 | public Exception getException() { 27 | return exception; 28 | } 29 | 30 | public static Result empty() { 31 | @SuppressWarnings("unchecked") 32 | Result t = (Result) EMPTY; 33 | return t; 34 | } 35 | 36 | private Result(T value) { 37 | this.value = Objects.requireNonNull(value); 38 | this.exception = null; 39 | } 40 | 41 | private Result(Exception exception) { 42 | this.exception = Objects.requireNonNull(exception); 43 | value = null; 44 | } 45 | 46 | public static Result of(T value) { 47 | if (value instanceof Result) { return (Result) value; } 48 | return new Result<>(value); 49 | } 50 | 51 | public static Result of(Exception exception) { 52 | return new Result<>(exception); 53 | } 54 | 55 | public static Result ofException(Exception exception) { 56 | return exception == null ? empty() : of(exception); 57 | } 58 | 59 | public T get() { 60 | if (exception != null) { throw new RuntimeException(exception); } 61 | return value; 62 | } 63 | 64 | public boolean isValuePresent() { 65 | return value != null; 66 | } 67 | 68 | public boolean isExceptionPresent() { 69 | return exception != null; 70 | } 71 | 72 | public void ifPresent(Consumer consumer) { 73 | if (exception == null) { 74 | consumer.accept(value); 75 | } 76 | } 77 | 78 | public boolean isEmpty() { 79 | return exception == null && value == null; 80 | } 81 | 82 | public void ifExceptionPresent(Consumer consumer) { 83 | if (exception != null) { 84 | consumer.accept(exception); 85 | } 86 | } 87 | 88 | public void ifExceptionPresent(Class targetType, Consumer consumer) { 89 | if (exception != null && targetType.isAssignableFrom(exception.getClass())) { 90 | consumer.accept(exception); 91 | } 92 | } 93 | 94 | public T orElse(T other) { 95 | if (exception == null) { return value == null ? other : value; } 96 | return other; 97 | } 98 | 99 | @Override 100 | public boolean equals(Object obj) { 101 | if (this == obj) { return true; } 102 | if (!(obj instanceof Result)) { return false; } 103 | Result other = (Result) obj; 104 | return Objects.equals(value, other.value); 105 | } 106 | 107 | @Override 108 | public int hashCode() { 109 | return Objects.hashCode(value); 110 | } 111 | 112 | @Override 113 | public String toString() { 114 | return exception == null ? String.format("Result[%s]", value) : String.format("Result[%s]", exception); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/api/validators/ValidationError.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.api.validators; 2 | 3 | import lombok.Data; 4 | 5 | import java.io.Serializable; 6 | 7 | @Data 8 | public class ValidationError implements Serializable { 9 | String field; 10 | String message; 11 | 12 | public ValidationError(String field, String message) { 13 | this.field = field; 14 | this.message = message; 15 | } 16 | 17 | public ValidationError(String message) { 18 | this.message = message; 19 | } 20 | 21 | public ValidationError(String message, Object... params) { 22 | this.message = String.format(message, params); 23 | } 24 | 25 | public ValidationError(String field, String message, Object... params) { 26 | this.field = field; 27 | this.message = String.format(message, params); 28 | } 29 | 30 | public static ValidationError of(String message) { 31 | return new ValidationError(message); 32 | } 33 | 34 | public static ValidationError of(String field, String message) { 35 | return new ValidationError(field, message); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/api/validators/ValidationException.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.api.validators; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.util.List; 5 | import java.util.regex.Pattern; 6 | import java.util.stream.Collectors; 7 | 8 | import lombok.Data; 9 | 10 | /** 11 | * An exception used to throw an exception with a ValidationResult which contains a collection of 12 | * ValidationErrors. These can then be used to understand all errors in the Validation not just the 13 | * first one. 14 | */ 15 | @Data 16 | public class ValidationException extends Exception { 17 | private final ValidationResult validationResult; 18 | 19 | public ValidationException(ValidationResult result) { 20 | super(result.get().stream().map(ValidationError::getMessage).collect(Collectors.joining(", "))); 21 | this.validationResult = result; 22 | } 23 | 24 | public static ValidationException of(String message) { 25 | return new ValidationException(ValidationResult.of(new ValidationError(message))); 26 | } 27 | 28 | public static ValidationException of(ValidationError error) { 29 | return new ValidationException(ValidationResult.of(error)); 30 | } 31 | 32 | public static ValidationException of(List errors) { 33 | return new ValidationException(ValidationResult.of(errors)); 34 | } 35 | 36 | public static ValidationException of(Exception exception) { 37 | if (exception instanceof ValidationException) { return (ValidationException) exception; } 38 | if (exception instanceof InvocationTargetException && ((InvocationTargetException) exception).getTargetException() instanceof ValidationException) { return (ValidationException) ((InvocationTargetException) exception).getTargetException(); } 39 | return ValidationException.of(exception.getMessage()); 40 | } 41 | 42 | public static ValidationException of(String message, Object... params) { 43 | message = message.replaceAll(Pattern.quote("{}"), "%s"); 44 | return new ValidationException(ValidationResult.of(new ValidationError(message, params))); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/api/validators/ValidationResult.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.api.validators; 2 | 3 | import java.io.Serializable; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class ValidationResult implements Serializable { 8 | 9 | private final List errors; 10 | 11 | protected ValidationResult() { 12 | this.errors = new ArrayList<>(); 13 | } 14 | 15 | public static ValidationResult of(String message) { 16 | ValidationResult result = new ValidationResult(); 17 | ValidationError error = new ValidationError(message); 18 | result.errors.add(error); 19 | return result; 20 | } 21 | 22 | public ValidationResult addAll(List errors) { 23 | this.errors.addAll(errors); 24 | return this; 25 | } 26 | 27 | public static ValidationResult of(List errors) { 28 | ValidationResult result = new ValidationResult(); 29 | 30 | if (errors.isEmpty()) { return result; } 31 | 32 | result.addAll(errors); 33 | return result; 34 | } 35 | 36 | public static ValidationResult of(ValidationError error) { 37 | ValidationResult result = new ValidationResult(); 38 | result.errors.add(error); 39 | return result; 40 | } 41 | 42 | public static ValidationResult valid() { 43 | return new ValidationResult(); 44 | } 45 | 46 | public boolean hasErrors() { 47 | return !this.errors.isEmpty(); 48 | } 49 | 50 | public boolean isValid() { 51 | return !hasErrors(); 52 | } 53 | 54 | public List get() { 55 | return this.errors; 56 | } 57 | 58 | public void add(ValidationError validationError) { 59 | this.errors.add(validationError); 60 | } 61 | 62 | public ValidationResult and(ValidationResult validationResult) { 63 | if (validationResult.hasErrors()) { 64 | this.errors.addAll(validationResult.get()); 65 | } 66 | return this; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/command/AbstractCommandHandlerMapper.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.command; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | public abstract class AbstractCommandHandlerMapper implements CommandMapper { 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/command/CommandHandler.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.command; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ElementType.METHOD}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface CommandHandler { 13 | Class value() default void.class; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/command/CommandHandlerMapper.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.command; 2 | 3 | import events.dewdrop.structure.api.Command; 4 | import events.dewdrop.utils.CommandHandlerUtils; 5 | import java.lang.reflect.Method; 6 | import java.util.Map; 7 | import java.util.Optional; 8 | import java.util.Set; 9 | import lombok.extern.log4j.Log4j2; 10 | import org.apache.commons.collections4.CollectionUtils; 11 | 12 | @Log4j2 13 | public class CommandHandlerMapper extends AbstractCommandHandlerMapper { 14 | Map, Method> commandHandlerToMethod = new java.util.HashMap<>(); 15 | 16 | public CommandHandlerMapper() { 17 | init(); 18 | } 19 | 20 | public void init() { 21 | log.debug("Finding all commandHandlerMethods annotated with @CommandHandler"); 22 | 23 | Set commandHandlerMethods = CommandHandlerUtils.getCommandHandlerMethods(); 24 | if (CollectionUtils.isEmpty(commandHandlerMethods)) { 25 | log.error("No command handlers found - Make sure to annotate your handlers with @CommandHandler - If in your AggregateRoot it should be handle(MyCustomCommand command) or if in a service handle(MyCustomCommand command, MyAggregateRoot aggregateRoot)"); 26 | } 27 | 28 | commandHandlerMethods.forEach(commandHandlerMethod -> { 29 | Class commandClass = commandHandlerMethod.getParameterTypes()[0]; 30 | 31 | if (commandHandlerToMethod.containsKey(commandClass)) { 32 | 33 | log.error("InvalidState - Duplicate @CommandHandler handle({} command) found in {}", commandClass.getSimpleName(), commandHandlerMethod.getDeclaringClass()); 34 | return; 35 | } 36 | 37 | log.info("Registering @CommandHandler for {} to be handled by {}", commandClass.getSimpleName(), commandHandlerMethod.getDeclaringClass().getSimpleName()); 38 | commandHandlerToMethod.put(commandClass, commandHandlerMethod); 39 | }); 40 | } 41 | 42 | public Optional getCommandHandlersThatSupportCommand(Command command) { 43 | Method method = commandHandlerToMethod.get(command.getClass()); 44 | if (method == null) { return Optional.empty(); } 45 | return Optional.of(method); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/command/CommandMapper.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.command; 2 | 3 | import events.dewdrop.structure.api.Command; 4 | import java.lang.reflect.Method; 5 | import java.util.Optional; 6 | 7 | public interface CommandMapper { 8 | 9 | Optional getCommandHandlersThatSupportCommand(Command command); 10 | } 11 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/config/DependencyInjectionAdapter.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.config; 2 | 3 | public interface DependencyInjectionAdapter { 4 | public T getBean(Class clazz); 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/config/DewdropProperties.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.config; 2 | 3 | import java.util.List; 4 | import lombok.AccessLevel; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Builder; 7 | import lombok.Getter; 8 | import lombok.NoArgsConstructor; 9 | import lombok.NonNull; 10 | import lombok.Singular; 11 | import org.apache.commons.lang3.StringUtils; 12 | 13 | @Getter 14 | @Builder(buildMethodName = "create") 15 | @AllArgsConstructor(access = AccessLevel.PRIVATE) 16 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 17 | public class DewdropProperties { 18 | @NonNull 19 | private String connectionString; 20 | private String streamPrefix; 21 | private String packageToScan; 22 | @Singular("packageToExclude") 23 | private List packageToExclude; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/config/ascii/Ascii.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.config.ascii; 2 | 3 | import java.time.YearMonth; 4 | import lombok.extern.log4j.Log4j2; 5 | 6 | @Log4j2 7 | public class Ascii { 8 | 9 | public static void writeAscii() { 10 | StringBuilder sb = new StringBuilder(); 11 | sb.append("\n"); 12 | sb.append(" ______ _______ _ _ ______ ______ _______ _______ \n"); 13 | sb.append("| | | || | _ | || | | _ | | || |\n"); 14 | sb.append("| _ || ___|| || || || _ || | || | _ || _ |\n"); 15 | sb.append("| | | || |___ | || | | || |_||_ | | | || |_| |\n"); 16 | sb.append("| |_| || ___|| || |_| || __ || |_| || ___|\n"); 17 | sb.append("| || |___ | _ || || | | || || | \n"); 18 | sb.append("|______| |_______||__| |__||______| |___| |_||_______||___| \n"); 19 | sb.append("\n"); 20 | sb.append("A framework for event sourcing."); 21 | sb.append("\n"); 22 | sb.append("Author: Matt Macchia"); 23 | sb.append("\n"); 24 | sb.append("Copyright: ").append(YearMonth.now().getYear()); 25 | sb.append("\n"); 26 | log.info(sb.toString()); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/QueryStateOrchestrator.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel; 2 | 3 | import events.dewdrop.api.result.Result; 4 | import events.dewdrop.structure.api.Event; 5 | import events.dewdrop.utils.AggregateIdUtils; 6 | import events.dewdrop.utils.QueryHandlerUtils; 7 | import lombok.extern.log4j.Log4j2; 8 | 9 | import java.util.Optional; 10 | import java.util.UUID; 11 | 12 | @Log4j2 13 | public class QueryStateOrchestrator { 14 | private final ReadModelMapper readModelMapper; 15 | 16 | public QueryStateOrchestrator(ReadModelMapper readModelMapper) { 17 | this.readModelMapper = readModelMapper; 18 | } 19 | 20 | public Result executeQuery(T query) { 21 | Optional> optReadModel = readModelMapper.getReadModelByQuery(query); 22 | if (optReadModel.isEmpty()) { 23 | log.error("no read model found for query: {}", query.getClass().getSimpleName()); 24 | return Result.ofException(new IllegalStateException("no read model found for query: " + query.getClass().getSimpleName())); 25 | } 26 | ReadModel readModel = optReadModel.get(); 27 | log.info("Querying read model: {} with QueryType: {}", readModel.getClass().getSimpleName(), query.getClass().getSimpleName()); 28 | 29 | Optional aggregateId = AggregateIdUtils.hasAggregateId(query) ? AggregateIdUtils.getAggregateId(query) : Optional.empty(); 30 | readModel.updateQueryState(aggregateId); 31 | return QueryHandlerUtils.callQueryHandler(readModel.getReadModelWrapper(), query); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/ReadModel.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel; 2 | 3 | import events.dewdrop.utils.CacheUtils; 4 | import events.dewdrop.read.readmodel.cache.InMemoryCacheProcessor; 5 | import events.dewdrop.read.readmodel.stream.Stream; 6 | import events.dewdrop.structure.api.Event; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.Optional; 10 | import java.util.UUID; 11 | import java.util.function.Consumer; 12 | import lombok.Data; 13 | import lombok.extern.log4j.Log4j2; 14 | 15 | @Data 16 | @Log4j2 17 | public class ReadModel { 18 | private ReadModelWrapper readModelWrapper; 19 | private Optional inMemoryCacheProcessor; 20 | protected List> streams = new ArrayList<>(); 21 | 22 | public ReadModel(ReadModelWrapper readModelWrapper, Optional inMemoryCacheProcessor) { 23 | this.readModelWrapper = readModelWrapper; 24 | this.inMemoryCacheProcessor = inMemoryCacheProcessor; 25 | 26 | } 27 | // on demand read through cache 28 | // live subscription to category stream and throw away any events for account not currently in 29 | // cache. 30 | // Only create the read model when we are querying for that specific accountId 31 | 32 | 33 | protected void subscribe() { 34 | getStreams().forEach(Stream::subscribe); 35 | } 36 | 37 | protected void process(T message) { 38 | Optional cacheRootKey = CacheUtils.getCacheRootKey(message); 39 | log.info("ReadModel:{}, Received event type:{} - id:{}, version:{}", readModelWrapper.getOriginalReadModelClass().getSimpleName(), message.getClass().getSimpleName(), cacheRootKey.orElse(null), message.getVersion()); 40 | 41 | inMemoryCacheProcessor.ifPresent(memoryCacheProcessor -> memoryCacheProcessor.process(message)); 42 | readModelWrapper.callEventHandlers(message); 43 | } 44 | 45 | public Consumer handler() { 46 | return this::process; 47 | } 48 | 49 | public void handle(T message) { 50 | process(message); 51 | } 52 | 53 | public R getCachedItems() { 54 | return inMemoryCacheProcessor.map(InMemoryCacheProcessor::getCache).orElse(null); 55 | } 56 | 57 | // public Object getReadModel() { 58 | // return readModel; 59 | // } 60 | 61 | public void addStream(Stream stream) { 62 | this.streams.add(stream); 63 | } 64 | 65 | 66 | public void updateQueryState(Optional aggregateId) { 67 | streams.forEach(stream -> stream.updateQueryState(aggregateId)); 68 | inMemoryCacheProcessor.ifPresent(memoryCacheProcessor -> readModelWrapper.updateReadModelCache(memoryCacheProcessor.getCache())); 69 | } 70 | 71 | public String getTargetClassSimpleName() { 72 | return readModelWrapper.getOriginalReadModelClass().getSimpleName(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/ReadModelConstructed.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import events.dewdrop.structure.api.Event; 6 | import lombok.Getter; 7 | 8 | @Getter 9 | public class ReadModelConstructed { 10 | private boolean ephemeral = false; 11 | private int destroyInMinutesUnused = -1; 12 | private ReadModel readModel; 13 | 14 | public ReadModelConstructed(ReadModel readModel) { 15 | requireNonNull(readModel, "readModel is required"); 16 | 17 | events.dewdrop.read.readmodel.annotation.ReadModel annotation = readModel.getReadModelWrapper().getOriginalReadModelClass().getAnnotation(events.dewdrop.read.readmodel.annotation.ReadModel.class); 18 | this.ephemeral = annotation.ephemeral(); 19 | this.destroyInMinutesUnused = annotation.destroyInMinutesUnused(); 20 | this.readModel = readModel; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/ReadModelMapper.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel; 2 | 3 | import events.dewdrop.structure.api.Event; 4 | import java.util.Optional; 5 | 6 | public interface ReadModelMapper { 7 | 8 | Optional> getReadModelByQuery(Object query); 9 | 10 | void init(ReadModelFactory readModelFactory); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/AggregateStream.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import events.dewdrop.structure.read.Direction; 4 | 5 | import java.lang.annotation.Repeatable; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | 9 | @Retention(RetentionPolicy.RUNTIME) 10 | @Repeatable(value = AggregateStreams.class) 11 | public @interface AggregateStream { 12 | String name(); 13 | 14 | Direction direction() default Direction.FORWARD; 15 | 16 | boolean subscribed() default true; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/AggregateStreams.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | @Retention(RetentionPolicy.RUNTIME) 7 | public @interface AggregateStreams { 8 | AggregateStream[] value(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/CacheRootKey.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ElementType.FIELD}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface CacheRootKey { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/CategoryStream.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import events.dewdrop.structure.read.Direction; 4 | 5 | import java.lang.annotation.Repeatable; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | 9 | @Retention(RetentionPolicy.RUNTIME) 10 | @Repeatable(value = CategoryStreams.class) 11 | public @interface CategoryStream { 12 | String name(); 13 | 14 | Direction direction() default Direction.FORWARD; 15 | 16 | boolean subscribed() default true; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/CategoryStreams.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | @Retention(RetentionPolicy.RUNTIME) 7 | public @interface CategoryStreams { 8 | CategoryStream[] value(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/CreationEvent.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ElementType.TYPE}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface CreationEvent { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/DewdropCache.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ElementType.FIELD}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface DewdropCache { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/EventHandler.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ElementType.METHOD}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface EventHandler { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/EventStream.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import events.dewdrop.structure.read.Direction; 4 | 5 | import java.lang.annotation.Repeatable; 6 | import java.lang.annotation.Retention; 7 | import java.lang.annotation.RetentionPolicy; 8 | 9 | @Retention(RetentionPolicy.RUNTIME) 10 | @Repeatable(value = EventStreams.class) 11 | public @interface EventStream { 12 | String name(); 13 | 14 | Direction direction() default Direction.FORWARD; 15 | 16 | boolean subscribed() default true; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/EventStreams.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import java.lang.annotation.Retention; 4 | import java.lang.annotation.RetentionPolicy; 5 | 6 | @Retention(RetentionPolicy.RUNTIME) 7 | public @interface EventStreams { 8 | EventStream[] value(); 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/ForeignCacheKey.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ElementType.FIELD}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface ForeignCacheKey { 13 | String eventKeyField(); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/OnEvent.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ElementType.METHOD}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface OnEvent { 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/PrimaryCacheKey.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ElementType.FIELD}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface PrimaryCacheKey { 13 | // This is needed for when we have @ForeignCacheKey to know which is the primary 14 | Class creationEvent(); 15 | 16 | String[] alternateCacheKeys() default ""; 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/ReadModel.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ElementType.TYPE}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface ReadModel { 13 | public static final int NEVER_DESTROY = -1; 14 | public static final int DESTROY_IMMEDIATELY = 0; 15 | 16 | boolean ephemeral() default false; 17 | 18 | int destroyInMinutesUnused() default NEVER_DESTROY; 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/annotation/StreamStartPosition.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.annotation; 2 | 3 | import static java.lang.annotation.ElementType.METHOD; 4 | import static java.lang.annotation.RetentionPolicy.RUNTIME; 5 | 6 | import events.dewdrop.read.readmodel.stream.StreamType; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.Target; 9 | 10 | @Target({METHOD}) 11 | @Retention(RUNTIME) 12 | public @interface StreamStartPosition { 13 | String name(); 14 | 15 | StreamType streamType() default StreamType.CATEGORY; 16 | } 17 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/cache/InMemoryCacheProcessor.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.cache; 2 | 3 | import events.dewdrop.structure.api.Message; 4 | 5 | public interface InMemoryCacheProcessor { 6 | void process(T message); 7 | 8 | T getCache(); 9 | 10 | Class getCachedStateObjectType(); 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/cache/SingleItemInMemoryCache.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.cache; 2 | 3 | 4 | import events.dewdrop.utils.DewdropReflectionUtils; 5 | import events.dewdrop.utils.EventHandlerUtils; 6 | import events.dewdrop.structure.api.Message; 7 | import java.util.Optional; 8 | import lombok.Data; 9 | import lombok.extern.log4j.Log4j2; 10 | 11 | @Log4j2 12 | @Data 13 | public class SingleItemInMemoryCache implements InMemoryCacheProcessor { 14 | private Class cachedStateObjectType; 15 | private T cache; 16 | 17 | public SingleItemInMemoryCache(Class cachedStateObjectType) { 18 | this.cachedStateObjectType = cachedStateObjectType; 19 | Optional optInstance = DewdropReflectionUtils.createInstance(cachedStateObjectType); 20 | if (optInstance.isPresent()) { 21 | cache = optInstance.get(); 22 | } 23 | } 24 | 25 | public void process(T message) { 26 | log.debug("Processing message: {} to cache ", message); 27 | 28 | EventHandlerUtils.callEventHandler(this.cache, message); 29 | } 30 | 31 | @Override 32 | public T getCache() { 33 | return cache; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/query/QueryHandler.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.query; 2 | 3 | import java.lang.annotation.Documented; 4 | import java.lang.annotation.ElementType; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | @Target({ElementType.METHOD}) 10 | @Retention(RetentionPolicy.RUNTIME) 11 | @Documented 12 | public @interface QueryHandler { 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/stream/NameAndPosition.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.stream; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import lombok.Builder; 6 | import lombok.Data; 7 | import org.apache.commons.lang3.StringUtils; 8 | 9 | @Data 10 | public class NameAndPosition { 11 | private String streamName; 12 | private Long position; 13 | 14 | private StreamType streamType; 15 | private String name; 16 | 17 | private NameAndPosition() {} 18 | 19 | @Builder(buildMethodName = "create") 20 | public NameAndPosition(StreamType streamType, String name) { 21 | requireNonNull(streamType, "streamType is required"); 22 | requireNonNull(name, "name is required"); 23 | 24 | this.streamType = streamType; 25 | this.name = name; 26 | } 27 | 28 | public boolean isComplete() { 29 | return StringUtils.isNotEmpty(streamName); 30 | } 31 | 32 | public NameAndPosition completeTask(String streamName, Long position) { 33 | this.streamName = streamName; 34 | this.position = position; 35 | return this; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/stream/StreamAnnotationDetails.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.stream; 2 | 3 | import events.dewdrop.read.readmodel.annotation.AggregateStream; 4 | import events.dewdrop.read.readmodel.annotation.CategoryStream; 5 | import events.dewdrop.read.readmodel.annotation.EventStream; 6 | import events.dewdrop.structure.read.Direction; 7 | import lombok.Data; 8 | 9 | import java.lang.annotation.Annotation; 10 | 11 | @Data 12 | public class StreamAnnotationDetails { 13 | String streamName; 14 | StreamType streamType; 15 | boolean subscribed = true; 16 | Direction direction = Direction.FORWARD; 17 | 18 | public StreamAnnotationDetails(Annotation streamAnnotation) { 19 | String annotationName = streamAnnotation.annotationType().getSimpleName(); 20 | switch (annotationName) { 21 | case "AggregateStream": 22 | this.streamType = StreamType.AGGREGATE; 23 | AggregateStream aggregateStream = (AggregateStream) streamAnnotation; 24 | this.streamName = aggregateStream.name(); 25 | this.subscribed = aggregateStream.subscribed(); 26 | this.direction = aggregateStream.direction(); 27 | break; 28 | case "EventStream": 29 | this.streamType = StreamType.EVENT; 30 | EventStream eventStream = (EventStream) streamAnnotation; 31 | this.streamName = eventStream.name(); 32 | this.subscribed = eventStream.subscribed(); 33 | this.direction = eventStream.direction(); 34 | break; 35 | case "CategoryStream": 36 | default: 37 | this.streamType = StreamType.CATEGORY; 38 | CategoryStream categoryStream = (CategoryStream) streamAnnotation; 39 | this.streamName = categoryStream.name(); 40 | this.subscribed = categoryStream.subscribed(); 41 | this.direction = categoryStream.direction(); 42 | break; 43 | } 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/stream/StreamDetails.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.stream; 2 | 3 | import events.dewdrop.structure.StreamNameGenerator; 4 | import events.dewdrop.structure.api.Event; 5 | import events.dewdrop.structure.read.Direction; 6 | import lombok.Builder; 7 | import lombok.Data; 8 | import org.apache.commons.collections4.CollectionUtils; 9 | 10 | import java.lang.reflect.Method; 11 | import java.util.ArrayList; 12 | import java.util.List; 13 | import java.util.Optional; 14 | import java.util.UUID; 15 | import java.util.function.Consumer; 16 | 17 | import static java.util.stream.Collectors.joining; 18 | 19 | @Data 20 | public class StreamDetails { 21 | private StreamType streamType; 22 | private Direction direction; 23 | private List> messageTypes = new ArrayList<>(); 24 | private String streamName; 25 | private Consumer eventHandler; 26 | private StreamNameGenerator streamNameGenerator; 27 | private boolean subscribed; 28 | private SubscriptionStartStrategy subscriptionStartStrategy; 29 | private Optional startPositionMethod; 30 | 31 | 32 | @Builder(buildMethodName = "create") 33 | public StreamDetails(StreamType streamType, String name, List> messageTypes, Consumer eventHandler, Direction direction, String aggregateName, UUID id, Boolean subscribed, StreamNameGenerator streamNameGenerator, 34 | SubscriptionStartStrategy subscriptionStartStrategy, Optional startPositionMethod) { 35 | this.streamType = streamType; 36 | if (CollectionUtils.isNotEmpty(messageTypes)) { 37 | this.messageTypes.addAll(messageTypes); 38 | } 39 | this.eventHandler = eventHandler; 40 | this.direction = direction; 41 | this.streamNameGenerator = streamNameGenerator; 42 | this.subscribed = Optional.ofNullable(subscribed).orElse(true); 43 | this.startPositionMethod = Optional.ofNullable(startPositionMethod).orElse(Optional.empty()); 44 | this.subscriptionStartStrategy = Optional.ofNullable(subscriptionStartStrategy).orElseGet(() -> getStartPositionMethod().isPresent() ? SubscriptionStartStrategy.START_FROM_POSITION : SubscriptionStartStrategy.READ_ALL_START_END); 45 | switch (streamType) { 46 | case EVENT: 47 | this.streamName = streamNameGenerator.generateForEvent(name); 48 | break; 49 | case AGGREGATE: 50 | this.streamName = streamNameGenerator.generateForAggregate(aggregateName, id); 51 | break; 52 | case CATEGORY: 53 | default: 54 | this.streamName = streamNameGenerator.generateForCategory(name); 55 | break; 56 | } 57 | } 58 | 59 | public String getMessageTypeNames() { 60 | return getMessageTypes().stream().map(Class::getSimpleName).collect(joining(", ")); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/stream/StreamListener.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.stream; 2 | 3 | import events.dewdrop.read.readmodel.stream.subscription.Subscription; 4 | import events.dewdrop.structure.NoStreamException; 5 | import events.dewdrop.structure.datastore.StreamStore; 6 | import events.dewdrop.structure.events.ReadEventData; 7 | import events.dewdrop.structure.serialize.EventSerializer; 8 | import events.dewdrop.structure.subscribe.SubscribeRequest; 9 | import events.dewdrop.structure.api.Event; 10 | import java.util.Optional; 11 | import java.util.concurrent.atomic.AtomicLong; 12 | import java.util.function.Consumer; 13 | import lombok.Data; 14 | import lombok.extern.log4j.Log4j2; 15 | 16 | @Log4j2 17 | @Data 18 | public class StreamListener { 19 | private StreamStore streamStore; 20 | private EventSerializer serializer; 21 | private String streamName; 22 | private AtomicLong streamPosition; 23 | 24 | private StreamListener(StreamStore streamStore, EventSerializer serializer) { 25 | this.streamStore = streamStore; 26 | this.serializer = serializer; 27 | this.streamPosition = new AtomicLong(0L); 28 | } 29 | 30 | public static StreamListener getInstance(StreamStore streamStore, EventSerializer serializer) { 31 | return new StreamListener(streamStore, serializer); 32 | } 33 | 34 | public boolean start(String streamName, Long checkpoint, Subscription subscription) throws NoStreamException { 35 | this.streamName = streamName; 36 | return subscribe(checkpoint, onEvent(subscription)); 37 | } 38 | 39 | protected Consumer onEvent(Subscription subscription) { 40 | return readEventData -> { 41 | Optional deserializedEvent = serializer.deserialize(readEventData); 42 | if (deserializedEvent.isPresent()) { 43 | subscription.publish(deserializedEvent.get()); 44 | streamPosition.setRelease(readEventData.getEventNumber()); 45 | return; 46 | } else { 47 | log.error("Failed to deserialize event:" + readEventData.getEventType()); 48 | } 49 | }; 50 | } 51 | 52 | boolean subscribe(Long lastCheckpoint, Consumer eventHandler) throws NoStreamException { 53 | SubscribeRequest subscribeRequest = new SubscribeRequest(streamName, lastCheckpoint, eventHandler); 54 | return streamStore.subscribeToStream(subscribeRequest); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/stream/StreamType.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.stream; 2 | 3 | public enum StreamType { 4 | CATEGORY, EVENT, AGGREGATE; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/stream/SubscriptionStartStrategy.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.stream; 2 | 3 | public enum SubscriptionStartStrategy { 4 | START_FROM_POSITION, READ_ALL_START_END, START_END_ONLY; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/read/readmodel/stream/subscription/Subscription.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.stream.subscription; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import events.dewdrop.read.readmodel.stream.NameAndPosition; 6 | import events.dewdrop.read.readmodel.stream.SubscriptionStartStrategy; 7 | import events.dewdrop.structure.subscribe.EventProcessor; 8 | import events.dewdrop.read.readmodel.stream.Stream; 9 | import events.dewdrop.read.readmodel.stream.StreamListener; 10 | import events.dewdrop.read.readmodel.stream.StreamReader; 11 | import events.dewdrop.structure.api.Event; 12 | import events.dewdrop.structure.read.Handler; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | import java.util.Map; 16 | import java.util.Optional; 17 | import java.util.concurrent.CompletableFuture; 18 | import java.util.concurrent.ConcurrentHashMap; 19 | import java.util.concurrent.Executors; 20 | import java.util.concurrent.ScheduledExecutorService; 21 | import java.util.concurrent.ScheduledFuture; 22 | import java.util.concurrent.TimeUnit; 23 | import lombok.Data; 24 | import lombok.extern.log4j.Log4j2; 25 | 26 | @Log4j2 27 | @Data 28 | public class Subscription { 29 | private final Map, List>> handlers = new ConcurrentHashMap<>(); 30 | protected final StreamListener listener; 31 | private final List> messageTypes; 32 | private final Handler handler; 33 | private final ScheduledExecutorService executorService; 34 | 35 | Subscription(Handler handler, List> messageTypes, StreamListener listener) { 36 | this.messageTypes = messageTypes; 37 | this.handler = handler; 38 | this.executorService = Executors.newSingleThreadScheduledExecutor(); 39 | this.listener = listener; 40 | registerHandlers(); 41 | } 42 | 43 | public static Subscription getInstance(Stream stream) { 44 | return new Subscription<>(stream, stream.getStreamDetails().getMessageTypes(), StreamListener.getInstance(stream.getStreamStore(), stream.getEventSerializer())); 45 | } 46 | 47 | void registerToMessageType(EventProcessor eventProcessor, Class eventType) { 48 | List> handlesFor = getHandlesFor(eventType); 49 | boolean isSame = handlesFor.stream().anyMatch(handle -> handle.isSame(eventType, eventProcessor)); 50 | if (!isSame) { 51 | synchronized (this.handlers) { 52 | this.handlers.computeIfAbsent(eventType, item -> new ArrayList<>()); 53 | this.handlers.get(eventType).add(eventProcessor); 54 | } 55 | } 56 | } 57 | 58 | List> getHandlesFor(Class type) { 59 | requireNonNull(type, "Type is required"); 60 | 61 | synchronized (this.handlers) { 62 | if (this.handlers.containsKey(type)) { return new ArrayList<>(this.handlers.get(type)); } 63 | return new ArrayList<>(); 64 | } 65 | } 66 | 67 | public void registerHandlers() { 68 | EventProcessor allHandler = new EventProcessor<>(handler, getMessageTypes()); 69 | 70 | for (Class currentMessageType : getMessageTypes()) { 71 | registerToMessageType(allHandler, currentMessageType); 72 | } 73 | } 74 | 75 | public void publish(T event) { 76 | requireNonNull(event, "event is required"); 77 | 78 | log.debug("Publishing event:{}, handlers: {}", event.getClass().getSimpleName(), this.handlers.size()); 79 | // Call each handler registered to the event type. 80 | List> eventProcessors = getHandlesFor(event.getClass()); 81 | 82 | eventProcessors.forEach(handle -> handle.process(event)); 83 | } 84 | 85 | public boolean subscribeByNameAndPosition(StreamReader streamReader) { 86 | if (!streamReader.validateStreamName(streamReader.getStreamName())) { return false; } 87 | NameAndPosition nameAndPosition = streamReader.nameAndPosition(); 88 | if (!streamReader.isStreamExists()) { return false; } 89 | boolean subscribed = listener.start(nameAndPosition.getStreamName(), nameAndPosition.getPosition(), this); 90 | if (subscribed) { 91 | log.info("Completed subscription to stream: {} from position:{}", nameAndPosition.getStreamName(), nameAndPosition.getPosition()); 92 | } 93 | return subscribed; 94 | } 95 | 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/streamstore/process/AggregateStateCommandProcessor.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.streamstore.process; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import events.dewdrop.aggregate.AggregateRoot; 6 | import events.dewdrop.api.result.Result; 7 | import events.dewdrop.api.validators.ValidationException; 8 | import events.dewdrop.utils.AggregateUtils; 9 | import events.dewdrop.structure.api.Command; 10 | import events.dewdrop.utils.AggregateIdUtils; 11 | import java.lang.reflect.Method; 12 | import java.util.Optional; 13 | import java.util.UUID; 14 | import lombok.extern.log4j.Log4j2; 15 | 16 | /** 17 | * This class is responsible for processing commands that mutate the state of an aggregate 18 | */ 19 | @Log4j2 20 | public class AggregateStateCommandProcessor { 21 | AggregateRootLifecycle streamProcessor; 22 | 23 | private AggregateStateCommandProcessor() {} 24 | 25 | public AggregateStateCommandProcessor(AggregateRootLifecycle aggregateRootLifecycle) { 26 | requireNonNull(aggregateRootLifecycle, "StreamProcessor is required"); 27 | 28 | this.streamProcessor = aggregateRootLifecycle; 29 | } 30 | 31 | /** 32 | * This method constructs the AggregateRoot and processes the command by invoking the AggregateRoot 33 | * lifecycle. 34 | * 35 | * @param command The command to be processed. 36 | * @param commandHandlerMethod The method that will be invoked to process the command. 37 | * @return A {@code Result} 38 | * @throws ValidationException If the command is invalid. 39 | */ 40 | public Result processCommand(Command command, Method commandHandlerMethod) throws ValidationException { 41 | Optional optAggregateRoot = AggregateUtils.createFromCommandHandlerMethod(commandHandlerMethod); 42 | if (optAggregateRoot.isPresent()) { 43 | AggregateRoot aggregateRoot = optAggregateRoot.get(); 44 | Optional optAggregateId = AggregateIdUtils.getAggregateId(command); 45 | if (optAggregateId.isPresent()) { return process(command, commandHandlerMethod, aggregateRoot, optAggregateId.get()); } 46 | } 47 | return Result.of(false); 48 | } 49 | 50 | /** 51 | * It takes a command, a command handler method, an aggregate root and an aggregate root id. Then it 52 | * invokes the AggregateRoot lifecycle and returns a result of a boolean 53 | * 54 | * @param command The command to be processed 55 | * @param commandHandlerMethod The method that will be invoked to process the command. 56 | * @param aggregateRoot The aggregate root that the command is being applied to. 57 | * @param aggregateRootId The id of the aggregate root that the command is being sent to. 58 | * @return A {@code Result} 59 | * @throws ValidationException If the command is invalid. 60 | */ 61 | Result process(Command command, Method commandHandlerMethod, AggregateRoot aggregateRoot, UUID aggregateRootId) throws ValidationException { 62 | log.debug("Processing command {}", command.getClass().getSimpleName()); 63 | return streamProcessor.process(command, commandHandlerMethod, aggregateRoot, aggregateRootId); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/streamstore/process/StandaloneAggregateProcessor.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.streamstore.process; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import events.dewdrop.aggregate.AggregateRoot; 6 | import events.dewdrop.read.readmodel.stream.StreamFactory; 7 | import events.dewdrop.utils.AggregateUtils; 8 | import events.dewdrop.read.readmodel.stream.Stream; 9 | import events.dewdrop.streamstore.repository.StreamStoreGetByIDRequest; 10 | import events.dewdrop.structure.api.Command; 11 | import events.dewdrop.utils.AggregateIdUtils; 12 | import java.util.Optional; 13 | import java.util.UUID; 14 | 15 | public class StandaloneAggregateProcessor { 16 | StreamFactory streamFactory; 17 | 18 | public StandaloneAggregateProcessor(StreamFactory streamFactory) { 19 | this.streamFactory = streamFactory; 20 | } 21 | 22 | public AggregateRoot getById(Object aggregateObject, UUID id) { 23 | return getById(aggregateObject, id, null); 24 | } 25 | 26 | public AggregateRoot getById(Object aggregateObject, UUID id, Command command) { 27 | requireNonNull(aggregateObject, "aggregate is required"); 28 | requireNonNull(id, "UUID is required"); 29 | 30 | AggregateRoot aggregateRoot = getAggregateRoot(aggregateObject); 31 | 32 | Stream stream = streamFactory.constructStreamFromAggregateRoot(aggregateRoot, id); 33 | 34 | StreamStoreGetByIDRequest request = StreamStoreGetByIDRequest.builder().aggregateRoot(aggregateRoot).id(id).command(command).create(); 35 | aggregateRoot = stream.getById(request); 36 | 37 | return aggregateRoot; 38 | } 39 | 40 | AggregateRoot getAggregateRoot(Object aggregateObject) { 41 | AggregateRoot aggregateRoot; 42 | if (aggregateObject instanceof AggregateRoot) { 43 | aggregateRoot = (AggregateRoot) aggregateObject; 44 | } else { 45 | aggregateRoot = AggregateUtils.create(aggregateObject.getClass()).orElse(null); 46 | } 47 | return aggregateRoot; 48 | } 49 | 50 | public AggregateRoot save(AggregateRoot aggregateRoot) { 51 | Optional aggregateId = AggregateIdUtils.getAggregateId(aggregateRoot.getTarget()); 52 | if (aggregateId.isEmpty()) { throw new IllegalArgumentException("Aggregate ID is not set"); } 53 | streamFactory.constructStreamFromAggregateRoot(aggregateRoot, aggregateId.get()).save(aggregateRoot); 54 | return aggregateRoot; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/streamstore/repository/StreamStoreGetByIDRequest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.streamstore.repository; 2 | 3 | import events.dewdrop.aggregate.AggregateRoot; 4 | import events.dewdrop.structure.api.Command; 5 | import java.util.Optional; 6 | import java.util.UUID; 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | 10 | @Getter 11 | public class StreamStoreGetByIDRequest { 12 | private AggregateRoot aggregateRoot; 13 | private UUID id; 14 | private int version; 15 | private Command command; 16 | 17 | @Builder(buildMethodName = "create") 18 | public StreamStoreGetByIDRequest(AggregateRoot aggregateRoot, UUID id, Integer version, Command command) { 19 | this.aggregateRoot = aggregateRoot; 20 | this.id = id; 21 | this.version = Optional.ofNullable(version).orElse(Integer.MAX_VALUE); 22 | this.command = command; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/streamstore/serialize/JsonSerializer.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.streamstore.serialize; 2 | 3 | import java.io.IOException; 4 | import java.util.HashMap; 5 | import java.util.Map; 6 | import java.util.Optional; 7 | import java.util.UUID; 8 | 9 | import com.fasterxml.jackson.core.JsonProcessingException; 10 | import com.fasterxml.jackson.databind.ObjectMapper; 11 | import events.dewdrop.streamstore.write.StreamWriter; 12 | import events.dewdrop.structure.api.Event; 13 | import events.dewdrop.structure.events.ReadEventData; 14 | import events.dewdrop.structure.events.WriteEventData; 15 | import events.dewdrop.structure.serialize.EventSerializer; 16 | import lombok.extern.log4j.Log4j2; 17 | import org.apache.commons.lang3.StringUtils; 18 | 19 | @Log4j2 20 | public class JsonSerializer implements EventSerializer { 21 | private ObjectMapper objectMapper; 22 | public static final String EVENT_CLASS = "EventFullClassName"; 23 | 24 | public JsonSerializer(ObjectMapper objectMapper) { 25 | this.objectMapper = objectMapper; 26 | } 27 | 28 | @Override 29 | public Optional serialize(Object event) { 30 | return serialize(event, null); 31 | } 32 | 33 | @Override 34 | public Optional serialize(Object event, Map headers) { 35 | headers = Optional.ofNullable(headers).orElse(new HashMap<>()); 36 | 37 | headers.computeIfAbsent(EVENT_CLASS, name -> event.getClass().getName()); 38 | 39 | String typeName = event.getClass().getSimpleName(); 40 | 41 | try { 42 | byte[] metadata = objectMapper.writeValueAsBytes(headers); 43 | byte[] data = objectMapper.writeValueAsBytes(event); 44 | WriteEventData writeEventData = new WriteEventData(UUID.randomUUID(), typeName, true, data, metadata); 45 | return Optional.of(writeEventData); 46 | } catch (JsonProcessingException e) { 47 | log.error("problem serializing json for type:" + typeName, e); 48 | return Optional.empty(); 49 | } 50 | } 51 | 52 | @Override 53 | public Optional deserialize(ReadEventData event) { 54 | Map metadata = new HashMap<>(); 55 | try { 56 | metadata = objectMapper.readValue(event.getMetadata(), Map.class); 57 | } catch (IOException e) { 58 | Integer length = event.getMetadata() == null ? 0 : event.getMetadata().length; 59 | 60 | log.error("problem deserialize metadata for event {} - size of metaData:{}", event.getEventType(), length, e); 61 | } 62 | String className = (String) metadata.get(EVENT_CLASS); 63 | if (StringUtils.isBlank(className)) { 64 | log.error("className not found for eventType:{}", event.getEventType()); 65 | return Optional.empty(); 66 | } 67 | 68 | return deserializeEvent(event, className, metadata); 69 | } 70 | 71 | public Optional deserializeEvent(ReadEventData event, String className, Map metadata) { 72 | try { 73 | T value = (T) objectMapper.readValue(event.getData(), Class.forName(className)); 74 | value.setEventId(event.getEventId()); 75 | value.setPosition(event.getEventNumber()); 76 | value.setCreated(event.getCreated()); 77 | if (metadata.containsKey(StreamWriter.CAUSATION_ID)) { 78 | String uuid = (String) metadata.get(StreamWriter.CAUSATION_ID); 79 | value.setCausationId(UUID.fromString(uuid)); 80 | } 81 | if (metadata.containsKey(StreamWriter.CORRELATION_ID)) { 82 | String uuid = (String) metadata.get(StreamWriter.CORRELATION_ID); 83 | value.setCorrelationId(UUID.fromString(uuid)); 84 | } 85 | return Optional.of(value); 86 | } catch (IOException e) { 87 | log.error("Unable to deserialize data for class:" + className, e); 88 | return Optional.empty(); 89 | } catch (ClassNotFoundException e) { 90 | log.error("Unable to deserialize data - class not found:" + className, e); 91 | return Optional.empty(); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/streamstore/stream/PrefixStreamNameGenerator.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.streamstore.stream; 2 | 3 | import events.dewdrop.structure.StreamNameGenerator; 4 | import org.apache.commons.lang3.StringUtils; 5 | 6 | import java.util.Locale; 7 | import java.util.UUID; 8 | 9 | /** 10 | * The type Prefix stream name generator. 11 | */ 12 | public class PrefixStreamNameGenerator implements StreamNameGenerator { 13 | private final String prefix; 14 | 15 | /** 16 | * Instantiates a new Prefix stream name generator without a prefix 17 | */ 18 | public PrefixStreamNameGenerator() { 19 | prefix = ""; 20 | } 21 | 22 | /** 23 | * Instantiates a new Prefix stream name generator that will generate streamNames with a given 24 | * prefix. 25 | * 26 | * @param prefix the prefix 27 | */ 28 | public PrefixStreamNameGenerator(String prefix) { 29 | this.prefix = StringUtils.isNotEmpty(prefix) ? prefix.toLowerCase(Locale.ROOT) : ""; 30 | } 31 | 32 | /** 33 | * It takes the aggregate class name, and the ID and generates the stream name. For Example: 34 | * DewdropUserAggregate-fc19e182-045a-4f91-9c61-ae081383ed36 35 | * 36 | * @param aggregateName The class of the aggregate. 37 | * @param id The id of the aggregate 38 | * @return A string that is the name of the stream. 39 | */ 40 | @Override 41 | public String generateForAggregate(String aggregateName, UUID id) { 42 | StringBuilder builder = new StringBuilder(); 43 | if (!StringUtils.startsWith(aggregateName, prefix)) { 44 | appendPrefix(builder); 45 | } 46 | builder.append(aggregateName); 47 | if (id != null) { 48 | builder.append("-").append(id); 49 | } 50 | return builder.toString(); 51 | } 52 | 53 | private void appendPrefix(StringBuilder builder) { 54 | if (StringUtils.isNotEmpty(prefix)) { 55 | builder.append(prefix).append("."); 56 | } 57 | } 58 | 59 | /** 60 | * It takes the aggregate class returns a string that is the name of the category stream 61 | * 62 | * @param aggregateClass The class of the aggregate root. 63 | * @return The name of the aggregate class. 64 | */ 65 | @Override 66 | public String generateForCategory(Class aggregateClass) { 67 | return generateForCategory(aggregateClass.getSimpleName()); 68 | } 69 | 70 | /** 71 | * It takes the aggregate class name and returns a string that is the name of the category stream 72 | * 73 | * @param aggregateClassName The name of the aggregate class. 74 | * @return A string that is the stream name of the category. 75 | */ 76 | @Override 77 | public String generateForCategory(String aggregateClassName) { 78 | StringBuilder builder = new StringBuilder(); 79 | builder.append("$ce").append("-"); 80 | appendPrefix(builder); 81 | builder.append(aggregateClassName); 82 | return builder.toString(); 83 | } 84 | 85 | /** 86 | * It takes the event class name and returns a string that is the name of the event stream 87 | * 88 | * @param aggregateClassName The class name of the event. 89 | * @return The event stream for the given event class name. 90 | */ 91 | @Override 92 | public String generateForEvent(String aggregateClassName) { 93 | StringBuilder builder = new StringBuilder(); 94 | builder.append("$et").append("-"); 95 | // appendPrefix(builder); 96 | builder.append(aggregateClassName); 97 | return builder.toString(); 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/streamstore/write/StreamWriter.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.streamstore.write; 2 | 3 | import events.dewdrop.aggregate.AggregateRoot; 4 | import events.dewdrop.read.readmodel.stream.StreamDetails; 5 | import events.dewdrop.structure.datastore.StreamStore; 6 | import events.dewdrop.structure.events.WriteEventData; 7 | import events.dewdrop.structure.serialize.EventSerializer; 8 | import events.dewdrop.structure.write.WriteRequest; 9 | import events.dewdrop.utils.AggregateIdUtils; 10 | import events.dewdrop.structure.api.Message; 11 | import java.util.ArrayList; 12 | import java.util.HashMap; 13 | import java.util.List; 14 | import java.util.Map; 15 | import java.util.Optional; 16 | import java.util.UUID; 17 | import lombok.extern.log4j.Log4j2; 18 | 19 | @Log4j2 20 | public class StreamWriter { 21 | 22 | protected StreamDetails streamDetails; 23 | private StreamStore streamStore; 24 | private EventSerializer eventSerializer; 25 | 26 | public static final String AGGREGATE_CLR_TYPE_NAME = "aggregateClassName"; 27 | public static final String COMMIT_ID_HEADER = "commitId"; 28 | public static final String MESSAGE_ID = "messageId"; 29 | public static final String CORRELATION_ID = "correlationId"; 30 | public static final String CAUSATION_ID = "causationId"; 31 | private static final int READ_PAGE_SIZE = 500; 32 | 33 | 34 | private StreamWriter(StreamDetails streamDetails, StreamStore streamStore, EventSerializer eventSerializer) { 35 | this.streamDetails = streamDetails; 36 | this.streamStore = streamStore; 37 | this.eventSerializer = eventSerializer; 38 | } 39 | 40 | public static StreamWriter getInstance(StreamDetails streamDetails, StreamStore streamStore, EventSerializer eventSerializer) { 41 | return new StreamWriter(streamDetails, streamStore, eventSerializer); 42 | } 43 | 44 | public void save(AggregateRoot aggregateRoot) { 45 | Optional aggregateId = AggregateIdUtils.getAggregateId(aggregateRoot.getTarget()); 46 | 47 | if (aggregateId.isEmpty()) { throw new IllegalArgumentException("There is no aggregateId to persist"); } 48 | 49 | long expectedVersion = aggregateRoot.getVersion(); 50 | List newMessages = aggregateRoot.takeEvents(); 51 | List eventsToSave = generateEventsToSave(aggregateRoot, newMessages); 52 | WriteRequest request = new WriteRequest(streamDetails.getStreamName(), expectedVersion, eventsToSave); 53 | streamStore.appendToStream(request); 54 | } 55 | 56 | Map commitHeaders(AggregateRoot aggregateRoot) { 57 | Map commitHeaders = new HashMap<>(); 58 | commitHeaders.put(COMMIT_ID_HEADER, UUID.randomUUID()); 59 | commitHeaders.put(AGGREGATE_CLR_TYPE_NAME, aggregateRoot.getTargetClassName()); 60 | 61 | if (aggregateRoot.getCausationId() != null) { 62 | commitHeaders.put(CAUSATION_ID, aggregateRoot.getCausationId()); 63 | } 64 | if (aggregateRoot.getCorrelationId() != null) { 65 | commitHeaders.put(CORRELATION_ID, aggregateRoot.getCorrelationId()); 66 | } 67 | return commitHeaders; 68 | } 69 | 70 | List generateEventsToSave(AggregateRoot aggregateRoot, List newMessages) { 71 | Map commitHeaders = commitHeaders(aggregateRoot); 72 | List eventsToSave = new ArrayList<>(); 73 | for (Message message : newMessages) { 74 | Optional serializedAggregate = eventSerializer.serialize(message, new HashMap<>(commitHeaders)); 75 | if (serializedAggregate.isEmpty()) { throw new IllegalStateException("Failed to serialize event: " + message.getClass().getSimpleName()); } 76 | eventsToSave.add(serializedAggregate.get()); 77 | } 78 | return eventsToSave; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/NoStreamException.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class NoStreamException extends RuntimeException { 7 | private String stream; 8 | 9 | public NoStreamException(String stream) { 10 | super(stream); 11 | this.stream = stream; 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/StreamNameGenerator.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure; 2 | 3 | import java.util.UUID; 4 | 5 | public interface StreamNameGenerator { 6 | String generateForAggregate(String aggregateName, UUID id); 7 | 8 | String generateForCategory(Class type); 9 | 10 | String generateForCategory(String category); 11 | 12 | String generateForEvent(String type); 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/api/AbstractMessage.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import java.util.UUID; 5 | 6 | 7 | public class AbstractMessage implements Message { 8 | @JsonIgnore 9 | private UUID messageId; 10 | 11 | public AbstractMessage() { 12 | this.messageId = UUID.randomUUID(); 13 | } 14 | 15 | @Override 16 | public UUID getMessageId() { 17 | return messageId; 18 | } 19 | 20 | public void setMessageId(UUID messageId) { 21 | this.messageId = messageId; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/api/Command.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.api; 2 | 3 | import events.dewdrop.structure.events.CorrelationCausation; 4 | 5 | public abstract class Command extends CorrelationCausation { 6 | 7 | public Command() { 8 | super(); 9 | } 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/api/Event.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.api; 2 | 3 | import java.time.Instant; 4 | import java.util.UUID; 5 | 6 | import events.dewdrop.structure.events.CorrelationCausation; 7 | import lombok.Data; 8 | 9 | @Data 10 | public abstract class Event extends CorrelationCausation { 11 | // version of your event 12 | private Long version; 13 | // event number - position in stream 14 | private Long position; 15 | private UUID eventId; 16 | private Instant created; 17 | 18 | public Event() {} 19 | 20 | public Long getVersion() { 21 | return version; 22 | } 23 | 24 | public void setVersion(Long version) { 25 | this.version = version; 26 | } 27 | 28 | public UUID getEventId() { 29 | return eventId; 30 | } 31 | 32 | public void setEventId(UUID eventId) { 33 | this.eventId = eventId; 34 | } 35 | 36 | public Long getPosition() { 37 | return position; 38 | } 39 | 40 | public void setPosition(Long position) { 41 | this.position = position; 42 | } 43 | 44 | public Instant getCreated() { 45 | return created; 46 | } 47 | 48 | public void setCreated(Instant created) { 49 | this.created = created; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/api/Message.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.api; 2 | 3 | import java.util.UUID; 4 | 5 | public interface Message { 6 | UUID getMessageId(); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/api/ValidationFunction.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.api; 2 | 3 | import events.dewdrop.api.validators.ValidationException; 4 | 5 | @FunctionalInterface 6 | public interface ValidationFunction { 7 | 8 | void validate() throws ValidationException; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/api/validator/DewdropValidator.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.api.validator; 2 | 3 | import static java.util.stream.Collectors.toList; 4 | 5 | import events.dewdrop.api.validators.ValidationError; 6 | import events.dewdrop.api.validators.ValidationException; 7 | import events.dewdrop.api.validators.ValidationResult; 8 | import events.dewdrop.structure.api.ValidationFunction; 9 | import java.util.ArrayList; 10 | import java.util.List; 11 | import java.util.Set; 12 | import jakarta.validation.ConstraintViolation; 13 | import jakarta.validation.Validation; 14 | import jakarta.validation.Validator; 15 | import jakarta.validation.ValidatorFactory; 16 | import events.dewdrop.structure.api.Command; 17 | 18 | public class DewdropValidator { 19 | private static final ValidatorFactory VALIDATOR_FACTORY = Validation.buildDefaultValidatorFactory(); 20 | private static final Validator VALIDATOR = VALIDATOR_FACTORY.getValidator(); 21 | private List validations = new ArrayList<>(); 22 | 23 | private DewdropValidator(ValidationFunction validationFunction) { 24 | validations.add(validationFunction); 25 | } 26 | 27 | public static DewdropValidator withRule(ValidationFunction validationFunction) { 28 | return new DewdropValidator(validationFunction); 29 | } 30 | 31 | public DewdropValidator andRule(ValidationFunction validationFunction) { 32 | validations.add(validationFunction); 33 | return this; 34 | } 35 | 36 | public void validate() throws ValidationException { 37 | ValidationResult results = ValidationResult.valid(); 38 | for (ValidationFunction validationFunction : validations) { 39 | try { 40 | validationFunction.validate(); 41 | } catch (Exception e) { 42 | results.add(ValidationError.of(e.getMessage())); 43 | } 44 | } 45 | if (results.hasErrors()) { throw new ValidationException(results); } 46 | } 47 | 48 | public static void validate(T command) throws ValidationException { 49 | Set> violations = VALIDATOR.validate(command); 50 | if (!violations.isEmpty()) { 51 | List errors = violations.stream().map(ConstraintViolation::getMessage).distinct().map(ValidationError::of).collect(toList()); 52 | ValidationResult result = ValidationResult.of(errors); 53 | throw new ValidationException(result); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/datastore/StreamStore.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.datastore; 2 | 3 | import events.dewdrop.structure.NoStreamException; 4 | import events.dewdrop.structure.events.StreamReadResults; 5 | import events.dewdrop.structure.read.ReadRequest; 6 | import events.dewdrop.structure.subscribe.SubscribeRequest; 7 | import events.dewdrop.structure.write.WriteRequest; 8 | 9 | public interface StreamStore { 10 | StreamReadResults read(ReadRequest readRequest) throws NoStreamException; 11 | 12 | boolean subscribeToStream(SubscribeRequest subscribeRequest) throws NoStreamException; 13 | 14 | void appendToStream(WriteRequest writeRequest); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/events/CorrelationCausation.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.events; 2 | 3 | import events.dewdrop.structure.api.AbstractMessage; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import java.util.UUID; 6 | import lombok.Data; 7 | 8 | @Data 9 | public abstract class CorrelationCausation extends AbstractMessage { 10 | @JsonIgnore 11 | protected UUID correlationId; 12 | @JsonIgnore 13 | protected UUID causationId; 14 | 15 | public CorrelationCausation() { 16 | super(); 17 | this.causationId = null; 18 | this.correlationId = UUID.randomUUID(); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/events/ReadEventData.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.events; 2 | 3 | import java.time.Instant; 4 | import java.util.UUID; 5 | import lombok.Data; 6 | import lombok.Getter; 7 | 8 | @Getter 9 | public class ReadEventData { 10 | protected UUID eventId; 11 | protected String eventType; 12 | protected boolean isJson; 13 | protected byte[] data; 14 | protected byte[] metadata; 15 | private final String eventStreamId; 16 | private final long eventNumber; 17 | private final Instant created; 18 | private final long createdEpoch; 19 | 20 | public ReadEventData(String eventStreamId, UUID eventId, long eventNumber, String eventType, byte[] data, byte[] metadata, boolean isJson, Instant created) { 21 | this.eventId = eventId; 22 | this.eventType = eventType; 23 | this.isJson = isJson; 24 | this.data = data; 25 | this.metadata = metadata; 26 | this.eventStreamId = eventStreamId; 27 | this.eventNumber = eventNumber; 28 | this.created = created; 29 | this.createdEpoch = created.toEpochMilli(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/events/StreamReadResults.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.events; 2 | 3 | import events.dewdrop.structure.read.Direction; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | import java.util.Optional; 7 | import lombok.Builder; 8 | import lombok.Data; 9 | import org.apache.commons.collections4.CollectionUtils; 10 | import org.apache.commons.lang3.StringUtils; 11 | 12 | @Data 13 | public class StreamReadResults { 14 | private String streamName; 15 | private long fromEventNumber; 16 | private Direction direction; 17 | private List events; 18 | private long nextEventPosition; 19 | private long lastEventPosition; 20 | private boolean isEndOfStream; 21 | private boolean streamExists = false; 22 | 23 | @Builder(builderMethodName = "create") 24 | public StreamReadResults(String streamName, long fromEventNumber, Direction direction, List events, long nextEventPosition, long lastEventPosition, boolean isEndOfStream) { 25 | 26 | if (StringUtils.isEmpty(streamName)) { throw new IllegalArgumentException("Stream cannot be null, empty or whitespace"); } 27 | 28 | this.streamName = streamName; 29 | this.fromEventNumber = fromEventNumber; 30 | this.direction = direction; 31 | this.events = Optional.ofNullable(events).orElse(new ArrayList<>()); 32 | this.nextEventPosition = nextEventPosition; 33 | this.lastEventPosition = lastEventPosition; 34 | this.isEndOfStream = isEndOfStream; 35 | this.streamExists = true; 36 | } 37 | 38 | private StreamReadResults() {} 39 | 40 | public static StreamReadResults noStream() { 41 | return new StreamReadResults(); 42 | } 43 | 44 | public boolean isEmpty() { 45 | return CollectionUtils.isEmpty(events); 46 | } 47 | 48 | public static StreamReadResults empty() { 49 | return new StreamReadResults(); 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/events/WriteEventData.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.events; 2 | 3 | import java.util.Optional; 4 | import java.util.UUID; 5 | import lombok.Getter; 6 | import org.apache.commons.lang3.StringUtils; 7 | 8 | @Getter 9 | public class WriteEventData { 10 | protected UUID eventId; 11 | protected String eventType; 12 | protected boolean isJson; 13 | protected byte[] data; 14 | protected byte[] metadata; 15 | 16 | public WriteEventData(UUID eventId, String eventType, boolean isJson, byte[] data, byte[] metadata) { 17 | if (StringUtils.isEmpty(eventType)) 18 | throw new IllegalArgumentException("Type cannot be null, empty or whitespace"); 19 | 20 | this.eventId = eventId; 21 | this.eventType = eventType; 22 | this.isJson = isJson; 23 | this.data = Optional.ofNullable(data).orElse(new byte[0]); 24 | this.metadata = Optional.ofNullable(metadata).orElse(new byte[0]); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/read/Direction.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.read; 2 | 3 | public enum Direction { 4 | FORWARD, BACKWARD; 5 | } 6 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/read/Handler.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.read; 2 | 3 | import events.dewdrop.structure.api.Message; 4 | 5 | public interface Handler { 6 | void handle(T event); 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/read/ReadRequest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.read; 2 | 3 | import events.dewdrop.read.readmodel.stream.StreamDetails; 4 | import java.util.Optional; 5 | import lombok.Data; 6 | import lombok.ToString; 7 | 8 | @Data 9 | @ToString 10 | public class ReadRequest { 11 | String streamName; 12 | Long start; 13 | Long count; 14 | Direction direction; 15 | 16 | public ReadRequest(String streamName, Long start, Long count, Direction direction) { 17 | this.streamName = streamName; 18 | this.count = Optional.ofNullable(count).orElse(Long.MAX_VALUE); 19 | this.start = start; 20 | this.direction = direction; 21 | } 22 | 23 | public static ReadRequest from(StreamDetails streamDetails, Long start, Long count) { 24 | return new ReadRequest(streamDetails.getStreamName(), start, count, streamDetails.getDirection()); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/serialize/EventSerializer.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.serialize; 2 | 3 | import events.dewdrop.structure.api.Event; 4 | import events.dewdrop.structure.events.ReadEventData; 5 | import events.dewdrop.structure.events.WriteEventData; 6 | import java.util.Map; 7 | import java.util.Optional; 8 | 9 | public interface EventSerializer { 10 | Optional serialize(Object event, Map headers); 11 | 12 | Optional serialize(Object event); 13 | 14 | Optional deserialize(ReadEventData event); 15 | } 16 | 17 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/subscribe/EventProcessor.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.subscribe; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import events.dewdrop.structure.api.Event; 6 | import events.dewdrop.structure.read.Handler; 7 | import java.util.List; 8 | import lombok.Data; 9 | 10 | @Data 11 | public class EventProcessor { 12 | private Handler handler; 13 | private List> messageTypes; 14 | 15 | public EventProcessor(Handler handler, List> messageTypes) { 16 | requireNonNull(handler, "Handler is required"); 17 | requireNonNull(messageTypes, "messageTypes is required"); 18 | 19 | this.handler = handler; 20 | this.messageTypes = messageTypes; 21 | } 22 | 23 | public void process(T event) { 24 | messageTypes.stream().forEach(messageType -> { 25 | if (event != null && messageType.isAssignableFrom(event.getClass())) { 26 | T msg = (T) messageType.cast(event); 27 | handler.handle(msg); // if this throws let it bubble up. 28 | } 29 | }); 30 | } 31 | 32 | public boolean isSame(Class messagesType, Object handler) { 33 | 34 | return messageTypes.contains(messagesType) && handler.getClass().equals(this.handler.getClass()); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/subscribe/SubscribeRequest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.subscribe; 2 | 3 | import events.dewdrop.structure.events.ReadEventData; 4 | import java.util.Optional; 5 | import java.util.function.Consumer; 6 | import lombok.Data; 7 | 8 | @Data 9 | public class SubscribeRequest { 10 | String streamName; 11 | Long lastCheckpoint; 12 | Consumer consumeEvent; 13 | 14 | public SubscribeRequest(String streamName, Long lastCheckpoint, Consumer consumeEvent) { 15 | this.streamName = streamName; 16 | this.lastCheckpoint = Optional.ofNullable(lastCheckpoint).orElse(0L); 17 | this.consumeEvent = consumeEvent; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/structure/write/WriteRequest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.write; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | import static org.apache.commons.lang3.ObjectUtils.requireNonEmpty; 5 | 6 | import events.dewdrop.structure.events.WriteEventData; 7 | import java.util.List; 8 | import lombok.Data; 9 | 10 | @Data 11 | public class WriteRequest { 12 | String streamName; 13 | Long expectedVersion; 14 | List events; 15 | 16 | public WriteRequest(String streamName, Long expectedVersion, List events) { 17 | requireNonNull(streamName, "streamName is required"); 18 | requireNonNull(events, "events is required"); 19 | requireNonEmpty(events, "events is required"); 20 | 21 | this.streamName = streamName; 22 | this.expectedVersion = expectedVersion; 23 | this.events = events; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/utils/AggregateIdUtils.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import events.dewdrop.aggregate.AggregateRoot; 4 | import events.dewdrop.aggregate.annotation.AggregateId; 5 | import lombok.extern.log4j.Log4j2; 6 | import org.apache.commons.collections4.CollectionUtils; 7 | 8 | import java.lang.reflect.Field; 9 | import java.util.ArrayList; 10 | import java.util.Optional; 11 | import java.util.Set; 12 | import java.util.UUID; 13 | 14 | @Log4j2 15 | public class AggregateIdUtils { 16 | private AggregateIdUtils() {} 17 | 18 | public static Optional getAggregateId(AggregateRoot aggregateRoot) { 19 | return getAggregateId(aggregateRoot.getTarget()); 20 | } 21 | 22 | public static boolean hasAggregateId(Object target) { 23 | Set annotatedFields = DewdropAnnotationUtils.getAnnotatedFields(target.getClass(), AggregateId.class); 24 | return CollectionUtils.isNotEmpty(annotatedFields); 25 | } 26 | 27 | public static Optional getAggregateId(Object target) { 28 | Set annotatedFields = DewdropAnnotationUtils.getAnnotatedFields(target.getClass(), AggregateId.class); 29 | Class superclass = target.getClass(); 30 | 31 | while (CollectionUtils.isEmpty(annotatedFields)) { 32 | annotatedFields = DewdropAnnotationUtils.getAnnotatedFields(superclass, AggregateId.class); 33 | superclass = superclass.getSuperclass(); 34 | if (superclass == null || superclass.getSimpleName().equals("Object")) { 35 | break; 36 | } 37 | } 38 | 39 | if (CollectionUtils.isEmpty(annotatedFields)) { 40 | log.error("No field was marked @AggregateId on {}", target.getClass().getSimpleName()); 41 | throw new IllegalArgumentException("Missing @AggregateId annotation"); 42 | } 43 | 44 | if (annotatedFields.size() > 1) { 45 | log.error("Too many @AggregateId annotations were found on {}", target.getClass().getSimpleName()); 46 | throw new IllegalArgumentException("Too many @AggregateId annotations"); 47 | } 48 | 49 | Field field = new ArrayList<>(annotatedFields).get(0); 50 | UUID instance = DewdropReflectionUtils.readFieldValue(field, target); 51 | if (instance != null) { return Optional.of(instance); } 52 | return Optional.empty(); 53 | 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/utils/AggregateUtils.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import events.dewdrop.aggregate.AggregateRoot; 4 | import events.dewdrop.aggregate.annotation.Aggregate; 5 | import events.dewdrop.structure.api.Command; 6 | import java.lang.reflect.InvocationTargetException; 7 | import java.lang.reflect.Method; 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.util.Optional; 13 | import java.util.Set; 14 | import lombok.extern.log4j.Log4j2; 15 | import org.apache.commons.collections4.CollectionUtils; 16 | import org.apache.commons.lang3.reflect.ConstructorUtils; 17 | 18 | @Log4j2 19 | public class AggregateUtils { 20 | private AggregateUtils() {} 21 | 22 | private static final List> AGGREGATE_ROOTS_CACHE = new ArrayList<>(); 23 | private static final Map, List> AGGREGATE_ROOTS_METHOD_CACHE = new HashMap<>(); 24 | 25 | public static List> getAggregateRootsThatSupportCommand(Command command) { 26 | if (AGGREGATE_ROOTS_CACHE.isEmpty()) { 27 | getAnnotatedAggregateRoots(); 28 | } 29 | 30 | List> result = new ArrayList<>(); 31 | 32 | List methods = AGGREGATE_ROOTS_METHOD_CACHE.get(command.getClass()); 33 | if (!CollectionUtils.isEmpty(methods)) { 34 | methods.forEach(method -> result.add(method.getDeclaringClass())); 35 | } 36 | 37 | if (CollectionUtils.isEmpty(result)) { 38 | log.error("No AggregateRoots found that have an @CommandHandler for handle({} command)", command.getClass().getSimpleName()); 39 | } 40 | 41 | return result; 42 | } 43 | 44 | public static List> getAnnotatedAggregateRoots() { 45 | if (!AGGREGATE_ROOTS_CACHE.isEmpty()) { return AGGREGATE_ROOTS_CACHE; } 46 | 47 | Set> aggregates = DewdropAnnotationUtils.getAnnotatedClasses(Aggregate.class); 48 | 49 | aggregates.forEach(aggregate -> { 50 | log.info("Registering class annotated as @AggregateRoot {}", aggregate.getSimpleName()); 51 | AGGREGATE_ROOTS_CACHE.add(aggregate); 52 | 53 | List commandHandlersForAggregateRoot = CommandHandlerUtils.getCommandHandlersForAggregateRoot(aggregate); 54 | commandHandlersForAggregateRoot.forEach(method -> { 55 | AGGREGATE_ROOTS_METHOD_CACHE.computeIfAbsent(method.getParameterTypes()[0], key -> new ArrayList<>()).add(method); 56 | }); 57 | }); 58 | 59 | if (CollectionUtils.isEmpty(AGGREGATE_ROOTS_CACHE)) { 60 | log.error("No AggregateRoots found - Make sure to annotate your aggregateRoots with @Aggregate"); 61 | } 62 | return AGGREGATE_ROOTS_CACHE; 63 | } 64 | 65 | static void clear() { 66 | AGGREGATE_ROOTS_CACHE.clear(); 67 | } 68 | 69 | public static Optional create(Class classToProxy) { 70 | // ClassPool pool = ClassPool.getDefault(); 71 | try { 72 | // ProxyFactory factory = new ProxyFactory(); 73 | // factory.setSuperclass(classToProxy); 74 | // 75 | // MethodHandler handler = new AggregateHandler<>(); 76 | // Object instance = factory.create(null, null, handler); 77 | 78 | Object instance = ConstructorUtils.invokeConstructor(classToProxy); 79 | return Optional.of(new AggregateRoot(instance)); 80 | } catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) { 81 | log.error("Failed to assign AggregateRoot", e); 82 | return Optional.empty(); 83 | } 84 | } 85 | 86 | public static Optional createFromCommandHandlerMethod(Method commandHandlerMethod) { 87 | Class aggregateClass = CommandHandlerUtils.getAggregateRootClassFromCommandHandlerMethod(commandHandlerMethod); 88 | 89 | return create(aggregateClass); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/utils/AssignCorrelationAndCausation.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import events.dewdrop.structure.events.CorrelationCausation; 4 | 5 | public class AssignCorrelationAndCausation { 6 | private AssignCorrelationAndCausation() {} 7 | 8 | public static T assignTo(CorrelationCausation previous, T command) { 9 | command.setCausationId(previous.getMessageId()); 10 | command.setCorrelationId(previous.getCorrelationId()); 11 | return command; 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/utils/CommandHandlerUtils.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import java.lang.reflect.Method; 4 | import java.util.List; 5 | import java.util.Set; 6 | 7 | import static java.util.Objects.requireNonNull; 8 | 9 | import events.dewdrop.aggregate.AggregateRoot; 10 | import events.dewdrop.api.result.Result; 11 | import events.dewdrop.command.CommandHandler; 12 | import events.dewdrop.structure.api.Command; 13 | import lombok.extern.log4j.Log4j2; 14 | import org.apache.commons.lang3.reflect.MethodUtils; 15 | 16 | @Log4j2 17 | public class CommandHandlerUtils { 18 | private CommandHandlerUtils() {} 19 | 20 | private static final String COMMAND_HANDLER = CommandHandler.class.getSimpleName(); 21 | 22 | public static Result executeCommand(Method commandHandlerMethod, Command command, AggregateRoot aggregateRoot) { 23 | requireNonNull(commandHandlerMethod, "We must have a method decorated with @" + COMMAND_HANDLER); 24 | requireNonNull(command, "We must have a command to pass to the @" + COMMAND_HANDLER); 25 | 26 | try { 27 | Object instance = aggregateRoot.getTarget(); 28 | return executeCommand(instance, commandHandlerMethod, command, aggregateRoot); 29 | } catch (IllegalArgumentException e) { 30 | log.error("We were unable to call the command handler on {} - message: {}", commandHandlerMethod.getDeclaringClass().getSimpleName(), e.getMessage(), e); 31 | Result.of(e); 32 | } 33 | 34 | return Result.empty(); 35 | } 36 | 37 | public static Result executeCommand(Object target, Method commandHandlerMethod, Command command, AggregateRoot aggregateRoot) { 38 | requireNonNull(target, "We must have a target to call"); 39 | requireNonNull(commandHandlerMethod, "We must have a method decorated with @" + COMMAND_HANDLER); 40 | requireNonNull(command, "We must have a command to pass to the @" + COMMAND_HANDLER); 41 | 42 | Class[] parameterTypes = commandHandlerMethod.getParameterTypes(); 43 | 44 | if (parameterTypes.length > 1) { 45 | log.debug("Calling method decorated with @{} {}.{}({} command, {} aggregateRoot)", COMMAND_HANDLER, commandHandlerMethod.getDeclaringClass().getSimpleName(), commandHandlerMethod.getName(), parameterTypes[0].getSimpleName(), 46 | parameterTypes[1].getSimpleName()); 47 | return DewdropReflectionUtils.callMethod(target, commandHandlerMethod.getName(), command, aggregateRoot.getTarget()); 48 | } 49 | 50 | log.debug("Calling method decorated with @{} {}.{}({} command)", COMMAND_HANDLER, commandHandlerMethod.getDeclaringClass().getSimpleName(), commandHandlerMethod.getName(), parameterTypes[0].getSimpleName()); 51 | return DewdropReflectionUtils.callMethod(target, commandHandlerMethod.getName(), command); 52 | } 53 | 54 | public static Class getAggregateRootClassFromCommandHandlerMethod(Method commandHandlerMethod) { 55 | CommandHandler annotation = commandHandlerMethod.getAnnotation(CommandHandler.class); 56 | 57 | if (annotation.value() == void.class) { return commandHandlerMethod.getDeclaringClass(); } 58 | 59 | return annotation.value(); 60 | } 61 | 62 | public static List getCommandHandlersForAggregateRoot(Class aggregateRootClass) { 63 | List methodsListWithAnnotation = MethodUtils.getMethodsListWithAnnotation(aggregateRootClass, CommandHandler.class); 64 | return methodsListWithAnnotation; 65 | } 66 | 67 | public static Set getCommandHandlerMethods() { 68 | return DewdropAnnotationUtils.getAnnotatedMethods(CommandHandler.class); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/utils/DependencyInjectionUtils.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import events.dewdrop.config.DependencyInjectionAdapter; 4 | import java.util.Optional; 5 | 6 | public class DependencyInjectionUtils { 7 | private DependencyInjectionUtils() {} 8 | 9 | public static DependencyInjectionAdapter dependencyInjectionAdapter; 10 | 11 | public static void setDependencyInjection(DependencyInjectionAdapter dependencyInjection) { 12 | if (dependencyInjection != null) { 13 | DependencyInjectionUtils.dependencyInjectionAdapter = dependencyInjection; 14 | } 15 | } 16 | 17 | public static Optional getInstance(Class clazz) { 18 | if (dependencyInjectionAdapter != null) { 19 | T instance = dependencyInjectionAdapter.getBean(clazz); 20 | if (instance != null) { return Optional.of(instance); } 21 | } 22 | return DewdropReflectionUtils.createInstance(clazz); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/utils/DewdropAnnotationUtils.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import static events.dewdrop.utils.ReflectionsConfigUtils.EXCLUDE_PACKAGES; 4 | import static events.dewdrop.utils.ReflectionsConfigUtils.REFLECTIONS; 5 | import static java.util.stream.Collectors.toSet; 6 | 7 | import java.lang.annotation.Annotation; 8 | import java.lang.reflect.Field; 9 | import java.lang.reflect.Method; 10 | import java.util.HashSet; 11 | import java.util.List; 12 | import java.util.Objects; 13 | import java.util.Set; 14 | import org.apache.commons.collections4.CollectionUtils; 15 | import org.apache.commons.lang3.reflect.FieldUtils; 16 | import org.apache.commons.lang3.reflect.MethodUtils; 17 | 18 | public class DewdropAnnotationUtils { 19 | private DewdropAnnotationUtils() {} 20 | 21 | public static Set getAnnotatedFields(Class targetClass, Class fieldAnnotation) { 22 | Set fields = FieldUtils.getFieldsListWithAnnotation(targetClass, fieldAnnotation).stream().collect(toSet()); 23 | return fields; 24 | } 25 | 26 | public static Set getAnnotatedMethods(Class annotationClass) { 27 | Set methods = REFLECTIONS.getMethodsAnnotatedWith(annotationClass); 28 | if (CollectionUtils.isNotEmpty(methods)) { 29 | methods = methods.stream() 30 | // We have to do this because Reflections is not excluding correctly. 31 | .filter(method -> !EXCLUDE_PACKAGES.contains(method.getDeclaringClass().getPackageName())).filter(method -> !Objects.equals(method.getParameterTypes()[0].getSimpleName(), "Object")).collect(toSet()); 32 | } 33 | return methods; 34 | } 35 | 36 | public static Set getAnnotatedMethods(Class target, Class annotationClass) { 37 | List methods = MethodUtils.getMethodsListWithAnnotation(target, annotationClass, true, true); 38 | if (CollectionUtils.isNotEmpty(methods)) { 39 | return methods.stream() 40 | // We have to do this because Reflections is not excluding correctly. 41 | .filter(method -> !EXCLUDE_PACKAGES.contains(method.getDeclaringClass().getPackageName())).filter(method -> { 42 | if (method.getParameterTypes().length == 0) { return true; } 43 | return !Objects.equals(method.getParameterTypes()[0].getSimpleName(), "Object"); 44 | }).collect(toSet()); 45 | } 46 | return new HashSet<>(); 47 | } 48 | 49 | public static Set> getAnnotatedClasses(Class annotationClass) { 50 | Set> classes = REFLECTIONS.getTypesAnnotatedWith(annotationClass); 51 | return classes.stream().filter(clazz -> { 52 | if (EXCLUDE_PACKAGES.contains(clazz.getPackageName())) { return false; } 53 | return true; 54 | }).collect(toSet()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/utils/QueryHandlerUtils.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import events.dewdrop.api.result.Result; 6 | import events.dewdrop.read.readmodel.ReadModelWrapper; 7 | import events.dewdrop.read.readmodel.query.QueryHandler; 8 | import java.lang.reflect.InvocationTargetException; 9 | import java.lang.reflect.Method; 10 | import java.util.Optional; 11 | import java.util.Set; 12 | import lombok.extern.log4j.Log4j2; 13 | 14 | @Log4j2 15 | public class QueryHandlerUtils { 16 | private QueryHandlerUtils() {} 17 | 18 | public static Result callQueryHandler(ReadModelWrapper readModelWrapper, T query) { 19 | requireNonNull(readModelWrapper, "ReadModelWrapper is required"); 20 | requireNonNull(query, "A Query is required"); 21 | 22 | Optional targetMethod = getMethodForQuery(readModelWrapper.getOriginalReadModelClass(), query); 23 | if (targetMethod.isEmpty()) { 24 | log.info("Unable to find method annotated with @QueryHandler with method signature query({} query) on target class: {}", query.getClass().getSimpleName(), query.getClass().getSimpleName()); 25 | return Result.empty(); 26 | } 27 | 28 | Optional method = DewdropReflectionUtils.getMatchingMethod(targetMethod.get(), readModelWrapper.getReadModel().getClass()); 29 | if (method.isEmpty()) { 30 | log.info("Unable to find method annotated with @QueryHandler with method signature query({} query) on target class: {}", query.getClass().getSimpleName(), readModelWrapper.getOriginalReadModelClass().getSimpleName()); 31 | return Result.empty(); 32 | } 33 | try { 34 | R invoke = (R) method.get().invoke(readModelWrapper.getReadModel(), query); 35 | if (invoke instanceof Result) { return (Result) invoke; } 36 | return invoke == null ? Result.empty() : Result.of(invoke); 37 | 38 | } catch (IllegalAccessException | InvocationTargetException | IllegalArgumentException e) { 39 | log.error("Unable to invoke method annotated with @QueryHandler with method signature query({} query) on {} - Make sure the method exists", query.getClass().getSimpleName(), readModelWrapper.getOriginalReadModelClass().getSimpleName(), 40 | e); 41 | } 42 | return Result.empty(); 43 | } 44 | 45 | public static Optional getMethodForQuery(Class target, T query) { 46 | Set methods = DewdropAnnotationUtils.getAnnotatedMethods(target, QueryHandler.class); 47 | return methods.stream().filter(method -> { 48 | if (method.getParameterTypes().length == 0) { return false; } 49 | return method.getParameterTypes()[0].equals(query.getClass()); 50 | }).findAny(); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/utils/ReflectionsConfigUtils.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import static org.reflections.scanners.Scanners.FieldsAnnotated; 4 | import static org.reflections.scanners.Scanners.MethodsAnnotated; 5 | import static org.reflections.scanners.Scanners.SubTypes; 6 | import static org.reflections.scanners.Scanners.TypesAnnotated; 7 | 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import java.util.Optional; 11 | import org.apache.commons.lang3.StringUtils; 12 | import org.reflections.Reflections; 13 | import org.reflections.util.ClasspathHelper; 14 | import org.reflections.util.ConfigurationBuilder; 15 | import org.reflections.util.FilterBuilder; 16 | 17 | public class ReflectionsConfigUtils { 18 | private ReflectionsConfigUtils() {} 19 | 20 | public static Reflections REFLECTIONS; 21 | public static List EXCLUDE_PACKAGES = new ArrayList<>(); 22 | 23 | public static void init(String packageToScan) { 24 | init(packageToScan, new ArrayList<>()); 25 | } 26 | 27 | public static void init(String packageToScan, List excludePackages) { 28 | if (StringUtils.isEmpty(packageToScan)) { throw new IllegalArgumentException("There is no package to scan for the annotations needed for dewdrop"); } 29 | EXCLUDE_PACKAGES = Optional.ofNullable(excludePackages).orElse(new ArrayList<>()); 30 | FilterBuilder filters = new FilterBuilder(); 31 | excludePackages.forEach(packageToExclude -> filters.excludePackage(packageToExclude)); 32 | REFLECTIONS = new Reflections(new ConfigurationBuilder().setUrls(ClasspathHelper.forPackage(packageToScan)).filterInputsBy(filters).setScanners(FieldsAnnotated, MethodsAnnotated, TypesAnnotated, SubTypes)); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/events/dewdrop/utils/StreamUtils.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import events.dewdrop.read.readmodel.ReadModel; 4 | import events.dewdrop.read.readmodel.ReadModelWrapper; 5 | import events.dewdrop.read.readmodel.annotation.AggregateStream; 6 | import events.dewdrop.read.readmodel.annotation.CategoryStream; 7 | import events.dewdrop.read.readmodel.annotation.EventStream; 8 | import events.dewdrop.read.readmodel.annotation.StreamStartPosition; 9 | import events.dewdrop.read.readmodel.stream.StreamAnnotationDetails; 10 | import events.dewdrop.read.readmodel.stream.StreamType; 11 | import events.dewdrop.structure.api.Event; 12 | import org.apache.commons.lang3.StringUtils; 13 | 14 | import java.lang.annotation.Annotation; 15 | import java.lang.reflect.Method; 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | import java.util.List; 19 | import java.util.Objects; 20 | import java.util.Optional; 21 | import java.util.Set; 22 | import java.util.stream.Collectors; 23 | 24 | import static java.util.stream.Collectors.toList; 25 | 26 | public class StreamUtils { 27 | private StreamUtils() {} 28 | 29 | /** 30 | * "Find the method in the read model that is annotated with @StreamStartPosition and has the same 31 | * name and streamType as the stream annotation." 32 | * 33 | * @param The type event supported by the ReadModel 34 | * @param streamName The name of the stream 35 | * @param streamType the type of stream 36 | * @param readModel The read model class 37 | * @return {@code Optional} - A method that is annotated with @StreamStartPosition and has 38 | * the same name as the stream. 39 | */ 40 | public static Optional getStreamStartPositionMethod(String streamName, StreamType streamType, ReadModel readModel) { 41 | final ReadModelWrapper readModelWrapper = readModel.getReadModelWrapper(); 42 | Set annotatedFields = DewdropAnnotationUtils.getAnnotatedMethods(readModelWrapper.getOriginalReadModelClass(), StreamStartPosition.class); 43 | return annotatedFields.stream().filter(method -> isCorrectStreamStartPosition(streamName, streamType, method)).map(method -> { 44 | Optional matchingMethod = DewdropReflectionUtils.getMatchingMethod(method, readModelWrapper.getOriginalReadModelClass()); 45 | if (matchingMethod.isPresent()) { return matchingMethod.get(); } 46 | return null; 47 | }).filter(Objects::nonNull).findAny(); 48 | } 49 | 50 | /** 51 | * > If the name and streamType match the @StreamStartPosition annotations name and streamType, then 52 | * return true. 53 | * 54 | * @param streamName The name of the stream 55 | * @param streamType the type of stream 56 | * @param method The method that is being invoked. 57 | * @return boolean 58 | */ 59 | static boolean isCorrectStreamStartPosition(String streamName, StreamType streamType, Method method) { 60 | StreamStartPosition fieldAnnotation = method.getAnnotation(StreamStartPosition.class); 61 | String fieldStreamName = fieldAnnotation.name(); 62 | return StringUtils.equalsAnyIgnoreCase(fieldStreamName, streamName) && fieldAnnotation.streamType() == streamType; 63 | } 64 | 65 | public static List getStreamAnnotationDetails(Class clazz) { 66 | CategoryStream[] categoryStreams = clazz.getAnnotationsByType(CategoryStream.class); 67 | AggregateStream[] aggregateStreams = clazz.getAnnotationsByType(AggregateStream.class); 68 | EventStream[] eventStreams = clazz.getAnnotationsByType(EventStream.class); 69 | 70 | List streams = new ArrayList<>(); 71 | streams.addAll(Arrays.asList(categoryStreams)); 72 | streams.addAll(Arrays.asList(aggregateStreams)); 73 | streams.addAll(Arrays.asList(eventStreams)); 74 | 75 | List details = streams.stream().map(stream -> new StreamAnnotationDetails(stream)).collect(toList()); 76 | return details; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/UserLifecycleTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop; 2 | 3 | import static java.util.concurrent.TimeUnit.SECONDS; 4 | import static org.awaitility.Awaitility.with; 5 | import static org.awaitility.pollinterval.FibonacciPollInterval.fibonacci; 6 | 7 | import events.dewdrop.api.result.Result; 8 | import events.dewdrop.api.validators.ValidationException; 9 | import events.dewdrop.config.DewdropProperties; 10 | import events.dewdrop.config.DewdropSettings; 11 | import events.dewdrop.fixture.command.user.UserClaimUsernameCommand; 12 | import events.dewdrop.fixture.command.user.UserSignupCommand; 13 | import events.dewdrop.fixture.readmodel.users.GetUserByIdQuery; 14 | import events.dewdrop.fixture.readmodel.users.User; 15 | import java.util.UUID; 16 | import java.util.function.Predicate; 17 | import lombok.extern.log4j.Log4j2; 18 | import org.apache.commons.lang3.StringUtils; 19 | import org.junit.jupiter.api.Test; 20 | 21 | @Log4j2 22 | public class UserLifecycleTest { 23 | 24 | DewdropProperties properties = DewdropProperties.builder().packageToScan("events.dewdrop").packageToExclude("events.dewdrop.fixture.customized").connectionString("esdb://localhost:2113?tls=false").create(); 25 | 26 | @Test 27 | void test() throws ValidationException { 28 | Dewdrop dewdrop = DewdropSettings.builder().properties(properties).create().start(); 29 | 30 | UserSignupCommand userSignupCommand = createUser(dewdrop); 31 | UserClaimUsernameCommand userClaimUsernameCommand = claimUsername(userSignupCommand.getUserId(), dewdrop); 32 | 33 | GetUserByIdQuery getUserById = new GetUserByIdQuery(userClaimUsernameCommand.getUserId()); 34 | retryUntilComplete(dewdrop, getUserById, (userResult) -> { 35 | log.info("userResult: {}", userResult); 36 | if (!userResult.isValuePresent()) { return false; } 37 | User user = (User) userResult.get(); 38 | if (StringUtils.isNotEmpty(user.getUsername()) && user.getUserId().equals(userSignupCommand.getUserId())) { return true; } 39 | return false; 40 | }); 41 | } 42 | 43 | private UserSignupCommand createUser(Dewdrop dewdrop) throws ValidationException { 44 | UserSignupCommand userSignupCommand = new UserSignupCommand(UUID.randomUUID(), "funkapuss@dendritemalfunction.com"); 45 | dewdrop.executeCommand(userSignupCommand); 46 | return userSignupCommand; 47 | } 48 | 49 | private UserClaimUsernameCommand claimUsername(UUID userId, Dewdrop dewdrop) throws ValidationException { 50 | UserClaimUsernameCommand claimUsernameCommand = new UserClaimUsernameCommand(userId, "funkapuss sassafrass"); 51 | dewdrop.executeCommand(claimUsernameCommand); 52 | return claimUsernameCommand; 53 | } 54 | 55 | private void retryUntilComplete(Dewdrop dewdrop, Object query, Predicate predicate) { 56 | with().pollInterval(fibonacci(SECONDS)).await().timeout(100000L, SECONDS).until(() -> predicate.test(dewdrop.executeQuery(query))); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/UserSignupTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop; 2 | 3 | import events.dewdrop.api.validators.ValidationException; 4 | import events.dewdrop.config.DewdropProperties; 5 | import events.dewdrop.config.DewdropSettings; 6 | import events.dewdrop.fixture.command.user.UserClaimUsernameCommand; 7 | import events.dewdrop.fixture.command.user.UserSignupCommand; 8 | import java.util.UUID; 9 | import lombok.extern.log4j.Log4j2; 10 | import org.junit.jupiter.api.Test; 11 | 12 | @Log4j2 13 | public class UserSignupTest { 14 | 15 | DewdropProperties properties = DewdropProperties.builder().packageToScan("events.dewdrop").packageToExclude("events.dewdrop.fixture.customized").connectionString("esdb://localhost:2113?tls=false").create(); 16 | 17 | @Test 18 | void test() throws ValidationException { 19 | Dewdrop dewdrop = DewdropSettings.builder().properties(properties).create().start(); 20 | 21 | UserSignupCommand userSignupCommand = createUser(dewdrop); 22 | UserClaimUsernameCommand userClaimUsernameCommand = claimUsername(userSignupCommand.getUserId(), dewdrop); 23 | 24 | 25 | } 26 | 27 | private UserSignupCommand createUser(Dewdrop dewdrop) throws ValidationException { 28 | UserSignupCommand userSignupCommand = new UserSignupCommand(UUID.randomUUID(), "funkapuss@dendritemalfunction.com"); 29 | dewdrop.executeCommand(userSignupCommand); 30 | return userSignupCommand; 31 | } 32 | 33 | private UserClaimUsernameCommand claimUsername(UUID userId, Dewdrop dewdrop) throws ValidationException { 34 | UserClaimUsernameCommand claimUsernameCommand = new UserClaimUsernameCommand(userId, "funkapuss sassafrass"); 35 | dewdrop.executeCommand(claimUsernameCommand); 36 | return claimUsernameCommand; 37 | } 38 | 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/aggregate/AggregateRootTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.aggregate; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.hamcrest.Matchers.notNullValue; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | 8 | import events.dewdrop.fixture.automated.DewdropUserAggregate; 9 | import events.dewdrop.fixture.command.DewdropCreateUserCommand; 10 | import events.dewdrop.fixture.events.DewdropUserCreated; 11 | import events.dewdrop.structure.events.CorrelationCausation; 12 | import java.util.UUID; 13 | import org.junit.jupiter.api.BeforeEach; 14 | import org.junit.jupiter.api.DisplayName; 15 | import org.junit.jupiter.api.Test; 16 | 17 | class AggregateRootTest { 18 | DewdropUserAggregate userAggregate; 19 | AggregateRoot aggregateRoot; 20 | TestAggregateRoot testAggregateRoot; 21 | 22 | @BeforeEach 23 | void setup() { 24 | userAggregate = new DewdropUserAggregate(); 25 | aggregateRoot = new AggregateRoot(userAggregate); 26 | testAggregateRoot = new TestAggregateRoot(); 27 | } 28 | 29 | @Test 30 | @DisplayName("new AggregateRoot() - Given a subclass of AggregateRoot, do we have a valid target and targetClassName") 31 | void constructor_empty() { 32 | assertThat(testAggregateRoot, is(notNullValue())); 33 | assertThat(testAggregateRoot.getTarget().getClass(), is(TestAggregateRoot.class)); 34 | assertThat(testAggregateRoot.getTargetClassName(), is(TestAggregateRoot.class.getName())); 35 | } 36 | 37 | @Test 38 | @DisplayName("new AggregateRoot() - Given a target, do we have a valid target and targetClassName") 39 | void constructor() { 40 | assertThat(aggregateRoot, is(notNullValue())); 41 | assertThat(aggregateRoot.getTarget().getClass(), is(DewdropUserAggregate.class)); 42 | assertThat(aggregateRoot.getTargetClassName(), is(DewdropUserAggregate.class.getName())); 43 | } 44 | 45 | @Test 46 | @DisplayName("setSource() - Given a CorrelationCausation, assign the correlationId and causationId") 47 | void setSource() { 48 | DewdropCreateUserCommand command = new DewdropCreateUserCommand(UUID.randomUUID(), "test"); 49 | command.setMessageId(UUID.randomUUID()); 50 | aggregateRoot.setSource(command); 51 | assertThat(aggregateRoot.getCausationId(), is(command.getMessageId())); 52 | assertThat(aggregateRoot.getCorrelationId(), is(command.getCorrelationId())); 53 | } 54 | 55 | @Test 56 | @DisplayName("setSource() - Given a CorrelationCausation without a correlationId, throw an IllegalStateException") 57 | void setSource_invalidState() { 58 | DewdropCreateUserCommand command = new DewdropCreateUserCommand(UUID.randomUUID(), "test"); 59 | EventRecorder eventRecorder = new EventRecorder(); 60 | eventRecorder.recordEvent(new DewdropUserCreated()); 61 | aggregateRoot.setSource(command); 62 | aggregateRoot.setSource(new InvalidAggregateRoot()); 63 | aggregateRoot.setRecorder(eventRecorder); 64 | assertThrows(IllegalStateException.class, () -> aggregateRoot.setSource(new InvalidAggregateRoot())); 65 | } 66 | 67 | @Test 68 | @DisplayName("equals() - id equals whether it's an aggregateRoot or a target") 69 | void equals() { 70 | AggregateRoot newAggregateRoot = new AggregateRoot(userAggregate); 71 | assertThat(aggregateRoot.equals(newAggregateRoot), is(true)); 72 | 73 | assertThat(aggregateRoot.equals(userAggregate), is(true)); 74 | } 75 | 76 | @Test 77 | @DisplayName("hashcode() - pass off hashcode to target") 78 | void hashcode() { 79 | AggregateRoot newAggregateRoot = new AggregateRoot(userAggregate); 80 | assertThat(aggregateRoot.hashCode(), is(newAggregateRoot.hashCode())); 81 | } 82 | 83 | private class TestAggregateRoot extends AggregateRoot { 84 | } 85 | 86 | private class InvalidAggregateRoot extends CorrelationCausation { 87 | 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/aggregate/EventRecorderTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.aggregate; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | 6 | import events.dewdrop.fixture.events.DewdropUserCreated; 7 | import org.junit.jupiter.api.BeforeEach; 8 | import org.junit.jupiter.api.DisplayName; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class EventRecorderTest { 12 | EventRecorder eventRecorder; 13 | 14 | @BeforeEach 15 | void setup() { 16 | this.eventRecorder = new EventRecorder(); 17 | } 18 | 19 | @Test 20 | @DisplayName("new EventRecorder() - Given a valid construction, we have an initialized recordedEvents field") 21 | void constructor() { 22 | assertThat(eventRecorder.recordedEvents().isEmpty(), is(true)); 23 | assertThat(eventRecorder.hasRecordedEvents(), is(false)); 24 | } 25 | 26 | @Test 27 | @DisplayName("recordEvent() - Given an event, after we call recordEvent() we can retrieve it from getRecorded()") 28 | void recordEvent() { 29 | DewdropUserCreated event = new DewdropUserCreated(); 30 | eventRecorder.recordEvent(event); 31 | assertThat(eventRecorder.hasRecordedEvents(), is(true)); 32 | assertThat(eventRecorder.recordedEvents().get(0), is(event)); 33 | } 34 | 35 | @Test 36 | @DisplayName("reset() - on reset, we will no longer have any recordedEvents") 37 | void reset() { 38 | recordEvent(); 39 | assertThat(eventRecorder.hasRecordedEvents(), is(true)); 40 | 41 | eventRecorder.reset(); 42 | assertThat(eventRecorder.hasRecordedEvents(), is(false)); 43 | } 44 | 45 | @Test 46 | @DisplayName("recordedEvents() - return a collection of recordedEvents") 47 | void recordedEvents() { 48 | recordEvent(); 49 | assertThat(eventRecorder.recordedEvents().size(), is(1)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/aggregate/QueryStateOrchestratorTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.aggregate; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.mockito.ArgumentMatchers.any; 6 | import static org.mockito.Mockito.doNothing; 7 | import static org.mockito.Mockito.doReturn; 8 | import static org.mockito.Mockito.mock; 9 | import static org.mockito.Mockito.mockStatic; 10 | import static org.mockito.Mockito.times; 11 | import static org.mockito.Mockito.verify; 12 | 13 | import events.dewdrop.api.result.Result; 14 | import events.dewdrop.read.readmodel.DefaultAnnotationReadModelMapper; 15 | import events.dewdrop.read.readmodel.ReadModel; 16 | import events.dewdrop.read.readmodel.ReadModelMapper; 17 | import events.dewdrop.read.readmodel.ReadModelWrapper; 18 | import events.dewdrop.fixture.readmodel.accountdetails.details.DewdropAccountDetails; 19 | import events.dewdrop.read.readmodel.QueryStateOrchestrator; 20 | import events.dewdrop.utils.QueryHandlerUtils; 21 | import java.util.Optional; 22 | import org.hamcrest.Matchers; 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.DisplayName; 25 | import org.junit.jupiter.api.Test; 26 | import org.mockito.MockedStatic; 27 | 28 | class QueryStateOrchestratorTest { 29 | QueryStateOrchestrator queryStateOrchestrator; 30 | ReadModelMapper readModelMapper; 31 | 32 | @BeforeEach 33 | void setup() { 34 | readModelMapper = mock(DefaultAnnotationReadModelMapper.class); 35 | queryStateOrchestrator = new QueryStateOrchestrator(readModelMapper); 36 | 37 | } 38 | 39 | @Test 40 | @DisplayName("executeQuery() - Given a query, when we find the read model, then we execute the query by calling QueryHandlerUtils.callQueryHandler()") 41 | void executeQuery() { 42 | ReadModel readModel = mock(ReadModel.class); 43 | doNothing().when(readModel).updateQueryState(any()); 44 | doReturn(Optional.of(readModel)).when(readModelMapper).getReadModelByQuery(any()); 45 | 46 | try (MockedStatic utilities = mockStatic(QueryHandlerUtils.class)) { 47 | utilities.when(() -> QueryHandlerUtils.callQueryHandler(any(ReadModelWrapper.class), any())).thenReturn(Result.of(new DewdropAccountDetails())); 48 | queryStateOrchestrator.executeQuery(new Object()); 49 | verify(readModel, times(1)).updateQueryState(Optional.empty()); 50 | } 51 | } 52 | 53 | @Test 54 | @DisplayName("executeQuery() - Given a query, when there is no read model, then return a Result with an exception") 55 | void executeQuer_noReadModel() { 56 | ReadModel readModel = mock(ReadModel.class); 57 | doNothing().when(readModel).updateQueryState(any()); 58 | doReturn(Optional.empty()).when(readModelMapper).getReadModelByQuery(any()); 59 | 60 | Result result = queryStateOrchestrator.executeQuery(new Object()); 61 | assertThat(result.isExceptionPresent(), is(true)); 62 | assertThat(result.getException().getMessage(), Matchers.containsString("no read model found for query")); 63 | verify(readModel, times(0)).updateQueryState(Optional.empty()); 64 | } 65 | 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/api/validators/ValidationExceptionTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.api.validators; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.lang.reflect.InvocationTargetException; 6 | import java.util.List; 7 | 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | import static org.hamcrest.Matchers.is; 10 | import static org.junit.jupiter.api.Assertions.assertEquals; 11 | 12 | class ValidationExceptionTest { 13 | @Test 14 | void validationException() { 15 | String message = "test"; 16 | ValidationResult result = ValidationResult.of(new ValidationError(message)); 17 | ValidationException exception = new ValidationException(result); 18 | 19 | assertEquals(message, exception.getValidationResult().get().get(0).getMessage()); 20 | } 21 | 22 | @Test 23 | void validationException_withField() { 24 | String field = "username"; 25 | String message = "REQUIRED"; 26 | ValidationResult result = ValidationResult.of(new ValidationError(field, message)); 27 | ValidationException exception = new ValidationException(result); 28 | 29 | assertEquals(message, exception.getValidationResult().get().get(0).getMessage()); 30 | assertEquals(field, exception.getValidationResult().get().get(0).getField()); 31 | } 32 | 33 | @Test 34 | void getMessage() { 35 | ValidationResult result = ValidationResult.of("Something bad"); 36 | result.and(ValidationResult.of("test")); 37 | ValidationException exception = new ValidationException(result); 38 | 39 | assertThat(exception.getMessage(), is("Something bad, test")); 40 | assertThat(new ValidationException(ValidationResult.valid()).getMessage(), is("")); 41 | } 42 | 43 | @Test 44 | void validationException_StringParam() { 45 | ValidationException exception = ValidationException.of("Something broke {}", "test"); 46 | assertEquals("Something broke test", exception.getMessage()); 47 | } 48 | 49 | @Test 50 | void of() { 51 | ValidationException exception = ValidationException.of("Something broke"); 52 | assertEquals("Something broke", exception.getMessage()); 53 | } 54 | 55 | @Test 56 | void of_exception() { 57 | ValidationException exception = ValidationException.of(new RuntimeException("Something broke")); 58 | assertEquals("Something broke", exception.getMessage()); 59 | } 60 | 61 | @Test 62 | void of_invocationTargetException() { 63 | InvocationTargetException targetException = new InvocationTargetException(ValidationException.of("Something broke")); 64 | ValidationException exception = ValidationException.of(targetException); 65 | assertEquals("Something broke", exception.getMessage()); 66 | } 67 | 68 | @Test 69 | void of_errors() { 70 | List validationErrors = List.of(ValidationError.of("test"), ValidationError.of("test2")); 71 | ValidationException exception = ValidationException.of(validationErrors); 72 | assertEquals(exception.getValidationResult().get().get(0).getMessage(), validationErrors.get(0).getMessage()); 73 | assertEquals(exception.getValidationResult().get().get(1).getMessage(), validationErrors.get(1).getMessage()); 74 | } 75 | 76 | @Test 77 | void of_error() { 78 | ValidationError validationError = ValidationError.of("test"); 79 | ValidationException exception = ValidationException.of(validationError); 80 | assertEquals(exception.getValidationResult().get().get(0).getMessage(), validationError.getMessage()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/api/validators/ValidationResultTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.api.validators; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class ValidationResultTest { 12 | String message = "test error message"; 13 | 14 | @Test 15 | void addAll() { 16 | List errors = List.of(new ValidationError(message)); 17 | ValidationResult result = ValidationResult.valid(); 18 | result.addAll(errors); 19 | 20 | assertTrue(result.hasErrors()); 21 | assertEquals(message, result.get().get(0).getMessage()); 22 | } 23 | 24 | @Test 25 | void of_withErrors() { 26 | List errors = List.of(new ValidationError(message)); 27 | ValidationResult result = ValidationResult.of(errors); 28 | 29 | assertTrue(result.hasErrors()); 30 | assertEquals(message, result.get().get(0).getMessage()); 31 | } 32 | 33 | @Test 34 | void of_noErrors() { 35 | List errors = new ArrayList<>(); 36 | ValidationResult result = ValidationResult.of(errors); 37 | 38 | assertFalse(result.hasErrors()); 39 | } 40 | 41 | @Test 42 | void of_withSingleError() { 43 | ValidationResult result = ValidationResult.of(new ValidationError(message)); 44 | 45 | assertTrue(result.hasErrors()); 46 | assertEquals(message, result.get().get(0).getMessage()); 47 | } 48 | 49 | @Test 50 | void valid() { 51 | ValidationResult result = ValidationResult.valid(); 52 | 53 | assertFalse(result.hasErrors()); 54 | } 55 | 56 | @Test 57 | void get() { 58 | ValidationResult result = ValidationResult.of(new ValidationError(message)); 59 | 60 | assertTrue(result.hasErrors()); 61 | assertEquals(message, result.get().get(0).getMessage()); 62 | } 63 | 64 | @Test 65 | void add() { 66 | String nextMessage = "next error message"; 67 | ValidationResult result = ValidationResult.of(new ValidationError(message)); 68 | result.add(new ValidationError(nextMessage)); 69 | 70 | assertTrue(result.hasErrors()); 71 | assertEquals(message, result.get().get(0).getMessage()); 72 | assertEquals(nextMessage, result.get().get(1).getMessage()); 73 | } 74 | 75 | @Test 76 | void and() { 77 | String nextMessage = "next error message"; 78 | ValidationResult result = ValidationResult.of(new ValidationError(message)); 79 | result.and(ValidationResult.of(new ValidationError(nextMessage))); 80 | result.and(ValidationResult.valid()); 81 | 82 | assertTrue(result.hasErrors()); 83 | assertEquals(message, result.get().get(0).getMessage()); 84 | assertEquals(nextMessage, result.get().get(1).getMessage()); 85 | } 86 | 87 | @Test 88 | void and_noErrorsYet() { 89 | ValidationResult result = ValidationResult.valid(); 90 | result.and(ValidationResult.of(new ValidationError(message))); 91 | 92 | assertTrue(result.hasErrors()); 93 | assertEquals(message, result.get().get(0).getMessage()); 94 | } 95 | 96 | @Test 97 | void isValid() { 98 | ValidationResult result = ValidationResult.valid(); 99 | assertTrue(result.isValid()); 100 | } 101 | 102 | @Test 103 | void isValid_butItIsnt() { 104 | ValidationResult result = ValidationResult.of(new ValidationError(message)); 105 | assertFalse(result.isValid()); 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/config/DewdropPropertiesTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.config; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class DewdropPropertiesTest { 8 | @Test 9 | void connectionString() { 10 | String connectionString = "esdb://localhost:2113?tls=false"; 11 | String packageToScan = "events.dewdrop"; 12 | String packageToExclude = "events.dewdrop.fixture.customized"; 13 | 14 | DewdropProperties dewdropProperties = DewdropProperties.builder().connectionString(connectionString).packageToScan(packageToScan).packageToExclude(packageToExclude).streamPrefix("").create(); 15 | 16 | assertEquals(connectionString, dewdropProperties.getConnectionString()); 17 | assertEquals(packageToScan, dewdropProperties.getPackageToScan()); 18 | assertEquals(packageToExclude, dewdropProperties.getPackageToExclude().get(0)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/config/DewdropSettingsTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.config; 2 | 3 | import static org.junit.jupiter.api.Assertions.*; 4 | 5 | import org.junit.jupiter.api.Test; 6 | 7 | class DewdropSettingsTest { 8 | @Test 9 | void construct() { 10 | DewdropProperties properties = DewdropProperties.builder().connectionString("esdb://localhost:2113?tls=false").streamPrefix("").packageToScan("events.dewdrop").create(); 11 | DewdropSettings dewdropSettings = DewdropSettings.builder().properties(properties).create(); 12 | assertNotNull(dewdropSettings); 13 | } 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/automated/DewdropAccountAggregate.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.automated; 2 | 3 | import events.dewdrop.aggregate.annotation.Aggregate; 4 | import events.dewdrop.aggregate.annotation.AggregateId; 5 | import events.dewdrop.api.validators.ValidationException; 6 | import events.dewdrop.command.CommandHandler; 7 | import events.dewdrop.fixture.events.DewdropAccountEvent; 8 | import events.dewdrop.read.readmodel.annotation.EventHandler; 9 | import events.dewdrop.fixture.command.DewdropAddFundsToAccountCommand; 10 | import events.dewdrop.fixture.command.DewdropCreateAccountCommand; 11 | import events.dewdrop.fixture.events.DewdropAccountCreated; 12 | import events.dewdrop.fixture.events.DewdropFundsAddedToAccount; 13 | import java.math.BigDecimal; 14 | import java.util.List; 15 | import java.util.UUID; 16 | import lombok.Data; 17 | import events.dewdrop.structure.api.validator.DewdropValidator; 18 | 19 | @Data 20 | @Aggregate 21 | public class DewdropAccountAggregate { 22 | @AggregateId 23 | UUID accountId; 24 | String name; 25 | BigDecimal balance = BigDecimal.ZERO; 26 | 27 | public DewdropAccountAggregate() {} 28 | 29 | @CommandHandler 30 | public List handle(DewdropCreateAccountCommand command) throws ValidationException { 31 | DewdropValidator.validate(command); 32 | 33 | return List.of(new DewdropAccountCreated(command.getAccountId(), command.getName(), command.getUserId())); 34 | } 35 | 36 | @CommandHandler 37 | public List handle(DewdropAddFundsToAccountCommand command) { 38 | if (command.getAccountId() == null) { throw new IllegalArgumentException("Id cannot be empty"); } 39 | 40 | DewdropFundsAddedToAccount dewdropFundsAddedToAccount = new DewdropFundsAddedToAccount(command.getAccountId(), command.getFunds()); 41 | return List.of(dewdropFundsAddedToAccount); 42 | } 43 | 44 | @EventHandler 45 | public void on(DewdropAccountCreated event) { 46 | // validate here as well different 47 | // check that teh aggregate invariance are always true 48 | this.accountId = event.getAccountId(); 49 | this.name = event.getName(); 50 | // DewdropAccountAggregate.from(this).with(); 51 | } 52 | 53 | @EventHandler 54 | public void on(DewdropFundsAddedToAccount event) { 55 | // this.accountId = event.getAccountId(); 56 | this.balance = this.balance.add(event.getFunds()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/automated/DewdropUserAggregate.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.automated; 2 | 3 | import events.dewdrop.aggregate.annotation.Aggregate; 4 | import events.dewdrop.aggregate.annotation.AggregateId; 5 | import events.dewdrop.command.CommandHandler; 6 | import events.dewdrop.fixture.command.DewdropCreateUserCommand; 7 | import events.dewdrop.fixture.command.DewdropDeactivateUserCommand; 8 | import events.dewdrop.fixture.events.DewdropUserCreated; 9 | import events.dewdrop.fixture.events.DewdropUserDeactivate; 10 | import events.dewdrop.read.readmodel.annotation.EventHandler; 11 | import jakarta.validation.Valid; 12 | import lombok.Data; 13 | import org.apache.commons.lang3.builder.EqualsBuilder; 14 | import org.apache.commons.lang3.builder.HashCodeBuilder; 15 | 16 | import java.util.UUID; 17 | 18 | @Aggregate 19 | @Data 20 | public class DewdropUserAggregate { 21 | @AggregateId 22 | UUID userId; 23 | private String username; 24 | 25 | public DewdropUserAggregate() {} 26 | 27 | @CommandHandler 28 | public DewdropUserCreated createUser(@Valid DewdropCreateUserCommand command) { 29 | return new DewdropUserCreated(command.getUserId(), command.getUsername()); 30 | } 31 | 32 | @CommandHandler 33 | public DewdropUserDeactivate deactivate(@Valid DewdropDeactivateUserCommand command) { 34 | return new DewdropUserDeactivate(command.getUserId()); 35 | } 36 | 37 | @EventHandler 38 | public void on(DewdropUserCreated userCreated) { 39 | this.userId = userCreated.getUserId(); 40 | this.username = userCreated.getUsername(); 41 | } 42 | 43 | @Override 44 | public boolean equals(Object o) { 45 | if (this == o) { return true; } 46 | 47 | if (o == null || getClass() != o.getClass()) { return false; } 48 | 49 | DewdropUserAggregate that = (DewdropUserAggregate) o; 50 | 51 | return new EqualsBuilder().append(userId, that.userId).isEquals(); 52 | } 53 | 54 | @Override 55 | public int hashCode() { 56 | return new HashCodeBuilder(17, 37).append(userId).toHashCode(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/automated/user/UserAggregate.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.automated.user; 2 | 3 | import events.dewdrop.aggregate.annotation.Aggregate; 4 | import events.dewdrop.aggregate.annotation.AggregateId; 5 | import events.dewdrop.api.validators.ValidationException; 6 | import events.dewdrop.command.CommandHandler; 7 | import events.dewdrop.fixture.command.user.CsrClaimUsernameCommand; 8 | import events.dewdrop.fixture.command.user.UserClaimUsernameCommand; 9 | import events.dewdrop.fixture.command.user.UserSignupCommand; 10 | import events.dewdrop.fixture.events.user.CsrClaimedUsername; 11 | import events.dewdrop.fixture.events.user.UserClaimedUsername; 12 | import events.dewdrop.fixture.events.user.UserSignedUp; 13 | import events.dewdrop.read.readmodel.annotation.EventHandler; 14 | import events.dewdrop.structure.api.validator.DewdropValidator; 15 | import java.util.UUID; 16 | import lombok.Data; 17 | 18 | @Aggregate 19 | @Data 20 | public class UserAggregate { 21 | @AggregateId 22 | UUID userId; 23 | private String username; 24 | private String email; 25 | 26 | public UserAggregate() {} 27 | 28 | @CommandHandler 29 | public UserSignedUp signup(UserSignupCommand command) throws ValidationException { 30 | DewdropValidator.validate(command); 31 | return new UserSignedUp(command.getUserId(), command.getEmail()); 32 | } 33 | 34 | @CommandHandler 35 | public UserClaimedUsername userClaimedUsername(UserClaimUsernameCommand command) throws ValidationException { 36 | DewdropValidator.validate(command); 37 | return new UserClaimedUsername(command.getUserId(), command.getUsername()); 38 | } 39 | 40 | @CommandHandler 41 | public CsrClaimedUsername csrClaimedUsername(CsrClaimUsernameCommand command) throws ValidationException { 42 | DewdropValidator.validate(command); 43 | return new CsrClaimedUsername(command.getUserId(), command.getUsername()); 44 | } 45 | 46 | // reportoffensive 47 | 48 | 49 | @EventHandler 50 | public void on(UserSignedUp userSignedup) { 51 | this.userId = userSignedup.getUserId(); 52 | this.email = userSignedup.getEmail(); 53 | } 54 | 55 | @EventHandler 56 | public void on(UserClaimedUsername userClaimedUsername) { 57 | this.userId = userClaimedUsername.getUserId(); 58 | this.username = userClaimedUsername.getUsername(); 59 | } 60 | 61 | @EventHandler 62 | public void on(CsrClaimedUsername csrClaimedUsername) { 63 | this.userId = csrClaimedUsername.getUserId(); 64 | this.username = csrClaimedUsername.getUsername(); 65 | } 66 | 67 | // reported offensive 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/command/DewdropAccountCommand.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.command; 2 | 3 | import events.dewdrop.aggregate.annotation.AggregateId; 4 | import events.dewdrop.structure.api.Command; 5 | import java.util.UUID; 6 | import lombok.Data; 7 | 8 | @Data 9 | 10 | public abstract class DewdropAccountCommand extends Command { 11 | @AggregateId 12 | private UUID accountId; 13 | 14 | public DewdropAccountCommand(UUID accountId) { 15 | super(); 16 | this.accountId = accountId; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/command/DewdropAddFundsToAccountCommand.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.command; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.UUID; 5 | import lombok.Data; 6 | 7 | @Data 8 | public class DewdropAddFundsToAccountCommand extends DewdropAccountCommand { 9 | BigDecimal funds; 10 | 11 | public DewdropAddFundsToAccountCommand(UUID accountId, BigDecimal funds) { 12 | super(accountId); 13 | this.funds = funds; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/command/DewdropCreateAccountCommand.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.command; 2 | 3 | import java.util.UUID; 4 | import lombok.Data; 5 | 6 | @Data 7 | public class DewdropCreateAccountCommand extends DewdropAccountCommand { 8 | private String name; 9 | private UUID userId; 10 | 11 | public DewdropCreateAccountCommand(UUID accountId, String name, UUID userId) { 12 | super(accountId); 13 | this.name = name; 14 | this.userId = userId; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/command/DewdropCreateUserCommand.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.command; 2 | 3 | import java.util.UUID; 4 | import jakarta.validation.constraints.NotBlank; 5 | import lombok.Data; 6 | 7 | @Data 8 | public class DewdropCreateUserCommand extends DewdropUserCommand { 9 | @NotBlank(message = "Username is required") 10 | String username; 11 | 12 | public DewdropCreateUserCommand(UUID userId, String username) { 13 | super(userId); 14 | this.username = username; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/command/DewdropDeactivateUserCommand.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.command; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.UUID; 6 | 7 | @Data 8 | public class DewdropDeactivateUserCommand extends DewdropUserCommand { 9 | public DewdropDeactivateUserCommand(UUID userId) { 10 | super(userId); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/command/DewdropUserCommand.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.command; 2 | 3 | import events.dewdrop.aggregate.annotation.AggregateId; 4 | import java.util.UUID; 5 | import jakarta.validation.constraints.NotNull; 6 | import lombok.AccessLevel; 7 | import lombok.Data; 8 | import lombok.NoArgsConstructor; 9 | import events.dewdrop.structure.api.Command; 10 | 11 | @Data 12 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 13 | public class DewdropUserCommand extends Command { 14 | @NotNull(message = "UserId is required") 15 | @AggregateId 16 | private UUID userId; 17 | 18 | public DewdropUserCommand(UUID userId) { 19 | super(); 20 | this.userId = userId; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/command/user/CsrClaimUsernameCommand.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.command.user; 2 | 3 | import java.util.UUID; 4 | 5 | import lombok.Data; 6 | 7 | @Data 8 | public class CsrClaimUsernameCommand extends UserClaimUsernameCommand { 9 | 10 | public CsrClaimUsernameCommand(UUID userId, String username) { 11 | super(userId, username); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/command/user/UserClaimUsernameCommand.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.command.user; 2 | 3 | import java.util.UUID; 4 | import jakarta.validation.constraints.NotBlank; 5 | import lombok.AccessLevel; 6 | import lombok.Data; 7 | import lombok.NoArgsConstructor; 8 | 9 | @Data 10 | @NoArgsConstructor(access = AccessLevel.PRIVATE) 11 | public class UserClaimUsernameCommand extends UserCommand { 12 | 13 | @NotBlank(message = "Username is required") 14 | String username; 15 | 16 | public UserClaimUsernameCommand(UUID userId, String username) { 17 | super(userId); 18 | this.username = username; 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/command/user/UserCommand.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.command.user; 2 | 3 | import events.dewdrop.aggregate.annotation.AggregateId; 4 | import events.dewdrop.structure.api.Command; 5 | import java.util.UUID; 6 | import jakarta.validation.constraints.NotNull; 7 | import lombok.AccessLevel; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | 11 | @Data 12 | @NoArgsConstructor(access = AccessLevel.PROTECTED) 13 | public class UserCommand extends Command { 14 | 15 | @NotNull(message = "UserId is required") 16 | @AggregateId 17 | private UUID userId; 18 | 19 | public UserCommand(UUID userId) { 20 | super(); 21 | this.userId = userId; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/command/user/UserSignupCommand.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.command.user; 2 | 3 | import java.util.UUID; 4 | import jakarta.validation.constraints.NotBlank; 5 | import lombok.Data; 6 | 7 | @Data 8 | public class UserSignupCommand extends UserCommand { 9 | @NotBlank(message = "Email is required") 10 | String email; 11 | 12 | public UserSignupCommand(UUID userId, String email) { 13 | super(userId); 14 | this.email = email; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/customized/DewdropAccountAggregateSubclass.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.customized; 2 | 3 | import events.dewdrop.aggregate.AggregateRoot; 4 | import events.dewdrop.aggregate.annotation.AggregateId; 5 | import events.dewdrop.fixture.events.DewdropAccountCreated; 6 | import events.dewdrop.fixture.command.DewdropAddFundsToAccountCommand; 7 | import events.dewdrop.fixture.command.DewdropCreateAccountCommand; 8 | import events.dewdrop.fixture.events.DewdropFundsAddedToAccount; 9 | import java.math.BigDecimal; 10 | import java.util.UUID; 11 | import org.apache.commons.lang3.StringUtils; 12 | 13 | public class DewdropAccountAggregateSubclass extends AggregateRoot { 14 | 15 | @AggregateId 16 | UUID accountId; 17 | String name; 18 | BigDecimal balance = BigDecimal.ZERO; 19 | 20 | public DewdropAccountAggregateSubclass() {} 21 | 22 | public UUID getAccountId() { 23 | return accountId; 24 | } 25 | 26 | public void setAccountId(UUID accountId) { 27 | this.accountId = accountId; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public void setName(String name) { 35 | this.name = name; 36 | } 37 | 38 | 39 | public DewdropAccountCreated handle(DewdropCreateAccountCommand command) { 40 | if (StringUtils.isEmpty(command.getName())) { throw new IllegalArgumentException("Name cannot be empty"); } 41 | 42 | DewdropAccountCreated testAccountCreated = new DewdropAccountCreated(command.getAccountId(), command.getName(), command.getUserId()); 43 | raise(testAccountCreated); 44 | return testAccountCreated; 45 | } 46 | 47 | public DewdropFundsAddedToAccount handle(DewdropAddFundsToAccountCommand command) { 48 | if (command.getAccountId() == null) { throw new IllegalArgumentException("Id cannot be empty"); } 49 | 50 | DewdropFundsAddedToAccount testFundsAddedToAccount = new DewdropFundsAddedToAccount(command.getAccountId(), command.getFunds()); 51 | raise(testFundsAddedToAccount); 52 | return testFundsAddedToAccount; 53 | } 54 | 55 | public void on(DewdropAccountCreated event) { 56 | this.accountId = event.getAccountId(); 57 | this.name = event.getName(); 58 | } 59 | 60 | public void on(DewdropFundsAddedToAccount event) { 61 | this.balance = this.balance.add(event.getFunds()); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/customized/DewdropCommandService.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.customized; 2 | 3 | import events.dewdrop.command.CommandHandler; 4 | import events.dewdrop.fixture.automated.DewdropAccountAggregate; 5 | import events.dewdrop.fixture.events.DewdropAccountCreated; 6 | import events.dewdrop.fixture.command.DewdropAddFundsToAccountCommand; 7 | import events.dewdrop.fixture.command.DewdropCreateAccountCommand; 8 | import events.dewdrop.fixture.events.DewdropFundsAddedToAccount; 9 | import java.util.List; 10 | import org.apache.commons.lang3.StringUtils; 11 | 12 | public class DewdropCommandService { 13 | @CommandHandler(value = DewdropAccountAggregate.class) 14 | public List handle(DewdropCreateAccountCommand command, DewdropAccountAggregate aggregate) { 15 | if (StringUtils.isEmpty(command.getName())) { throw new IllegalArgumentException("Name cannot be empty"); } 16 | 17 | return List.of(new DewdropAccountCreated(command.getAccountId(), command.getName(), command.getUserId())); 18 | } 19 | 20 | @CommandHandler(value = DewdropAccountAggregate.class) 21 | public List handle(DewdropAddFundsToAccountCommand command, DewdropAccountAggregate aggregate) { 22 | if (command.getAccountId() == null) { throw new IllegalArgumentException("Id cannot be empty"); } 23 | 24 | return List.of(new DewdropFundsAddedToAccount(command.getAccountId(), command.getFunds())); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/customized/DewdropStandaloneCommandService.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.customized; 2 | 3 | import events.dewdrop.aggregate.AggregateRoot; 4 | import events.dewdrop.fixture.events.DewdropFundsAddedToAccount; 5 | import events.dewdrop.read.readmodel.stream.StreamFactory; 6 | import events.dewdrop.streamstore.process.StandaloneAggregateProcessor; 7 | import events.dewdrop.fixture.command.DewdropAddFundsToAccountCommand; 8 | import events.dewdrop.fixture.command.DewdropCreateAccountCommand; 9 | import events.dewdrop.utils.AggregateIdUtils; 10 | import java.util.Optional; 11 | import java.util.UUID; 12 | 13 | public class DewdropStandaloneCommandService { 14 | private StandaloneAggregateProcessor standaloneAggregateProcessor; 15 | private StreamFactory streamFactory; 16 | 17 | public DewdropStandaloneCommandService(StandaloneAggregateProcessor standaloneAggregateProcessor) { 18 | this.standaloneAggregateProcessor = standaloneAggregateProcessor; 19 | } 20 | 21 | public DewdropAccountAggregateSubclass process(DewdropCreateAccountCommand command) { 22 | DewdropAccountAggregateSubclass accountAggregate = new DewdropAccountAggregateSubclass(); 23 | accountAggregate.handle(command); 24 | standaloneAggregateProcessor.save(accountAggregate); 25 | return accountAggregate; 26 | } 27 | 28 | public DewdropAccountAggregateSubclass process(DewdropAddFundsToAccountCommand command) { 29 | DewdropAccountAggregateSubclass accountAggregate = new DewdropAccountAggregateSubclass(); 30 | 31 | Optional optId = AggregateIdUtils.getAggregateId(command); 32 | if (optId.isEmpty()) { return accountAggregate; } 33 | 34 | UUID id = optId.get(); 35 | 36 | AggregateRoot aggregateRoot = standaloneAggregateProcessor.getById(accountAggregate, id); 37 | accountAggregate.handle(command); 38 | standaloneAggregateProcessor.save(aggregateRoot); 39 | return accountAggregate; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/events/DewdropAccountCreated.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.events; 2 | 3 | import events.dewdrop.read.readmodel.annotation.CreationEvent; 4 | import java.util.UUID; 5 | import lombok.Data; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.NoArgsConstructor; 8 | import lombok.ToString; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @ToString(callSuper = true) 13 | @EqualsAndHashCode(callSuper = true) 14 | @CreationEvent 15 | public class DewdropAccountCreated extends DewdropAccountEvent { 16 | private String name; 17 | private UUID userId; 18 | 19 | public DewdropAccountCreated(UUID accountId, String name, UUID userId) { 20 | super(accountId); 21 | this.name = name; 22 | this.userId = userId; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/events/DewdropAccountEvent.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.events; 2 | 3 | import events.dewdrop.aggregate.annotation.AggregateId; 4 | import events.dewdrop.structure.api.Event; 5 | import java.util.UUID; 6 | import lombok.AllArgsConstructor; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.NoArgsConstructor; 10 | 11 | @Data 12 | @AllArgsConstructor 13 | @NoArgsConstructor 14 | @EqualsAndHashCode(of = "accountId") 15 | public abstract class DewdropAccountEvent extends Event { 16 | @AggregateId 17 | private UUID accountId; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/events/DewdropFundsAddedToAccount.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.events; 2 | 3 | import java.math.BigDecimal; 4 | import java.util.UUID; 5 | import lombok.Data; 6 | import lombok.EqualsAndHashCode; 7 | import lombok.NoArgsConstructor; 8 | import lombok.ToString; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @ToString(callSuper = true) 13 | @EqualsAndHashCode(callSuper = true) 14 | public class DewdropFundsAddedToAccount extends DewdropAccountEvent { 15 | private BigDecimal funds; 16 | 17 | public DewdropFundsAddedToAccount(UUID accountId, BigDecimal funds) { 18 | super(accountId); 19 | this.funds = funds; 20 | } 21 | 22 | public BigDecimal getFunds() { 23 | return funds; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/events/DewdropUserCreated.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.events; 2 | 3 | import events.dewdrop.read.readmodel.annotation.CreationEvent; 4 | import java.util.UUID; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | @CreationEvent 11 | public class DewdropUserCreated extends DewdropUserEvent { 12 | String username; 13 | 14 | public DewdropUserCreated(UUID userId, String username) { 15 | super(userId); 16 | this.username = username; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/events/DewdropUserDeactivate.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.events; 2 | 3 | import lombok.Data; 4 | import lombok.NoArgsConstructor; 5 | 6 | import java.util.UUID; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | public class DewdropUserDeactivate extends DewdropUserEvent { 11 | public DewdropUserDeactivate(UUID userId) { 12 | super(userId); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/events/DewdropUserEvent.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.events; 2 | 3 | import events.dewdrop.aggregate.annotation.AggregateId; 4 | import events.dewdrop.structure.api.Event; 5 | import java.util.UUID; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @EqualsAndHashCode(of = "userId") 13 | public abstract class DewdropUserEvent extends Event { 14 | @AggregateId 15 | private UUID userId; 16 | 17 | public DewdropUserEvent(UUID userId) { 18 | this.userId = userId; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/events/UserLoggedIn.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.events; 2 | 3 | import java.time.LocalDateTime; 4 | import java.util.UUID; 5 | 6 | import events.dewdrop.fixture.events.user.UserEvent; 7 | import lombok.Data; 8 | 9 | @Data 10 | public class UserLoggedIn extends UserEvent { 11 | private LocalDateTime login; 12 | 13 | public UserLoggedIn(UUID userId, LocalDateTime login) { 14 | super(userId); 15 | this.login = login; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/events/user/CsrClaimedUsername.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.events.user; 2 | 3 | import events.dewdrop.read.readmodel.annotation.CreationEvent; 4 | import java.util.UUID; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | @CreationEvent 11 | public class CsrClaimedUsername extends UserClaimedUsername { 12 | 13 | public CsrClaimedUsername(UUID userId, String username) { 14 | super(userId, username); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/events/user/UserClaimedUsername.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.events.user; 2 | 3 | import events.dewdrop.read.readmodel.annotation.CreationEvent; 4 | import java.util.UUID; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | 8 | @Data 9 | @NoArgsConstructor 10 | public class UserClaimedUsername extends UserEvent { 11 | String username; 12 | 13 | public UserClaimedUsername(UUID userId, String username) { 14 | super(userId); 15 | this.username = username; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/events/user/UserEvent.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.events.user; 2 | 3 | import events.dewdrop.aggregate.annotation.AggregateId; 4 | import events.dewdrop.structure.api.Event; 5 | import java.util.UUID; 6 | import lombok.Data; 7 | import lombok.EqualsAndHashCode; 8 | import lombok.NoArgsConstructor; 9 | 10 | @Data 11 | @NoArgsConstructor 12 | @EqualsAndHashCode(of = "userId") 13 | public abstract class UserEvent extends Event { 14 | @AggregateId 15 | private UUID userId; 16 | 17 | public UserEvent(UUID userId) { 18 | this.userId = userId; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/events/user/UserSignedUp.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.events.user; 2 | 3 | import events.dewdrop.read.readmodel.annotation.CreationEvent; 4 | import java.util.UUID; 5 | import lombok.Data; 6 | import lombok.NoArgsConstructor; 7 | import lombok.ToString; 8 | 9 | @Data 10 | @NoArgsConstructor 11 | @CreationEvent 12 | @ToString(callSuper = true) 13 | public class UserSignedUp extends UserEvent { 14 | String email; 15 | 16 | public UserSignedUp(UUID userId, String email) { 17 | super(userId); 18 | this.email = email; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/AccountCreatedService.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel; 2 | 3 | import events.dewdrop.fixture.events.DewdropAccountCreated; 4 | import events.dewdrop.read.readmodel.annotation.OnEvent; 5 | import events.dewdrop.read.readmodel.annotation.StreamStartPosition; 6 | import events.dewdrop.read.readmodel.stream.StreamType; 7 | import lombok.extern.log4j.Log4j2; 8 | 9 | @Log4j2 10 | public class AccountCreatedService { 11 | @OnEvent 12 | public void onAccountCreated(DewdropAccountCreated event) { 13 | log.info("AccountCreatedService.onAccountCreated"); 14 | } 15 | 16 | @StreamStartPosition(name = "DewdropAccountCreated", streamType = StreamType.EVENT) 17 | public long startPosition() { 18 | return 0; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/accountdetails/details/DewdropAccountDetails.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.accountdetails.details; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | import java.util.UUID; 6 | 7 | import events.dewdrop.fixture.events.DewdropAccountCreated; 8 | import events.dewdrop.fixture.events.DewdropFundsAddedToAccount; 9 | import events.dewdrop.fixture.events.DewdropUserCreated; 10 | import events.dewdrop.fixture.events.UserLoggedIn; 11 | import events.dewdrop.read.readmodel.annotation.EventHandler; 12 | import events.dewdrop.read.readmodel.annotation.ForeignCacheKey; 13 | import events.dewdrop.read.readmodel.annotation.PrimaryCacheKey; 14 | import lombok.Data; 15 | import lombok.NoArgsConstructor; 16 | import lombok.extern.log4j.Log4j2; 17 | 18 | @Data 19 | @NoArgsConstructor 20 | @Log4j2 21 | public class DewdropAccountDetails { 22 | @PrimaryCacheKey(creationEvent = DewdropAccountCreated.class) 23 | private UUID accountId; 24 | private String name; 25 | private BigDecimal balance = BigDecimal.ZERO; 26 | @ForeignCacheKey(eventKeyField = "userId") 27 | private UUID userId; 28 | private String username; 29 | private UUID causationId; 30 | private LocalDateTime lastLogin; 31 | 32 | @EventHandler 33 | public void on(DewdropAccountCreated event) { 34 | this.causationId = event.getCausationId(); 35 | this.accountId = event.getAccountId(); 36 | this.name = event.getName(); 37 | this.userId = event.getUserId(); 38 | } 39 | 40 | @EventHandler 41 | public void on(DewdropFundsAddedToAccount event) { 42 | log.info("processing DewdropFundsAddedToAccount:{}", event); 43 | this.balance = this.balance.add(event.getFunds()); 44 | } 45 | 46 | @EventHandler 47 | public void on(DewdropUserCreated userCreated) { 48 | log.info("processing DewdropUserCreated:{}", userCreated); 49 | this.username = userCreated.getUsername(); 50 | } 51 | 52 | @EventHandler 53 | public void on(UserLoggedIn event) { 54 | log.info("processing UserLoggedIn:{}", event); 55 | this.lastLogin = event.getLogin(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/accountdetails/details/DewdropAccountDetailsReadModel.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.accountdetails.details; 2 | 3 | import events.dewdrop.api.result.Result; 4 | import events.dewdrop.fixture.events.DewdropAccountCreated; 5 | import events.dewdrop.read.readmodel.annotation.CategoryStream; 6 | import events.dewdrop.read.readmodel.annotation.DewdropCache; 7 | import events.dewdrop.read.readmodel.annotation.EventHandler; 8 | import events.dewdrop.read.readmodel.annotation.ReadModel; 9 | import events.dewdrop.read.readmodel.query.QueryHandler; 10 | import lombok.extern.log4j.Log4j2; 11 | 12 | import java.util.Map; 13 | import java.util.UUID; 14 | 15 | @Log4j2 16 | @ReadModel(destroyInMinutesUnused = ReadModel.DESTROY_IMMEDIATELY) 17 | @CategoryStream(name = "DewdropAccountAggregate", subscribed = true) 18 | @CategoryStream(name = "DewdropUserAggregate", subscribed = false) 19 | public class DewdropAccountDetailsReadModel { 20 | @DewdropCache 21 | Map cache; 22 | 23 | @EventHandler 24 | public void on(DewdropAccountCreated event) { 25 | log.debug("This was called"); 26 | } 27 | 28 | @QueryHandler 29 | public Result handle(DewdropGetAccountByIdQuery query) { 30 | log.info("Querying:{}, cache:{}", query, cache.values()); 31 | DewdropAccountDetails dewdropAccountDetails = cache.get(query.getAccountId()); 32 | log.info("dewdropAccountDetails: {}", dewdropAccountDetails); 33 | return (dewdropAccountDetails != null) ? Result.of(dewdropAccountDetails) : Result.empty(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/accountdetails/details/DewdropGetAccountByIdQuery.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.accountdetails.details; 2 | 3 | import java.util.UUID; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | public class DewdropGetAccountByIdQuery { 10 | private UUID accountId; 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/accountdetails/summary/DewdropAccountSummary.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.accountdetails.summary; 2 | 3 | import events.dewdrop.read.readmodel.annotation.EventHandler; 4 | import events.dewdrop.fixture.events.DewdropAccountCreated; 5 | import events.dewdrop.fixture.events.DewdropFundsAddedToAccount; 6 | import java.math.BigDecimal; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | import lombok.extern.log4j.Log4j2; 11 | 12 | @Data 13 | @NoArgsConstructor 14 | @AllArgsConstructor 15 | @Log4j2 16 | public class DewdropAccountSummary { 17 | private int countOfAccounts = 0; 18 | private BigDecimal totalFunds = new BigDecimal(0); 19 | 20 | @EventHandler 21 | public void on(DewdropFundsAddedToAccount event) { 22 | log.info("=====================> Adding funds to account:{}, funds:{}", event.getAccountId(), event.getFunds()); 23 | this.totalFunds = totalFunds.add(event.getFunds()); 24 | } 25 | 26 | @EventHandler 27 | public void on(DewdropAccountCreated event) { 28 | this.countOfAccounts++; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/accountdetails/summary/DewdropAccountSummaryQuery.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.accountdetails.summary; 2 | 3 | public class DewdropAccountSummaryQuery { 4 | } 5 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/accountdetails/summary/DewdropAccountSummaryReadModel.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.accountdetails.summary; 2 | 3 | import events.dewdrop.read.readmodel.annotation.DewdropCache; 4 | import events.dewdrop.read.readmodel.annotation.EventStream; 5 | import events.dewdrop.read.readmodel.annotation.ReadModel; 6 | import events.dewdrop.read.readmodel.query.QueryHandler; 7 | import lombok.extern.log4j.Log4j2; 8 | 9 | @Log4j2 10 | @ReadModel 11 | @EventStream(name = "DewdropFundsAddedToAccount") 12 | @EventStream(name = "DewdropAccountCreated") 13 | public class DewdropAccountSummaryReadModel { 14 | @DewdropCache 15 | DewdropAccountSummary dewdropAccountSummary; 16 | 17 | @QueryHandler 18 | public DewdropAccountSummary handle(DewdropAccountSummaryQuery query) { 19 | 20 | return dewdropAccountSummary; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/users/DewdropGetUserByIdQuery.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.users; 2 | 3 | import java.util.UUID; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | public class DewdropGetUserByIdQuery { 10 | private UUID userId; 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/users/DewdropGetUserByIdQueryForAggregate.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.users; 2 | 3 | import events.dewdrop.aggregate.annotation.AggregateId; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | 7 | import java.util.UUID; 8 | 9 | @Data 10 | @AllArgsConstructor 11 | public class DewdropGetUserByIdQueryForAggregate { 12 | @AggregateId 13 | private UUID userId; 14 | } 15 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/users/DewdropUser.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.users; 2 | 3 | import events.dewdrop.fixture.events.DewdropUserCreated; 4 | import events.dewdrop.fixture.events.DewdropUserDeactivate; 5 | import events.dewdrop.read.readmodel.annotation.EventHandler; 6 | import events.dewdrop.read.readmodel.annotation.PrimaryCacheKey; 7 | import lombok.AllArgsConstructor; 8 | import lombok.Data; 9 | import lombok.NoArgsConstructor; 10 | import lombok.extern.log4j.Log4j2; 11 | 12 | import java.util.UUID; 13 | 14 | @Data 15 | @NoArgsConstructor 16 | @AllArgsConstructor 17 | @Log4j2 18 | public class DewdropUser { 19 | @PrimaryCacheKey(creationEvent = DewdropUserCreated.class) 20 | private UUID userId; 21 | private String username; 22 | private Long version; 23 | private boolean active = true; 24 | 25 | @EventHandler 26 | private void on(DewdropUserCreated event) { 27 | log.info("Processing DewdropUserCreated,{}", event); 28 | this.userId = event.getUserId(); 29 | this.username = event.getUsername(); 30 | } 31 | 32 | @EventHandler 33 | private void on(DewdropUserDeactivate event) { 34 | this.active = false; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/users/DewdropUserAggregateReadModel.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.users; 2 | 3 | import events.dewdrop.read.readmodel.annotation.AggregateStream; 4 | import events.dewdrop.read.readmodel.annotation.DewdropCache; 5 | import events.dewdrop.read.readmodel.annotation.ReadModel; 6 | import events.dewdrop.read.readmodel.query.QueryHandler; 7 | import lombok.extern.slf4j.Slf4j; 8 | 9 | @Slf4j 10 | @ReadModel 11 | @AggregateStream(name = "DewdropUserAggregate", subscribed = false) 12 | public class DewdropUserAggregateReadModel { 13 | @DewdropCache 14 | DewdropUser dewdropUser; 15 | 16 | @QueryHandler 17 | public DewdropUser query(DewdropGetUserByIdQueryForAggregate userById) { 18 | log.info("Looking for userById:" + userById + ", dewdropUser:" + dewdropUser); 19 | return dewdropUser; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/users/DewdropUsersReadModel.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.users; 2 | 3 | import events.dewdrop.read.readmodel.annotation.DewdropCache; 4 | import events.dewdrop.read.readmodel.annotation.EventStream; 5 | import events.dewdrop.read.readmodel.annotation.ReadModel; 6 | import events.dewdrop.read.readmodel.query.QueryHandler; 7 | import lombok.Getter; 8 | import lombok.extern.log4j.Log4j2; 9 | 10 | import java.util.Map; 11 | import java.util.UUID; 12 | 13 | @Log4j2 14 | @ReadModel 15 | @EventStream(name = "DewdropUserCreated") 16 | @Getter 17 | public class DewdropUsersReadModel { 18 | @DewdropCache 19 | Map cache; 20 | 21 | @QueryHandler 22 | public DewdropUser query(DewdropGetUserByIdQuery userById) { 23 | log.info("Looking for userById:" + userById + ", cache:" + cache.values()); 24 | 25 | DewdropUser dewdropUser = cache.get(userById.getUserId()); 26 | return dewdropUser; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/users/GetUserByIdQuery.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.users; 2 | 3 | import java.util.UUID; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Data; 6 | 7 | @Data 8 | @AllArgsConstructor 9 | public class GetUserByIdQuery { 10 | private UUID userId; 11 | } 12 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/users/User.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.users; 2 | 3 | import events.dewdrop.fixture.events.user.UserClaimedUsername; 4 | import events.dewdrop.fixture.events.user.UserSignedUp; 5 | import events.dewdrop.read.readmodel.annotation.EventHandler; 6 | import events.dewdrop.read.readmodel.annotation.PrimaryCacheKey; 7 | import java.util.UUID; 8 | import lombok.AllArgsConstructor; 9 | import lombok.Data; 10 | import lombok.NoArgsConstructor; 11 | import lombok.extern.log4j.Log4j2; 12 | 13 | @Data 14 | @NoArgsConstructor 15 | @AllArgsConstructor 16 | @Log4j2 17 | public class User { 18 | @PrimaryCacheKey(creationEvent = UserSignedUp.class) 19 | private UUID userId; 20 | private String username; 21 | private String email; 22 | private Long version; 23 | 24 | @EventHandler 25 | private void on(UserSignedUp event) { 26 | log.info("Processing UserSignedUp:{}", event); 27 | this.userId = event.getUserId(); 28 | this.email = event.getEmail(); 29 | } 30 | 31 | @EventHandler 32 | private void on(UserClaimedUsername event) { 33 | log.info("Processing UserClaimedUsername:{}", event); 34 | this.username = event.getUsername(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/fixture/readmodel/users/lifecycle/UsersReadModel.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.fixture.readmodel.users.lifecycle; 2 | 3 | import events.dewdrop.fixture.readmodel.users.GetUserByIdQuery; 4 | import events.dewdrop.fixture.readmodel.users.User; 5 | import events.dewdrop.read.readmodel.annotation.CategoryStream; 6 | import events.dewdrop.read.readmodel.annotation.DewdropCache; 7 | import events.dewdrop.read.readmodel.annotation.ReadModel; 8 | import events.dewdrop.read.readmodel.query.QueryHandler; 9 | import lombok.Getter; 10 | import lombok.extern.log4j.Log4j2; 11 | 12 | import java.util.Map; 13 | import java.util.UUID; 14 | 15 | @Log4j2 16 | @ReadModel 17 | @CategoryStream(name = "UserAggregate", subscribed = true) 18 | @Getter 19 | public class UsersReadModel { 20 | @DewdropCache 21 | Map cache; 22 | 23 | @QueryHandler 24 | public User query(GetUserByIdQuery userById) { 25 | log.info("Looking for userById:" + userById + ", cacheSize:" + cache.size()); 26 | 27 | User user = cache.get(userById.getUserId()); 28 | return user; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/read/readmodel/cache/ImprovedMapBackedInMemoryCacheProcessorTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.cache; 2 | 3 | import java.math.BigDecimal; 4 | import java.time.LocalDateTime; 5 | import java.util.Map; 6 | import java.util.UUID; 7 | 8 | import static org.hamcrest.MatcherAssert.assertThat; 9 | import static org.hamcrest.Matchers.is; 10 | import static org.mockito.Mockito.spy; 11 | 12 | import events.dewdrop.fixture.events.DewdropAccountCreated; 13 | import events.dewdrop.fixture.events.DewdropFundsAddedToAccount; 14 | import events.dewdrop.fixture.events.DewdropUserCreated; 15 | import events.dewdrop.fixture.events.UserLoggedIn; 16 | import events.dewdrop.fixture.readmodel.accountdetails.details.DewdropAccountDetails; 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.DisplayName; 19 | import org.junit.jupiter.api.Test; 20 | 21 | class ImprovedMapBackedInMemoryCacheProcessorTest { 22 | private ImprovedMapBackedInMemoryCacheProcessor sut; 23 | private Map cache; 24 | 25 | UUID accountId = UUID.randomUUID(); 26 | UUID userId = UUID.randomUUID(); 27 | 28 | DewdropAccountCreated accountCreated = new DewdropAccountCreated(accountId, "test", userId); 29 | DewdropUserCreated userCreated = new DewdropUserCreated(userId, "tester guy"); 30 | 31 | @BeforeEach 32 | void setup() { 33 | sut = spy(new ImprovedMapBackedInMemoryCacheProcessor<>(DewdropAccountDetails.class)); 34 | } 35 | 36 | @Test 37 | @DisplayName("constructor() - confirm that we construct properly") 38 | void constructor() { 39 | assertThat(sut.getCachedStateObjectType(), is(DewdropAccountDetails.class)); 40 | assertThat(sut.getCache().size(), is(0)); 41 | } 42 | 43 | @Test 44 | @DisplayName("process() - Confirm primary creation event is processed") 45 | void process() { 46 | sut.process(accountCreated); 47 | DewdropAccountDetails result = sut.getCache().get(accountId); 48 | assertThat(result.getAccountId(), is(accountId)); 49 | } 50 | 51 | @Test 52 | @DisplayName("process() - Confirm primary creation event is processed out of order") 53 | void process_outOfOrder() { 54 | DewdropFundsAddedToAccount message = new DewdropFundsAddedToAccount(accountId, new BigDecimal(34)); 55 | sut.process(message); 56 | sut.process(accountCreated); 57 | DewdropAccountDetails result = sut.getCache().get(accountId); 58 | assertThat(result.getAccountId(), is(accountId)); 59 | assertThat(result.getBalance(), is(message.getFunds())); 60 | } 61 | 62 | @Test 63 | @DisplayName("process() - Confirm foreign creation event is processed") 64 | void process_foreignEvent() { 65 | sut.process(accountCreated); 66 | sut.process(userCreated); 67 | DewdropAccountDetails result = sut.getCache().get(accountId); 68 | assertThat(result.getUsername(), is(userCreated.getUsername())); 69 | } 70 | 71 | @Test 72 | @DisplayName("process() - Confirm foreign creation event is processed, even if it came first") 73 | void process_foreignEvent_tooSoon() { 74 | sut.process(userCreated); 75 | sut.process(accountCreated); 76 | DewdropAccountDetails result = sut.getCache().get(accountId); 77 | assertThat(result.getUsername(), is(userCreated.getUsername())); 78 | assertThat(sut.getForeignStashedMessages().get(userId).isEmpty(), is(true)); 79 | } 80 | 81 | @Test 82 | @DisplayName("process() - Confirm foreign creation event is processed, even if it came first") 83 | void process_foreignEvent_tooSoon_withMultiples() { 84 | UserLoggedIn login = new UserLoggedIn(userId, LocalDateTime.now()); 85 | sut.process(userCreated); 86 | sut.process(login); 87 | sut.process(accountCreated); 88 | DewdropAccountDetails result = sut.getCache().get(accountId); 89 | assertThat(result.getLastLogin(), is(login.getLogin())); 90 | assertThat(sut.getForeignStashedMessages().get(userId).isEmpty(), is(true)); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/read/readmodel/stream/NameAndPositionTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.stream; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | 7 | import events.dewdrop.structure.api.Event; 8 | import java.util.function.Consumer; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class NameAndPositionTest { 14 | NameAndPosition nameAndPosition; 15 | StreamType streamType = StreamType.CATEGORY; 16 | String name = "name"; 17 | Consumer eventConsumer = event -> { 18 | }; 19 | 20 | @BeforeEach 21 | void setup() { 22 | nameAndPosition = NameAndPosition.builder().name(name).streamType(streamType).create(); 23 | } 24 | 25 | 26 | @Test 27 | @DisplayName("Given a name, a streamType and a consumer, when we call the constructor, then the object is created") 28 | void constructor() { 29 | assertEquals(name, nameAndPosition.getName()); 30 | assertEquals(streamType, nameAndPosition.getStreamType()); 31 | } 32 | 33 | @Test 34 | @DisplayName("Given a nameAndPosition, when we call isComplete(), then isComplete() returns false") 35 | void isComplete() { 36 | assertEquals(false, nameAndPosition.isComplete()); 37 | 38 | } 39 | 40 | @Test 41 | @DisplayName("Given a nameAndPosition, when we call completeTask(), then isComplete() returns true") 42 | void completeTask() { 43 | NameAndPosition result = nameAndPosition.completeTask("streamName", 1L); 44 | assertThat(nameAndPosition, is(result)); 45 | assertEquals(true, nameAndPosition.isComplete()); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/read/readmodel/stream/StreamListenerTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.read.readmodel.stream; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.mockito.ArgumentMatchers.any; 6 | import static org.mockito.ArgumentMatchers.anyLong; 7 | import static org.mockito.Mockito.doNothing; 8 | import static org.mockito.Mockito.doReturn; 9 | import static org.mockito.Mockito.mock; 10 | import static org.mockito.Mockito.spy; 11 | import static org.mockito.Mockito.times; 12 | import static org.mockito.Mockito.verify; 13 | import static org.mockito.Mockito.when; 14 | 15 | import events.dewdrop.fixture.events.DewdropUserCreated; 16 | import events.dewdrop.read.readmodel.stream.subscription.Subscription; 17 | import events.dewdrop.streamstore.eventstore.EventStore; 18 | import events.dewdrop.streamstore.serialize.JsonSerializer; 19 | import events.dewdrop.structure.datastore.StreamStore; 20 | import events.dewdrop.structure.events.ReadEventData; 21 | import events.dewdrop.structure.serialize.EventSerializer; 22 | import events.dewdrop.structure.subscribe.SubscribeRequest; 23 | import java.util.Optional; 24 | import java.util.function.Consumer; 25 | import org.junit.jupiter.api.BeforeEach; 26 | import org.junit.jupiter.api.DisplayName; 27 | import org.junit.jupiter.api.Test; 28 | import org.junit.jupiter.api.extension.ExtendWith; 29 | import org.mockito.junit.jupiter.MockitoExtension; 30 | 31 | @ExtendWith(MockitoExtension.class) 32 | class StreamListenerTest { 33 | StreamStore streamStore; 34 | EventSerializer eventSerializer; 35 | StreamListener streamListener; 36 | Subscription subscription; 37 | ReadEventData readEventData; 38 | 39 | @BeforeEach 40 | void setup() { 41 | subscription = mock(Subscription.class); 42 | readEventData = mock(ReadEventData.class); 43 | streamStore = mock(EventStore.class); 44 | eventSerializer = mock(JsonSerializer.class); 45 | streamListener = spy(StreamListener.getInstance(streamStore, eventSerializer)); 46 | } 47 | 48 | @Test 49 | void start() { 50 | doReturn(true).when(streamListener).subscribe(anyLong(), any(Consumer.class)); 51 | 52 | assertThat(streamListener.start("streamName", 0L, subscription), is(true)); 53 | assertThat(streamListener.getStreamName(), is("streamName")); 54 | } 55 | 56 | @Test 57 | @DisplayName("onEvent() - Given a valid ReadEventData, our consumer should publish the event to the subscription and update the stream position") 58 | void onEvent() { 59 | DewdropUserCreated event = new DewdropUserCreated(); 60 | when(eventSerializer.deserialize(any(ReadEventData.class))).thenReturn(Optional.of(event)); 61 | doNothing().when(subscription).publish(any(DewdropUserCreated.class)); 62 | Consumer readEventDataConsumer = streamListener.onEvent(subscription); 63 | doReturn(50L).when(readEventData).getEventNumber(); 64 | readEventDataConsumer.accept(readEventData); 65 | 66 | assertThat(streamListener.getStreamPosition().get(), is(50L)); 67 | verify(subscription, times(1)).publish(event); 68 | } 69 | 70 | @Test 71 | @DisplayName("onEvent() - Given a ReadEventData that we cannot deserialize, log and do nothing") 72 | void onEvent_unableToDeserialize() { 73 | DewdropUserCreated event = new DewdropUserCreated(); 74 | doReturn(Optional.empty()).when(eventSerializer).deserialize(any(ReadEventData.class)); 75 | 76 | Consumer readEventDataConsumer = streamListener.onEvent(subscription); 77 | 78 | readEventDataConsumer.accept(readEventData); 79 | 80 | assertThat(streamListener.getStreamPosition().get(), is(0L)); 81 | verify(subscription, times(0)).publish(event); 82 | } 83 | 84 | @Test 85 | @DisplayName("subscribe() - Given a valid checkpoint and a consumer, subscribe to the stream and return true") 86 | void subscribe() { 87 | doReturn(true).when(streamStore).subscribeToStream(any(SubscribeRequest.class)); 88 | Consumer consumer = mock(Consumer.class); 89 | assertThat(streamListener.subscribe(0L, consumer), is(true)); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/streamstore/stream/PrefixStreamNameGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.streamstore.stream; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | 6 | import events.dewdrop.fixture.automated.DewdropUserAggregate; 7 | import events.dewdrop.fixture.events.DewdropUserCreated; 8 | import java.util.UUID; 9 | import org.junit.jupiter.api.BeforeEach; 10 | import org.junit.jupiter.api.DisplayName; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class PrefixStreamNameGeneratorTest { 14 | PrefixStreamNameGenerator nameGenerator; 15 | PrefixStreamNameGenerator prefixedNameGenerator; 16 | 17 | @BeforeEach 18 | void setup() { 19 | nameGenerator = new PrefixStreamNameGenerator(); 20 | prefixedNameGenerator = new PrefixStreamNameGenerator("test"); 21 | } 22 | 23 | @Test 24 | void generateForCategory() { 25 | assertThat(nameGenerator.generateForCategory(DewdropUserAggregate.class), is("$ce-DewdropUserAggregate")); 26 | assertThat(prefixedNameGenerator.generateForCategory(DewdropUserAggregate.class), is("$ce-test.DewdropUserAggregate")); 27 | } 28 | 29 | @Test 30 | void generateForAggregate() { 31 | UUID id = UUID.randomUUID(); 32 | assertThat(nameGenerator.generateForAggregate(DewdropUserAggregate.class.getSimpleName(), id), is("DewdropUserAggregate-" + id)); 33 | assertThat(prefixedNameGenerator.generateForAggregate(DewdropUserAggregate.class.getSimpleName(), id), is("test.DewdropUserAggregate-" + id)); 34 | assertThat(prefixedNameGenerator.generateForAggregate("test." + DewdropUserAggregate.class.getSimpleName(), id), is("test.DewdropUserAggregate-" + id)); 35 | } 36 | 37 | @Test 38 | @DisplayName("Prefix is not honored for events in eventstore") 39 | void generateForEvent() { 40 | assertThat(nameGenerator.generateForEvent(DewdropUserCreated.class.getSimpleName()), is("$et-DewdropUserCreated")); 41 | assertThat(prefixedNameGenerator.generateForEvent(DewdropUserCreated.class.getSimpleName()), is("$et-DewdropUserCreated")); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/structure/api/validator/DewdropValidatorTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.structure.api.validator; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.junit.jupiter.api.Assertions.fail; 6 | 7 | import events.dewdrop.api.validators.ValidationError; 8 | import events.dewdrop.api.validators.ValidationException; 9 | import events.dewdrop.api.validators.ValidationResult; 10 | import java.util.List; 11 | import java.util.UUID; 12 | import org.apache.commons.lang3.Validate; 13 | import events.dewdrop.fixture.command.DewdropCreateUserCommand; 14 | import org.junit.jupiter.api.Test; 15 | 16 | class DewdropValidatorTest { 17 | @Test 18 | void invalid() { 19 | DewdropCreateUserCommand command = new DewdropCreateUserCommand(null, null); 20 | 21 | try { 22 | DewdropValidator.withRule(() -> Validate.notBlank(command.getUsername(), "username is required")).andRule(() -> Validate.notNull(command.getUserId(), "userId is required")).validate(); 23 | } catch (ValidationException e) { 24 | ValidationResult validationResult = e.getValidationResult(); 25 | List validationErrors = validationResult.get(); 26 | assertThat(validationErrors.size(), is(2)); 27 | assertThat(validationErrors.get(0).getMessage(), is("username is required")); 28 | assertThat(validationErrors.get(1).getMessage(), is("userId is required")); 29 | } 30 | } 31 | 32 | @Test 33 | void valid() { 34 | DewdropCreateUserCommand command = new DewdropCreateUserCommand(UUID.randomUUID(), "Test"); 35 | try { 36 | DewdropValidator.withRule(() -> Validate.notBlank(command.getUsername(), "username is required")).andRule(() -> Validate.notNull(command.getUserId(), "userId is required")).validate(); 37 | } catch (ValidationException e) { 38 | fail("Should not throw exception"); 39 | } 40 | } 41 | 42 | @Test 43 | void jsr303_valid() { 44 | DewdropCreateUserCommand command = new DewdropCreateUserCommand(UUID.randomUUID(), "Test"); 45 | try { 46 | DewdropValidator.validate(command); 47 | } catch (ValidationException e) { 48 | fail("Should not throw exception"); 49 | } 50 | } 51 | 52 | @Test 53 | void jsr303_invalid() { 54 | DewdropCreateUserCommand command = new DewdropCreateUserCommand(null, null); 55 | try { 56 | DewdropValidator.validate(command); 57 | } catch (ValidationException e) { 58 | ValidationResult validationResult = e.getValidationResult(); 59 | List validationErrors = validationResult.get(); 60 | assertThat(validationErrors.size(), is(2)); 61 | 62 | assertThat(validationErrors.stream().filter(error -> error.getMessage().equals("UserId is required")).findAny().get().getMessage(), is("UserId is required")); 63 | assertThat(validationErrors.stream().filter(error -> error.getMessage().equals("Username is required")).findAny().get().getMessage(), is("Username is required")); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/utils/AggregateUtilsTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.empty; 5 | import static org.hamcrest.Matchers.hasItems; 6 | import static org.hamcrest.Matchers.is; 7 | import static org.mockito.ArgumentMatchers.any; 8 | import static org.mockito.Mockito.mockStatic; 9 | 10 | import events.dewdrop.fixture.automated.DewdropAccountAggregate; 11 | import events.dewdrop.fixture.automated.DewdropUserAggregate; 12 | import events.dewdrop.fixture.command.DewdropCreateUserCommand; 13 | import events.dewdrop.fixture.command.DewdropAccountCommand; 14 | import java.util.HashSet; 15 | import java.util.List; 16 | import java.util.UUID; 17 | import org.junit.jupiter.api.BeforeEach; 18 | import org.junit.jupiter.api.DisplayName; 19 | import org.junit.jupiter.api.Test; 20 | import org.mockito.MockedStatic; 21 | 22 | class AggregateUtilsTest { 23 | @BeforeEach 24 | void setup() { 25 | ReflectionsConfigUtils.init("events.dewdrop"); 26 | } 27 | 28 | @Test 29 | @DisplayName("getAggregateRootsThatSupportCommand() - Given a valid command we should test that we can find the AggregateRoot object that uses this command") 30 | void getAggregateRootsThatSupportCommand() { 31 | DewdropCreateUserCommand command = new DewdropCreateUserCommand(UUID.randomUUID(), "Test"); 32 | assertThat(AggregateUtils.getAggregateRootsThatSupportCommand(command).get(0), is(DewdropUserAggregate.class)); 33 | } 34 | 35 | @Test 36 | @DisplayName("getAggregateRootsThatSupportCommand() - Given an invalid command we should test that when there is no AggregateRoot that handles this command we return nothing") 37 | void getAggregateRootsThatSupportCommand_invalidCommand() { 38 | InvalidCommand instance = new InvalidCommand(UUID.randomUUID()); 39 | assertThat(AggregateUtils.getAggregateRootsThatSupportCommand(instance).isEmpty(), is(true)); 40 | } 41 | 42 | @Test 43 | @DisplayName("getAnnotatedAggregateRoots() - This should find all the existing annotated @AggregateRoot classes") 44 | void getAnnotatedAggregateRoots() { 45 | List> aggregateRoots = AggregateUtils.getAnnotatedAggregateRoots(); 46 | int size = aggregateRoots.size(); 47 | assertThat(aggregateRoots, hasItems(DewdropAccountAggregate.class, DewdropUserAggregate.class)); 48 | 49 | aggregateRoots = AggregateUtils.getAnnotatedAggregateRoots(); 50 | assertThat(aggregateRoots.size(), is(size)); 51 | } 52 | 53 | @Test 54 | @DisplayName("getAnnotatedAggregateRoots() - This should return empty since there are no annotated @AggregateRoot classes") 55 | void getAnnotatedAggregateRoots_noneFound() throws ReflectiveOperationException { 56 | AggregateUtils.clear(); 57 | try (MockedStatic utilities = mockStatic(DewdropAnnotationUtils.class)) { 58 | utilities.when(() -> DewdropAnnotationUtils.getAnnotatedClasses(any(Class.class))).thenReturn(new HashSet()); 59 | 60 | List> aggregateRoots = AggregateUtils.getAnnotatedAggregateRoots(); 61 | assertThat(aggregateRoots, is(empty())); 62 | } 63 | } 64 | 65 | private class InvalidCommand extends DewdropAccountCommand { 66 | public InvalidCommand(UUID accountId) { 67 | super(accountId); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/utils/AssignCorrelationAndCausationTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | 6 | import events.dewdrop.fixture.command.DewdropCreateAccountCommand; 7 | import events.dewdrop.fixture.command.DewdropCreateUserCommand; 8 | import java.util.UUID; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class AssignCorrelationAndCausationTest { 13 | @Test 14 | @DisplayName("assignTo() - Given an object does it have the field associated with it") 15 | void assignTo() { 16 | DewdropCreateUserCommand firstCommand = new DewdropCreateUserCommand(UUID.randomUUID(), "test"); 17 | DewdropCreateAccountCommand secondCommand = new DewdropCreateAccountCommand(UUID.randomUUID(), "Test", UUID.randomUUID()); 18 | 19 | secondCommand = AssignCorrelationAndCausation.assignTo(firstCommand, secondCommand); 20 | assertThat(secondCommand.getCorrelationId(), is(secondCommand.getCorrelationId())); 21 | assertThat(secondCommand.getCausationId(), is(secondCommand.getCausationId())); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/utils/DependencyInjectionUtilsTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.mockito.ArgumentMatchers.any; 6 | import static org.mockito.Mockito.doReturn; 7 | import static org.mockito.Mockito.mockStatic; 8 | import static org.mockito.Mockito.spy; 9 | import static org.mockito.Mockito.times; 10 | import static org.mockito.Mockito.verify; 11 | 12 | import events.dewdrop.config.DependencyInjectionAdapter; 13 | import events.dewdrop.fixture.events.DewdropUserCreated; 14 | import java.util.Optional; 15 | import org.junit.jupiter.api.BeforeEach; 16 | import org.junit.jupiter.api.DisplayName; 17 | import org.junit.jupiter.api.Test; 18 | import org.mockito.ArgumentMatchers; 19 | import org.mockito.MockedStatic; 20 | 21 | class DependencyInjectionUtilsTest { 22 | DependencyInjectionAdapter dependencyInjection; 23 | 24 | @BeforeEach 25 | void setup() { 26 | dependencyInjection = spy(new TestDependencyInjectionAdapter()); 27 | DependencyInjectionUtils.setDependencyInjection(dependencyInjection); 28 | } 29 | 30 | @Test 31 | @DisplayName("setDependencyInjection() - Given a dependency injection adapter, When the method is called, Then the adapter is set") 32 | void setDependencyInjection() { 33 | assertThat(DependencyInjectionUtils.dependencyInjectionAdapter, is(dependencyInjection)); 34 | } 35 | 36 | 37 | @Test 38 | @DisplayName("getInstance() - Given a dependency injection adapter, when getInstance() is called, Then getBean() is called on the adapter") 39 | void getInstance() { 40 | doReturn(new Object()).when(dependencyInjection).getBean(any(Class.class)); 41 | assertThat(DependencyInjectionUtils.getInstance(DewdropUserCreated.class).isPresent(), is(true)); 42 | verify(dependencyInjection, times(1)).getBean(ArgumentMatchers.any(Class.class)); 43 | } 44 | 45 | @Test 46 | @DisplayName("getInstance() - Given a null object from the dependency injection adapter, when getInstance() is called, Then DewdropReflectionUtils.createInstance() is called") 47 | void getInstance_nullFromDependencyInjector() { 48 | doReturn(null).when(dependencyInjection).getBean(any(Class.class)); 49 | 50 | try (MockedStatic utilities = mockStatic(DewdropReflectionUtils.class)) { 51 | utilities.when(() -> DewdropReflectionUtils.createInstance(any(Class.class))).thenReturn(Optional.of(new Object())); 52 | 53 | assertThat(DependencyInjectionUtils.getInstance(DewdropUserCreated.class).isPresent(), is(true)); 54 | verify(dependencyInjection, times(1)).getBean(ArgumentMatchers.any(Class.class)); 55 | } 56 | } 57 | 58 | @Test 59 | @DisplayName("getInstance() - Given a null dependency injection adapter, when getInstance() is called, Then DewdropReflectionUtils.createInstance() is called") 60 | void getInstance_nullDependencyInjector() { 61 | DependencyInjectionUtils.dependencyInjectionAdapter = null; 62 | 63 | try (MockedStatic utilities = mockStatic(DewdropReflectionUtils.class)) { 64 | utilities.when(() -> DewdropReflectionUtils.createInstance(any(Class.class))).thenReturn(Optional.of(new Object())); 65 | 66 | assertThat(DependencyInjectionUtils.getInstance(DewdropUserCreated.class).isPresent(), is(true)); 67 | verify(dependencyInjection, times(0)).getBean(ArgumentMatchers.any(Class.class)); 68 | } 69 | } 70 | 71 | private class TestDependencyInjectionAdapter implements DependencyInjectionAdapter { 72 | @Override 73 | public T getBean(Class clazz) { 74 | return (T) new String(); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/events/dewdrop/utils/ReflectionsConfigUtilsTest.java: -------------------------------------------------------------------------------- 1 | package events.dewdrop.utils; 2 | 3 | import static org.hamcrest.MatcherAssert.assertThat; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.hamcrest.Matchers.notNullValue; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | 8 | import java.util.List; 9 | import org.junit.jupiter.api.DisplayName; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class ReflectionsConfigUtilsTest { 13 | @Test 14 | @DisplayName("init() - Given a packageToScan, initialize the REFLECTIONS public static final") 15 | void init() { 16 | ReflectionsConfigUtils.init("events.dewdrop"); 17 | assertThat(ReflectionsConfigUtils.REFLECTIONS, is(notNullValue())); 18 | assertThat(ReflectionsConfigUtils.EXCLUDE_PACKAGES.isEmpty(), is(true)); 19 | } 20 | 21 | @Test 22 | @DisplayName("init() - Given no packages to scan, throw an IllegalArgumentException ") 23 | void init_noPackageToScan() { 24 | assertThrows(IllegalArgumentException.class, () -> ReflectionsConfigUtils.init("")); 25 | } 26 | 27 | @Test 28 | @DisplayName("init() - Given a packageToScan and a list of excluded packages, initialize the REFLECTIONS public static final and confirm that the EXCLUDED_PACKAGES reflects exclusions") 29 | void init_exclusions() { 30 | ReflectionsConfigUtils.init("events.dewdrop", List.of("events.dewdrop")); 31 | assertThat(ReflectionsConfigUtils.REFLECTIONS, is(notNullValue())); 32 | assertThat(ReflectionsConfigUtils.EXCLUDE_PACKAGES.get(0), is("events.dewdrop")); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/test/resources/dewdropper.yml: -------------------------------------------------------------------------------- 1 | dewdropper: 2 | eventstoredb: 3 | connection-string: esdb://localhost:2113?tls=false -------------------------------------------------------------------------------- /src/test/resources/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | --------------------------------------------------------------------------------