├── .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 super T> 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 super Exception> consumer) {
83 | if (exception != null) {
84 | consumer.accept(exception);
85 | }
86 | }
87 |
88 | public void ifExceptionPresent(Class extends Exception> targetType, Consumer super Exception> 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 extends Annotation> fieldAnnotation) {
22 | Set fields = FieldUtils.getFieldsListWithAnnotation(targetClass, fieldAnnotation).stream().collect(toSet());
23 | return fields;
24 | }
25 |
26 | public static Set getAnnotatedMethods(Class extends Annotation> 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 extends Annotation> 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 extends Annotation> 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 |
--------------------------------------------------------------------------------