├── .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 | --------------------------------------------------------------------------------