├── .github
├── CODEOWNERS
├── ISSUE_TEMPLATE.md
├── ISSUE_TEMPLATE
│ ├── 1_feature_request.md
│ ├── 2_enhancement_request.md
│ └── 3_bug_report.md
├── close-label.yml
├── dependabot.yml
└── workflows
│ ├── dependabot-automation.yml
│ ├── docs.yml
│ └── main.yml
├── .gitignore
├── .mvn
└── wrapper
│ ├── MavenWrapperDownloader.java
│ └── maven-wrapper.properties
├── .run
├── Gift Card Application - Command Side.run.xml
├── Gift Card Application - Command, Query and GUI.run.xml
├── Gift Card Application - GUI.run.xml
└── Gift Card Application - Query Side.run.xml
├── LICENSE
├── README.md
├── axon-data-protection-config.json
├── axon-data-protection-plugin-config.yaml
├── docker
└── docker-compose.yml
├── docs
├── _playbook
│ ├── .gitignore
│ ├── .vale.ini
│ ├── package-lock.json
│ ├── package.json
│ └── playbook.yaml
└── tutorial
│ ├── antora.yml
│ └── modules
│ └── ROOT
│ ├── examples
│ └── src
│ ├── nav.adoc
│ └── pages
│ ├── architecture.adoc
│ ├── commands.adoc
│ ├── deployment.adoc
│ ├── design.adoc
│ ├── events.adoc
│ ├── index.adoc
│ ├── next_steps.adoc
│ └── projections.adoc
├── kubernetes
└── axonserver.yaml
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
├── java
│ └── io
│ │ └── axoniq
│ │ └── demo
│ │ └── giftcard
│ │ ├── AxonConfig.java
│ │ ├── GiftCardApp.java
│ │ ├── api
│ │ ├── Address.java
│ │ ├── CancelCardCommand.java
│ │ ├── CardCanceledEvent.java
│ │ ├── CardIssuedEvent.java
│ │ ├── CardRedeemedEvent.java
│ │ ├── CardSummary.java
│ │ ├── CountCardSummariesQuery.java
│ │ ├── CountCardSummariesResponse.java
│ │ ├── ExampleEvent.java
│ │ ├── FetchCardSummariesQuery.java
│ │ ├── IssueCardCommand.java
│ │ └── RedeemCardCommand.java
│ │ ├── command
│ │ └── GiftCard.java
│ │ ├── query
│ │ └── CardSummaryProjection.java
│ │ └── rest
│ │ ├── GiftCardController.java
│ │ └── Result.java
└── resources
│ ├── application-command.properties
│ ├── application-gui.properties
│ ├── application-query.properties
│ ├── application.properties
│ └── static
│ ├── app.js
│ └── index.html
└── test
└── java
└── io
└── axoniq
└── demo
└── giftcard
├── command
└── GiftCardTest.java
└── query
└── CardSummaryProjectionTest.java
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @CodeDrivenMitch
2 | * @MateuszNaKodach
3 | * @smcvb
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE.md:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/1_feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Feature request'
3 | about: 'Suggest a feature for the GiftCard Demo'
4 | title:
5 | labels: 'Type: Feature'
6 | ---
7 |
8 |
9 |
10 | ### Feature Description
11 |
12 |
16 |
17 | ### Current Behaviour
18 |
19 |
20 |
21 | ### Wanted Behaviour
22 |
23 |
24 |
25 | ### Possible Workarounds
26 |
27 |
28 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/2_enhancement_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Enhancement request'
3 | about: 'Suggest an enhancement/change to an existing feature for the GiftCard Demo'
4 | title:
5 | labels: 'Type: Enhancement'
6 | ---
7 |
8 |
9 |
10 | ### Enhancement Description
11 |
12 |
13 |
14 | ### Current Behaviour
15 |
16 |
17 |
18 | ### Wanted Behaviour
19 |
20 |
21 |
22 | ### Possible Workarounds
23 |
24 |
25 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/3_bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: 'Bug report'
3 | about: 'Report a bug for the GiftCard Demo'
4 | title:
5 | labels: 'Type: Bug'
6 | ---
7 |
8 |
9 |
10 | ### Basic information
11 |
12 | * JDK version:
13 | * Complete executable reproducer if available (e.g. GitHub Repo):
14 |
15 | ### Steps to reproduce
16 |
17 |
21 |
22 | ### Expected behaviour
23 |
24 |
25 |
26 | ### Actual behaviour
27 |
28 |
32 |
--------------------------------------------------------------------------------
/.github/close-label.yml:
--------------------------------------------------------------------------------
1 | "Type: Bug": "Status: Resolved"
2 | "Type: Enhancement": "Status: Resolved"
3 | "Type: Feature": "Status: Resolved"
4 | "Type: Dependency Upgrade": "Status: Resolved"
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 |
3 | updates:
4 | - package-ecosystem: github-actions
5 | directory: "/"
6 | schedule:
7 | interval: weekly
8 | day: "sunday"
9 | open-pull-requests-limit: 5
10 | labels:
11 | - "Type: Dependency Upgrade"
12 | - "Priority 1: Must"
13 | milestone: 7
14 | groups:
15 | github-dependencies:
16 | update-types:
17 | - "patch"
18 | - "minor"
19 | - "major"
20 |
21 | - package-ecosystem: maven
22 | directory: "/"
23 | schedule:
24 | interval: weekly
25 | day: "sunday"
26 | open-pull-requests-limit: 5
27 | labels:
28 | - "Type: Dependency Upgrade"
29 | - "Priority 1: Must"
30 | milestone: 7
31 | groups:
32 | maven-dependencies:
33 | update-types:
34 | - "patch"
35 | - "minor"
36 | - "major"
37 |
38 | - package-ecosystem: npm
39 | directory: "/"
40 | schedule:
41 | interval: weekly
42 | day: "sunday"
43 | open-pull-requests-limit: 5
44 | labels:
45 | - "Type: Dependency Upgrade"
46 | - "Priority 1: Must"
47 | milestone: 7
48 | groups:
49 | maven-dependencies:
50 | update-types:
51 | - "patch"
52 | - "minor"
53 | - "major"
54 |
--------------------------------------------------------------------------------
/.github/workflows/dependabot-automation.yml:
--------------------------------------------------------------------------------
1 | name: Dependabot Automation
2 | on: pull_request
3 |
4 | permissions:
5 | contents: write
6 | pull-requests: write
7 |
8 | jobs:
9 | dependabot-approve:
10 | name: Approve PR
11 |
12 | runs-on: ubuntu-latest
13 | if: ${{ github.actor == 'dependabot[bot]' }}
14 | steps:
15 | - name: Retrieve Dependabot metadata
16 | id: metadata
17 | uses: dependabot/fetch-metadata@v2
18 | with:
19 | github-token: "${{ secrets.GITHUB_TOKEN }}"
20 |
21 | - name: Approve Pull Request
22 | run: gh pr review --approve "$PR_URL"
23 | env:
24 | PR_URL: ${{github.event.pull_request.html_url}}
25 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
26 |
27 | dependabot-auto-merge:
28 | name: Auto Merge
29 |
30 | runs-on: ubuntu-latest
31 | if: ${{ github.actor == 'dependabot[bot]' }}
32 | steps:
33 | - name: Retrieve Dependabot metadata
34 | id: metadata
35 | uses: dependabot/fetch-metadata@v2
36 | with:
37 | github-token: "${{ secrets.GITHUB_TOKEN }}"
38 |
39 | - name: Auto-merge Pull Request
40 | if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}}
41 | run: gh pr merge --auto --merge "$PR_URL"
42 | env:
43 | PR_URL: ${{github.event.pull_request.html_url}}
44 | GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: Trigger documentation build
2 |
3 | on:
4 | push:
5 | branches:
6 | - 'master'
7 | paths:
8 | - 'docs/**'
9 | pull_request:
10 | branches:
11 | - 'master'
12 | paths:
13 | - 'docs/**'
14 |
15 | jobs:
16 | build:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - name: Checkout repository
20 | uses: actions/checkout@v4
21 |
22 | - name: Install Node.js
23 | uses: actions/setup-node@v4
24 | with:
25 | node-version: '20'
26 |
27 | - name: Install vale
28 | run: |
29 | wget https://github.com/errata-ai/vale/releases/download/v2.23.0/vale_2.23.0_Linux_64-bit.tar.gz
30 | sudo tar -xvzf vale_2.23.0_Linux_64-bit.tar.gz -C /usr/local/bin vale
31 |
32 | - name: Generate Site
33 | run: |
34 | cd docs/_playbook/
35 | npm install
36 | export GIT_CREDENTIALS='https://axoniq-devops:${{ secrets.LIBRARY_DEVBOT_TOKEN }}@github.com'
37 | npx antora playbook.yaml
38 |
39 | - name: Notify AxonIQ Library (if a push to a tracked branch)
40 | if: ${{ github.event_name == 'push'}}
41 | uses: actions/github-script@v7
42 | with:
43 | github-token: ${{ secrets.LIBRARY_DEVBOT_TOKEN }}
44 | script: |
45 | await github.rest.actions.createWorkflowDispatch({
46 | owner: 'AxonIQ',
47 | repo: 'axoniq-library-site',
48 | workflow_id: 'publish.yml',
49 | ref: 'main'
50 | })
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: GiftCard Demo
2 |
3 | on:
4 | pull_request:
5 | push:
6 | branches:
7 | - main
8 | - master
9 | paths-ignore:
10 | - 'docs/**'
11 | - '.github/workflows/docs.yml'
12 |
13 | jobs:
14 | build:
15 | name: Test and Build on JDK ${{ matrix.java-version }}
16 | runs-on: ubuntu-latest
17 | continue-on-error: true # do not fail the whole job if one of the steps fails
18 |
19 | strategy:
20 | matrix:
21 | include:
22 | - java-version: 17
23 | fail-fast: false
24 |
25 | steps:
26 | - name: Checkout code
27 | uses: actions/checkout@v4
28 |
29 | - name: Set up JDK ${{ matrix.java-version }}
30 | uses: actions/setup-java@v4.7.0
31 | with:
32 | distribution: 'zulu'
33 | java-version: ${{ matrix.java-version }}
34 | cache: "maven"
35 | server-id: sonatype
36 | server-username: MAVEN_USERNAME
37 | server-password: MAVEN_PASSWORD
38 |
39 | - name: Build and Verify
40 | run: ./mvnw -B -U -Dstyle.color=always clean verify
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 |
4 | ### STS ###
5 | .apt_generated
6 | .classpath
7 | .factorypath
8 | .project
9 | .settings
10 | .springBeans
11 |
12 | ### IntelliJ IDEA ###
13 | .idea
14 | *.iws
15 | *.iml
16 | *.ipr
17 |
18 | ### NetBeans ###
19 | nbproject/private/
20 | build/
21 | nbbuild/
22 | dist/
23 | nbdist/
24 | .nb-gradle/
25 | *.jar
26 | docs/_playbook/vale
27 | .DS_Store
28 | .java-version
29 |
--------------------------------------------------------------------------------
/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2007-present the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 | import java.net.*;
17 | import java.io.*;
18 | import java.nio.channels.*;
19 | import java.util.Properties;
20 |
21 | public class MavenWrapperDownloader {
22 |
23 | private static final String WRAPPER_VERSION = "0.5.6";
24 | /**
25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
26 | */
27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
29 |
30 | /**
31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
32 | * use instead of the default one.
33 | */
34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
35 | ".mvn/wrapper/maven-wrapper.properties";
36 |
37 | /**
38 | * Path where the maven-wrapper.jar will be saved to.
39 | */
40 | private static final String MAVEN_WRAPPER_JAR_PATH =
41 | ".mvn/wrapper/maven-wrapper.jar";
42 |
43 | /**
44 | * Name of the property which should be used to override the default download url for the wrapper.
45 | */
46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
47 |
48 | public static void main(String args[]) {
49 | System.out.println("- Downloader started");
50 | File baseDirectory = new File(args[0]);
51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
52 |
53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
54 | // wrapperUrl parameter.
55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
56 | String url = DEFAULT_DOWNLOAD_URL;
57 | if(mavenWrapperPropertyFile.exists()) {
58 | FileInputStream mavenWrapperPropertyFileInputStream = null;
59 | try {
60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
61 | Properties mavenWrapperProperties = new Properties();
62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
64 | } catch (IOException e) {
65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
66 | } finally {
67 | try {
68 | if(mavenWrapperPropertyFileInputStream != null) {
69 | mavenWrapperPropertyFileInputStream.close();
70 | }
71 | } catch (IOException e) {
72 | // Ignore ...
73 | }
74 | }
75 | }
76 | System.out.println("- Downloading from: " + url);
77 |
78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
79 | if(!outputFile.getParentFile().exists()) {
80 | if(!outputFile.getParentFile().mkdirs()) {
81 | System.out.println(
82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
83 | }
84 | }
85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
86 | try {
87 | downloadFileFromURL(url, outputFile);
88 | System.out.println("Done");
89 | System.exit(0);
90 | } catch (Throwable e) {
91 | System.out.println("- Error downloading");
92 | e.printStackTrace();
93 | System.exit(1);
94 | }
95 | }
96 |
97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
99 | String username = System.getenv("MVNW_USERNAME");
100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
101 | Authenticator.setDefault(new Authenticator() {
102 | @Override
103 | protected PasswordAuthentication getPasswordAuthentication() {
104 | return new PasswordAuthentication(username, password);
105 | }
106 | });
107 | }
108 | URL website = new URL(urlString);
109 | ReadableByteChannel rbc;
110 | rbc = Channels.newChannel(website.openStream());
111 | FileOutputStream fos = new FileOutputStream(destination);
112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
113 | fos.close();
114 | rbc.close();
115 | }
116 |
117 | }
118 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.1/apache-maven-3.8.1-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
3 |
--------------------------------------------------------------------------------
/.run/Gift Card Application - Command Side.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.run/Gift Card Application - Command, Query and GUI.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/.run/Gift Card Application - GUI.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/.run/Gift Card Application - Query Side.run.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Getting started with Axon
2 | =========================
3 |
4 | This Axon Framework demo application focuses around a simple giftcard domain, designed to show various aspects of the framework.
5 | The app can be run in various modes, using [Spring-boot Profiles](https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-profiles.html): by selecting a specific profile, only the corresponding parts of the app will be active.
6 | Select none, and the default behaviour is activated, which activates everything.
7 | This way you can experiment with Axon in a (structured) monolith as well as in micro-services.
8 |
9 | Where to find more information:
10 | -------------------------------
11 |
12 | * The [Axon Reference Guide](https://docs.axoniq.io/reference-guide/) is definitive guide on the Axon Framework and Axon Server.
13 | * Visit [www.axoniq.io](https://www.axoniq.io) to find out about AxonIQ, the team behind the Axon Framework and Server.
14 | * Subscribe to the [AxonIQ YouTube channel](https://www.youtube.com/AxonIQ) to get the latest Webinars, announcements, and customer stories.
15 | * To start a fresh Axon Application, you can go to [start.axoniq.io](https://start.axoniq.io/).
16 | * Additional information may be gained by following some of AxonIQ's courses on the [AxonIQ Academy](https://academy.axoniq.io/).
17 | * The latest version of the Giftcard App can be found [on GitHub](https://github.com/AxonIQ/giftcard-demo).
18 | * Docker images for Axon Server are pushed to [Docker Hub](https://hub.docker.com/u/axoniq).
19 | * If there are any Axon related questions remaining, you can always go to the [forum](https://discuss.axoniq.io/).
20 |
21 | The Giftcard app
22 | ----------------
23 |
24 | ### Background story
25 |
26 | See [the wikipedia article](https://en.wikipedia.org/wiki/Gift_card) for a basic definition of gift cards. Essentially, there are just two events in the life cycle of a gift card:
27 | * They get _issued_: a new gift card gets created with some amount of money stored.
28 | * They get _redeemed_: all or part of the monetary value stored on the gift card is used to purchase something.
29 |
30 | ### Structure of the App
31 |
32 | The Giftcard application is split into four parts, using four sub-packages of `io.axoniq.demo.giftcard`:
33 | * The `api` package contains the ([Java Records](https://www.baeldung.com/java-record-keyword)) sourcecode of the messages and entity. They form the API (sic) of the application.
34 | * The `command` package contains the GiftCard Aggregate class, with all command- and associated eventsourcing handlers.
35 | * The `query` package provides the query handlers, with their associated event handlers.
36 | * The `rest` package contains the [Spring Webflux](https://www.baeldung.com/spring-webflux)-based Web API.
37 |
38 | Of these packages, `command`, `query`, and `gui` (enabling the `rest` package) are also configured as profiles.
39 |
40 | ### Building the Giftcard app from the sources
41 |
42 | To build the demo app, simply run the provided [Maven wrapper](https://www.baeldung.com/maven-wrapper):
43 |
44 | ```
45 | ./mvnw clean package
46 | ```
47 |
48 | Note that the Giftcard app expects JDK 17 to be used.
49 |
50 | Running the Giftcard app
51 | ------------------------
52 |
53 | The simplest way to run the app is by using the Spring-boot maven plugin:
54 |
55 | ```
56 | ./mvnw spring-boot:run
57 | ```
58 | However, if you have copied the jar file `giftcard-demo-4.8.jar` from the Maven `target` directory to some other location, you can also start it with:
59 |
60 | ```
61 | java -jar giftcard-demo-4.8.jar
62 | ```
63 | The Web GUI can be found at [`http://localhost:8080`](http://localhost:8080).
64 |
65 | If you want to activate only the `command` profile, use:
66 |
67 | ```
68 | java -Dspring.profiles.active=command -jar giftcard-demo-4.8.jar
69 | ```
70 | Idem for `query` and `gui`.
71 |
72 | ### Running the Giftcard app as microservices
73 |
74 | To run the Giftcard app as if it were three separate microservices, use the Spring-boot `spring.profiles.active` option as follows:
75 |
76 | ```
77 | $ java -Dspring.profiles.active=command -jar giftcard-demo-4.8.jar
78 | ```
79 | This will start only the command part. To complete the app, open two other command shells, and start one with profile `query`, and the last one with `gui`. Again you can open the Web GUI at [`http://localhost:8080`](http://localhost:8080). The three parts of the application work together through the running instance of the Axon Server, which distributes the Commands, Queries, and Events.
80 | It's also possible to explore the REST API using [Swagger](http://localhost:8080/webjars/swagger-ui/index.html) or get the [Open Api definition](http://localhost:8080/v3/api-docs) to create a client.
81 |
82 | Running Axon Server
83 | ------------------
84 |
85 | By default, the Axon Framework is configured to expect a running Axon Server instance, and it will complain if the server is not found.
86 | To run Axon Server, you'll need a Java runtime.
87 | A copy of the server JAR file has been provided in the demo package.
88 | You can run it locally, in a Docker container (including Kubernetes or even Mini-kube), or on a separate server.
89 |
90 | The section below give a fair description on how to run Axon Server for this sample project.
91 | If you are looking for more in depth information on the subject, we recommend this three-part blog series:
92 |
93 | 1. [Running Axon Server - Going from local developer install to full-featured cluster in the cloud](https://axoniq.io/blog-overview/running-axon-server)
94 | 2. [Running Axon Server in Docker - Continuing from local developer install to containerized](https://axoniq.io/blog-overview/running-axon-server-in-docker)
95 | 3. [Running Axon Server in a Virtual Machine](https://axoniq.io/blog-overview/running-axon-server-in-a-virtual-machine)
96 |
97 | ### Running Axon Server locally
98 |
99 | To run Axon Server locally, all you need to do is put the server JAR file in the directory where you want it to live, and start it using:
100 |
101 | ```
102 | java -jar axonserver.jar
103 | ```
104 |
105 | You will see that it creates a subdirectory `data` where it will store its information.
106 |
107 | ### Running Axon Server in a Docker container
108 |
109 | To run Axon Server in Docker you can use the image provided on Docker Hub:
110 |
111 | ```
112 | $ docker run -d --name my-axon-server -p 8024:8024 -p 8124:8124 axoniq/axonserver
113 | ...some container id...
114 | $
115 | ```
116 |
117 | *WARNING* This is not a supported image for production purposes. Please use with caution.
118 |
119 | If you want to run the clients in Docker containers as well, and are not using something like Kubernetes, use the "`--hostname`" option of the `docker` command to set a useful name like "axonserver", and pass the `AXONSERVER_HOSTNAME` environment variable to adjust the properties accordingly:
120 |
121 | ```
122 | $ docker run -d --name my-axon-server -p 8024:8024 -p 8124:8124 --hostname axonserver -e AXONSERVER_HOSTNAME=axonserver axoniq/axonserver
123 | ```
124 |
125 | When you start the client containers, you can now use "`--link axonserver`" to provide them with the correct DNS entry.
126 | The Axon Server Connector looks at the "`axon.axonserver.servers`" property to determine where Axon Server lives, so don't forget to set it to "`axonserver`".
127 |
128 | ### Running Axon Server in Kubernetes and Mini-Kube
129 |
130 | *WARNING*: Although you can get a pretty functional cluster running locally using Mini-Kube, you can run into trouble when you want to let it serve clients outside the cluster.
131 | Mini-Kube can provide access to HTTP servers running in the cluster, for other protocols you have to run a special protocol-agnostic proxy like you can with "`kubectl port-forward` _<pod-name>_ _<port-number>_".
132 | Thus, for non-development scenarios, we don't recommend using Mini-Kube.
133 |
134 | Deployment requires the use of a YAML descriptor, a working example of which can be found in the "`kubernetes`" directory.
135 | To run it, use the following commands in a separate window:
136 |
137 | ```
138 | $ kubectl apply -f kubernetes/axonserver.yaml
139 | statefulset.apps "axonserver" created
140 | service "axonserver-gui" created
141 | service "axonserver" created
142 | $ kubectl port-forward axonserver-0 8124
143 | Forwarding from 127.0.0.1:8124 -> 8124
144 | Forwarding from [::1]:8124 -> 8124
145 | ```
146 |
147 | You can now run the Giftcard app, which will connect through the proxied gRPC port.
148 | To see the Axon Server Web GUI, use "`minikube service --url axonserver-gui`" to obtain the URL for your browser.
149 | Actually, if you leave out the "`--url`", minikube will open the GUI in your default browser for you.
150 |
151 | To clean up the deployment, use:
152 |
153 | ```
154 | $ kubectl delete sts axonserver
155 | statefulset.apps "axonserver" deleted
156 | $ kubectl delete svc axonserver
157 | service "axonserver" deleted
158 | $ kubectl delete svc axonserver-gui
159 | service "axonserver-gui" deleted
160 | ```
161 |
162 | If you're using a 'real' Kubernetes cluster, you'll naturally not want to use "`localhost`" as hostname for Axon Server, so you need to add three lines to the container spec to specify the "`AXONSERVER_HOSTNAME`" setting:
163 |
164 | ```
165 | ...
166 | containers:
167 | - name: axonserver
168 | image: axoniq/axonserver
169 | imagePullPolicy: Always
170 | ports:
171 | - name: grpc
172 | containerPort: 8124
173 | protocol: TCP
174 | - name: gui
175 | containerPort: 8024
176 | protocol: TCP
177 | readinessProbe:
178 | httpGet:
179 | port: 8024
180 | path: /actuator/health
181 | initialDelaySeconds: 5
182 | periodSeconds: 5
183 | timeoutSeconds: 1
184 | env:
185 | - name: AXONSERVER_HOSTNAME
186 | value: axonserver
187 | ---
188 | apiVersion: v1
189 | kind: Service
190 | ...
191 | ```
192 |
193 | Use "`axonserver`" (as that is the name of the Kubernetes service) if you're going to deploy the client next to the server in the cluster, which is what you'd probably want.
194 | Running the client outside the cluster, with Axon Server *inside*, entails extra work to enable and secure this, and is definitely beyond the scope of this example.
195 |
196 | Configuring Axon Server
197 | -----------------------
198 |
199 | Axon Server uses sensible defaults for all of its settings, so it will actually run fine without any further configuration.
200 | However, if you want to make some changes, below are the most common options.
201 | For an exhaustive list, we recommend checking out the [Configuration section](https://docs.axoniq.io/reference-guide/axon-server/administration/admin-configuration/configuration) of the Reference Guide.
202 |
203 | ### Environment variables for customizing the Docker image of Axon Server
204 |
205 | The `axoniq/axonserver` image can be customized at start by using one of the following environment variables.
206 | If no default is mentioned, leaving the environment variable unspecified will not add a line to the properties file.
207 |
208 | * `AXONSERVER_NAME`
209 |
210 | This is the name the Axon Server uses for itself.
211 | * `AXONSERVER_HOSTNAME`
212 |
213 | This is the hostname Axon Server communicates to the client as its contact point. Default is "`localhost`", because Docker generates a random name that is not resolvable outside the container.
214 | * `AXONSERVER_DOMAIN`
215 |
216 | This is the domain Axon Server can suffix the hostname with.
217 | * `AXONSERVER_HTTP_PORT`
218 |
219 | This is the port Axon Server uses for its Web GUI and REST API.
220 | * `AXONSERVER_GRPC_PORT`
221 |
222 | This is the gRPC port used by clients to exchange data with the server.
223 | * `AXONSERVER_TOKEN`
224 |
225 | Setting this will enable access control, which means the clients need to pass this token with each request.
226 | * `AXONSERVER_EVENTSTORE`
227 |
228 | This is the directory used for storing the Events.
229 | * `AXONSERVER_CONTROLDB`
230 |
231 | This is where Axon Server stores information of clients and what types of messages they are interested in.
232 |
233 | ### Axon Server configuration
234 |
235 | There are a number of things you can fine-tune in the server configuration. You can do this using an "`axonserver.properties`" file.
236 | All settings have sensible defaults.
237 |
238 | * `axoniq.axonserver.name`
239 |
240 | This is the name Axon Server uses for itself. The default is to use the hostname.
241 | * `axoniq.axonserver.hostname`
242 |
243 | This is the hostname clients will use to connect to the server. Note that an IP address can be used if the name cannot be resolved through DNS. The default value is the actual hostname reported by the OS.
244 | * `server.port`
245 |
246 | This is the port where Axon Server will listen for HTTP requests, by default `8024`.
247 | * `axoniq.axonserver.port`
248 |
249 | This is the port where Axon Server will listen for gRPC requests, by default `8124`.
250 | * `axoniq.axonserver.event.storage`
251 |
252 | This setting determines where event messages are stored, so make sure there is enough diskspace here. Losing this data means losing your Events-sourced Aggregates' state! Conversely, if you want a quick way to start from scratch, here's where to clean.
253 | * `axoniq.axonserver.controldb-path`
254 |
255 | This setting determines where the message hub stores its information. Losing this data will affect Axon Server's ability to determine which applications are connected, and what types of messages they are interested in.
256 | * `axoniq.axonserver.accesscontrol.enabled`
257 |
258 | Setting this to `true` will require clients to pass a token.
259 | * `axoniq.axonserver.accesscontrol.token`
260 |
261 | This is the token used for access control.
262 |
263 | ### The Axon Server HTTP server
264 |
265 | Axon Server provides two servers; one serving HTTP requests, the other gRPC.
266 | By default, these use ports 8024 and 8124 respectively, but you can change these in the settings.
267 |
268 | The HTTP server has in its root context a management Web GUI, a health indicator is available at `/actuator/health`, and the REST API at `/v1`.
269 | The API's Swagger endpoint finally, is available at `/swagger-ui.html`, and gives the documentation on the REST API.
270 |
271 | Data protection plugin
272 | -------------------
273 | The data protection plugin can serve as an alternative to the Data Protection Module commonly used inside Axon application.
274 |
275 | ### Data protection plugin config generation
276 | The data protection maven plugin has been added to this project. It will automatically create a configuration output called `axon-data-protection-config.json` during the `compile` phase of the Maven Lifecycle. This output should be used for the configuration of the Data Protection Plugin on Axon Server.
277 |
278 | Since at this moment the `dataprotection-config-api` and `dataprotection-maven-plugin` do not have any releases available on public repositories, these two projects will first need to be run locally to install these dependencies. Their repositories can be found here:
279 | - https://github.com/AxonIQ/axon-dataprotection-config-api
280 | - https://github.com/AxonIQ/axon-dataprotection-maven-plugin/
281 |
282 | Two events have been included in the sample:
283 | - `RedeemedEvent`, an event used by the application
284 | - `ExampleEvent`, an unused event, but with a more complex data structure to show the `config-api` usage in more detail.
285 |
286 | ### Using the data protection plugin in Axon Server
287 | Running the docker-compose.yml file (rather than the kubernetes deployment) found in the docker directory will bring up an instance of axon server and and instance
288 | of vault to be able to run the example.
289 |
290 | Use the axonserver-cli to upload and configure the data protection plugin (taken from https://docs.axoniq.io/reference-guide/axon-server/administration/plugins#plugin-administration)
291 |
292 | Upload the data protection plugin
293 | ```
294 | java -jar axonserver-cli.jar upload-plugin -t $(cat ./axonserver.token) -f axon-server-plugin-data-protection.jar -S https://localhost:8024 -i
295 | ```
296 |
297 | Configure the plugin (https://docs.axoniq.io/reference-guide/axon-server/administration/plugins#configuring-a-plugin)
298 | - update the axon-data-protection-plugin-config.yaml file
299 | - set the vault configuration for your environment (if running the docker example, the vault token must match what is defined in the docker-compose file)
300 | - add the contents of the axon-data-protection-config.json file
301 | - run the following command to upload the configuration
302 | ```
303 | java -jar ./axonserver-cli.jar configure-plugin -t $(cat ./axonserver.token) -p io.axoniq.axon-server-plugin-data-protection -v 1.0.0.SNAPSHOT -S https://localhost:8024 -i -c default -f axon-data-protection-config.yaml
304 | ```
305 |
306 | Activate the plugin for a context with the -c flag (https://docs.axoniq.io/reference-guide/axon-server/administration/plugins#activating-a-plugin)
307 | ```
308 | java -jar ./axonserver-cli.jar activate-plugin -t $(cat ./axonserver.token) -p io.axoniq.axon-server-plugin-data-protection -v 1.0.0.SNAPSHOT -S https://localhost:8024 -i -c default
309 | ```
310 |
311 |
--------------------------------------------------------------------------------
/axon-data-protection-config.json:
--------------------------------------------------------------------------------
1 | {
2 | "config" : [ {
3 | "type" : "io.axoniq.demo.giftcard.api.CardRedeemedEvent",
4 | "revision" : "",
5 | "subjectId" : {
6 | "path" : "$.id"
7 | },
8 | "sensitiveData" : [ {
9 | "path" : "$.amount",
10 | "replacementValue" : "hidden amount"
11 | } ]
12 | }, {
13 | "type" : "io.axoniq.demo.giftcard.api.ExampleEvent",
14 | "revision" : "1",
15 | "subjectId" : {
16 | "path" : "$.id"
17 | },
18 | "sensitiveData" : [ {
19 | "path" : "$.ssn",
20 | "replacementValue" : ""
21 | }, {
22 | "path" : "$.address.addressLine1",
23 | "replacementValue" : ""
24 | } ]
25 | } ]
26 | }
--------------------------------------------------------------------------------
/axon-data-protection-plugin-config.yaml:
--------------------------------------------------------------------------------
1 | Vault configuration:
2 | address: http://somevaultserver:9430
3 | token: token4context
4 | prefix: prefix4context
5 | MetaModel configuration:
6 | metamodel: 'paste raw json contents of axon-data-protection-config.json here'
--------------------------------------------------------------------------------
/docker/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.3'
2 | services:
3 | axonserver:
4 | image: axoniq/axonserver:latest
5 | hostname: axonserver
6 | environment:
7 | - SPRING_SERVLET_MULTIPART_MAX-FILE-SIZE=35MB
8 | - SPRING_SERVLET_MULTIPART_MAX-REQUEST-SIZE=35MB
9 | - AXONIQ_AXONSERVER_DEVMODE_ENABLED=TRUE
10 | ports:
11 | - '8024:8024'
12 | - '8124:8124'
13 | networks:
14 | - axon-server-plugin-data-protection
15 |
16 | vault:
17 | image: vault:latest
18 | hostname: vault
19 | ports:
20 | - '8200:8200'
21 | environment:
22 | - VAULT_DEV_ROOT_TOKEN_ID=localVaultToken
23 | networks:
24 | - axon-server-plugin-data-protection
25 |
26 | networks:
27 | axon-server-plugin-data-protection:
--------------------------------------------------------------------------------
/docs/_playbook/.gitignore:
--------------------------------------------------------------------------------
1 | build
2 | node_modules
3 | .vscode
4 | vale
5 |
--------------------------------------------------------------------------------
/docs/_playbook/.vale.ini:
--------------------------------------------------------------------------------
1 | StylesPath = vale
2 |
3 | MinAlertLevel = suggestion
4 |
5 | Packages = http://github.com/AxonIQ/axoniq-vale-package/releases/latest/download/axoniq-vale-package.zip
6 |
7 | [*.{adoc,html}]
8 | BasedOnStyles = AxonIQ
9 |
--------------------------------------------------------------------------------
/docs/_playbook/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "devDependencies": {
3 | "@antora/atlas-extension": "^1.0.0-alpha.2",
4 | "@antora/cli": "^3.2.0-alpha.2",
5 | "@antora/lunr-extension": "^1.0.0-alpha.8",
6 | "@antora/site-generator": "^3.2.0-alpha.2",
7 | "@asciidoctor/tabs": "^1.0.0-beta.6",
8 | "@axoniq/antora-vale-extension": "^0.1.2",
9 | "asciidoctor-kroki": "^0.17.0"
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/docs/_playbook/playbook.yaml:
--------------------------------------------------------------------------------
1 | site:
2 | title: Giftcard demo docs PREVIEW
3 | start_page: giftcard::index.adoc
4 |
5 | content:
6 | sources:
7 | - url: ../..
8 | start_paths: ['docs/*', '!docs/_playbook']
9 |
10 | asciidoc:
11 | attributes:
12 | experimental: true
13 | page-pagination: true
14 | kroki-fetch-diagram: true
15 | primary-site-manifest-url: https://library.axoniq.io/site-manifest.json
16 | extensions:
17 | - asciidoctor-kroki
18 | - '@asciidoctor/tabs'
19 |
20 | antora:
21 | extensions:
22 | - id: prose-linting
23 | require: '@axoniq/antora-vale-extension'
24 | enabled: true
25 | vale_config: .vale.ini
26 | update_styles: true
27 | - id: lunr
28 | require: '@antora/lunr-extension'
29 | enabled: true
30 | index_latest_only: true
31 | - id: atlas
32 | require: '@antora/atlas-extension'
33 |
34 | runtime:
35 | fetch: true # fetch remote repos
36 | log:
37 | level: info
38 | failure_level: error
39 |
40 | ui:
41 | bundle:
42 | url: https://github.com/AxonIQ/axoniq-library-ui/releases/download/v.0.0.9/ui-bundle.zip
43 |
--------------------------------------------------------------------------------
/docs/tutorial/antora.yml:
--------------------------------------------------------------------------------
1 | name: giftcard
2 | title: Building a Giftcard Application
3 | version: true
4 |
5 | asciidoc:
6 | attributes:
7 | component_description: Getting started with designing, building, and deploying Axon applications
8 | type: tutorial
9 | group: beginner
10 |
11 |
12 | nav:
13 | - modules/ROOT/nav.adoc
14 |
--------------------------------------------------------------------------------
/docs/tutorial/modules/ROOT/examples/src:
--------------------------------------------------------------------------------
1 | ../../../../../src
--------------------------------------------------------------------------------
/docs/tutorial/modules/ROOT/nav.adoc:
--------------------------------------------------------------------------------
1 | * xref:index.adoc[]
2 | * xref:design.adoc[]
3 | * xref:commands.adoc[]
4 | * xref:events.adoc[]
5 | * xref:projections.adoc[]
6 | * xref:architecture.adoc[]
7 | * xref:deployment.adoc[]
8 | * xref:next_steps.adoc[]
9 |
--------------------------------------------------------------------------------
/docs/tutorial/modules/ROOT/pages/architecture.adoc:
--------------------------------------------------------------------------------
1 | = Architecture
2 | :page-needs-improvement: content
3 | :page-needs-content: This page is a placeholder. Please add content.
--------------------------------------------------------------------------------
/docs/tutorial/modules/ROOT/pages/commands.adoc:
--------------------------------------------------------------------------------
1 | = Commands
2 | :page-needs-improvement: content
3 | :page-needs-content: This page is a placeholder. Add meaningful content.
4 |
5 |
6 | == Command definition
7 |
8 | [source,java]
9 | ----
10 | include::example$src/main/java/io/axoniq/demo/giftcard/api/IssueCardCommand.java[tag=IssueCardCommand]
11 | ----
12 |
13 | [source,java]
14 | ----
15 | include::example$src/main/java/io/axoniq/demo/giftcard/api/RedeemCardCommand.java[tag=RedeemCardCommand]
16 | ----
17 |
18 | [source,java]
19 | ----
20 | include::example$src/main/java/io/axoniq/demo/giftcard/api/CancelCardCommand.java[tag=CancelCardCommand]
21 | ----
22 |
23 | == Command handlers
24 |
25 | === Issue card
26 |
27 |
28 | [source,java]
29 | ----
30 | include::example$src/main/java/io/axoniq/demo/giftcard/command/GiftCard.java[tag=IssueCardCommandHandler]
31 | ----
32 |
33 | === Redeem card
34 |
35 |
36 | [source,java]
37 | ----
38 | include::example$src/main/java/io/axoniq/demo/giftcard/command/GiftCard.java[tag=RedeemCardCommandHandler]
39 | ----
--------------------------------------------------------------------------------
/docs/tutorial/modules/ROOT/pages/deployment.adoc:
--------------------------------------------------------------------------------
1 | = Deployment
2 | :page-needs-improvement: content
3 | :page-needs-content: This page is a placeholder. Please add meaningful content.
4 |
--------------------------------------------------------------------------------
/docs/tutorial/modules/ROOT/pages/design.adoc:
--------------------------------------------------------------------------------
1 | = Design
2 | :page-needs-improvement: content
3 | :page-needs-content: This page is a placeholder. Add meaningful content.
4 |
--------------------------------------------------------------------------------
/docs/tutorial/modules/ROOT/pages/events.adoc:
--------------------------------------------------------------------------------
1 | = Events
2 | :page-needs-improvement: content
3 | :page-needs-content: This page is a placeholder. Add meaningful content.
4 |
--------------------------------------------------------------------------------
/docs/tutorial/modules/ROOT/pages/index.adoc:
--------------------------------------------------------------------------------
1 | = Introduction
2 | :navtitle: Introduction
3 | :reftext: Building a Giftcard Application
4 |
5 | Building a new application can be a daunting task. This tutorial walks you trough the various phases of development, explains the capabilities of Axon Framework and provides useful tips and tricks along the way.
6 |
7 | The result is a simple application you deploy in "production". Then the tutorial teaches you how to evolve the application, make changes and deploy those in different scenarios.
--------------------------------------------------------------------------------
/docs/tutorial/modules/ROOT/pages/next_steps.adoc:
--------------------------------------------------------------------------------
1 | = Next Steps
2 | :page-needs-improvement: content
3 | :page-needs-content: This page is a placeholder. Add meaningful content.
4 |
--------------------------------------------------------------------------------
/docs/tutorial/modules/ROOT/pages/projections.adoc:
--------------------------------------------------------------------------------
1 | = Projections
2 | :page-needs-improvement: content
3 | :page-needs-content: This page is a placeholder. Add meaningful content.
4 |
--------------------------------------------------------------------------------
/kubernetes/axonserver.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: apps/v1
3 | kind: StatefulSet
4 | metadata:
5 | name: axonserver
6 | labels:
7 | app: axonserver
8 | spec:
9 | serviceName: axonserver
10 | replicas: 1
11 | selector:
12 | matchLabels:
13 | app: axonserver
14 | template:
15 | metadata:
16 | labels:
17 | app: axonserver
18 | spec:
19 | containers:
20 | - name: axonserver
21 | image: axoniq/axonserver
22 | imagePullPolicy: Always
23 | ports:
24 | - name: grpc
25 | containerPort: 8124
26 | protocol: TCP
27 | - name: gui
28 | containerPort: 8024
29 | protocol: TCP
30 | readinessProbe:
31 | httpGet:
32 | port: 8024
33 | path: /actuator/health
34 | initialDelaySeconds: 5
35 | periodSeconds: 5
36 | timeoutSeconds: 1
37 | ---
38 | apiVersion: v1
39 | kind: Service
40 | metadata:
41 | name: axonserver-gui
42 | labels:
43 | app: axonserver-gui
44 | spec:
45 | ports:
46 | - name: gui
47 | port: 8024
48 | targetPort: 8024
49 | selector:
50 | app: axonserver
51 | type: LoadBalancer
52 | ---
53 | apiVersion: v1
54 | kind: Service
55 | metadata:
56 | name: axonserver
57 | labels:
58 | app: axonserver
59 | spec:
60 | ports:
61 | - name: grpc
62 | port: 8124
63 | targetPort: 8124
64 | clusterIP: None
65 | selector:
66 | app: axonserver
67 | ---
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # https://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /etc/mavenrc ] ; then
40 | . /etc/mavenrc
41 | fi
42 |
43 | if [ -f "$HOME/.mavenrc" ] ; then
44 | . "$HOME/.mavenrc"
45 | fi
46 |
47 | fi
48 |
49 | # OS specific support. $var _must_ be set to either true or false.
50 | cygwin=false;
51 | darwin=false;
52 | mingw=false
53 | case "`uname`" in
54 | CYGWIN*) cygwin=true ;;
55 | MINGW*) mingw=true;;
56 | Darwin*) darwin=true
57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
59 | if [ -z "$JAVA_HOME" ]; then
60 | if [ -x "/usr/libexec/java_home" ]; then
61 | export JAVA_HOME="`/usr/libexec/java_home`"
62 | else
63 | export JAVA_HOME="/Library/Java/Home"
64 | fi
65 | fi
66 | ;;
67 | esac
68 |
69 | if [ -z "$JAVA_HOME" ] ; then
70 | if [ -r /etc/gentoo-release ] ; then
71 | JAVA_HOME=`java-config --jre-home`
72 | fi
73 | fi
74 |
75 | if [ -z "$M2_HOME" ] ; then
76 | ## resolve links - $0 may be a link to maven's home
77 | PRG="$0"
78 |
79 | # need this for relative symlinks
80 | while [ -h "$PRG" ] ; do
81 | ls=`ls -ld "$PRG"`
82 | link=`expr "$ls" : '.*-> \(.*\)$'`
83 | if expr "$link" : '/.*' > /dev/null; then
84 | PRG="$link"
85 | else
86 | PRG="`dirname "$PRG"`/$link"
87 | fi
88 | done
89 |
90 | saveddir=`pwd`
91 |
92 | M2_HOME=`dirname "$PRG"`/..
93 |
94 | # make it fully qualified
95 | M2_HOME=`cd "$M2_HOME" && pwd`
96 |
97 | cd "$saveddir"
98 | # echo Using m2 at $M2_HOME
99 | fi
100 |
101 | # For Cygwin, ensure paths are in UNIX format before anything is touched
102 | if $cygwin ; then
103 | [ -n "$M2_HOME" ] &&
104 | M2_HOME=`cygpath --unix "$M2_HOME"`
105 | [ -n "$JAVA_HOME" ] &&
106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
107 | [ -n "$CLASSPATH" ] &&
108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
109 | fi
110 |
111 | # For Mingw, ensure paths are in UNIX format before anything is touched
112 | if $mingw ; then
113 | [ -n "$M2_HOME" ] &&
114 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
115 | [ -n "$JAVA_HOME" ] &&
116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
117 | fi
118 |
119 | if [ -z "$JAVA_HOME" ]; then
120 | javaExecutable="`which javac`"
121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
122 | # readlink(1) is not available as standard on Solaris 10.
123 | readLink=`which readlink`
124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
125 | if $darwin ; then
126 | javaHome="`dirname \"$javaExecutable\"`"
127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
128 | else
129 | javaExecutable="`readlink -f \"$javaExecutable\"`"
130 | fi
131 | javaHome="`dirname \"$javaExecutable\"`"
132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
133 | JAVA_HOME="$javaHome"
134 | export JAVA_HOME
135 | fi
136 | fi
137 | fi
138 |
139 | if [ -z "$JAVACMD" ] ; then
140 | if [ -n "$JAVA_HOME" ] ; then
141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
142 | # IBM's JDK on AIX uses strange locations for the executables
143 | JAVACMD="$JAVA_HOME/jre/sh/java"
144 | else
145 | JAVACMD="$JAVA_HOME/bin/java"
146 | fi
147 | else
148 | JAVACMD="`which java`"
149 | fi
150 | fi
151 |
152 | if [ ! -x "$JAVACMD" ] ; then
153 | echo "Error: JAVA_HOME is not defined correctly." >&2
154 | echo " We cannot execute $JAVACMD" >&2
155 | exit 1
156 | fi
157 |
158 | if [ -z "$JAVA_HOME" ] ; then
159 | echo "Warning: JAVA_HOME environment variable is not set."
160 | fi
161 |
162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
163 |
164 | # traverses directory structure from process work directory to filesystem root
165 | # first directory with .mvn subdirectory is considered project base directory
166 | find_maven_basedir() {
167 |
168 | if [ -z "$1" ]
169 | then
170 | echo "Path not specified to find_maven_basedir"
171 | return 1
172 | fi
173 |
174 | basedir="$1"
175 | wdir="$1"
176 | while [ "$wdir" != '/' ] ; do
177 | if [ -d "$wdir"/.mvn ] ; then
178 | basedir=$wdir
179 | break
180 | fi
181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
182 | if [ -d "${wdir}" ]; then
183 | wdir=`cd "$wdir/.."; pwd`
184 | fi
185 | # end of workaround
186 | done
187 | echo "${basedir}"
188 | }
189 |
190 | # concatenates all lines of a file
191 | concat_lines() {
192 | if [ -f "$1" ]; then
193 | echo "$(tr -s '\n' ' ' < "$1")"
194 | fi
195 | }
196 |
197 | BASE_DIR=`find_maven_basedir "$(pwd)"`
198 | if [ -z "$BASE_DIR" ]; then
199 | exit 1;
200 | fi
201 |
202 | ##########################################################################################
203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
204 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
205 | ##########################################################################################
206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
207 | if [ "$MVNW_VERBOSE" = true ]; then
208 | echo "Found .mvn/wrapper/maven-wrapper.jar"
209 | fi
210 | else
211 | if [ "$MVNW_VERBOSE" = true ]; then
212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
213 | fi
214 | if [ -n "$MVNW_REPOURL" ]; then
215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
216 | else
217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
218 | fi
219 | while IFS="=" read key value; do
220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
221 | esac
222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
223 | if [ "$MVNW_VERBOSE" = true ]; then
224 | echo "Downloading from: $jarUrl"
225 | fi
226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
227 | if $cygwin; then
228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
229 | fi
230 |
231 | if command -v wget > /dev/null; then
232 | if [ "$MVNW_VERBOSE" = true ]; then
233 | echo "Found wget ... using wget"
234 | fi
235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
236 | wget "$jarUrl" -O "$wrapperJarPath"
237 | else
238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
239 | fi
240 | elif command -v curl > /dev/null; then
241 | if [ "$MVNW_VERBOSE" = true ]; then
242 | echo "Found curl ... using curl"
243 | fi
244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
245 | curl -o "$wrapperJarPath" "$jarUrl" -f
246 | else
247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
248 | fi
249 |
250 | else
251 | if [ "$MVNW_VERBOSE" = true ]; then
252 | echo "Falling back to using Java to download"
253 | fi
254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
255 | # For Cygwin, switch paths to Windows format before running javac
256 | if $cygwin; then
257 | javaClass=`cygpath --path --windows "$javaClass"`
258 | fi
259 | if [ -e "$javaClass" ]; then
260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
261 | if [ "$MVNW_VERBOSE" = true ]; then
262 | echo " - Compiling MavenWrapperDownloader.java ..."
263 | fi
264 | # Compiling the Java class
265 | ("$JAVA_HOME/bin/javac" "$javaClass")
266 | fi
267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
268 | # Running the downloader
269 | if [ "$MVNW_VERBOSE" = true ]; then
270 | echo " - Running MavenWrapperDownloader.java ..."
271 | fi
272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
273 | fi
274 | fi
275 | fi
276 | fi
277 | ##########################################################################################
278 | # End of extension
279 | ##########################################################################################
280 |
281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
282 | if [ "$MVNW_VERBOSE" = true ]; then
283 | echo $MAVEN_PROJECTBASEDIR
284 | fi
285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
286 |
287 | # For Cygwin, switch paths to Windows format before running java
288 | if $cygwin; then
289 | [ -n "$M2_HOME" ] &&
290 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
291 | [ -n "$JAVA_HOME" ] &&
292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
293 | [ -n "$CLASSPATH" ] &&
294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
295 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
297 | fi
298 |
299 | # Provide a "standardized" way to retrieve the CLI args that will
300 | # work with both Windows and non-Windows executions.
301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
302 | export MAVEN_CMD_LINE_ARGS
303 |
304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
305 |
306 | exec "$JAVACMD" \
307 | $MAVEN_OPTS \
308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
311 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
124 |
125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
162 | if ERRORLEVEL 1 goto error
163 | goto end
164 |
165 | :error
166 | set ERROR_CODE=1
167 |
168 | :end
169 | @endlocal & set ERROR_CODE=%ERROR_CODE%
170 |
171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
175 | :skipRcPost
176 |
177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
179 |
180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
181 |
182 | exit /B %ERROR_CODE%
183 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | io.axoniq.demo
8 | giftcard-demo
9 | 4.11
10 | jar
11 |
12 | giftcard-demo
13 | Axon Framework demo project
14 |
15 |
16 | org.springframework.boot
17 | spring-boot-starter-parent
18 | 3.4.2
19 |
20 |
21 |
22 |
23 | UTF-8
24 | UTF-8
25 |
26 | 17
27 |
28 | 4.11.3
29 |
30 | 1.0
31 | 1.0
32 |
33 | 2.8.5
34 | 2.18.0
35 | 3.14.0
36 |
37 | 5.12.0
38 |
39 |
40 |
41 |
42 |
43 | org.axonframework
44 | axon-bom
45 | ${axon.version}
46 | pom
47 | import
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | org.axonframework
56 | axon-spring-boot-starter
57 |
58 |
59 |
60 |
61 | org.springframework.boot
62 | spring-boot-starter
63 |
64 |
65 |
66 |
67 | org.springframework.boot
68 | spring-boot-starter-webflux
69 |
70 |
71 |
72 | org.springdoc
73 | springdoc-openapi-starter-webflux-ui
74 | ${springdoc-openapi-starter.version}
75 |
76 |
77 |
78 |
79 |
80 | io.projectreactor
81 | reactor-core
82 |
83 |
84 |
85 |
86 | org.slf4j
87 | slf4j-ext
88 |
89 |
90 |
91 | org.axonframework
92 | axon-micrometer
93 |
94 |
95 |
96 | org.springframework.boot
97 | spring-boot-starter-actuator
98 |
99 |
100 |
101 |
102 | org.axonframework
103 | axon-test
104 |
105 |
106 |
107 | org.junit.jupiter
108 | junit-jupiter
109 | ${junit.jupiter.version}
110 |
111 |
112 |
113 | org.springframework.boot
114 | spring-boot-starter-test
115 | test
116 |
117 |
118 | org.junit.vintage
119 | junit-vintage-engine
120 |
121 |
122 |
123 |
124 | io.axoniq
125 | axon-dataprotection-config-api
126 | ${dataprotection-config-api.version}
127 | compile
128 |
129 |
130 |
131 |
132 |
133 |
134 | org.codehaus.mojo
135 | versions-maven-plugin
136 | ${versions-maven-plugin.version}
137 |
138 | false
139 |
140 |
141 |
142 |
143 | org.apache.maven.plugins
144 | maven-compiler-plugin
145 | ${maven.compiler.plugin.version}
146 |
147 | ${java.version}
148 | ${java.version}
149 |
150 |
151 |
152 |
153 | default-compile
154 | none
155 |
156 |
157 |
158 | default-testCompile
159 | none
160 |
161 |
162 | java-compile
163 | compile
164 |
165 | compile
166 |
167 |
168 |
169 | java-test-compile
170 | test-compile
171 |
172 | testCompile
173 |
174 |
175 |
176 |
177 |
178 |
179 | org.springframework.boot
180 | spring-boot-maven-plugin
181 |
182 |
183 |
184 | io.axoniq
185 | axon-dataprotection-maven-plugin
186 | ${dataprotection-maven-plugin.version}
187 |
188 |
189 |
190 | io.axoniq.demo.giftcard.api
191 |
192 |
193 | axon-data-protection-config.json
194 |
195 |
196 |
197 |
198 | generate
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/AxonConfig.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard;
2 |
3 | import com.thoughtworks.xstream.XStream;
4 | import org.axonframework.common.caching.Cache;
5 | import org.axonframework.common.caching.WeakReferenceCache;
6 | import org.axonframework.config.Configurer;
7 | import org.axonframework.config.ConfigurerModule;
8 | import org.axonframework.lifecycle.Phase;
9 | import org.axonframework.messaging.Message;
10 | import org.axonframework.messaging.interceptors.LoggingInterceptor;
11 | import org.jetbrains.annotations.NotNull;
12 | import org.springframework.context.annotation.Bean;
13 | import org.springframework.context.annotation.Configuration;
14 | import org.springframework.context.annotation.Profile;
15 |
16 | @Configuration
17 | public class AxonConfig {
18 |
19 | @Bean
20 | public LoggingInterceptor> loggingInterceptor() {
21 | return new LoggingInterceptor<>();
22 | }
23 |
24 | @Bean
25 | public ConfigurerModule loggingInterceptorConfigurerModule(LoggingInterceptor> loggingInterceptor) {
26 | return new LoggingInterceptorConfigurerModule(loggingInterceptor);
27 | }
28 |
29 | @Bean
30 | @Profile("command")
31 | public Cache giftCardCache() {
32 | return new WeakReferenceCache();
33 | }
34 |
35 | /**
36 | * An example {@link ConfigurerModule} implementation to attach configuration to Axon's configuration life cycle.
37 | */
38 | private static class LoggingInterceptorConfigurerModule implements ConfigurerModule {
39 |
40 | private final LoggingInterceptor> loggingInterceptor;
41 |
42 | private LoggingInterceptorConfigurerModule(LoggingInterceptor> loggingInterceptor) {
43 | this.loggingInterceptor = loggingInterceptor;
44 | }
45 |
46 | @Override
47 | public void configureModule(@NotNull Configurer configurer) {
48 | configurer.eventProcessing(
49 | processingConfigurer -> processingConfigurer.registerDefaultHandlerInterceptor(
50 | (config, processorName) -> loggingInterceptor
51 | )
52 | )
53 | .onInitialize(this::registerInterceptorForBusses);
54 | }
55 |
56 | /**
57 | * Registers the {@link LoggingInterceptor} on the {@link org.axonframework.commandhandling.CommandBus},
58 | * {@link com.google.common.eventbus.EventBus}, {@link org.axonframework.queryhandling.QueryBus}, and
59 | * {@link org.axonframework.queryhandling.QueryUpdateEmitter}.
60 | *
61 | * It does so right after the {@link Phase#INSTRUCTION_COMPONENTS}, to ensure all these infrastructure
62 | * components are constructed.
63 | *
64 | * @param config The {@link org.axonframework.config.Configuration} to retrieve the infrastructure components
65 | * from.
66 | */
67 | @SuppressWarnings("resource") // We do not require to handle the returned Registration object.
68 | private void registerInterceptorForBusses(org.axonframework.config.Configuration config) {
69 | config.onStart(Phase.INSTRUCTION_COMPONENTS + 1, () -> {
70 | config.commandBus().registerHandlerInterceptor(loggingInterceptor);
71 | config.commandBus().registerDispatchInterceptor(loggingInterceptor);
72 | config.eventBus().registerDispatchInterceptor(loggingInterceptor);
73 | config.queryBus().registerHandlerInterceptor(loggingInterceptor);
74 | config.queryBus().registerDispatchInterceptor(loggingInterceptor);
75 | config.queryUpdateEmitter().registerDispatchInterceptor(loggingInterceptor);
76 | });
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/GiftCardApp.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class GiftCardApp {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(GiftCardApp.class, args);
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/api/Address.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.api;
2 |
3 | import io.axoniq.plugin.data.protection.annotation.SensitiveData;
4 |
5 | public record Address(
6 | @SensitiveData String addressLine1,
7 | String postalCode
8 | ) {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/api/CancelCardCommand.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.api;
2 |
3 | import org.axonframework.modelling.command.TargetAggregateIdentifier;
4 |
5 | // Tag this command to use it as code sample in the documentation
6 | // tag::CancelCardCommand[]
7 | public record CancelCardCommand(
8 | @TargetAggregateIdentifier String id
9 | ) {
10 |
11 | }
12 | // end::CancelCardCommand[]
13 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/api/CardCanceledEvent.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.api;
2 |
3 | public record CardCanceledEvent(
4 | String id
5 | ) {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/api/CardIssuedEvent.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.api;
2 |
3 | public record CardIssuedEvent(
4 | String id,
5 | int amount
6 | ) {
7 |
8 | }
9 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/api/CardRedeemedEvent.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.api;
2 |
3 | import io.axoniq.plugin.data.protection.annotation.SensitiveData;
4 | import io.axoniq.plugin.data.protection.annotation.SensitiveDataHolder;
5 | import io.axoniq.plugin.data.protection.annotation.SubjectId;
6 |
7 | @SensitiveDataHolder
8 | public record CardRedeemedEvent(
9 | @SubjectId String id,
10 | @SensitiveData(replacementValue = "hidden amount") int amount
11 | ) {
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/api/CardSummary.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.api;
2 |
3 | import java.time.Instant;
4 |
5 | public record CardSummary(
6 | String id,
7 | int initialValue,
8 | int remainingValue,
9 | Instant issued,
10 | Instant lastUpdated
11 | ) {
12 |
13 | public static CardSummary issue(String id, int initialValue, Instant issuedAt) {
14 | return new CardSummary(id, initialValue, initialValue, issuedAt, issuedAt);
15 | }
16 |
17 | public CardSummary redeem(int amount, Instant redeemedAt) {
18 | return new CardSummary(
19 | this.id,
20 | this.initialValue,
21 | this.remainingValue - amount,
22 | this.issued,
23 | redeemedAt);
24 | }
25 |
26 | public CardSummary cancel(Instant cancelledAt) {
27 | return new CardSummary(
28 | this.id,
29 | this.initialValue,
30 | 0,
31 | this.issued,
32 | cancelledAt);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/api/CountCardSummariesQuery.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.api;
2 |
3 | public record CountCardSummariesQuery() {
4 |
5 | @Override
6 | public String toString() {
7 | return "CountCardSummariesQuery";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/api/CountCardSummariesResponse.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.api;
2 |
3 | import java.time.Instant;
4 |
5 | public record CountCardSummariesResponse(
6 | int count,
7 | Instant lastEvent
8 | ) {
9 |
10 | }
11 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/api/ExampleEvent.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.api;
2 |
3 | import io.axoniq.plugin.data.protection.annotation.SensitiveData;
4 | import io.axoniq.plugin.data.protection.annotation.SensitiveDataHolder;
5 | import io.axoniq.plugin.data.protection.annotation.SubjectId;
6 | import org.axonframework.serialization.Revision;
7 |
8 | /**
9 | * Example event for data protection plugin config generation
10 | *
11 | * @param id
12 | * @param ssn
13 | * @param address
14 | */
15 | @SensitiveDataHolder
16 | //Only needs to be placed on the events, not on any contained objects such as the address in this example.
17 | @Revision("1")
18 | public record ExampleEvent(
19 | @SubjectId String id,
20 | @SensitiveData String ssn,
21 | Address address
22 | ) {
23 |
24 | }
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/api/FetchCardSummariesQuery.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.api;
2 |
3 | public record FetchCardSummariesQuery(
4 | int limit
5 | ) {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/api/IssueCardCommand.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.api;
2 |
3 | import org.axonframework.modelling.command.TargetAggregateIdentifier;
4 |
5 | // Tag this command to use it as code sample in the documentation
6 | // tag::IssueCardCommand[]
7 | public record IssueCardCommand(
8 | @TargetAggregateIdentifier String id,
9 | int amount
10 | ) {
11 |
12 | }
13 | // end::IssueCardCommand[]
14 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/api/RedeemCardCommand.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.api;
2 |
3 | import org.axonframework.modelling.command.TargetAggregateIdentifier;
4 |
5 | // Tag this command to use it as code sample in the documentation
6 | // tag::RedeemCardCommand[]
7 | public record RedeemCardCommand(
8 | @TargetAggregateIdentifier String id,
9 | int amount
10 | ) {
11 |
12 | }
13 | // end::RedeemCardCommand[]
14 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/command/GiftCard.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.command;
2 |
3 | import io.axoniq.demo.giftcard.api.CancelCardCommand;
4 | import io.axoniq.demo.giftcard.api.CardCanceledEvent;
5 | import io.axoniq.demo.giftcard.api.CardIssuedEvent;
6 | import io.axoniq.demo.giftcard.api.CardRedeemedEvent;
7 | import io.axoniq.demo.giftcard.api.IssueCardCommand;
8 | import io.axoniq.demo.giftcard.api.RedeemCardCommand;
9 | import org.axonframework.commandhandling.CommandHandler;
10 | import org.axonframework.eventsourcing.EventSourcingHandler;
11 | import org.axonframework.modelling.command.AggregateIdentifier;
12 | import org.axonframework.spring.stereotype.Aggregate;
13 | import org.springframework.context.annotation.Profile;
14 |
15 | import static org.axonframework.modelling.command.AggregateLifecycle.apply;
16 |
17 | @Profile("command")
18 | @Aggregate(cache = "giftCardCache")
19 | public class GiftCard {
20 |
21 | @AggregateIdentifier
22 | private String giftCardId;
23 | private int remainingValue;
24 |
25 | // Tag this handler to use it as code sample in the documentation
26 | // tag::IssueCardCommandHandler[]
27 | @CommandHandler
28 | public GiftCard(IssueCardCommand command) {
29 | if (command.amount() <= 0) {
30 | throw new IllegalArgumentException("amount <= 0");
31 | }
32 | apply(new CardIssuedEvent(command.id(), command.amount()));
33 | }
34 | // end::IssueCardCommandHandler[]
35 |
36 | // Tag this handler to use it as code sample in the documentation
37 | // tag::RedeemCardCommandHandler[]
38 | @CommandHandler
39 | public void handle(RedeemCardCommand command) {
40 | if (command.amount() <= 0) {
41 | throw new IllegalArgumentException("amount <= 0");
42 | }
43 | if (command.amount() > remainingValue) {
44 | throw new IllegalStateException("amount > remaining value");
45 | }
46 | apply(new CardRedeemedEvent(giftCardId, command.amount()));
47 | }
48 | // end::RedeemCardCommandHandler[]
49 |
50 | @SuppressWarnings("unused")
51 | @CommandHandler
52 | public void handle(CancelCardCommand command) {
53 | apply(new CardCanceledEvent(giftCardId));
54 | }
55 |
56 | @EventSourcingHandler
57 | public void on(CardIssuedEvent event) {
58 | giftCardId = event.id();
59 | remainingValue = event.amount();
60 | }
61 |
62 | @EventSourcingHandler
63 | public void on(CardRedeemedEvent event) {
64 | remainingValue -= event.amount();
65 | }
66 |
67 | @EventSourcingHandler
68 | public void on(CardCanceledEvent event) {
69 | remainingValue = 0;
70 | }
71 |
72 | public GiftCard() {
73 | // Required by Axon to construct an empty instance to initiate Event Sourcing.
74 | }
75 | }
76 |
77 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/query/CardSummaryProjection.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.query;
2 |
3 | import io.axoniq.demo.giftcard.api.CardCanceledEvent;
4 | import io.axoniq.demo.giftcard.api.CardIssuedEvent;
5 | import io.axoniq.demo.giftcard.api.CardRedeemedEvent;
6 | import io.axoniq.demo.giftcard.api.CardSummary;
7 | import io.axoniq.demo.giftcard.api.CountCardSummariesQuery;
8 | import io.axoniq.demo.giftcard.api.CountCardSummariesResponse;
9 | import io.axoniq.demo.giftcard.api.FetchCardSummariesQuery;
10 | import org.axonframework.config.ProcessingGroup;
11 | import org.axonframework.eventhandling.EventHandler;
12 | import org.axonframework.eventhandling.Timestamp;
13 | import org.axonframework.queryhandling.QueryHandler;
14 | import org.axonframework.queryhandling.QueryUpdateEmitter;
15 | import org.springframework.context.annotation.Profile;
16 | import org.springframework.stereotype.Service;
17 |
18 | import java.time.Instant;
19 | import java.util.Comparator;
20 | import java.util.List;
21 | import java.util.Map;
22 | import java.util.concurrent.ConcurrentHashMap;
23 |
24 | @Profile("query")
25 | @Service
26 | @ProcessingGroup("card-summary")
27 | public class CardSummaryProjection {
28 |
29 | private final Map cardSummaryReadModel;
30 | private final QueryUpdateEmitter queryUpdateEmitter;
31 | private Instant lastUpdate;
32 |
33 | public CardSummaryProjection(
34 | QueryUpdateEmitter queryUpdateEmitter
35 | ) {
36 | this.cardSummaryReadModel = new ConcurrentHashMap<>();
37 | this.queryUpdateEmitter = queryUpdateEmitter;
38 | }
39 |
40 | @EventHandler
41 | public void on(CardIssuedEvent event, @Timestamp Instant timestamp) {
42 | lastUpdate = timestamp;
43 | /*
44 | * Update our read model by inserting the new card. This is done so that upcoming regular
45 | * (non-subscription) queries get correct data.
46 | */
47 | CardSummary summary = CardSummary.issue(event.id(), event.amount(), timestamp);
48 | cardSummaryReadModel.put(event.id(), summary);
49 | /*
50 | * Serve the subscribed queries by emitting an update. This reads as follows:
51 | * - to all current subscriptions of type CountCardSummariesQuery,
52 | * - for any CountCardSummariesQuery, since true is returned by default, and
53 | * - send a message that the count of queries matching this query has been changed.
54 | */
55 | queryUpdateEmitter.emit(CountCardSummariesQuery.class,
56 | query -> true,
57 | new CountCardSummariesResponse(cardSummaryReadModel.size(), lastUpdate));
58 | /*
59 | * Serve the subscribed queries by emitting an update. This reads as follows:
60 | * - to all current subscriptions of type CountCardSummariesQuery,
61 | * - for any CountCardSummariesQuery, since true is returned by default, and
62 | * - send a message that the count of queries matching this query has been changed.
63 | */
64 | queryUpdateEmitter.emit(FetchCardSummariesQuery.class, query -> true, summary);
65 | }
66 |
67 | @EventHandler
68 | public void on(CardRedeemedEvent event, @Timestamp Instant timestamp) {
69 | /*
70 | * Update our read model by updating the existing card. This is done so that upcoming regular
71 | * (non-subscription) queries get correct data.
72 | */
73 | CardSummary summary = cardSummaryReadModel.computeIfPresent(
74 | event.id(), (id, card) -> card.redeem(event.amount(), timestamp)
75 | );
76 | /*
77 | * Serve the subscribed queries by emitting an update. This reads as follows:
78 | * - to all current subscriptions of type FetchCardSummariesQuery
79 | * - for any FetchCardSummariesQuery, since true is returned by default, and
80 | * - send a message containing the new state of this gift card summary
81 | */
82 | queryUpdateEmitter.emit(FetchCardSummariesQuery.class, query -> true, summary);
83 | }
84 |
85 | @EventHandler
86 | public void on(CardCanceledEvent event, @Timestamp Instant timestamp) {
87 | /*
88 | * Update our read model by updating the existing card. This is done so that upcoming regular
89 | * (non-subscription) queries get correct data.
90 | */
91 | CardSummary summary = cardSummaryReadModel.computeIfPresent(
92 | event.id(), (id, card) -> card.cancel(timestamp)
93 | );
94 | /*
95 | * Serve the subscribed queries by emitting an update. This reads as follows:
96 | * - to all current subscriptions of type FetchCardSummariesQuery
97 | * - for any FetchCardSummariesQuery, since true is returned by default, and
98 | * - send a message containing the new state of this gift card summary
99 | */
100 | queryUpdateEmitter.emit(FetchCardSummariesQuery.class, query -> true, summary);
101 | }
102 |
103 | @QueryHandler
104 | public List handle(FetchCardSummariesQuery query) {
105 | return cardSummaryReadModel.values()
106 | .stream()
107 | .sorted(Comparator.comparing(CardSummary::lastUpdated))
108 | .limit(query.limit())
109 | .toList();
110 | }
111 |
112 | @SuppressWarnings("unused")
113 | @QueryHandler
114 | public CountCardSummariesResponse handle(CountCardSummariesQuery query) {
115 | return new CountCardSummariesResponse(cardSummaryReadModel.size(), lastUpdate);
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/rest/GiftCardController.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.rest;
2 |
3 | import io.axoniq.demo.giftcard.api.IssueCardCommand;
4 | import io.axoniq.demo.giftcard.api.RedeemCardCommand;
5 | import io.axoniq.demo.giftcard.api.CardSummary;
6 | import io.axoniq.demo.giftcard.api.CountCardSummariesQuery;
7 | import io.axoniq.demo.giftcard.api.CountCardSummariesResponse;
8 | import io.axoniq.demo.giftcard.api.FetchCardSummariesQuery;
9 | import org.axonframework.commandhandling.gateway.CommandGateway;
10 | import org.axonframework.messaging.responsetypes.ResponseTypes;
11 | import org.axonframework.queryhandling.QueryGateway;
12 | import org.axonframework.queryhandling.SubscriptionQueryResult;
13 | import org.springframework.context.annotation.Profile;
14 | import org.springframework.http.MediaType;
15 | import org.springframework.web.bind.annotation.GetMapping;
16 | import org.springframework.web.bind.annotation.PathVariable;
17 | import org.springframework.web.bind.annotation.PostMapping;
18 | import org.springframework.web.bind.annotation.RequestMapping;
19 | import org.springframework.web.bind.annotation.RestController;
20 | import reactor.core.publisher.Flux;
21 | import reactor.core.publisher.Mono;
22 |
23 | import java.time.Duration;
24 | import java.util.List;
25 | import java.util.UUID;
26 |
27 | @RestController
28 | @Profile("gui")
29 | @RequestMapping("/giftcard")
30 | public class GiftCardController {
31 |
32 | private final CommandGateway commandGateway;
33 | private final QueryGateway queryGateway;
34 |
35 | public GiftCardController(CommandGateway commandGateway, QueryGateway queryGateway) {
36 | this.commandGateway = commandGateway;
37 | this.queryGateway = queryGateway;
38 | }
39 |
40 | @GetMapping(path = "/subscribe/limit/{limit}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
41 | public Flux subscribe(
42 | @PathVariable int limit
43 | ) {
44 | return summerySubscription(limit);
45 | }
46 |
47 | @GetMapping(path = "/subscribe-count", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
48 | public Flux subscribe() {
49 | return countSubscription();
50 | }
51 |
52 | @PostMapping("issue/id/{id}/amount/{amount}")
53 | public Mono issue(
54 | @PathVariable String id,
55 | @PathVariable int amount
56 | ) {
57 | var command = new IssueCardCommand(id, amount);
58 | return Mono.fromFuture(commandGateway.send(command))
59 | .then(Mono.just(Result.ok()))
60 | .onErrorResume(e -> Mono.just(Result.Error(id, e.getMessage())))
61 | .timeout(Duration.ofSeconds(5L));
62 | }
63 |
64 | @PostMapping("redeem/id/{id}/amount/{amount}")
65 | public Mono redeem(
66 | @PathVariable String id,
67 | @PathVariable int amount
68 | ) {
69 | var command = new RedeemCardCommand(id, amount);
70 | return Mono.fromFuture(commandGateway.send(command))
71 | .then(Mono.just(Result.ok()))
72 | .onErrorResume(e -> Mono.just(Result.Error(id, e.getMessage())))
73 | .timeout(Duration.ofSeconds(5L));
74 | }
75 |
76 | @GetMapping(path = "bulkissue/number/{number}/amount/{amount}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
77 | public Flux bulkIssue(
78 | @PathVariable int number,
79 | @PathVariable int amount
80 | ) {
81 | return Flux.range(0, number).flatMap(i -> issue(amount));
82 | }
83 |
84 | private Mono issue(int amount) {
85 | String id = UUID.randomUUID().toString().substring(0, 11).toUpperCase();
86 | var command = new IssueCardCommand(id, amount);
87 | return Mono.fromFuture(commandGateway.send(command))
88 | .then(Mono.just(Result.ok()))
89 | .onErrorResume(e -> Mono.just(Result.Error(id, e.getMessage())))
90 | .timeout(Duration.ofSeconds(10L));
91 | }
92 |
93 |
94 | private Flux summerySubscription(int limit) {
95 | var query = new FetchCardSummariesQuery(limit);
96 | SubscriptionQueryResult, CardSummary> result = queryGateway.subscriptionQuery(
97 | query,
98 | ResponseTypes.multipleInstancesOf(CardSummary.class),
99 | ResponseTypes.instanceOf(CardSummary.class));
100 | return result.initialResult()
101 | .flatMapMany(Flux::fromIterable)
102 | .concatWith(result.updates())
103 | .doFinally(signal -> result.close());
104 | }
105 |
106 | private Flux countSubscription() {
107 | var query = new CountCardSummariesQuery();
108 | SubscriptionQueryResult result = queryGateway.subscriptionQuery(
109 | query,
110 | ResponseTypes.instanceOf(CountCardSummariesResponse.class),
111 | ResponseTypes.instanceOf(CountCardSummariesResponse.class));
112 | return result.initialResult()
113 | .concatWith(result.updates())
114 | .doFinally(signal -> result.close());
115 | }
116 | }
--------------------------------------------------------------------------------
/src/main/java/io/axoniq/demo/giftcard/rest/Result.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.demo.giftcard.rest;
2 |
3 | import org.axonframework.axonserver.connector.ErrorCode;
4 |
5 | public record Result(
6 | boolean isSuccess,
7 | String error
8 | ) {
9 |
10 | public static Result ok() {
11 | return new Result(true, null);
12 | }
13 |
14 | public static Result Error(String id, String error) {
15 | if (error.contains(ErrorCode.INVALID_EVENT_SEQUENCE.errorCode())) {
16 | return new Result(false,
17 | "An event for aggregate [" + id + "] at sequence ["
18 | + error.substring(error.length() - 1) + "] was already inserted. "
19 | + "You are either reusing the aggregate identifier "
20 | + "or concurrently dispatching commands for the same aggregate.");
21 | }
22 | return new Result(false, "Error on aggregate [" + id + "]. " + error);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/src/main/resources/application-command.properties:
--------------------------------------------------------------------------------
1 | server.port=8082
--------------------------------------------------------------------------------
/src/main/resources/application-gui.properties:
--------------------------------------------------------------------------------
1 | server.port=8080
--------------------------------------------------------------------------------
/src/main/resources/application-query.properties:
--------------------------------------------------------------------------------
1 | server.port=8083
2 |
3 | # Configuring the "card-summary" processing group to use two threads and two segments
4 | axon.eventhandling.processors.card-summary.thread-count=2
5 | axon.eventhandling.processors.card-summary.initial-segment-count=2
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | # The name of this app:
2 | spring.application.name=GiftCard-App-${spring.profiles.active}
3 | server.port=8080
4 |
5 | # Debugging on
6 | logging.level.io.axoniq.demo=debug
7 |
8 | # We look for Axon Server locally, unless we find a PCF Binding for AxonServer
9 | axon.axonserver.servers=${vcap.services.AxonServer.credentials.uri:localhost}
10 |
11 | # The default profiles are "all of them"
12 | spring.profiles.active=command,query,gui
13 |
14 | # Management endpoints inclusion
15 | management.endpoint.health.show-details=always
16 | management.endpoints.web.exposure.include=*
17 |
18 | axon.serializer.general=jackson
19 |
20 |
--------------------------------------------------------------------------------
/src/main/resources/static/app.js:
--------------------------------------------------------------------------------
1 | let actionInProgress = false
2 | let idInputIssue
3 | let amountInputIssue
4 | let issueButton
5 | let numberInputBulk
6 | let amountInputBulk
7 | let bulkButton
8 | let idInputRedeem
9 | let amountInputRedeem
10 | let redeemButton
11 | let notification
12 | let notificationText
13 | let notificationButton
14 | let tableSource
15 |
16 | function setDomElements() {
17 | idInputIssue = document.getElementById("id-input-issue")
18 | amountInputIssue = document.getElementById("amount-input-issue")
19 | issueButton = document.getElementById("issue-button")
20 | numberInputBulk = document.getElementById("number-input-bulk")
21 | amountInputBulk = document.getElementById("amount-input-bulk")
22 | bulkButton = document.getElementById("bulk-button")
23 | idInputRedeem = document.getElementById("id-input-redeem")
24 | amountInputRedeem = document.getElementById("amount-input-redeem")
25 | redeemButton = document.getElementById("redeem-button")
26 | notification = document.getElementById("notification")
27 | notificationText = document.getElementById("notification-text")
28 | notificationButton = document.getElementById("notification-button")
29 | }
30 |
31 | function maybeSwitchIssueState() {
32 | if (issueButton.disabled && !actionInProgress && idInputIssue.value !== "" && amountInputIssue.value !== "") {
33 | issueButton.disabled = false
34 | } else if (idInputIssue.value === "" || amountInputIssue.value === "") {
35 | issueButton.disabled = true
36 | }
37 | }
38 |
39 | function maybeSwitchBulkState() {
40 | if (bulkButton.disabled && !actionInProgress && numberInputBulk.value !== "" && amountInputBulk.value !== "") {
41 | bulkButton.disabled = false
42 | } else if (numberInputBulk.value === "" && amountInputBulk.value === "") {
43 | bulkButton.disabled = true
44 | }
45 | }
46 |
47 | function maybeSwitchRedeemState() {
48 | if (redeemButton.disabled && !actionInProgress && idInputRedeem.value !== "" && amountInputRedeem.value !== "") {
49 | redeemButton.disabled = false
50 | } else if (idInputRedeem.value === "" || amountInputRedeem.value === "") {
51 | redeemButton.disabled = true
52 | }
53 | }
54 |
55 | function maybeSwitchAll(){
56 | maybeSwitchIssueState()
57 | maybeSwitchBulkState()
58 | maybeSwitchRedeemState()
59 | }
60 |
61 | function disableAllButtons() {
62 | issueButton.disabled = true
63 | bulkButton.disabled = true
64 | redeemButton.disabled = true
65 | }
66 |
67 | function hideNotification() {
68 | notification.style.visibility = "hidden"
69 | }
70 |
71 | function removeColorClassesFromNotification() {
72 | notification.classList.remove("is-success", "is-info", "is-danger")
73 | }
74 |
75 | async function handleResult(result) {
76 | actionInProgress = false
77 | hideNotification()
78 | removeColorClassesFromNotification()
79 | if (result["isSuccess"] === true) {
80 | notification.classList.add("is-success")
81 | notificationText.innerHTML = "Success"
82 | } else {
83 | notification.classList.add("is-danger")
84 | notificationText.innerHTML = result["error"]
85 | }
86 | notification.style.visibility = ""
87 | maybeSwitchAll()
88 | }
89 |
90 |
91 | async function issueCard() {
92 | actionInProgress = true
93 | disableAllButtons()
94 | const value = idInputIssue.value
95 | const amount = amountInputIssue.value
96 | idInputIssue.value = ""
97 | amountInputIssue.value = ""
98 | const response = await fetch("/giftcard/issue/id/" + value + "/amount/" + amount, {
99 | method: "POST"
100 | });
101 | response.json().then(result => handleResult(result))
102 | }
103 |
104 | async function bulkCard() {
105 | actionInProgress = true
106 | disableAllButtons()
107 | const number = numberInputBulk.value
108 | const amount = amountInputBulk.value
109 | numberInputBulk.value = ""
110 | amountInputBulk.value = ""
111 | removeColorClassesFromNotification()
112 | notification.classList.add("is-info")
113 | const progressBar = document.createElement('progress')
114 | notificationText.innerHTML = ""
115 | notificationText.appendChild(progressBar)
116 | progressBar["value"] = 0
117 | progressBar["max"] = number
118 | progressBar.classList.add("progress")
119 | notificationButton.style.display = "none"
120 | notification.style.visibility = ""
121 | let successCount = 0
122 | let failedCount = 0
123 | const source = new EventSource("/giftcard/bulkissue/number/" + number + "/amount/" + amount)
124 | source.onmessage = function (event) {
125 | const result = JSON.parse(event.data)
126 | if (result["isSuccess"] === true) {
127 | successCount++
128 | } else {
129 | failedCount++
130 | console.log(result["error"])
131 | }
132 | const total = successCount + failedCount
133 | progressBar["value"] = total
134 | if (total >= number) {
135 | source.close()
136 | notificationButton.style.display = ""
137 | if (failedCount === 0) {
138 | handleResult({"isSuccess": true})
139 | } else {
140 | handleResult({"isSuccess": false, "error": failedCount + " of the " + number + " ended in failure."})
141 | }
142 | }
143 | };
144 | source.onerror = function (error) {
145 | source.close()
146 | console.log(error)
147 | notificationButton.style.display = ""
148 | handleResult({"isSuccess": false, "error": "Failed to connect to the server"})
149 | }
150 | }
151 |
152 | async function redeemCard() {
153 | actionInProgress = true
154 | disableAllButtons()
155 | const id = idInputRedeem.value
156 | const amount = amountInputRedeem.value
157 | idInputRedeem.value = ""
158 | amountInputRedeem.value = ""
159 | const response = await fetch("/giftcard/redeem/id/" + id + "/amount/" + amount, {
160 | method: "POST"
161 | });
162 | response.json().then(result => {
163 | handleResult(result)
164 | })
165 | }
166 |
167 | async function updateCount() {
168 | const count = document.getElementById("count")
169 | const lastEvent = document.getElementById("last-event")
170 | const source = new EventSource("/giftcard/subscribe-count")
171 | source.onmessage = function (event) {
172 | const cardData = JSON.parse(event.data)
173 | count.innerHTML = cardData["count"]
174 | lastEvent.innerHTML = new Date(cardData["lastEvent"]).toLocaleString()
175 | };
176 | }
177 |
178 | async function updateTable(maxRows) {
179 | if (tableSource !== undefined){
180 | tableSource.close()
181 | }
182 | let tableIds = []
183 | const tableBody = document.getElementById("table-body")
184 | tableBody.innerHTML = ""
185 | tableSource = new EventSource("/giftcard/subscribe/limit/" + maxRows)
186 | tableSource.onmessage = function (event) {
187 | const cardData = JSON.parse(event.data)
188 | const cardId = "card_" + cardData["id"]
189 | if (tableIds.includes(cardId)) {
190 | document.getElementById(cardId).remove()
191 | tableIds = tableIds.filter(item => item !== cardId)
192 | } else if (tableIds.length >= maxRows) {
193 | const toRemove = tableIds.pop()
194 | document.getElementById(toRemove).remove()
195 | }
196 | tableIds.unshift(cardId)
197 | const row = tableBody.insertRow(0)
198 | row.id = cardId
199 | const cell1 = row.insertCell(0)
200 | const cell2 = row.insertCell(1)
201 | const cell3 = row.insertCell(2)
202 | const cell4 = row.insertCell(3)
203 | const cell5 = row.insertCell(4)
204 | cell1.innerHTML = cardData["id"]
205 | cell2.innerHTML = cardData["initialValue"]
206 | cell3.innerHTML = cardData["remainingValue"]
207 | cell4.innerHTML = new Date(cardData["issued"]).toLocaleString()
208 | cell5.innerHTML = new Date(cardData["lastUpdated"]).toLocaleString()
209 | };
210 | }
211 |
212 | function setListeners() {
213 | notificationButton.addEventListener("click", () => {
214 | void hideNotification()
215 | });
216 | issueButton.addEventListener("click", () => {
217 | void issueCard();
218 | });
219 | bulkButton.addEventListener("click", () => {
220 | void bulkCard();
221 | });
222 | redeemButton.addEventListener("click", () => {
223 | void redeemCard();
224 | });
225 | idInputIssue.addEventListener("keyup", () => {
226 | maybeSwitchIssueState()
227 | })
228 | amountInputIssue.addEventListener("keyup", () => {
229 | maybeSwitchIssueState()
230 | })
231 | numberInputBulk.addEventListener("keyup", () => {
232 | maybeSwitchBulkState()
233 | })
234 | amountInputBulk.addEventListener("keyup", () => {
235 | maybeSwitchBulkState()
236 | })
237 | idInputRedeem.addEventListener("keyup", () => {
238 | maybeSwitchRedeemState()
239 | })
240 | amountInputRedeem.addEventListener("keyup", () => {
241 | maybeSwitchRedeemState()
242 | })
243 | addEventListener("paste", () => {
244 | setTimeout(maybeSwitchAll, 10)
245 | })
246 | document.getElementById("table-size-select").addEventListener("change", event => {
247 | void updateTable(event.target.value)
248 | })
249 | }
250 |
251 | window.addEventListener("load", () => {
252 | setDomElements()
253 | setListeners()
254 | void updateCount()
255 | void updateTable(20)
256 | })
--------------------------------------------------------------------------------
/src/main/resources/static/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Giftcard Demo
7 |
8 |
9 |
10 |
11 |
12 |