├── .github
├── dependabot.yml
└── workflows
│ └── maven.yml
├── .gitignore
├── .mvn
└── wrapper
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── README.md
├── chat-getting-started
├── README.md
├── command-requests.http
├── pom.xml
├── query-requests.http
└── src
│ ├── main
│ ├── java
│ │ └── io
│ │ │ └── axoniq
│ │ │ └── labs
│ │ │ └── chat
│ │ │ ├── ChatGettingStartedApplication.java
│ │ │ ├── commandmodel
│ │ │ └── ChatRoom.java
│ │ │ ├── coreapi
│ │ │ └── messages.kt
│ │ │ ├── query
│ │ │ └── rooms
│ │ │ │ ├── messages
│ │ │ │ ├── ChatMessage.java
│ │ │ │ ├── ChatMessageProjection.java
│ │ │ │ └── ChatMessageRepository.java
│ │ │ │ ├── participants
│ │ │ │ ├── RoomParticipant.java
│ │ │ │ ├── RoomParticipantsProjection.java
│ │ │ │ └── RoomParticipantsRepository.java
│ │ │ │ └── summary
│ │ │ │ ├── RoomSummary.java
│ │ │ │ ├── RoomSummaryProjection.java
│ │ │ │ └── RoomSummaryRepository.java
│ │ │ └── restapi
│ │ │ ├── CommandController.java
│ │ │ └── QueryController.java
│ └── resources
│ │ └── application.properties
│ └── test
│ ├── java
│ └── io
│ │ └── axoniq
│ │ └── labs
│ │ └── chat
│ │ ├── ChatGettingStartedApplicationTests.java
│ │ └── commandmodel
│ │ └── ChatRoomTest.java
│ └── resources
│ └── logback-test.xml
├── chat-scaling-out
├── README.md
├── command-requests.http
├── pom.xml
├── query-requests.http
├── requests.http
└── src
│ ├── main
│ ├── java
│ │ └── io
│ │ │ └── axoniq
│ │ │ └── labs
│ │ │ └── chat
│ │ │ ├── ChatScalingOutApplication.java
│ │ │ ├── Servers.java
│ │ │ ├── commandmodel
│ │ │ └── ChatRoom.java
│ │ │ ├── coreapi
│ │ │ └── messages.kt
│ │ │ ├── query
│ │ │ └── rooms
│ │ │ │ ├── messages
│ │ │ │ ├── ChatMessage.java
│ │ │ │ ├── ChatMessageProjection.java
│ │ │ │ └── ChatMessageRepository.java
│ │ │ │ ├── participants
│ │ │ │ ├── RoomParticipant.java
│ │ │ │ ├── RoomParticipantsProjection.java
│ │ │ │ └── RoomParticipantsRepository.java
│ │ │ │ └── summary
│ │ │ │ ├── RoomSummary.java
│ │ │ │ ├── RoomSummaryProjection.java
│ │ │ │ └── RoomSummaryRepository.java
│ │ │ └── restapi
│ │ │ ├── CommandController.java
│ │ │ └── QueryController.java
│ └── resources
│ │ └── application.properties
│ └── test
│ ├── java
│ └── io
│ │ └── axoniq
│ │ └── labs
│ │ └── chat
│ │ ├── ChatScalingOutApplicationTests.java
│ │ └── commandmodel
│ │ └── ChatRoomTest.java
│ └── resources
│ └── logback-test.xml
├── mvnw
├── mvnw.cmd
└── pom.xml
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: maven
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | open-pull-requests-limit: 5
8 | # Specify labels for pull requests
9 | labels:
10 | - "Type: Dependency Upgrade"
11 | - "Priority 1: Must"
12 | # Add reviewers
13 | reviewers:
14 | - "idugalic"
15 | - "saratry"
16 | - "YvonneCeelie"
17 |
--------------------------------------------------------------------------------
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | name: Axon Code Samples
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | strategy:
9 | matrix:
10 | java: [ 8, 11 ]
11 | fail-fast: false #do not cancel other jobs if one fail
12 |
13 | name: Build and tests on JDK ${{ matrix.java }}
14 |
15 | steps:
16 | - name: Checkout code
17 | uses: actions/checkout@v2
18 |
19 | - name: Set up JDK ${{ matrix.java }}
20 | uses: actions/setup-java@v1
21 | with:
22 | java-version: ${{ matrix.java }}
23 |
24 | - name: Cache .m2
25 | uses: actions/cache@v1
26 | with:
27 | path: ~/.m2/repository
28 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
29 | restore-keys: |
30 | ${{ runner.os }}-maven
31 |
32 | - name: Build and verify
33 | run: mvn -B clean verify -DskipTests
34 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AxonIQ/axon-quick-start/8316f75ce73b86530d9c9d20a14fa05ec15c4e3c/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.2/apache-maven-3.5.2-bin.zip
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Axon Labs - Chat Application
2 | ============================
3 |
4 | This project contains the source code for the Axon Labs. It contains two modules:
5 |
6 | - *chat-getting-started*:
7 | Use this module if you want to get a feel of how Axon works. The project contains a minimal
8 | setup. You will need to implement the Command Model, the Event Handlers and the Query Handlers,
9 | and connect the public API to the command bus and the query bus.
10 |
11 | Want to do this lab? Visit [getting started exercises](chat-getting-started/README.md).
12 |
13 | - *chat-scaling-out*:
14 | Use this module if you want to take on a more advanced challenge. The project contains a fully
15 | operational chat application. It's your task to change the configuration to distribute commands
16 | and events across multiple instances of the application.
17 |
18 | Want to do this lab? Visit [scaling out exercises](chat-scaling-out/README.md).
19 |
20 |
21 | If you have any questions, don't hesitate to ask your Lab coordinator for guidance.
22 |
23 |
--------------------------------------------------------------------------------
/chat-getting-started/README.md:
--------------------------------------------------------------------------------
1 | Axon Lab - Getting Started
2 | ==========================
3 |
4 | So, you're new to Axon and want to get started. Awesome!
5 |
6 | Application overview
7 | --------------------
8 |
9 | The main application is called `ChatGettingStartedApplication`. It's a Spring Boot application with
10 | the following main dependencies:
11 | - Axon (Spring Boot starter)
12 | - Spring Data JPA
13 | - Freemarker
14 | - Web
15 | - Reactor
16 | - H2 (Embedded Database)
17 | - Spring Boot Test
18 | - Axon Test
19 |
20 | There are a few test cases. One will check if the application can start, while the others
21 | validate the Aggregate's behavior. They should all fail, as most of the stuff needs yet to be implemented.
22 |
23 | ### Application layout ###
24 |
25 | The application's logic is divided among a number of packages.
26 |
27 | - `io.axoniq.labs.chat`
28 | The main package. Contains the Application class with the configuration.
29 | - `io.axoniq.labs.chat.commandmodel`
30 | Contains the Command Model. In our case, just the `ChatRoom` Aggregate that has been provided to make the project
31 | compile.
32 | - `io.axoniq.labs.chat.coreapi`
33 | The so called *core api*. This is where we put the Commands, Events and Queries.
34 | Since commands, events and queries are immutable, we have used Kotlin to define them. Kotlin allows you to
35 | concisely define each event, command and query on a single line.
36 | To make sure you don't waste your precious time, we've implemented these Commands, Events and Queries for you.
37 | - `io.axoniq.labs.chat.query.rooms.messages`
38 | Contains the Projections (also called View Model or Query Model) for the Messages that have been broadcast in a
39 | specific room. This package contains both the Event Handlers for updating the Projections,
40 | as well as the Query Handlers to read the data.
41 | - `io.axoniq.labs.chat.query.rooms.participants`
42 | Contains the Projection to serve the list of participants in a given Chat Room.
43 | - `io.axoniq.labs.chat.query.rooms.summary`
44 | Contains the Projection to serve a list of available chat rooms and the number of participants.
45 | - `io.axoniq.labs.chat.restapi`
46 | This is the REST Command and Query API to change and read the application's state.
47 | API calls here are translated into Commands and Queries for the application to process.
48 |
49 | ### Swagger UI ###
50 | The application has 'Swagger' enabled. You can use Swagger to send requests.
51 |
52 | Visit: [http://localhost:8080/swagger-ui/](http://localhost:8080/swagger-ui/)
53 |
54 | Note: The Swagger UI does not support the 'Subscription Query' further on in the assignment,
55 | as Swagger does not support streaming results.
56 | Issuing a regular `curl` operation, or something along those lines, is recommended to check the Subscription Query.
57 |
58 | Note 2: If you are on Intellij IDEA, you can also use the `command-request.http`
59 | and `query-request.http` files in this project to send requests directly from your IDE.
60 | Several defaults have been provided, but feel free to play around here!
61 |
62 | ### H2 Console ###
63 | The application has the 'H2 Console' configured, so you can peek into the database's contents.
64 |
65 | Visit: [http://localhost:8080/h2-console](http://localhost:8080/h2-console)
66 | Enter JDBC URL: `jdbc:h2:mem:testdb`
67 | Leave other values to defaults and click 'connect'.
68 |
69 | Preparation
70 | -----------
71 |
72 | Axon Framework works best with AxonServer, and in this sample project we assume that you are using it.
73 | AxonServer needs to be downloaded separately.
74 | You can run AxonServer as a docker container by running:
75 | ```shell script
76 | docker run -d -p 8024:8024 -p 8124:8124 -p 8224:8224 --name axonserver axoniq/axonserver
77 | ```
78 |
79 | Our goal
80 | --------
81 |
82 | Basically, the goal is simple: make the tests pass!
83 |
84 | But testing isn't the ultimate goal: we want to have an application that we can take into production.
85 |
86 | Exercises
87 | ---------
88 |
89 | ### Implement the Command Model ###
90 |
91 | First of all, we're going to implement the Command Model. In this application, there is a single Aggregate: `ChatRoom`.
92 | This aggregate processes Commands and produces Events as a result.
93 |
94 | The expected behavior has been described in the `ChatRoomTest` class, using the Axon Test Fixtures.
95 |
96 | To make these tests pass, you will need to implement the following command handlers:
97 | 1. The handler for the `CreateRoomCommand` creates a new instance of a `ChatRoom`.
98 | Therefore, this command handler is a constructor, instead of a regular method.
99 | The method should `apply` (static method on `AggregateLifecycle`) a `RoomCreatedEvent`.
100 |
101 | Axon requires a no-arg constructor, we will also need to create one.
102 |
103 | Axon requires one field to be present: the aggregate's identifier.
104 | Create a field called roomId of type String, and annotate it with `@AggregateIdentifier`.
105 |
106 | We will also need to set this field to the correct value.
107 | As we are using event sourcing, we must do so in an `@EventSourcingHandler`.
108 | Create one that reacts to the `RoomCreatedEvent` and sets the `roomId` to the correct value.
109 | 2. The handler for the `JoinRoomCommand` should apply a `ParticipantJoinedRoomEvent`,
110 | but only if the joining participant hasn't already joined this room.
111 | Otherwise, nothing happens. To do this, we will need to maintain some state.
112 | We do this in `@EventSourcingHandler`, remember? Create the required handlers.
113 | 3. The handler for the `LeaveRoomCommand` should apply a `ParticipantLeftRoomEvent`,
114 | but only if the leaving participant has joined the room.
115 | Otherwise, again, nothing happens.
116 | Don't forget to update state in the right location.
117 | 4. Finally, implement the handler for the `PostMessageCommand`.
118 | A participant may only post messages to rooms he/she has joined.
119 | Otherwise, an exception is thrown.
120 |
121 | Now, there is only one thing left to do:
122 |
123 | 5. We need to tell Axon that we want to configure this class as an Aggregate.
124 | Annotate it with `@Aggregate` to have the Axon Spring Boot Auto-Configuration module configure
125 | the necessary components to work with this aggregate.
126 |
127 | That's it. All tests should pass now. If not, implement the missing behavior and try again...
128 |
129 | ### Connect the REST API to the Command Bus ###
130 |
131 | We've got a component that can handle commands now. Now, it's time to allow external components to trigger these
132 | commands. The `CommandController` class defines some API endpoints that should trigger commands to be sent.
133 |
134 | In Axon, we can use either the Command Bus or the CommandGateway to send commands.
135 | The latter has a friendlier API, so we've decided to use that one.
136 |
137 | 1. Implement the TODOs in the `CommandController` class to forward Commands to the Command Bus.
138 | Note that the API Endpoint methods declare a return type of `Future<...>`.
139 | The `CommandGateway.send()` method also returns a Future.
140 | This is a nice way to prepare the API layer for asynchronous execution of commands (perhaps later).
141 |
142 | 2. The `CreateChatRoom` API declares an ID as part of the HTTP Entity it expects.
143 | Although we generally favor client-generated identifiers, Javascript is notoriously bad at generating random values.
144 | Therefore, we would want the `roomId` to default to a proper randomly generated UUID (use `UUID.randomUUID().toString()`).
145 |
146 | Note that this API Endpoint returns a `Future` (as opposed to `Future`).
147 | Axon returns the identifier of an Aggregate as a result of a Command creating a new Aggregate instance.
148 |
149 | That's it! Once you're done, you should be able to start the application and send messages.
150 | Remember that [Swagger](http://localhost:8080/swagger-ui/) is in place to help with this.
151 | Additionally, if you are on Intellij IDEA you can use the `command-request.http` file to execute some REST operations too.
152 |
153 | Note that the queries are not implemented yet. That's fixed in the next step.
154 |
155 | ### Implement the Projections ###
156 |
157 | Now that the application is able to change state, it would be nice to expose that state.
158 | This is done in projections.
159 |
160 | We need to implement three projections for this application:
161 |
162 | 1. The `ChatMessageProjection` exposes the list of messages for a given chat room.
163 | Implement an `@EventHandler` for the `MessagePostedEvent`.
164 |
165 | Note that the `ChatMessage` Entity expects a timestamp. Axon attaches information to Events, which you can
166 | access in the Event Handlers. Add an extra parameter: `@Timestamp Instant timestamp`. Axon will automatically
167 | inject the timestamp at which the message was originally created. Use `timestamp.toEpochMilli()` to convert it to
168 | milliseconds-since-epoch.
169 |
170 | 2. Implement the `@EventHandler`s for the `RoomParticipantProjection`. This projection keeps track of all the
171 | participants in each chatroom. You will need to implement an `@EventHandler` for each of the Events that describe
172 | a change in the participants of a room...
173 |
174 | 3. The last projection, the `RoomSummaryProjection`, gives us a summary of all the available chat rooms. The summary
175 | contains the name of the room, and the number of participants in it. It's up to you to implement it.
176 |
177 | 4. Implement the `@QueryHandler`s needed to extract data from all the projections. You will need to implement a
178 | `@QueryHandler` for each Query defined in *core api*. So, implement an `AllRoomsQuery` handler in
179 | `RoomSummaryProjection`, a `RoomParticipantsQuery` handler in `RoomParticipantProjection`, and a
180 | `RoomMessagesQuery` handler in `ChatMessageProjection`.
181 |
182 | 5. The projection `ChatMessageProjection` gives us a list of all the messages in a chat rooms. In order to support
183 | a 'Subscription Query' we will need to use the `QueryUpdateEmitter` in the `@EventHandler` to send an update for
184 | each new chat message.
185 |
186 | ### Connect the REST API to the Query Bus ###
187 |
188 | We've got a component that can handle queries now. Now, it's time to allow external components to trigger these
189 | queries. The `QueryController` class defines some API endpoints that should trigger queries to be sent.
190 |
191 | We can use either the `QueryBus` or the `QueryGateway` to send queries. The latter has a friendlier API, so
192 | we've decided to use that one.
193 |
194 | 1. Implement the TODOs in the `QueryController` class to forward Queries to the `QueryBus`. Note that the API
195 | Endpoint methods declare a return type of `Future<...>`. The `QueryGateway.query()` method also returns a `Future`.
196 | 2. Note that `subscribeRoomMessages()` endpoint declares a return type of `Flux<...>`. In this service you can send a
197 | Subscription Query, by using the `QueryGateway#subscriptionQuery(...)` method, instead of a regular query in order
198 | to be notified about new messages sent into the room.
199 |
200 | When you think you're done, give the application a spin and see what happens...
201 | Remember, you can use [Swagger](http://localhost:8080/swagger-ui/)
202 | or the `query-request.http` (if you use Intellij IDEA) to test the new endpoints.
203 |
204 | # Done! Hurrah! #
205 |
--------------------------------------------------------------------------------
/chat-getting-started/command-requests.http:
--------------------------------------------------------------------------------
1 | ### Room Creation ###
2 | ### Create Chat Room with random UUID from CommandController
3 |
4 | POST localhost:8080/rooms
5 | Accept: application/json
6 | Content-Type: application/json
7 |
8 | {
9 | "name": "Axon Chat Room - Random UUID"
10 | }
11 |
12 | ### Create Chat Room with static UUID
13 |
14 | POST localhost:8080/rooms
15 | Accept: application/json
16 | Content-Type: application/json
17 |
18 | {
19 | "roomId": "309378bc-c393-4820-98b6-b45cc68dc8be",
20 | "name": "Axon Chat Room - Static UUID"
21 | }
22 |
23 | ### Joining the ChatRoom ###
24 | ### "Steven" is joining the static ChatRoom
25 |
26 | POST localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
27 | Accept: application/json
28 | Content-Type: application/json
29 |
30 | {
31 | "name": "Steven"
32 | }
33 |
34 | ### "Milan" is joining the static ChatRoom
35 |
36 | POST localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
37 | Accept: application/json
38 | Content-Type: application/json
39 |
40 | {
41 | "name": "Milan"
42 | }
43 |
44 | ### "Yvonne" is joining the static ChatRoom
45 |
46 | POST localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
47 | Accept: application/json
48 | Content-Type: application/json
49 |
50 | {
51 | "name": "Yvonne"
52 | }
53 |
54 | ### "Allard" is joining the static ChatRoom
55 |
56 | POST localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
57 | Accept: application/json
58 | Content-Type: application/json
59 |
60 | {
61 | "name": "Allard"
62 | }
63 |
64 | ### Posting Messages ###
65 | ### "Steven" posts a message in the static ChatRoom
66 |
67 | POST localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/messages
68 | Accept: application/json
69 | Content-Type: application/json
70 |
71 | {
72 | "participant": "Steven",
73 | "message": "I am happy to be your trainer!"
74 | }
75 |
76 | ### "Milan" posts a message in the static ChatRoom
77 |
78 | POST localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/messages
79 | Accept: application/json
80 | Content-Type: application/json
81 |
82 | {
83 | "participant": "Milan",
84 | "message": "I am happy to be your trainer!"
85 | }
86 |
87 | ### Leaving the ChatRoom ###
88 | ### "Allard" leaves to the static ChatRoom
89 |
90 | DELETE localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
91 | Accept: application/json
92 | Content-Type: application/json
93 |
94 | {
95 | "name": "Allard"
96 | }
97 |
98 | ### "Steven" leaves to the static ChatRoom
99 |
100 | DELETE localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
101 | Accept: application/json
102 | Content-Type: application/json
103 |
104 | {
105 | "name": "Steven"
106 | }
107 |
108 | ###
--------------------------------------------------------------------------------
/chat-getting-started/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | chat-getting-started
8 | 0.0.1-SNAPSHOT
9 | jar
10 |
11 | chat-getting-started
12 | AxonIQ Getting Started - Chat application
13 |
14 |
15 | io.axoniq.labs
16 | chat-parent
17 | 0.0.1-SNAPSHOT
18 |
19 |
20 |
21 |
22 | org.axonframework
23 | axon-spring-boot-starter
24 | ${axon.version}
25 |
26 |
27 | io.projectreactor
28 | reactor-core
29 | ${projectreactor.version}
30 |
31 |
32 |
33 | org.springframework.boot
34 | spring-boot-starter-freemarker
35 |
36 |
37 | org.springframework.boot
38 | spring-boot-starter-web
39 |
40 |
41 | org.springframework.boot
42 | spring-boot-starter-data-jpa
43 |
44 |
45 | com.h2database
46 | h2
47 | runtime
48 |
49 |
50 |
51 | org.axonframework
52 | axon-test
53 | ${axon.version}
54 | test
55 |
56 |
57 | org.springframework.boot
58 | spring-boot-starter-test
59 | test
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/chat-getting-started/query-requests.http:
--------------------------------------------------------------------------------
1 | ### Retrieve all rooms present
2 |
3 | GET localhost:8080/rooms
4 | Accept: application/json
5 |
6 | ### Retrieve all participants from the static-ID ChatRoom
7 |
8 | GET localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
9 | Accept: application/json
10 |
11 | ### Retrieve all messages from the static-ID ChatRoom
12 |
13 | GET localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/messages
14 | Accept: application/json
15 |
16 | ### Subscribe to all messages from the static-ID ChatRoom
17 |
18 | GET localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/messages/subscribe
19 | Accept: text/event-stream
20 |
21 | ###
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/ChatGettingStartedApplication.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat;
2 |
3 | import io.swagger.v3.oas.models.OpenAPI;
4 | import io.swagger.v3.oas.models.info.Info;
5 | import org.springframework.boot.SpringApplication;
6 | import org.springframework.boot.autoconfigure.SpringBootApplication;
7 | import org.springframework.context.annotation.Bean;
8 | import org.springframework.context.annotation.Configuration;
9 |
10 | @SpringBootApplication
11 | public class ChatGettingStartedApplication {
12 |
13 | public static void main(String[] args) {
14 | SpringApplication.run(ChatGettingStartedApplication.class, args);
15 | }
16 |
17 | @Configuration
18 | public static class SwaggerConfig {
19 |
20 | @Bean
21 | public OpenAPI cloudOpenAPI() {
22 | return new OpenAPI()
23 | .info(new Info().title("Chat Getting Started")
24 | .description(
25 | "Application for developers that would like to take the first steps with Axon Framework"));
26 | }
27 | }
28 | }
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/commandmodel/ChatRoom.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.commandmodel;
2 |
3 | public class ChatRoom {
4 |
5 | // TODO: This class has just been created to make the test compile. It's missing, well, everything...
6 | }
7 |
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/coreapi/messages.kt:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.coreapi
2 |
3 | import org.axonframework.modelling.command.TargetAggregateIdentifier
4 |
5 | data class CreateRoomCommand(@TargetAggregateIdentifier val roomId: String, val name: String)
6 | data class JoinRoomCommand(@TargetAggregateIdentifier val roomId: String, val participant: String)
7 | data class PostMessageCommand(@TargetAggregateIdentifier val roomId: String, val participant: String, val message: String)
8 | data class LeaveRoomCommand(@TargetAggregateIdentifier val roomId: String, val participant: String)
9 |
10 | data class RoomCreatedEvent(val roomId: String, val name: String)
11 | data class ParticipantJoinedRoomEvent(val roomId: String, val participant: String)
12 | data class MessagePostedEvent(val roomId: String, val participant: String, val message: String)
13 | data class ParticipantLeftRoomEvent(val roomId: String, val participant: String)
14 |
15 | class AllRoomsQuery
16 | data class RoomParticipantsQuery(val roomId: String)
17 | data class RoomMessagesQuery(val roomId: String)
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/query/rooms/messages/ChatMessage.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.messages;
2 |
3 | import javax.persistence.Entity;
4 | import javax.persistence.GeneratedValue;
5 | import javax.persistence.Id;
6 |
7 | @Entity
8 | public class ChatMessage {
9 |
10 | @Id
11 | @GeneratedValue
12 | private Long id;
13 |
14 | private long timestamp;
15 | private String roomId;
16 | private String message;
17 | private String participant;
18 |
19 | public ChatMessage() {
20 | }
21 |
22 | public ChatMessage(String participant, String roomId, String message, long timestamp) {
23 | this.participant = participant;
24 | this.roomId = roomId;
25 | this.message = message;
26 | this.timestamp = timestamp;
27 | }
28 |
29 | public long getTimestamp() {
30 | return timestamp;
31 | }
32 |
33 | public String getMessage() {
34 | return message;
35 | }
36 |
37 | public String getParticipant() {
38 | return participant;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/query/rooms/messages/ChatMessageProjection.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.messages;
2 |
3 | import org.axonframework.queryhandling.QueryUpdateEmitter;
4 | import org.springframework.stereotype.Component;
5 |
6 | @Component
7 | public class ChatMessageProjection {
8 |
9 | private final ChatMessageRepository repository;
10 | private final QueryUpdateEmitter updateEmitter;
11 |
12 | public ChatMessageProjection(ChatMessageRepository repository, QueryUpdateEmitter updateEmitter) {
13 | this.repository = repository;
14 | this.updateEmitter = updateEmitter;
15 | }
16 |
17 | // TODO: Create some event handlers that update this model when necessary.
18 |
19 | // TODO: Create the query handler to read data from this model.
20 |
21 | // TODO: Emit updates when new message arrive to notify subscription query by modifying the event handler.
22 | }
23 |
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/query/rooms/messages/ChatMessageRepository.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.messages;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 |
5 | import java.util.List;
6 |
7 | public interface ChatMessageRepository extends JpaRepository {
8 |
9 | List findAllByRoomIdOrderByTimestamp(String roomId);
10 | }
11 |
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/query/rooms/participants/RoomParticipant.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.participants;
2 |
3 | import javax.persistence.Entity;
4 | import javax.persistence.GeneratedValue;
5 | import javax.persistence.Id;
6 | import javax.persistence.Table;
7 | import javax.persistence.UniqueConstraint;
8 |
9 | @Entity
10 | @Table(uniqueConstraints = @UniqueConstraint(columnNames = {"roomId", "participant"}))
11 | public class RoomParticipant {
12 |
13 | @Id
14 | @GeneratedValue
15 | private Long id;
16 |
17 | private String roomId;
18 | private String participant;
19 |
20 | public RoomParticipant() {
21 | }
22 |
23 | public RoomParticipant(String roomId, String participant) {
24 | this.roomId = roomId;
25 | this.participant = participant;
26 | }
27 |
28 | public String getRoomId() {
29 | return roomId;
30 | }
31 |
32 | public String getParticipant() {
33 | return participant;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/query/rooms/participants/RoomParticipantsProjection.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.participants;
2 |
3 | import org.springframework.stereotype.Component;
4 |
5 | @Component
6 | public class RoomParticipantsProjection {
7 |
8 | private final RoomParticipantsRepository repository;
9 |
10 | public RoomParticipantsProjection(RoomParticipantsRepository repository) {
11 | this.repository = repository;
12 | }
13 |
14 | // TODO: Create some event handlers that update this model when necessary.
15 |
16 | // TODO: Create the query handler to read data from this model.
17 | }
18 |
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/query/rooms/participants/RoomParticipantsRepository.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.participants;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 |
5 | import java.util.List;
6 |
7 | public interface RoomParticipantsRepository extends JpaRepository {
8 |
9 | List findRoomParticipantsByRoomId(String roomId);
10 |
11 | void deleteByParticipantAndRoomId(String participant, String roomId);
12 | }
13 |
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/query/rooms/summary/RoomSummary.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.summary;
2 |
3 | import javax.persistence.Entity;
4 | import javax.persistence.Id;
5 |
6 | @Entity
7 | public class RoomSummary {
8 |
9 | @Id
10 | private String roomId;
11 | private String name;
12 | private int participants;
13 |
14 | public RoomSummary() {
15 | }
16 |
17 | public RoomSummary(String roomId, String name) {
18 | this.roomId = roomId;
19 | this.name = name;
20 | }
21 |
22 | public String getRoomId() {
23 | return roomId;
24 | }
25 |
26 | public String getName() {
27 | return name;
28 | }
29 |
30 | public void addParticipant() {
31 | this.participants++;
32 | }
33 |
34 | public void removeParticipant() {
35 | this.participants--;
36 | }
37 |
38 | public int getParticipants() {
39 | return participants;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/query/rooms/summary/RoomSummaryProjection.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.summary;
2 |
3 | import org.springframework.stereotype.Component;
4 |
5 | @Component
6 | public class RoomSummaryProjection {
7 |
8 | private final RoomSummaryRepository roomSummaryRepository;
9 |
10 | public RoomSummaryProjection(RoomSummaryRepository roomSummaryRepository) {
11 | this.roomSummaryRepository = roomSummaryRepository;
12 | }
13 |
14 | // TODO: Create some event handlers that update this model when necessary.
15 |
16 | // TODO: Create the query handler to read data from this model.
17 | }
18 |
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/query/rooms/summary/RoomSummaryRepository.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.summary;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 |
5 | public interface RoomSummaryRepository extends JpaRepository {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/restapi/CommandController.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.restapi;
2 |
3 | import org.axonframework.commandhandling.gateway.CommandGateway;
4 | import org.springframework.web.bind.annotation.DeleteMapping;
5 | import org.springframework.web.bind.annotation.PathVariable;
6 | import org.springframework.web.bind.annotation.PostMapping;
7 | import org.springframework.web.bind.annotation.RequestBody;
8 | import org.springframework.web.bind.annotation.RestController;
9 |
10 | import java.util.concurrent.Future;
11 | import javax.validation.Valid;
12 | import javax.validation.constraints.NotEmpty;
13 |
14 | @RestController
15 | public class CommandController {
16 |
17 | private final CommandGateway commandGateway;
18 |
19 | public CommandController(@SuppressWarnings("SpringJavaAutowiringInspection") CommandGateway commandGateway) {
20 | this.commandGateway = commandGateway;
21 | }
22 |
23 | @PostMapping("/rooms")
24 | public Future createChatRoom(@RequestBody @Valid Room room) {
25 | // TODO: Send a command for this API call.
26 | throw new UnsupportedOperationException("Not implemented yet");
27 | }
28 |
29 | @PostMapping("/rooms/{roomId}/participants")
30 | public Future joinChatRoom(@PathVariable String roomId, @RequestBody @Valid Participant participant) {
31 | // TODO: Send a command for this API call.
32 | throw new UnsupportedOperationException("Not implemented yet");
33 | }
34 |
35 | @PostMapping("/rooms/{roomId}/messages")
36 | public Future postMessage(@PathVariable String roomId, @RequestBody @Valid PostMessageRequest message) {
37 | // TODO: Send a command for this API call.
38 | throw new UnsupportedOperationException("Not implemented yet");
39 | }
40 |
41 | @DeleteMapping("/rooms/{roomId}/participants")
42 | public Future leaveChatRoom(@PathVariable String roomId, @RequestBody @Valid Participant participant) {
43 | // TODO: Send a command for this API call.
44 | throw new UnsupportedOperationException("Not implemented yet");
45 | }
46 |
47 | public static class PostMessageRequest {
48 |
49 | @NotEmpty
50 | private String participant;
51 | @NotEmpty
52 | private String message;
53 |
54 | public String getParticipant() {
55 | return participant;
56 | }
57 |
58 | public void setParticipant(String participant) {
59 | this.participant = participant;
60 | }
61 |
62 | public String getMessage() {
63 | return message;
64 | }
65 |
66 | public void setMessage(String message) {
67 | this.message = message;
68 | }
69 | }
70 |
71 | public static class Participant {
72 |
73 | @NotEmpty
74 | private String name;
75 |
76 | public String getName() {
77 | return name;
78 | }
79 |
80 | public void setName(String name) {
81 | this.name = name;
82 | }
83 | }
84 |
85 | public static class Room {
86 |
87 | private String roomId;
88 | @NotEmpty
89 | private String name;
90 |
91 | public String getRoomId() {
92 | return roomId;
93 | }
94 |
95 | public void setRoomId(String roomId) {
96 | this.roomId = roomId;
97 | }
98 |
99 | public String getName() {
100 | return name;
101 | }
102 |
103 | public void setName(String name) {
104 | this.name = name;
105 | }
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/chat-getting-started/src/main/java/io/axoniq/labs/chat/restapi/QueryController.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.restapi;
2 |
3 | import io.axoniq.labs.chat.query.rooms.messages.ChatMessage;
4 | import io.axoniq.labs.chat.query.rooms.summary.RoomSummary;
5 | import org.axonframework.queryhandling.QueryGateway;
6 | import org.springframework.http.MediaType;
7 | import org.springframework.web.bind.annotation.GetMapping;
8 | import org.springframework.web.bind.annotation.PathVariable;
9 | import org.springframework.web.bind.annotation.RestController;
10 | import reactor.core.publisher.Flux;
11 |
12 | import java.util.List;
13 | import java.util.concurrent.Future;
14 |
15 | @RestController
16 | public class QueryController {
17 |
18 | private final QueryGateway queryGateway;
19 |
20 | public QueryController(QueryGateway queryGateway) {
21 | this.queryGateway = queryGateway;
22 | }
23 |
24 | @GetMapping("rooms")
25 | public Future> listRooms() {
26 | // TODO: Send a query for this API call.
27 | throw new UnsupportedOperationException("Not implemented yet");
28 | }
29 |
30 | @GetMapping("/rooms/{roomId}/participants")
31 | public Future> participantsInRoom(@PathVariable String roomId) {
32 | // TODO: Send a query for this API call.
33 | throw new UnsupportedOperationException("Not implemented yet");
34 | }
35 |
36 | @GetMapping("/rooms/{roomId}/messages")
37 | public Future> roomMessages(@PathVariable String roomId) {
38 | // TODO: Send a query for this API call.
39 | throw new UnsupportedOperationException("Not implemented yet");
40 | }
41 |
42 | @GetMapping(value = "/rooms/{roomId}/messages/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
43 | public Flux subscribeRoomMessages(@PathVariable String roomId) {
44 | // TODO: Send a subscription query for this API call.
45 | throw new UnsupportedOperationException("Not implemented yet");
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/chat-getting-started/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.h2.console.enabled=true
2 |
--------------------------------------------------------------------------------
/chat-getting-started/src/test/java/io/axoniq/labs/chat/ChatGettingStartedApplicationTests.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat;
2 |
3 | import org.junit.jupiter.api.*;
4 | import org.junit.jupiter.api.extension.*;
5 | import org.springframework.boot.test.context.SpringBootTest;
6 | import org.springframework.test.context.junit.jupiter.SpringExtension;
7 |
8 |
9 | @SpringBootTest
10 | @ExtendWith(SpringExtension.class)
11 | class ChatGettingStartedApplicationTests {
12 |
13 | @Test
14 | void contextLoads() {
15 | }
16 | }
17 |
18 |
--------------------------------------------------------------------------------
/chat-getting-started/src/test/java/io/axoniq/labs/chat/commandmodel/ChatRoomTest.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.commandmodel;
2 |
3 | import io.axoniq.labs.chat.coreapi.CreateRoomCommand;
4 | import io.axoniq.labs.chat.coreapi.JoinRoomCommand;
5 | import io.axoniq.labs.chat.coreapi.LeaveRoomCommand;
6 | import io.axoniq.labs.chat.coreapi.MessagePostedEvent;
7 | import io.axoniq.labs.chat.coreapi.ParticipantJoinedRoomEvent;
8 | import io.axoniq.labs.chat.coreapi.ParticipantLeftRoomEvent;
9 | import io.axoniq.labs.chat.coreapi.PostMessageCommand;
10 | import io.axoniq.labs.chat.coreapi.RoomCreatedEvent;
11 | import org.axonframework.test.aggregate.AggregateTestFixture;
12 | import org.junit.jupiter.api.*;
13 |
14 | class ChatRoomTest {
15 |
16 | private AggregateTestFixture testFixture;
17 |
18 | @BeforeEach
19 | void setUp() {
20 | testFixture = new AggregateTestFixture<>(ChatRoom.class);
21 | }
22 |
23 | @Test
24 | void testCreateChatRoom() {
25 | testFixture.givenNoPriorActivity()
26 | .when(new CreateRoomCommand("roomId", "test-room"))
27 | .expectEvents(new RoomCreatedEvent("roomId", "test-room"));
28 | }
29 |
30 | @Test
31 | void testJoinChatRoom() {
32 | testFixture.given(new RoomCreatedEvent("roomId", "test-room"))
33 | .when(new JoinRoomCommand("roomId", "participant"))
34 | .expectEvents(new ParticipantJoinedRoomEvent("roomId", "participant"));
35 | }
36 |
37 | @Test
38 | void testLeaveChatRoom() {
39 | testFixture.given(new RoomCreatedEvent("roomId", "test-room"),
40 | new ParticipantJoinedRoomEvent("roomId", "participant"))
41 | .when(new LeaveRoomCommand("roomId", "participant"))
42 | .expectSuccessfulHandlerExecution()
43 | .expectEvents(new ParticipantLeftRoomEvent("roomId", "participant"));
44 | }
45 |
46 | @Test
47 | void testPostMessage() {
48 | testFixture.given(new RoomCreatedEvent("roomId", "test-room"),
49 | new ParticipantJoinedRoomEvent("roomId", "participant"))
50 | .when(new PostMessageCommand("roomId", "participant", "Hi there!"))
51 | .expectEvents(new MessagePostedEvent("roomId", "participant", "Hi there!"));
52 | }
53 |
54 | @Test
55 | void testCannotJoinChatRoomTwice() {
56 | testFixture.given(new RoomCreatedEvent("roomId", "test-room"),
57 | new ParticipantJoinedRoomEvent("roomId", "participant"))
58 | .when(new JoinRoomCommand("roomId", "participant"))
59 | .expectSuccessfulHandlerExecution()
60 | .expectNoEvents();
61 | }
62 |
63 | @Test
64 | void testCannotLeaveChatRoomTwice() {
65 | testFixture.given(new RoomCreatedEvent("roomId", "test-room"),
66 | new ParticipantJoinedRoomEvent("roomId", "participant"),
67 | new ParticipantLeftRoomEvent("roomId", "participant"))
68 | .when(new LeaveRoomCommand("roomId", "participant"))
69 | .expectSuccessfulHandlerExecution()
70 | .expectNoEvents();
71 | }
72 |
73 | @Test
74 | void testParticipantCannotPostMessagesOnceHeLeftTheRoom() {
75 | testFixture.given(new RoomCreatedEvent("roomId", "test-room"),
76 | new ParticipantJoinedRoomEvent("roomId", "participant"),
77 | new ParticipantLeftRoomEvent("roomId", "participant"))
78 | .when(new PostMessageCommand("roomId", "participant", "Hi there!"))
79 | .expectException(IllegalStateException.class)
80 | .expectNoEvents();
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/chat-getting-started/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/chat-scaling-out/README.md:
--------------------------------------------------------------------------------
1 | Axon Lab - Scaling out
2 | ======================
3 |
4 | So, you have selected the advanced Lab. Good job!
5 |
6 | Application overview
7 | --------------------
8 |
9 | The main application is called `ChatScalingOutApplication`. It's a Spring Boot application with the following main
10 | dependencies:
11 | - Axon (Spring Boot starter)
12 | - Spring Data JPA
13 | - Freemarker
14 | - Web
15 | - Reactor
16 | - Spring Boot Test
17 | - Axon Test
18 |
19 | Because we will be having multiple instances cooperating on the same database, we can't use an
20 | embedded H2 Database anymore. You can run the `Servers` class to start an H2 database with a
21 | TCP endpoint. The application is configured to connect to this database.
22 |
23 | There are a few test cases. One will check if the application can start, while the others
24 | validate the Aggregate's behavior. They should all pass.
25 |
26 | ### Application layout ###
27 |
28 | The application's logic is divided among a number of packages.
29 |
30 | - `io.axoniq.labs.chat`
31 | The main package. Contains the Application class with the configuration.
32 | - `io.axoniq.labs.chat.commandmodel`
33 | Contains the Command Model. In our case, just the `ChatRoom` Aggregate that has been provided to make the project
34 | compile.
35 | - `io.axoniq.labs.chat.coreapi`
36 | The so called *core api*. This is where we put the Commands, Events and Queries.
37 | Since commands, events and queries are immutable, we have used Kotlin to define them. Kotlin allows you to
38 | concisely define each event, command and query on a single line.
39 | To make sure you don't waste your precious time, we've implemented these Commands, Events and Queries for you.
40 | - `io.axoniq.labs.chat.query.rooms.messages`
41 | Contains the Projections (also called View Model or Query Model) for the Messages that have been broadcast in a
42 | specific room. This package contains both the Event Handlers for updating the Projections,
43 | as well as the Query Handlers to read the data.
44 | - `io.axoniq.labs.chat.query.rooms.participants`
45 | Contains the Projection to serve the list of participants in a given Chat Room.
46 | - `io.axoniq.labs.chat.query.rooms.summary`
47 | Contains the Projection to serve a list of available chat rooms and the number of participants.
48 | - `io.axoniq.labs.chat.restapi`
49 | This is the REST Command and Query API to change and read the application's state.
50 | API calls here are translated into Commands and Queries for the application to process.
51 |
52 | ### Swagger UI ###
53 | The application has 'Swagger' enabled. You can use Swagger to send requests.
54 |
55 | Visit: [http://localhost:8080/swagger-ui/](http://localhost:8080/swagger-ui/)
56 |
57 | Note: The Swagger UI does not support the 'Subscription Query' further on in the assignment,
58 | as Swagger does not support streaming results.
59 | Issuing a regular `curl` operation, or something along those lines, is recommended to check the Subscription Query.
60 |
61 | Note 2: If you are on Intellij IDEA, you can also use the `command-request.http`
62 | and `query-request.http` files in this project to send requests directly from your IDE.
63 | Several defaults have been provided, but feel free to play around here!
64 |
65 | ### H2 Console ###
66 | The application has the H2 Console configured, so you can peek into the database's contents.
67 |
68 | Visit: [http://localhost:8080/h2-console](http://localhost:8080/h2-console)
69 | Enter JDBC URL: jdbc:h2:tcp://localhost:9092/mem:testdb
70 | Leave other values to defaults and click 'connect'
71 |
72 | Our goal
73 | --------
74 |
75 | Obviously, this Chat application is expected to be a massive success, and it needs to be ready to scale to massive
76 | proportions. Therefore, we are going to configure the application to work with multiple nodes efficiently.
77 |
78 | Commands will have to be consistently routed based on the Chat Room ID that they target. Event processing will have to
79 | be distributed as well. We are going to use AxonServer to do this.
80 |
81 | Preparation
82 | -----------
83 |
84 | Axon Framework works best with AxonServer, and in this sample project we assume that you are using it.
85 | AxonServer needs to be downloaded separately.
86 | You can run AxonServer as a docker container by running
87 | ```shell script
88 | docker run -d -p 8024:8024 -p 8124:8124 -p 8224:8224 --name axonserver axoniq/axonserver
89 | ```
90 |
91 | Exercises
92 | ---------
93 |
94 | ### Fire up another instance ###
95 |
96 | Connect a second instance of application to AxonServer. You just need to startup another instance; if you run them locally,
97 | remember to change the server port in `application.properties`, setting the `server.port=9090`.
98 |
99 | Now you can invoke the rest APIs on both instances interchangeably.
100 | Try for example to subscribe for room messages in one instance, and then to post messages from the other.
101 |
102 | ### Parallel Processing of Events ###
103 |
104 | To configure a Tracking Processor for parallel processing:
105 |
106 | 1. We first want to override the Processing Group's name. By default, this name of a processing group (and the processor
107 | that will process events on behalf of it) is the package name of the event handlers that are assigned to it.
108 | The easiest way to override is to put a `@ProcessingGroup` annotation on the `ChatMessageProjection` class. Give it the
109 | value `messages`.
110 | 2. In `application.properties`, configure the `messages` processor initial number of segments to define the maximum number of overall threads:
111 | `axon.eventhandling.processors.messages.initialSegmentCount=4`. (Note that the `messages` part is the name of the processor)
112 | 3. In `application.properties`, also set the maximum number threads to start on this node:
113 | `axon.eventhandling.processors.messages.threadCount=2`.
114 | 4. In `application.properties`, set the processor mode to tracking:
115 | `axon.eventhandling.processors.messages.mode=tracking`. ]
116 |
117 | Restart your applications. Event processing is now occurring in parallel. Check out the "TOKEN_ENTRY" table in the H2
118 | Console to see the token being updated.
119 |
120 | Note:
121 | Remember to restart the `Servers` process to reset the database.
122 | The `initialSegmentCount` property is used only if the segments for that Tracking Processor are not yet defined in
123 | "TOKEN_ENTRY" table.
124 |
125 | ### Off the beaten track (Bonus Exercises) ###
126 |
127 | At the moment, each application is exactly identical. You split the Command from the Query by using Spring Profiles.
128 |
129 | Assign a Profile to the ChatRoom aggregate. By not enabling this profile, the instance will not register any command handlers.
130 |
131 | Do the same (but with a different profile) for the Query components.
132 |
133 | Since it's a bonus exercise, we're not giving too many hints. Play around a bit, and have fun!!
134 |
135 | # Done! Hurrah! #
136 |
--------------------------------------------------------------------------------
/chat-scaling-out/command-requests.http:
--------------------------------------------------------------------------------
1 | ### Room Creation ###
2 | ### Create Chat Room with random UUID from CommandController
3 |
4 | POST localhost:8080/rooms
5 | Accept: application/json
6 | Content-Type: application/json
7 |
8 | {
9 | "name": "Axon Chat Room - Random UUID"
10 | }
11 |
12 | ### Create Chat Room with static UUID
13 |
14 | POST localhost:8080/rooms
15 | Accept: application/json
16 | Content-Type: application/json
17 |
18 | {
19 | "roomId": "309378bc-c393-4820-98b6-b45cc68dc8be",
20 | "name": "Axon Chat Room - Static UUID"
21 | }
22 |
23 | ### Joining the ChatRoom ###
24 | ### "Steven" is joining the static ChatRoom
25 |
26 | POST localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
27 | Accept: application/json
28 | Content-Type: application/json
29 |
30 | {
31 | "name": "Steven"
32 | }
33 |
34 | ### "Milan" is joining the static ChatRoom
35 |
36 | POST localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
37 | Accept: application/json
38 | Content-Type: application/json
39 |
40 | {
41 | "name": "Milan"
42 | }
43 |
44 | ### "Yvonne" is joining the static ChatRoom
45 |
46 | POST localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
47 | Accept: application/json
48 | Content-Type: application/json
49 |
50 | {
51 | "name": "Yvonne"
52 | }
53 |
54 | ### "Allard" is joining the static ChatRoom
55 |
56 | POST localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
57 | Accept: application/json
58 | Content-Type: application/json
59 |
60 | {
61 | "name": "Allard"
62 | }
63 |
64 | ### Posting Messages ###
65 | ### "Steven" posts a message in the static ChatRoom
66 |
67 | POST localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/messages
68 | Accept: application/json
69 | Content-Type: application/json
70 |
71 | {
72 | "participant": "Steven",
73 | "message": "I am happy to be your trainer!"
74 | }
75 |
76 | ### "Milan" posts a message in the static ChatRoom
77 |
78 | POST localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/messages
79 | Accept: application/json
80 | Content-Type: application/json
81 |
82 | {
83 | "participant": "Milan",
84 | "message": "I am happy to be your trainer!"
85 | }
86 |
87 | ### Leaving the ChatRoom ###
88 | ### "Allard" leaves to the static ChatRoom
89 |
90 | DELETE localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
91 | Accept: application/json
92 | Content-Type: application/json
93 |
94 | {
95 | "name": "Allard"
96 | }
97 |
98 | ### "Steven" leaves to the static ChatRoom
99 |
100 | DELETE localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
101 | Accept: application/json
102 | Content-Type: application/json
103 |
104 | {
105 | "name": "Steven"
106 | }
107 |
108 | ###
--------------------------------------------------------------------------------
/chat-scaling-out/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | chat-scaling-out
8 | 0.0.1-SNAPSHOT
9 | jar
10 |
11 | chat-scaling-out
12 | AxonIQ Getting Started - Chat application
13 |
14 |
15 | io.axoniq.labs
16 | chat-parent
17 | 0.0.1-SNAPSHOT
18 |
19 |
20 |
21 | UTF-8
22 | UTF-8
23 | 1.8
24 |
25 |
26 |
27 |
28 | org.axonframework
29 | axon-spring-boot-starter
30 | ${axon.version}
31 |
32 |
33 | io.projectreactor
34 | reactor-core
35 | ${projectreactor.version}
36 |
37 |
38 |
39 | org.springframework.boot
40 | spring-boot-starter-freemarker
41 |
42 |
43 | org.springframework.boot
44 | spring-boot-starter-web
45 |
46 |
47 | org.springframework.boot
48 | spring-boot-starter-data-jpa
49 |
50 |
51 | com.h2database
52 | h2
53 |
54 |
55 |
56 | org.springframework.boot
57 | spring-boot-starter-test
58 | test
59 |
60 |
61 | org.axonframework
62 | axon-test
63 | ${axon.version}
64 | test
65 |
66 |
67 |
68 |
--------------------------------------------------------------------------------
/chat-scaling-out/query-requests.http:
--------------------------------------------------------------------------------
1 | ### Retrieve all rooms present
2 |
3 | GET localhost:8080/rooms
4 | Accept: application/json
5 |
6 | ### Retrieve all participants from the static-ID ChatRoom
7 |
8 | GET localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/participants
9 | Accept: application/json
10 |
11 | ### Retrieve all messages from the static-ID ChatRoom
12 |
13 | GET localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/messages
14 | Accept: application/json
15 |
16 | ### Subscribe to all messages from the static-ID ChatRoom
17 |
18 | GET localhost:8080/rooms/309378bc-c393-4820-98b6-b45cc68dc8be/messages/subscribe
19 | Accept: text/event-stream
20 |
21 | ###
--------------------------------------------------------------------------------
/chat-scaling-out/requests.http:
--------------------------------------------------------------------------------
1 | POST http://localhost:8080/rooms
2 | Content-Type: application/json
3 |
4 | {
5 | "roomId": "56f85087-64fa-456f-bc2c-d009e6462f35",
6 | "name": "room1"
7 | }
8 |
9 | ###
10 |
11 | GET http://localhost:8080/rooms
12 |
13 | ###
14 |
15 | POST http://localhost:8080/rooms/56f85087-64fa-456f-bc2c-d009e6462f35/participants
16 | Content-Type: application/json
17 |
18 | {
19 | "name": "Sara"
20 | }
21 |
22 | ###
23 |
24 | GET http://localhost:8080/rooms/56f85087-64fa-456f-bc2c-d009e6462f35/participants
25 |
26 | ###
27 |
28 | POST http://localhost:8080/rooms/56f85087-64fa-456f-bc2c-d009e6462f35/messages
29 | Content-Type: application/json
30 |
31 | {
32 | "name": "Sara",
33 | "message": "Ciao Marc"
34 | }
35 |
36 | ###
37 |
38 | GET http://localhost:8080/rooms/56f85087-64fa-456f-bc2c-d009e6462f35/messages
39 |
40 | ###
41 |
42 |
43 | POST http://localhost:9090/rooms/56f85087-64fa-456f-bc2c-d009e6462f35/participants
44 | Content-Type: application/json
45 |
46 | {
47 | "name": "Marc"
48 | }
49 |
50 | ###
51 |
52 |
53 | POST http://localhost:9090/rooms/56f85087-64fa-456f-bc2c-d009e6462f35/messages
54 | Content-Type: application/json
55 |
56 | {
57 | "name": "Marc",
58 | "message": "Good afternoon Sara"
59 | }
60 |
61 | ###
62 |
63 | GET http://localhost:9090/rooms/56f85087-64fa-456f-bc2c-d009e6462f35/messages
64 |
65 | ###
66 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/ChatScalingOutApplication.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat;
2 |
3 | import io.swagger.v3.oas.models.OpenAPI;
4 | import io.swagger.v3.oas.models.info.Info;
5 | import org.springframework.boot.SpringApplication;
6 | import org.springframework.boot.autoconfigure.SpringBootApplication;
7 | import org.springframework.context.annotation.Bean;
8 | import org.springframework.context.annotation.Configuration;
9 |
10 | import java.sql.SQLException;
11 |
12 | @SpringBootApplication
13 | public class ChatScalingOutApplication {
14 |
15 | public static void main(String[] args) {
16 | SpringApplication.run(ChatScalingOutApplication.class, args);
17 | }
18 |
19 | @Configuration
20 | public static class SwaggerConfig {
21 |
22 | @Bean
23 | public OpenAPI cloudOpenAPI() {
24 | return new OpenAPI()
25 | .info(new Info().title("Chat Getting Started")
26 | .description(
27 | "Application for developers that would like to take the first steps with Axon Framework"));
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/Servers.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat;
2 |
3 | import org.h2.tools.Server;
4 |
5 | import java.lang.reflect.Constructor;
6 | import java.lang.reflect.Method;
7 |
8 | public class Servers {
9 |
10 | public static void main(String[] args) throws Exception {
11 | Server server = Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", "9092", "-ifNotExists");
12 | server.start();
13 | System.out.println("Database running on port 9092");
14 |
15 | Method startMethod = null;
16 | Object gossipRouter = null;
17 | Method stopMethod = null;
18 | try {
19 | Class> gossipRouterClass = Servers.class.getClassLoader().loadClass("org.jgroups.stack.GossipRouter");
20 | Constructor> constructor = gossipRouterClass.getDeclaredConstructor(String.class, int.class);
21 | gossipRouter = constructor.newInstance("127.0.0.1", 12001);
22 | startMethod = gossipRouterClass.getMethod("start");
23 | stopMethod = gossipRouterClass.getMethod("stop");
24 | } catch (ClassNotFoundException e) {
25 | // Gossip Router not on class path
26 | }
27 |
28 | if (startMethod != null) {
29 | startMethod.invoke(gossipRouter);
30 | System.out.println("Gossip Router started on port 12001");
31 | }
32 |
33 | System.out.println("Press any key to shut down");
34 | System.in.read();
35 |
36 | System.out.println("Stopping database.");
37 | server.stop();
38 |
39 | if (stopMethod != null) {
40 | System.out.println("Stopping Gossip Router");
41 | stopMethod.invoke(gossipRouter);
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/commandmodel/ChatRoom.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.commandmodel;
2 |
3 | import io.axoniq.labs.chat.coreapi.*;
4 | import org.axonframework.commandhandling.CommandHandler;
5 | import org.axonframework.eventsourcing.EventSourcingHandler;
6 | import org.axonframework.modelling.command.AggregateIdentifier;
7 | import org.axonframework.spring.stereotype.Aggregate;
8 | import org.springframework.util.Assert;
9 |
10 | import java.util.HashSet;
11 | import java.util.Set;
12 |
13 | import static org.axonframework.modelling.command.AggregateLifecycle.apply;
14 |
15 | @Aggregate
16 | public class ChatRoom {
17 |
18 | @AggregateIdentifier
19 | private String roomId;
20 | private Set participants;
21 |
22 | public ChatRoom() {
23 | }
24 |
25 | @CommandHandler
26 | public ChatRoom(CreateRoomCommand command) {
27 | apply(new RoomCreatedEvent(command.getRoomId(), command.getName()));
28 | }
29 |
30 | @CommandHandler
31 | public void handle(JoinRoomCommand command) {
32 | if (!participants.contains(command.getParticipant())) {
33 | apply(new ParticipantJoinedRoomEvent(roomId, command.getParticipant()));
34 | }
35 | }
36 |
37 | @CommandHandler
38 | public void handle(LeaveRoomCommand command) {
39 | if (participants.contains(command.getParticipant())) {
40 | apply(new ParticipantLeftRoomEvent(roomId, command.getParticipant()));
41 | }
42 | }
43 |
44 | @CommandHandler
45 | public void handle(PostMessageCommand command) {
46 | Assert.state(participants.contains(command.getParticipant()),
47 | "You cannot post messages unless you've joined the chat room");
48 | apply(new MessagePostedEvent(roomId, command.getParticipant(), command.getMessage()));
49 | }
50 |
51 | @EventSourcingHandler
52 | protected void on(RoomCreatedEvent event) {
53 | this.roomId = event.getRoomId();
54 | this.participants = new HashSet<>();
55 | }
56 |
57 | @EventSourcingHandler
58 | protected void on(ParticipantJoinedRoomEvent event) {
59 | this.participants.add(event.getParticipant());
60 | }
61 |
62 | @EventSourcingHandler
63 | protected void on(ParticipantLeftRoomEvent event) {
64 | this.participants.remove(event.getParticipant());
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/coreapi/messages.kt:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.coreapi
2 |
3 | import org.axonframework.modelling.command.TargetAggregateIdentifier
4 |
5 | data class CreateRoomCommand(@TargetAggregateIdentifier val roomId: String, val name: String)
6 | data class JoinRoomCommand(@TargetAggregateIdentifier val roomId: String, val participant: String)
7 | data class PostMessageCommand(@TargetAggregateIdentifier val roomId: String, val participant: String, val message: String)
8 | data class LeaveRoomCommand(@TargetAggregateIdentifier val roomId: String, val participant: String)
9 |
10 | data class RoomCreatedEvent(val roomId: String, val name: String)
11 | data class ParticipantJoinedRoomEvent(val roomId: String, val participant: String)
12 | data class MessagePostedEvent(val roomId: String, val participant: String, val message: String)
13 | data class ParticipantLeftRoomEvent(val roomId: String, val participant: String)
14 |
15 | class AllRoomsQuery
16 | data class RoomParticipantsQuery(val roomId: String)
17 | data class RoomMessagesQuery(val roomId: String)
18 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/query/rooms/messages/ChatMessage.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.messages;
2 |
3 | import javax.persistence.Entity;
4 | import javax.persistence.GeneratedValue;
5 | import javax.persistence.Id;
6 |
7 | @Entity
8 | public class ChatMessage {
9 |
10 | @Id
11 | @GeneratedValue
12 | private Long id;
13 |
14 | private long timestamp;
15 | private String roomId;
16 | private String message;
17 | private String participant;
18 |
19 | public ChatMessage() {
20 | }
21 |
22 | public ChatMessage(String participant, String roomId, String message, long timestamp) {
23 | this.participant = participant;
24 | this.roomId = roomId;
25 | this.message = message;
26 | this.timestamp = timestamp;
27 | }
28 |
29 | public long getTimestamp() {
30 | return timestamp;
31 | }
32 |
33 | public String getMessage() {
34 | return message;
35 | }
36 |
37 | public String getParticipant() {
38 | return participant;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/query/rooms/messages/ChatMessageProjection.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.messages;
2 |
3 | import io.axoniq.labs.chat.coreapi.MessagePostedEvent;
4 | import io.axoniq.labs.chat.coreapi.RoomMessagesQuery;
5 | import org.axonframework.eventhandling.EventHandler;
6 | import org.axonframework.eventhandling.Timestamp;
7 | import org.axonframework.queryhandling.QueryHandler;
8 | import org.axonframework.queryhandling.QueryUpdateEmitter;
9 | import org.springframework.stereotype.Component;
10 |
11 | import java.time.Instant;
12 | import java.util.List;
13 |
14 | @Component
15 | public class ChatMessageProjection {
16 |
17 | private final ChatMessageRepository repository;
18 | private final QueryUpdateEmitter updateEmitter;
19 |
20 | public ChatMessageProjection(ChatMessageRepository repository, QueryUpdateEmitter updateEmitter) {
21 | this.repository = repository;
22 | this.updateEmitter = updateEmitter;
23 | }
24 |
25 | @QueryHandler
26 | public List handle(RoomMessagesQuery query) {
27 | return repository.findAllByRoomIdOrderByTimestamp(query.getRoomId());
28 | }
29 |
30 | @EventHandler
31 | public void on(MessagePostedEvent event, @Timestamp Instant timestamp) {
32 | ChatMessage chatMessage = new ChatMessage(event.getParticipant(),
33 | event.getRoomId(),
34 | event.getMessage(),
35 | timestamp.toEpochMilli());
36 | repository.save(chatMessage);
37 | updateEmitter.emit(RoomMessagesQuery.class, query -> query.getRoomId().equals(event.getRoomId()), chatMessage);
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/query/rooms/messages/ChatMessageRepository.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.messages;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 |
5 | import java.util.List;
6 |
7 | public interface ChatMessageRepository extends JpaRepository {
8 |
9 | List findAllByRoomIdOrderByTimestamp(String roomId);
10 | }
11 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/query/rooms/participants/RoomParticipant.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.participants;
2 |
3 | import javax.persistence.Entity;
4 | import javax.persistence.GeneratedValue;
5 | import javax.persistence.Id;
6 | import javax.persistence.Table;
7 | import javax.persistence.UniqueConstraint;
8 |
9 | @Entity
10 | @Table(uniqueConstraints = @UniqueConstraint(columnNames = {"roomId", "participant"}))
11 | public class RoomParticipant {
12 |
13 | @Id
14 | @GeneratedValue
15 | private Long id;
16 |
17 | private String roomId;
18 | private String participant;
19 |
20 | public RoomParticipant() {
21 | }
22 |
23 | public RoomParticipant(String roomId, String participant) {
24 | this.roomId = roomId;
25 | this.participant = participant;
26 | }
27 |
28 | public String getRoomId() {
29 | return roomId;
30 | }
31 |
32 | public String getParticipant() {
33 | return participant;
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/query/rooms/participants/RoomParticipantsProjection.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.participants;
2 |
3 | import io.axoniq.labs.chat.coreapi.ParticipantJoinedRoomEvent;
4 | import io.axoniq.labs.chat.coreapi.ParticipantLeftRoomEvent;
5 | import io.axoniq.labs.chat.coreapi.RoomParticipantsQuery;
6 | import org.axonframework.eventhandling.EventHandler;
7 | import org.axonframework.queryhandling.QueryHandler;
8 | import org.springframework.stereotype.Component;
9 |
10 | import java.util.List;
11 |
12 | import static java.util.stream.Collectors.toList;
13 |
14 | @Component
15 | public class RoomParticipantsProjection {
16 |
17 | private final RoomParticipantsRepository repository;
18 |
19 | public RoomParticipantsProjection(RoomParticipantsRepository repository) {
20 | this.repository = repository;
21 | }
22 |
23 | @QueryHandler
24 | public List handle(RoomParticipantsQuery query) {
25 | return repository.findRoomParticipantsByRoomId(query.getRoomId())
26 | .stream()
27 | .map(RoomParticipant::getParticipant).sorted().collect(toList());
28 | }
29 |
30 | @EventHandler
31 | public void on(ParticipantJoinedRoomEvent event) {
32 | repository.save(new RoomParticipant(event.getRoomId(), event.getParticipant()));
33 | }
34 |
35 | @EventHandler
36 | public void on(ParticipantLeftRoomEvent event) {
37 | repository.deleteByParticipantAndRoomId(event.getParticipant(), event.getRoomId());
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/query/rooms/participants/RoomParticipantsRepository.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.participants;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 |
5 | import java.util.List;
6 |
7 | public interface RoomParticipantsRepository extends JpaRepository {
8 |
9 | List findRoomParticipantsByRoomId(String roomId);
10 |
11 | void deleteByParticipantAndRoomId(String participant, String roomId);
12 | }
13 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/query/rooms/summary/RoomSummary.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.summary;
2 |
3 | import javax.persistence.Entity;
4 | import javax.persistence.Id;
5 |
6 | @Entity
7 | public class RoomSummary {
8 |
9 | @Id
10 | private String roomId;
11 | private String name;
12 | private int participants;
13 |
14 | public RoomSummary() {
15 | }
16 |
17 | public RoomSummary(String roomId, String name) {
18 | this.roomId = roomId;
19 | this.name = name;
20 | }
21 |
22 | public String getRoomId() {
23 | return roomId;
24 | }
25 |
26 | public String getName() {
27 | return name;
28 | }
29 |
30 | public void addParticipant() {
31 | this.participants++;
32 | }
33 |
34 | public void removeParticipant() {
35 | this.participants--;
36 | }
37 |
38 | public int getParticipants() {
39 | return participants;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/query/rooms/summary/RoomSummaryProjection.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.summary;
2 |
3 | import io.axoniq.labs.chat.coreapi.AllRoomsQuery;
4 | import io.axoniq.labs.chat.coreapi.ParticipantJoinedRoomEvent;
5 | import io.axoniq.labs.chat.coreapi.ParticipantLeftRoomEvent;
6 | import io.axoniq.labs.chat.coreapi.RoomCreatedEvent;
7 | import org.axonframework.eventhandling.EventHandler;
8 | import org.axonframework.queryhandling.QueryHandler;
9 | import org.springframework.stereotype.Component;
10 |
11 | import java.util.List;
12 |
13 | @Component
14 | public class RoomSummaryProjection {
15 |
16 | private final RoomSummaryRepository roomSummaryRepository;
17 |
18 | public RoomSummaryProjection(RoomSummaryRepository roomSummaryRepository) {
19 | this.roomSummaryRepository = roomSummaryRepository;
20 | }
21 |
22 | @QueryHandler
23 | public List handle(AllRoomsQuery query) {
24 | return roomSummaryRepository.findAll();
25 | }
26 |
27 | @EventHandler
28 | public void on(RoomCreatedEvent event) {
29 | roomSummaryRepository.save(new RoomSummary(event.getRoomId(), event.getName()));
30 | }
31 |
32 | @EventHandler
33 | public void on(ParticipantJoinedRoomEvent event) {
34 | roomSummaryRepository.getOne(event.getRoomId()).addParticipant();
35 | }
36 |
37 | @EventHandler
38 | public void on(ParticipantLeftRoomEvent event) {
39 | roomSummaryRepository.getOne(event.getRoomId()).removeParticipant();
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/query/rooms/summary/RoomSummaryRepository.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.query.rooms.summary;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 |
5 | public interface RoomSummaryRepository extends JpaRepository {
6 |
7 | }
8 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/restapi/CommandController.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.restapi;
2 |
3 | import io.axoniq.labs.chat.coreapi.CreateRoomCommand;
4 | import io.axoniq.labs.chat.coreapi.JoinRoomCommand;
5 | import io.axoniq.labs.chat.coreapi.LeaveRoomCommand;
6 | import io.axoniq.labs.chat.coreapi.PostMessageCommand;
7 | import org.axonframework.commandhandling.gateway.CommandGateway;
8 | import org.springframework.util.Assert;
9 | import org.springframework.web.bind.annotation.DeleteMapping;
10 | import org.springframework.web.bind.annotation.PathVariable;
11 | import org.springframework.web.bind.annotation.PostMapping;
12 | import org.springframework.web.bind.annotation.RequestBody;
13 | import org.springframework.web.bind.annotation.RestController;
14 |
15 | import java.util.UUID;
16 | import java.util.concurrent.Future;
17 | import javax.validation.Valid;
18 | import javax.validation.constraints.NotEmpty;
19 |
20 | @RestController
21 | public class CommandController {
22 |
23 | private final CommandGateway commandGateway;
24 |
25 | public CommandController(CommandGateway commandGateway) {
26 | this.commandGateway = commandGateway;
27 | }
28 |
29 | @PostMapping("/rooms")
30 | public Future createChatRoom(@RequestBody @Valid Room room) {
31 | Assert.notNull(room.getName(), "name is mandatory for a chatroom");
32 | String roomId = room.getRoomId() == null ? UUID.randomUUID().toString() : room.getRoomId();
33 | return commandGateway.send(new CreateRoomCommand(roomId, room.getName()));
34 | }
35 |
36 | @PostMapping("/rooms/{roomId}/participants")
37 | public Future joinChatRoom(@PathVariable String roomId, @RequestBody @Valid Participant participant) {
38 | Assert.notNull(participant.getName(), "name is mandatory for a chatroom");
39 | return commandGateway.send(new JoinRoomCommand(roomId, participant.getName()));
40 | }
41 |
42 | @PostMapping("/rooms/{roomId}/messages")
43 | public Future postMessage(@PathVariable String roomId, @RequestBody @Valid Message message) {
44 | Assert.notNull(message.getName(), "'name' missing - please provide a participant name");
45 | Assert.notNull(message.getMessage(), "'message' missing - please provide a message to post");
46 | return commandGateway.send(new PostMessageCommand(roomId, message.getName(), message.getMessage()));
47 | }
48 |
49 | @DeleteMapping("/rooms/{roomId}/participants")
50 | public Future leaveChatRoom(@PathVariable String roomId, @RequestBody @Valid Participant participant) {
51 | Assert.notNull(participant.getName(), "name is mandatory for a chatroom");
52 | return commandGateway.send(new LeaveRoomCommand(roomId, participant.getName()));
53 | }
54 |
55 | public static class Message {
56 |
57 | @NotEmpty
58 | private String name;
59 | @NotEmpty
60 | private String message;
61 |
62 | public String getName() {
63 | return name;
64 | }
65 |
66 | public void setName(String name) {
67 | this.name = name;
68 | }
69 |
70 | public String getMessage() {
71 | return message;
72 | }
73 |
74 | public void setMessage(String message) {
75 | this.message = message;
76 | }
77 | }
78 |
79 | public static class Participant {
80 |
81 | @NotEmpty
82 | private String name;
83 |
84 | public String getName() {
85 | return name;
86 | }
87 |
88 | public void setName(String name) {
89 | this.name = name;
90 | }
91 | }
92 |
93 | public static class Room {
94 |
95 | private String roomId;
96 | @NotEmpty
97 | private String name;
98 |
99 | public String getRoomId() {
100 | return roomId;
101 | }
102 |
103 | public void setRoomId(String roomId) {
104 | this.roomId = roomId;
105 | }
106 |
107 | public String getName() {
108 | return name;
109 | }
110 |
111 | public void setName(String name) {
112 | this.name = name;
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/java/io/axoniq/labs/chat/restapi/QueryController.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.restapi;
2 |
3 | import io.axoniq.labs.chat.coreapi.AllRoomsQuery;
4 | import io.axoniq.labs.chat.coreapi.RoomMessagesQuery;
5 | import io.axoniq.labs.chat.coreapi.RoomParticipantsQuery;
6 | import io.axoniq.labs.chat.query.rooms.messages.ChatMessage;
7 | import io.axoniq.labs.chat.query.rooms.summary.RoomSummary;
8 | import org.axonframework.messaging.responsetypes.MultipleInstancesResponseType;
9 | import org.axonframework.queryhandling.QueryGateway;
10 | import org.axonframework.queryhandling.SubscriptionQueryResult;
11 | import org.springframework.http.MediaType;
12 | import org.springframework.web.bind.annotation.GetMapping;
13 | import org.springframework.web.bind.annotation.PathVariable;
14 | import org.springframework.web.bind.annotation.RestController;
15 | import reactor.core.publisher.Flux;
16 |
17 | import java.util.List;
18 | import java.util.concurrent.Future;
19 |
20 | import static org.axonframework.messaging.responsetypes.ResponseTypes.instanceOf;
21 | import static org.axonframework.messaging.responsetypes.ResponseTypes.multipleInstancesOf;
22 |
23 |
24 | @RestController
25 | public class QueryController {
26 |
27 | private final QueryGateway queryGateway;
28 |
29 | public QueryController(QueryGateway queryGateway) {
30 | this.queryGateway = queryGateway;
31 | }
32 |
33 | @GetMapping("rooms")
34 | public Future> listRooms() {
35 | return queryGateway.query(new AllRoomsQuery(), new MultipleInstancesResponseType<>(RoomSummary.class));
36 | }
37 |
38 | @GetMapping("/rooms/{roomId}/participants")
39 | public Future> participantsInRoom(@PathVariable String roomId) {
40 | return queryGateway.query(new RoomParticipantsQuery(roomId), new MultipleInstancesResponseType<>(String.class));
41 | }
42 |
43 | @GetMapping("/rooms/{roomId}/messages")
44 | public Future> roomMessages(@PathVariable String roomId) {
45 | return queryGateway.query(new RoomMessagesQuery(roomId), new MultipleInstancesResponseType<>(ChatMessage.class));
46 | }
47 |
48 | @GetMapping(value = "/rooms/{roomId}/messages/subscribe", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
49 | public Flux subscribeRoomMessages(@PathVariable String roomId) {
50 | RoomMessagesQuery query = new RoomMessagesQuery(roomId);
51 | SubscriptionQueryResult, ChatMessage> result;
52 | result = queryGateway.subscriptionQuery(
53 | query, multipleInstancesOf(ChatMessage.class), instanceOf(ChatMessage.class)
54 | );
55 | /* If you only want to send new messages to the client, you could simply do:
56 | return result.updates();
57 | However, in our implementation we want to provide both existing messages and new ones,
58 | so we combine the initial result and the updates in a single flux. */
59 | Flux initialResult = result.initialResult().flatMapMany(Flux::fromIterable);
60 | return Flux.concat(initialResult, result.updates());
61 | }
62 | }
63 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.datasource.url=jdbc:h2:mem:testdb
2 | spring.datasource.driverClassName=org.h2.Driver
3 | spring.datasource.username=sa
4 | spring.datasource.password=
5 | spring.jpa.hibernate.ddl-auto=update
6 |
7 | spring.h2.console.enabled=true
8 | server.port=9000
9 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/test/java/io/axoniq/labs/chat/ChatScalingOutApplicationTests.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat;
2 |
3 | import org.h2.tools.Server;
4 | import org.junit.jupiter.api.*;
5 | import org.junit.jupiter.api.extension.*;
6 | import org.springframework.boot.test.context.SpringBootTest;
7 | import org.springframework.test.context.junit.jupiter.SpringExtension;
8 |
9 | @SpringBootTest
10 | @ExtendWith(SpringExtension.class)
11 | class ChatScalingOutApplicationTests {
12 |
13 | private static Server server;
14 |
15 | @BeforeAll
16 | static void beforeClass() throws Exception {
17 | server = Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", "9092");
18 | server.start();
19 | }
20 |
21 | @AfterAll
22 | static void afterClass() {
23 | if (server != null) {
24 | server.stop();
25 | }
26 | }
27 |
28 | @Test
29 | void contextLoads() {
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/test/java/io/axoniq/labs/chat/commandmodel/ChatRoomTest.java:
--------------------------------------------------------------------------------
1 | package io.axoniq.labs.chat.commandmodel;
2 |
3 | import io.axoniq.labs.chat.coreapi.CreateRoomCommand;
4 | import io.axoniq.labs.chat.coreapi.JoinRoomCommand;
5 | import io.axoniq.labs.chat.coreapi.LeaveRoomCommand;
6 | import io.axoniq.labs.chat.coreapi.MessagePostedEvent;
7 | import io.axoniq.labs.chat.coreapi.ParticipantJoinedRoomEvent;
8 | import io.axoniq.labs.chat.coreapi.ParticipantLeftRoomEvent;
9 | import io.axoniq.labs.chat.coreapi.PostMessageCommand;
10 | import io.axoniq.labs.chat.coreapi.RoomCreatedEvent;
11 | import org.axonframework.test.aggregate.AggregateTestFixture;
12 | import org.junit.jupiter.api.*;
13 |
14 | class ChatRoomTest {
15 |
16 | private AggregateTestFixture testFixture;
17 |
18 | @BeforeEach
19 | void setUp() {
20 | testFixture = new AggregateTestFixture<>(ChatRoom.class);
21 | }
22 |
23 | @Test
24 | void testCreateChatRoom() {
25 | testFixture.givenNoPriorActivity()
26 | .when(new CreateRoomCommand("roomId", "test-room"))
27 | .expectEvents(new RoomCreatedEvent("roomId", "test-room"));
28 | }
29 |
30 | @Test
31 | void testJoinChatRoom() {
32 | testFixture.given(new RoomCreatedEvent("roomId", "test-room"))
33 | .when(new JoinRoomCommand("roomId", "participant"))
34 | .expectEvents(new ParticipantJoinedRoomEvent("roomId", "participant"));
35 | }
36 |
37 | @Test
38 | void testPostMessage() {
39 | testFixture.given(new RoomCreatedEvent("roomId", "test-room"),
40 | new ParticipantJoinedRoomEvent("roomId", "participant"))
41 | .when(new PostMessageCommand("roomId", "participant", "Hi there!"))
42 | .expectEvents(new MessagePostedEvent("roomId", "participant", "Hi there!"));
43 | }
44 |
45 | @Test
46 | void testCannotJoinChatRoomTwice() {
47 | testFixture.given(new RoomCreatedEvent("roomId", "test-room"),
48 | new ParticipantJoinedRoomEvent("roomId", "participant"))
49 | .when(new JoinRoomCommand("roomId", "participant"))
50 | .expectSuccessfulHandlerExecution()
51 | .expectNoEvents();
52 | }
53 |
54 | @Test
55 | void testCannotLeaveChatRoomTwice() {
56 | testFixture.given(new RoomCreatedEvent("roomId", "test-room"),
57 | new ParticipantJoinedRoomEvent("roomId", "participant"),
58 | new ParticipantLeftRoomEvent("roomId", "participant"))
59 | .when(new LeaveRoomCommand("roomId", "participant"))
60 | .expectSuccessfulHandlerExecution()
61 | .expectNoEvents();
62 | }
63 |
64 | @Test
65 | void testParticipantCannotPostMessagesOnceHeLeftTheRoom() {
66 | testFixture.given(new RoomCreatedEvent("roomId", "test-room"),
67 | new ParticipantJoinedRoomEvent("roomId", "participant"),
68 | new ParticipantLeftRoomEvent("roomId", "participant"))
69 | .when(new PostMessageCommand("roomId", "participant", "Hi there!"))
70 | .expectException(IllegalStateException.class)
71 | .expectNoEvents();
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/chat-scaling-out/src/test/resources/logback-test.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/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 | # http://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 | # Maven2 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 Migwn, 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 | # TODO classpath?
118 | fi
119 |
120 | if [ -z "$JAVA_HOME" ]; then
121 | javaExecutable="`which javac`"
122 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
123 | # readlink(1) is not available as standard on Solaris 10.
124 | readLink=`which readlink`
125 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
126 | if $darwin ; then
127 | javaHome="`dirname \"$javaExecutable\"`"
128 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
129 | else
130 | javaExecutable="`readlink -f \"$javaExecutable\"`"
131 | fi
132 | javaHome="`dirname \"$javaExecutable\"`"
133 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
134 | JAVA_HOME="$javaHome"
135 | export JAVA_HOME
136 | fi
137 | fi
138 | fi
139 |
140 | if [ -z "$JAVACMD" ] ; then
141 | if [ -n "$JAVA_HOME" ] ; then
142 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
143 | # IBM's JDK on AIX uses strange locations for the executables
144 | JAVACMD="$JAVA_HOME/jre/sh/java"
145 | else
146 | JAVACMD="$JAVA_HOME/bin/java"
147 | fi
148 | else
149 | JAVACMD="`which java`"
150 | fi
151 | fi
152 |
153 | if [ ! -x "$JAVACMD" ] ; then
154 | echo "Error: JAVA_HOME is not defined correctly." >&2
155 | echo " We cannot execute $JAVACMD" >&2
156 | exit 1
157 | fi
158 |
159 | if [ -z "$JAVA_HOME" ] ; then
160 | echo "Warning: JAVA_HOME environment variable is not set."
161 | fi
162 |
163 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
164 |
165 | # traverses directory structure from process work directory to filesystem root
166 | # first directory with .mvn subdirectory is considered project base directory
167 | find_maven_basedir() {
168 |
169 | if [ -z "$1" ]
170 | then
171 | echo "Path not specified to find_maven_basedir"
172 | return 1
173 | fi
174 |
175 | basedir="$1"
176 | wdir="$1"
177 | while [ "$wdir" != '/' ] ; do
178 | if [ -d "$wdir"/.mvn ] ; then
179 | basedir=$wdir
180 | break
181 | fi
182 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
183 | if [ -d "${wdir}" ]; then
184 | wdir=`cd "$wdir/.."; pwd`
185 | fi
186 | # end of workaround
187 | done
188 | echo "${basedir}"
189 | }
190 |
191 | # concatenates all lines of a file
192 | concat_lines() {
193 | if [ -f "$1" ]; then
194 | echo "$(tr -s '\n' ' ' < "$1")"
195 | fi
196 | }
197 |
198 | BASE_DIR=`find_maven_basedir "$(pwd)"`
199 | if [ -z "$BASE_DIR" ]; then
200 | exit 1;
201 | fi
202 |
203 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
204 | echo $MAVEN_PROJECTBASEDIR
205 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
206 |
207 | # For Cygwin, switch paths to Windows format before running java
208 | if $cygwin; then
209 | [ -n "$M2_HOME" ] &&
210 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
211 | [ -n "$JAVA_HOME" ] &&
212 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
213 | [ -n "$CLASSPATH" ] &&
214 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
215 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
216 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
217 | fi
218 |
219 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
220 |
221 | exec "$JAVACMD" \
222 | $MAVEN_OPTS \
223 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
224 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
225 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
226 |
--------------------------------------------------------------------------------
/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 http://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 Maven2 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 key stroke 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 enable echoing my setting MAVEN_BATCH_ECHO to 'on'
39 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
40 |
41 | @REM set %HOME% to equivalent of $HOME
42 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
43 |
44 | @REM Execute a user defined script before this one
45 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
46 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
47 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
48 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
49 | :skipRcPre
50 |
51 | @setlocal
52 |
53 | set ERROR_CODE=0
54 |
55 | @REM To isolate internal variables from possible post scripts, we use another setlocal
56 | @setlocal
57 |
58 | @REM ==== START VALIDATION ====
59 | if not "%JAVA_HOME%" == "" goto OkJHome
60 |
61 | echo.
62 | echo Error: JAVA_HOME not found in your environment. >&2
63 | echo Please set the JAVA_HOME variable in your environment to match the >&2
64 | echo location of your Java installation. >&2
65 | echo.
66 | goto error
67 |
68 | :OkJHome
69 | if exist "%JAVA_HOME%\bin\java.exe" goto init
70 |
71 | echo.
72 | echo Error: JAVA_HOME is set to an invalid directory. >&2
73 | echo JAVA_HOME = "%JAVA_HOME%" >&2
74 | echo Please set the JAVA_HOME variable in your environment to match the >&2
75 | echo location of your Java installation. >&2
76 | echo.
77 | goto error
78 |
79 | @REM ==== END VALIDATION ====
80 |
81 | :init
82 |
83 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
84 | @REM Fallback to current working directory if not found.
85 |
86 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
87 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
88 |
89 | set EXEC_DIR=%CD%
90 | set WDIR=%EXEC_DIR%
91 | :findBaseDir
92 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
93 | cd ..
94 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
95 | set WDIR=%CD%
96 | goto findBaseDir
97 |
98 | :baseDirFound
99 | set MAVEN_PROJECTBASEDIR=%WDIR%
100 | cd "%EXEC_DIR%"
101 | goto endDetectBaseDir
102 |
103 | :baseDirNotFound
104 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
105 | cd "%EXEC_DIR%"
106 |
107 | :endDetectBaseDir
108 |
109 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
110 |
111 | @setlocal EnableExtensions EnableDelayedExpansion
112 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
113 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
114 |
115 | :endReadAdditionalConfig
116 |
117 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
118 |
119 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
120 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
121 |
122 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
123 | if ERRORLEVEL 1 goto error
124 | goto end
125 |
126 | :error
127 | set ERROR_CODE=1
128 |
129 | :end
130 | @endlocal & set ERROR_CODE=%ERROR_CODE%
131 |
132 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
133 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
134 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
135 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
136 | :skipRcPost
137 |
138 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
139 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
140 |
141 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
142 |
143 | exit /B %ERROR_CODE%
144 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | io.axoniq.labs
8 | chat-parent
9 | 0.0.1-SNAPSHOT
10 | pom
11 |
12 | chat
13 | AxonIQ Getting Started - Chat application
14 |
15 |
16 | org.springframework.boot
17 | spring-boot-starter-parent
18 | 2.7.4
19 |
20 |
21 |
22 |
23 | chat-getting-started
24 | chat-scaling-out
25 |
26 |
27 |
28 | UTF-8
29 | UTF-8
30 |
31 | 1.8
32 | 1.7.20
33 |
34 | 4.6.1
35 |
36 | 3.4.24
37 | 1.6.12
38 |
39 |
40 |
41 |
42 | org.springframework.boot
43 | spring-boot-starter-validation
44 |
45 |
46 | org.jetbrains.kotlin
47 | kotlin-stdlib-jdk8
48 | ${kotlin.version}
49 |
50 |
51 | org.springdoc
52 | springdoc-openapi-ui
53 | ${springdoc.version}
54 |
55 |
56 |
57 |
58 |
59 |
60 | org.springframework.boot
61 | spring-boot-maven-plugin
62 |
63 |
64 |
65 | org.jetbrains.kotlin
66 | kotlin-maven-plugin
67 | ${kotlin.version}
68 |
69 |
70 | compile
71 | compile
72 |
73 | compile
74 |
75 |
76 |
77 | ${project.basedir}/src/main/java
78 |
79 |
80 |
81 |
82 | test-compile
83 | test-compile
84 |
85 | test-compile
86 |
87 |
88 |
89 | ${project.basedir}/src/test/java
90 |
91 |
92 |
93 |
94 |
95 | 1.8
96 |
97 |
98 |
99 |
100 | org.apache.maven.plugins
101 | maven-compiler-plugin
102 | 3.10.1
103 |
104 |
105 |
106 | default-compile
107 | none
108 |
109 |
110 |
111 | default-testCompile
112 | none
113 |
114 |
115 | java-compile
116 | compile
117 |
118 | compile
119 |
120 |
121 |
122 | java-test-compile
123 | test-compile
124 |
125 | testCompile
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
--------------------------------------------------------------------------------