├── .gitignore ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ └── maven-wrapper.properties ├── LICENSE ├── README.md ├── api-gateway ├── .gitignore ├── .mvn │ └── wrapper │ │ ├── MavenWrapperDownloader.java │ │ ├── maven-wrapper.jar │ │ └── maven-wrapper.properties ├── Dockerfile ├── mvnw ├── mvnw.cmd ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── mz │ │ │ └── apigateway │ │ │ ├── ApiGatewayApplication.java │ │ │ └── RouteConfiguration.java │ └── resources │ │ └── application.yml │ └── test │ └── java │ └── com │ └── mz │ └── apigateway │ └── ApiGatewayApplicationTests.java ├── common-api ├── pom.xml └── src │ └── main │ └── java │ └── com.mz.reactivedemo.common.api │ └── events │ ├── Command.java │ ├── Document.java │ ├── DomainEvent.java │ └── Event.java ├── common-persistence ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── mz │ │ └── reactivedemo │ │ └── adapter │ │ └── persistance │ │ ├── AggregateFactory.java │ │ ├── AggregatePersistenceConfiguration.java │ │ ├── AggregateRepository.java │ │ ├── AggregateService.java │ │ ├── actor │ │ ├── AggregatePersistenceActor.java │ │ ├── RecoveryActor.java │ │ └── RepositoryActor.java │ │ ├── document │ │ ├── DocumentReadOnlyRepository.java │ │ └── impl │ │ │ └── DocumentReadOnlyRepositoryImpl.java │ │ └── impl │ │ ├── AggregateFactoryImpl.java │ │ ├── AggregateRepositoryImpl.java │ │ └── AggregateServiceImpl.java │ └── test │ └── java │ └── com │ └── mz │ └── reactivedemo │ └── adapter │ └── persistance │ └── actor │ └── RecoveryActorTest.java ├── common ├── pom.xml └── src │ ├── main │ └── java │ │ └── com │ │ └── mz │ │ └── reactivedemo │ │ └── common │ │ ├── CommandResult.java │ │ ├── CommandResultState.java │ │ ├── KafkaMapper.java │ │ ├── ValidateResult.java │ │ ├── aggregate │ │ ├── AbstractRootAggregate.java │ │ ├── Aggregate.java │ │ ├── AggregateStatus.java │ │ ├── Id.java │ │ └── StringValue.java │ │ ├── http │ │ ├── ErrorMessage.java │ │ ├── HttpErrorHandler.java │ │ └── HttpHandler.java │ │ └── util │ │ ├── AbstractMatch.java │ │ ├── CaseMatch.java │ │ ├── Logger.java │ │ ├── Match.java │ │ └── Try.java │ └── test │ └── java │ └── com │ └── mz │ └── reactivedemo │ └── common │ └── util │ ├── CaseMatchTest.java │ ├── MatchTest.java │ └── TryTest.java ├── docker ├── create-kafka-topics.sh ├── docker-compose-api-gateway.yml ├── docker-compose-shortener-service.yml ├── docker-compose-statistic-service.yml ├── docker-compose-user-service.yml ├── docker-compose.sh └── docker-compose.yml ├── mvnw ├── mvnw.cmd ├── pom.xml ├── shortener-api ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── mz │ └── reactivedemo │ └── shortener │ └── api │ ├── command │ ├── CreateShortener.java │ └── UpdateShortener.java │ ├── dto │ └── ShortenerDto.java │ ├── event │ ├── ShortenerChangedEvent.java │ ├── ShortenerEvent.java │ ├── ShortenerEventType.java │ ├── ShortenerPayload.java │ └── ShortenerViewed.java │ └── topics │ └── ShortenerTopics.java ├── shortener-impl ├── Dockerfile ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── mz │ │ │ └── reactivedemo │ │ │ └── shortener │ │ │ ├── ShortenerApplication.java │ │ │ ├── ShortenerConfiguration.java │ │ │ ├── ShortenerHandler.java │ │ │ ├── ShortenerMapper.java │ │ │ ├── ShortenerRepository.java │ │ │ ├── ShortenerService.java │ │ │ ├── adapter │ │ │ └── user │ │ │ │ └── UserAdapterImpl.java │ │ │ ├── domain │ │ │ ├── aggregate │ │ │ │ ├── ShortUrl.java │ │ │ │ ├── ShortenerAggregate.java │ │ │ │ └── Url.java │ │ │ └── event │ │ │ │ ├── ShortenerChanged.java │ │ │ │ ├── ShortenerCreated.java │ │ │ │ └── ShortenerUpdated.java │ │ │ ├── impl │ │ │ ├── ShortenerApplicationServiceImpl.java │ │ │ └── ShortenerFunctions.java │ │ │ ├── port │ │ │ └── kafka │ │ │ │ └── ShortenerProcessor.java │ │ │ ├── streams │ │ │ ├── ApplicationMessageBus.java │ │ │ └── ApplicationMessageBusImpl.java │ │ │ └── view │ │ │ ├── ShortenerDocument.java │ │ │ ├── ShortenerQuery.java │ │ │ └── impl │ │ │ └── ShortenerQueryImpl.java │ └── resources │ │ ├── application.conf │ │ └── application.yaml │ └── test │ └── java │ └── com │ └── mz │ └── reactivedemo │ └── shortener │ ├── ShortenerHandlerTest.java │ ├── ShortenerMapperTest.java │ ├── aggregate │ └── IdValueTest.java │ ├── domain │ └── aggregate │ │ └── ShortenerAggregateTest.java │ └── impl │ ├── ShortenerQueryImplTest.java │ └── ShortenerServiceImplTest.java ├── statistic-impl ├── Dockerfile ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── mz │ │ │ └── statistic │ │ │ ├── StatisticApplication.java │ │ │ ├── StatisticConfiguration.java │ │ │ ├── StatisticHandler.java │ │ │ ├── StatisticRepository.java │ │ │ ├── StatisticService.java │ │ │ ├── adapters │ │ │ ├── shortener │ │ │ │ ├── ShortenerSubscriber.java │ │ │ │ └── impl │ │ │ │ │ └── ShortenerSubscriberImpl.java │ │ │ └── user │ │ │ │ ├── UserSubscriber.java │ │ │ │ └── impl │ │ │ │ └── UserSubscriberImpl.java │ │ │ ├── impl │ │ │ └── StatisticServiceImpl.java │ │ │ └── model │ │ │ ├── EventType.java │ │ │ └── StatisticDocument.java │ └── resources │ │ └── application.yaml │ └── test │ └── java │ └── com │ └── mz │ └── statistic │ ├── StatisticHandlerTest.java │ └── impl │ └── StatisticServiceImplTest.java ├── user-api ├── pom.xml └── src │ └── main │ └── java │ └── com │ └── mz │ └── user │ ├── dto │ ├── BasicDto.java │ ├── ContactInfoDto.java │ └── UserDto.java │ ├── message │ ├── ContactInfoPayload.java │ ├── UserPayload.java │ ├── command │ │ ├── CreateContactInfo.java │ │ └── CreateUser.java │ └── event │ │ ├── UserChangedEvent.java │ │ └── UserEventType.java │ └── topics │ └── UserTopics.java └── user-impl ├── Dockerfile ├── pom.xml └── src ├── main ├── java │ └── com │ │ └── mz │ │ └── user │ │ ├── UserApplication.java │ │ ├── UserApplicationConfiguration.java │ │ ├── UserApplicationMessageBus.java │ │ ├── UserApplicationService.java │ │ ├── UserHandler.java │ │ ├── UserMapper.java │ │ ├── adapter │ │ └── shortener │ │ │ ├── ShortenerAdapter.java │ │ │ └── impl │ │ │ └── ShortenerAdapterImpl.java │ │ ├── domain │ │ ├── aggregate │ │ │ ├── ContactInfo.java │ │ │ ├── Email.java │ │ │ ├── FirstName.java │ │ │ ├── LastName.java │ │ │ ├── PhoneNumber.java │ │ │ └── UserAggregate.java │ │ ├── command │ │ │ └── AddShortener.java │ │ └── event │ │ │ ├── ContactInfoCreated.java │ │ │ ├── ShortenerAdded.java │ │ │ └── UserCreated.java │ │ ├── impl │ │ ├── UserApplicationMessageBusImpl.java │ │ ├── UserApplicationServiceImpl.java │ │ └── UserFunctions.java │ │ ├── port │ │ ├── kafka │ │ │ └── UserProcessor.java │ │ └── rest │ │ │ └── CreateContactInfoRequest.java │ │ └── view │ │ ├── ContactInfoDocument.java │ │ ├── UserDocument.java │ │ ├── UserQuery.java │ │ ├── UserRepository.java │ │ └── impl │ │ └── UserQueryImpl.java └── resources │ ├── application.conf │ └── application.yaml └── test └── java └── com └── mz └── user ├── UserHandlerTest.java ├── domain └── aggregate │ ├── EmailTest.java │ ├── PhoneNumberTest.java │ └── UserAggregateTest.java ├── impl └── UserAggregateServiceImplTest.java └── port └── kafka └── UserProcessorTest.java /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | **/target/ 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | /nbproject/private/ 22 | /build/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michalzeman/spring-reactive-microservices/3cefbfe631b295b010a881ae8e2a4a045fc08067/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.3/apache-maven-3.5.3-bin.zip 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Copyright 2023 Michal Zeman, zeman.michal@yahoo.com 2 | 3 | Licensed under the Creative Commons Attribution (CC BY) license. You are free to share, copy, distribute, 4 | and adapt this work, provided you give appropriate credit to the original author Michal Zeman, zeman.michal@yahoo.com. 5 | 6 | To view a copy of the license, visit https://creativecommons.org/licenses/by/4.0/ 7 | 8 | # spring-reactive-microservices 9 | 10 | This demo is based on: 11 | - Spring boot 12 | - Spring Webflux 13 | - Apache Kafka 14 | - Akka persistence for event sourcing support 15 | 16 | There are services: 17 | 18 | - Api-gateway 19 | - Statistic MS 20 | - Shortener MS 21 | - User MS 22 | 23 | ## Shortener MS 24 | - responsible by creating of short url 25 | - when user is loading some url based on some key representing shortened url, MS is generating event ShortenerViewed and publishing it into the Kafka topic "shortener-viewed" 26 | - this MS is designed or trying to be designed in DDD style and is representing Shortener Bounded Context. 27 | - Changes of Shortener aggregate like it was created, updated ... are captured by ShortenerChangedEvent and published into the Kafka topic "shortener-changed" 28 | - Result of changes done on aggregate is also published as a document event into the Kafka topic "shortener-document". 29 | This document event representing current state of Shortener aggregate, and it could be used for others services for 30 | building of local views in order to avoid direct communication between service. This document event is also possible to use for creating of views e.g. consumed by ElasticSearch. Maybe it would be implemented later like CQRS. 31 | 32 | ## Statistic MS 33 | - this MS is downstream of Shortener MS 34 | - responsible by calculation of some static related with Shortener MS 35 | - consumer of "shortener-viewed" Kafka topic 36 | - providing of API for VIEWS like how many times was displayed particular URL 37 | 38 | ## User MS 39 | - TBD not ready 40 | 41 | # How to run microservices locally 42 | ## Requirements 43 | - installed Docker 44 | - Java SDK 11 45 | - Insomnia for the REST API collection 46 | - CURL 47 | ## Build and run locally 48 | ### Run it as Dockerized services 49 | 1) From to root directory, execute the build with skipped tests 50 | ``` 51 | ./mvnw clean install -DskipTests 52 | ``` 53 | - this will start all services as docker containers 54 | 2) From to `./docker` dir. execute 55 | ``` 56 | docker-compose -f docker-compose.yml -f docker-compose-api-gateway.yml -f docker-compose-shortener-service.yml -f docker-compose-statistic-service.yml -f docker-compose-user-service.yml up -d 57 | ``` 58 | 3) For the verification of running services, use CURL commands 59 | - Create user: 60 | ``` 61 | curl --request POST \ 62 | --url http://localhost:8080/users \ 63 | --header 'Content-Type: application/json' \ 64 | --data '{ 65 | "firstName": "FirstNameTest", 66 | "lastName": "LastNameTest" 67 | }' 68 | ``` 69 | - this will verify the running User MS 70 | - List all users 71 | ``` 72 | curl --request GET \ 73 | --url http://localhost:8080/users/ 74 | ``` 75 | - Add contact information to the created user 76 | ``` 77 | curl --request PUT \ 78 | --url http://localhost:8080/users/[user_id]/contactinformation \ 79 | --header 'Content-Type: application/json' \ 80 | --data '{ 81 | "email": "test@email.com", 82 | "phoneNumber": "+421999009001" 83 | }' 84 | ``` 85 | - Get details of the created user 86 | ``` 87 | curl --request GET \ 88 | --url http://localhost:8080/users/[user_id] 89 | ``` 90 | - List all events published by User MS into the Kafka topic and consumed by Statistic MS 91 | ``` 92 | curl --request GET \ 93 | --url http://localhost:8080/statistics 94 | ``` 95 | - Create a shortener, this will test Shortener MS 96 | ``` 97 | curl --request POST \ 98 | --url http://localhost:8080/shorteners/ \ 99 | --header 'Content-Type: application/json' \ 100 | --data '{ 101 | "url":"https://www.reactivemanifesto.org", 102 | "userId": "a6ed70a3-8f55-4513-acd0-96768a0e0e0c" 103 | } 104 | ' 105 | ``` 106 | - where "userId": "a6ed70a3-8f55-4513-acd0-96768a0e0e0c" is user id of some created user, it will be different from case to case 107 | - Test created shortener link 108 | - into the browser, enter the link `http://localhost:8080/shorteners/map/${shortener-key}` 109 | - placeholder `${shortener-key}` is an attribute you have received as a response body from the creation e.g. `"key": "2be71aba-4ff5-45c5-a3bb-7287a69e7e9d",` 110 | 111 | You can check all events related to any action or changes done in the microservices ` user MS` and `Shortener MS` via `Statistic MS` with the following command: 112 | ``` 113 | curl --request GET \ 114 | --url http://localhost:8080/statistics 115 | ``` 116 | 117 | ### Run necessary Docker infrastructure for the local development 118 | - from the `docker` directory, execute the following command to run Apache Kafka and other services: 119 | ``` 120 | docker compose -f docker-compose.yml up -d 121 | ``` 122 | - Then, you can build the project with unit tests: 123 | ``` 124 | ./mvnw clean install 125 | ``` -------------------------------------------------------------------------------- /api-gateway/.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | /target/ 3 | !.mvn/wrapper/maven-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | 20 | ### NetBeans ### 21 | /nbproject/private/ 22 | /nbbuild/ 23 | /dist/ 24 | /nbdist/ 25 | /.nb-gradle/ 26 | /build/ 27 | -------------------------------------------------------------------------------- /api-gateway/.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | Licensed to the Apache Software Foundation (ASF) under one 3 | or more contributor license agreements. See the NOTICE file 4 | distributed with this work for additional information 5 | regarding copyright ownership. The ASF licenses this file 6 | to you under the Apache License, Version 2.0 (the 7 | "License"); you may not use this file except in compliance 8 | with the License. You may obtain a copy of the License at 9 | 10 | http://www.apache.org/licenses/LICENSE-2.0 11 | 12 | Unless required by applicable law or agreed to in writing, 13 | software distributed under the License is distributed on an 14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | KIND, either express or implied. See the License for the 16 | specific language governing permissions and limitations 17 | under the License. 18 | */ 19 | 20 | import java.io.File; 21 | import java.io.FileInputStream; 22 | import java.io.FileOutputStream; 23 | import java.io.IOException; 24 | import java.net.URL; 25 | import java.nio.channels.Channels; 26 | import java.nio.channels.ReadableByteChannel; 27 | import java.util.Properties; 28 | 29 | public class MavenWrapperDownloader { 30 | 31 | /** 32 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 33 | */ 34 | private static final String DEFAULT_DOWNLOAD_URL = 35 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"; 36 | 37 | /** 38 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 39 | * use instead of the default one. 40 | */ 41 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 42 | ".mvn/wrapper/maven-wrapper.properties"; 43 | 44 | /** 45 | * Path where the maven-wrapper.jar will be saved to. 46 | */ 47 | private static final String MAVEN_WRAPPER_JAR_PATH = 48 | ".mvn/wrapper/maven-wrapper.jar"; 49 | 50 | /** 51 | * Name of the property which should be used to override the default download url for the wrapper. 52 | */ 53 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 54 | 55 | public static void main(String args[]) { 56 | System.out.println("- Downloader started"); 57 | File baseDirectory = new File(args[0]); 58 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 59 | 60 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 61 | // wrapperUrl parameter. 62 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 63 | String url = DEFAULT_DOWNLOAD_URL; 64 | if(mavenWrapperPropertyFile.exists()) { 65 | FileInputStream mavenWrapperPropertyFileInputStream = null; 66 | try { 67 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 68 | Properties mavenWrapperProperties = new Properties(); 69 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 70 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 71 | } catch (IOException e) { 72 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 73 | } finally { 74 | try { 75 | if(mavenWrapperPropertyFileInputStream != null) { 76 | mavenWrapperPropertyFileInputStream.close(); 77 | } 78 | } catch (IOException e) { 79 | // Ignore ... 80 | } 81 | } 82 | } 83 | System.out.println("- Downloading from: : " + url); 84 | 85 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 86 | if(!outputFile.getParentFile().exists()) { 87 | if(!outputFile.getParentFile().mkdirs()) { 88 | System.out.println( 89 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 90 | } 91 | } 92 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 93 | try { 94 | downloadFileFromURL(url, outputFile); 95 | System.out.println("Done"); 96 | System.exit(0); 97 | } catch (Throwable e) { 98 | System.out.println("- Error downloading"); 99 | e.printStackTrace(); 100 | System.exit(1); 101 | } 102 | } 103 | 104 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 105 | URL website = new URL(urlString); 106 | ReadableByteChannel rbc; 107 | rbc = Channels.newChannel(website.openStream()); 108 | FileOutputStream fos = new FileOutputStream(destination); 109 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 110 | fos.close(); 111 | rbc.close(); 112 | } 113 | 114 | } 115 | -------------------------------------------------------------------------------- /api-gateway/.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/michalzeman/spring-reactive-microservices/3cefbfe631b295b010a881ae8e2a4a045fc08067/api-gateway/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /api-gateway/.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip 2 | -------------------------------------------------------------------------------- /api-gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11-jdk 2 | VOLUME /tmp 3 | 4 | ENV STATISTIC_SERVICE_DOMAIN=localhost 5 | ENV SHORTENER_SERVICE_DOMAIN=localhost 6 | ENV USER_SERVICE_DOMAIN=localhost 7 | 8 | COPY api-gateway/target/*SNAPSHOT.jar app.jar 9 | 10 | ENTRYPOINT ["java","-jar","/app.jar",""] -------------------------------------------------------------------------------- /api-gateway/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 4.0.0 5 | 6 | 7 | spring-reactive-microservices 8 | com.mz 9 | 0.0.1-SNAPSHOT 10 | 11 | 12 | com.mz 13 | api-gateway 14 | 0.0.1-SNAPSHOT 15 | api-gateway 16 | Demo project for Spring Boot 17 | 18 | 19 | 20 | org.springframework.cloud 21 | spring-cloud-starter-gateway 22 | 23 | 24 | 25 | org.springframework.boot 26 | spring-boot-starter-test 27 | test 28 | 29 | 30 | 31 | 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-maven-plugin 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/mz/apigateway/ApiGatewayApplication.java: -------------------------------------------------------------------------------- 1 | package com.mz.apigateway; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ApiGatewayApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ApiGatewayApplication.class, args); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /api-gateway/src/main/java/com/mz/apigateway/RouteConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.mz.apigateway; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.cloud.gateway.route.RouteLocator; 5 | import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | 10 | /** 11 | * Created by zemi on 31/10/2018. 12 | */ 13 | @Configuration 14 | public class RouteConfiguration { 15 | 16 | final String statisticUri; 17 | 18 | final String shortenerUri; 19 | 20 | final String userUri; 21 | 22 | public RouteConfiguration(@Value("${service.statistic.uri}") String statisticUri, 23 | @Value("${service.shortener.uri}") String shortenerUri, 24 | @Value("${service.user.uri}") String userUri) { 25 | this.statisticUri = statisticUri; 26 | this.shortenerUri = shortenerUri; 27 | this.userUri = userUri; 28 | } 29 | 30 | @Bean 31 | public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { 32 | return builder.routes() 33 | .route("statistic_id", r -> r.path("/statistics/**") 34 | .uri(statisticUri) 35 | ) 36 | .route("shortener_id", r -> r.path("/shorteners/**") 37 | .uri(shortenerUri) 38 | ) 39 | .route("user_id", r -> r.path("/users/**") 40 | .uri(userUri) 41 | ) 42 | .build(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /api-gateway/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | service: 2 | statistic: 3 | uri: http://${STATISTIC_SERVICE_DOMAIN:localhost}:8091 4 | shortener: 5 | uri: http://${SHORTENER_SERVICE_DOMAIN:localhost}:8092 6 | user: 7 | uri: http://${USER_SERVICE_DOMAIN:localhost}:8093 8 | 9 | spring: 10 | cloud: 11 | gateway: 12 | # routes: 13 | # - id: statistic_id 14 | # url: http://localhost:8081 15 | # predicates: 16 | # - Path: /statistics/** 17 | -------------------------------------------------------------------------------- /api-gateway/src/test/java/com/mz/apigateway/ApiGatewayApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.mz.apigateway; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.junit4.SpringRunner; 7 | 8 | @RunWith(SpringRunner.class) 9 | @SpringBootTest 10 | public class ApiGatewayApplicationTests { 11 | 12 | @Test 13 | public void contextLoads() { 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /common-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-reactive-microservices 7 | com.mz 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | common-api 13 | 14 | 15 | 16 | 17 | 18 | org.immutables 19 | value 20 | provided 21 | 22 | 23 | 24 | 25 | org.junit.jupiter 26 | junit-jupiter-api 27 | test 28 | 29 | 30 | 31 | org.junit.jupiter 32 | junit-jupiter-engine 33 | test 34 | 35 | 36 | 37 | org.springframework.boot 38 | spring-boot-starter-test 39 | test 40 | 42 | 43 | 44 | junit 45 | junit 46 | 47 | 48 | org.mockito 49 | mockito-core 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /common-api/src/main/java/com.mz.reactivedemo.common.api/events/Command.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.api.events; 2 | 3 | /** 4 | * Created by zemi on 29/09/2018. 5 | */ 6 | public interface Command { 7 | } 8 | -------------------------------------------------------------------------------- /common-api/src/main/java/com.mz.reactivedemo.common.api/events/Document.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.api.events; 2 | 3 | /** 4 | * Created by zemi on 29/09/2018. 5 | */ 6 | public interface Document { 7 | } 8 | -------------------------------------------------------------------------------- /common-api/src/main/java/com.mz.reactivedemo.common.api/events/DomainEvent.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.api.events; 2 | 3 | public interface DomainEvent extends Event { 4 | String aggregateId(); 5 | } 6 | -------------------------------------------------------------------------------- /common-api/src/main/java/com.mz.reactivedemo.common.api/events/Event.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.api.events; 2 | 3 | 4 | import org.immutables.value.Value; 5 | 6 | import java.io.Serializable; 7 | import java.time.Instant; 8 | import java.util.UUID; 9 | 10 | /** 11 | * Created by zemi on 29/09/2018. 12 | */ 13 | public interface Event extends Serializable { 14 | 15 | @Value.Default 16 | default String eventId() { 17 | return UUID.randomUUID().toString(); 18 | } 19 | 20 | @Value.Default 21 | default Instant eventCreatedAt() { 22 | return Instant.now(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /common-persistence/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-reactive-microservices 7 | com.mz 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | common-persistence 13 | 14 | 15 | 16 | 17 | org.springframework.cloud 18 | spring-cloud-stream-binder-kafka-streams 19 | 20 | 21 | 22 | org.springframework.cloud 23 | spring-cloud-stream-binder-kafka 24 | 25 | 26 | 27 | org.apache.kafka 28 | kafka-streams 29 | 30 | 31 | 32 | 33 | com.typesafe.akka 34 | akka-persistence-query_2.12 35 | 36 | 37 | com.typesafe.akka 38 | akka-persistence_2.12 39 | 40 | 41 | 42 | com.typesafe.akka 43 | akka-cluster_2.12 44 | 45 | 46 | 47 | com.typesafe.akka 48 | akka-testkit_2.12 49 | 2.5.21 50 | 51 | 52 | 53 | 54 | com.github.scullxbones 55 | akka-persistence-mongo-common_2.12 56 | 57 | 58 | 59 | 60 | com.github.scullxbones 61 | akka-persistence-mongo-casbah_2.12 62 | 63 | 64 | 65 | 66 | org.mongodb 67 | casbah_2.12 68 | pom 69 | 70 | 71 | 72 | 73 | org.mongodb.scala 74 | mongo-scala-driver_2.12 75 | 76 | 77 | 78 | org.mongodb.scala 79 | mongo-scala-bson_2.12 80 | 81 | 82 | 83 | 84 | common 85 | com.mz 86 | 87 | 88 | 89 | com.mz 90 | common-api 91 | 92 | 93 | com.mz 94 | common-api 95 | 0.0.1-SNAPSHOT 96 | 97 | 98 | 99 | 100 | org.junit.jupiter 101 | junit-jupiter-api 102 | test 103 | 104 | 105 | 106 | org.junit.jupiter 107 | junit-jupiter-engine 108 | test 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/AggregateFactory.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance; 2 | 3 | import com.mz.reactivedemo.adapter.persistance.impl.AggregateFactoryImpl; 4 | import com.mz.reactivedemo.common.aggregate.Aggregate; 5 | 6 | import java.util.function.Function; 7 | 8 | public interface AggregateFactory { 9 | 10 | Aggregate of(String id); 11 | 12 | Aggregate of(S state); 13 | 14 | static AggregateFactory build(Function> createAggregateById, 15 | Function> createAggregateByState) { 16 | return new AggregateFactoryImpl(createAggregateById, createAggregateByState); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/AggregatePersistenceConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance; 2 | 3 | import akka.actor.ActorSystem; 4 | import com.mz.reactivedemo.adapter.persistance.impl.AggregateRepositoryImpl; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.beans.factory.config.BeanDefinition; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Scope; 10 | 11 | @Configuration 12 | public class AggregatePersistenceConfiguration { 13 | 14 | @Bean 15 | @Scope(BeanDefinition.SCOPE_SINGLETON) 16 | public ActorSystem actorSystem(@Value("${kafka.consumer.group-id}") String actorySystemName) { 17 | return ActorSystem.create(actorySystemName); 18 | } 19 | 20 | @Bean 21 | @Scope(BeanDefinition.SCOPE_PROTOTYPE) 22 | public AggregateRepository persistenceRepository(ActorSystem actorSystem) { 23 | return new AggregateRepositoryImpl(actorSystem); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/AggregateRepository.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance; 2 | 3 | import com.mz.reactivedemo.common.CommandResult; 4 | import com.mz.reactivedemo.common.api.events.Command; 5 | import reactor.core.publisher.Mono; 6 | 7 | public interface AggregateRepository { 8 | 9 | Mono> execute(String aggregateId, Command cmd, AggregateFactory aggregateFactory); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/AggregateService.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance; 2 | 3 | import com.mz.reactivedemo.adapter.persistance.impl.AggregateServiceImpl; 4 | import com.mz.reactivedemo.common.api.events.Command; 5 | import com.mz.reactivedemo.common.api.events.DomainEvent; 6 | import reactor.core.publisher.Mono; 7 | 8 | import java.util.function.Consumer; 9 | import java.util.function.Function; 10 | 11 | public interface AggregateService { 12 | 13 | Mono execute(String aggregateId, Command cmd); 14 | 15 | static AggregateService of(AggregateRepository repository, 16 | AggregateFactory aggregateFactory, 17 | Function> updateView, 18 | Consumer publishChangedEvent, 19 | Consumer publishDocumentMessage) { 20 | return new AggregateServiceImpl<>(repository, aggregateFactory, updateView, publishChangedEvent, 21 | publishDocumentMessage); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/actor/AggregatePersistenceActor.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance.actor; 2 | 3 | import akka.actor.Props; 4 | import akka.event.Logging; 5 | import akka.event.LoggingAdapter; 6 | import akka.persistence.AbstractPersistentActor; 7 | import akka.persistence.PersistentRepr; 8 | import akka.persistence.RecoveryCompleted; 9 | import com.mz.reactivedemo.adapter.persistance.AggregateFactory; 10 | import com.mz.reactivedemo.common.CommandResult; 11 | import com.mz.reactivedemo.common.ValidateResult; 12 | import com.mz.reactivedemo.common.aggregate.Aggregate; 13 | import com.mz.reactivedemo.common.api.events.Command; 14 | import com.mz.reactivedemo.common.api.events.DomainEvent; 15 | import org.eclipse.collections.api.list.ImmutableList; 16 | 17 | import java.util.Optional; 18 | 19 | public class AggregatePersistenceActor extends AbstractPersistentActor { 20 | 21 | public static Props props(String id, AggregateFactory aggregateFactory) { 22 | return Props.create(AggregatePersistenceActor.class, () -> new AggregatePersistenceActor(id, aggregateFactory)); 23 | } 24 | 25 | private final LoggingAdapter log = Logging.getLogger(this); 26 | 27 | private final String id; 28 | 29 | private final AggregateFactory aggregateFactory; 30 | 31 | private Optional> aggregate; 32 | 33 | private AggregatePersistenceActor(String id, AggregateFactory aggregateFactory) { 34 | this.aggregateFactory = aggregateFactory; 35 | this.id = id; 36 | this.aggregate = Optional.of(aggregateFactory.of(id)); 37 | } 38 | 39 | @Override 40 | public Receive createReceiveRecover() { 41 | return receiveBuilder() 42 | .match(PersistentRepr.class, m -> log.info(m.toString())) 43 | .match(DomainEvent.class, event -> { 44 | log.info("Event to apply in recovery -> ", event); 45 | this.aggregate = Optional.of 46 | (this.aggregate.orElseGet(() -> aggregateFactory.of(this.id)).apply(event)); 47 | }) 48 | .match(RecoveryCompleted.class, evt -> log.info("Recovery completed. Current sequence: {}", lastSequenceNr())) 49 | .build(); 50 | } 51 | 52 | @Override 53 | public Receive createReceive() { 54 | return receiveBuilder() 55 | .match(Command.class, this::processUpdate) 56 | .build(); 57 | } 58 | 59 | @Override 60 | public String persistenceId() { 61 | return this.id; 62 | } 63 | 64 | private void processUpdate(Command cmd) { 65 | aggregate.ifPresentOrElse( 66 | a -> a.validate(cmd) 67 | .map(ValidateResult::events) 68 | .onFailure(this::onFailure) 69 | .toOptional() 70 | .filter(domainEvents -> !domainEvents.isEmpty()) 71 | .ifPresentOrElse( 72 | domainEvents -> onSuccess(a, domainEvents), 73 | () -> { 74 | log.debug("No changes on {} aggregate", a.getClass()); 75 | sender().tell(CommandResult.none(), self()); 76 | }), 77 | () -> sender().tell(CommandResult.error(new RuntimeException("Wrong actor state")), self()) 78 | ); 79 | } 80 | 81 | private void onSuccess(Aggregate aggregate, ImmutableList events) { 82 | persistAll(events, (evt) -> { 83 | log.debug("persistAllAsync for event: {}", evt); 84 | aggregate.apply(evt); 85 | }); 86 | 87 | deferAsync(events, (evt) -> { 88 | log.debug("Defer for event: {}", evt); 89 | this.aggregate.ifPresent(a -> { 90 | CommandResult commandResult = CommandResult.of(a.state(), events); 91 | sender().tell(commandResult, self()); 92 | }); 93 | }); 94 | } 95 | 96 | private void onFailure(Throwable error) { 97 | CommandResult commandResult = CommandResult.error(error); 98 | sender().tell(commandResult, self()); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/actor/RecoveryActor.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance.actor; 2 | 3 | import akka.actor.*; 4 | 5 | import java.util.UUID; 6 | import java.util.function.Supplier; 7 | 8 | public class RecoveryActor extends AbstractActor { 9 | 10 | public static Props props() { 11 | return Props.create(RecoveryActor.class); 12 | } 13 | 14 | public static class RecoverActor { 15 | public final ActorPath actorPath; 16 | 17 | public final Supplier createActor; 18 | 19 | public RecoverActor(ActorPath actorPath, Supplier createActor) { 20 | this.createActor = createActor; 21 | this.actorPath = actorPath; 22 | } 23 | } 24 | 25 | @Override 26 | public Receive createReceive() { 27 | return receiveBuilder() 28 | .match(RecoverActor.class, r -> { 29 | getContext().getSystem() 30 | .actorSelection(r.actorPath) 31 | .tell(new Identify(UUID.randomUUID().toString()), getSelf()); 32 | getContext().become(actorIdentity(getSender(), r)); 33 | }) 34 | .build(); 35 | } 36 | 37 | private Receive actorIdentity(ActorRef orgRequester, RecoverActor recoverActor) { 38 | return receiveBuilder() 39 | .match(ActorIdentity.class, actorIdentity -> { 40 | orgRequester.tell(actorIdentity.getActorRef().orElseGet(() -> recoverActor.createActor.get()), getSelf()); 41 | getContext().getSystem().stop(getSelf()); 42 | }) 43 | .build(); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/actor/RepositoryActor.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance.actor; 2 | 3 | import akka.actor.AbstractActor; 4 | import akka.actor.Props; 5 | import com.mz.reactivedemo.adapter.persistance.AggregateFactory; 6 | import com.mz.reactivedemo.common.api.events.Command; 7 | 8 | public class RepositoryActor extends AbstractActor { 9 | 10 | public static Props props() { 11 | return Props.create(RepositoryActor.class); 12 | } 13 | 14 | public static class CommandMsg { 15 | 16 | final Command cmd; 17 | 18 | final String aggregateId; 19 | 20 | final AggregateFactory aggregateFactory; 21 | 22 | public CommandMsg(Command cmd, String aggregateId, AggregateFactory aggregateFactory) { 23 | this.cmd = cmd; 24 | this.aggregateId = aggregateId; 25 | this.aggregateFactory = aggregateFactory; 26 | } 27 | } 28 | 29 | @Override 30 | public Receive createReceive() { 31 | return receiveBuilder() 32 | .match(CommandMsg.class, c -> getContext().findChild(c.aggregateId) 33 | .orElseGet(() -> getContext().actorOf(AggregatePersistenceActor.props(c.aggregateId, c.aggregateFactory), 34 | c.aggregateId)) 35 | .tell(c.cmd, sender())) 36 | .build(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/document/DocumentReadOnlyRepository.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance.document; 2 | 3 | import reactor.core.publisher.Mono; 4 | 5 | public interface DocumentReadOnlyRepository { 6 | 7 | Mono get(K key); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/document/impl/DocumentReadOnlyRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance.document.impl; 2 | 3 | import com.mz.reactivedemo.adapter.persistance.document.DocumentReadOnlyRepository; 4 | import org.apache.kafka.streams.state.QueryableStoreTypes; 5 | import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; 6 | import org.springframework.cloud.stream.binder.kafka.streams.InteractiveQueryService; 7 | import reactor.core.publisher.Mono; 8 | import reactor.core.scheduler.Scheduler; 9 | 10 | import java.util.Optional; 11 | import java.util.function.Function; 12 | 13 | import static java.util.Objects.requireNonNull; 14 | 15 | public class DocumentReadOnlyRepositoryImpl implements DocumentReadOnlyRepository { 16 | 17 | private final InteractiveQueryService queryService; 18 | 19 | private final Scheduler scheduler; 20 | 21 | private final String documentStorageName; 22 | 23 | public DocumentReadOnlyRepositoryImpl(InteractiveQueryService queryService, Scheduler scheduler, String documentStorageName) { 24 | this.queryService = requireNonNull(queryService, "queryService is required"); 25 | this.scheduler = requireNonNull(scheduler, "scheduler is required"); 26 | this.documentStorageName = requireNonNull(documentStorageName, "documentStorageName is required"); 27 | } 28 | 29 | @Override 30 | public Mono get(K key) { 31 | return Mono.fromCallable(this::getReadOnlyKeyValueStore) 32 | .flatMap(getValue(key)) 33 | .publishOn(scheduler); 34 | } 35 | 36 | private Function, Mono> getValue(K key) { 37 | return storage -> Mono.justOrEmpty(Optional.ofNullable(key).map(storage::get)); 38 | } 39 | 40 | private ReadOnlyKeyValueStore getReadOnlyKeyValueStore() { 41 | return queryService.getQueryableStore(documentStorageName, QueryableStoreTypes.keyValueStore()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/impl/AggregateFactoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance.impl; 2 | 3 | import com.mz.reactivedemo.adapter.persistance.AggregateFactory; 4 | import com.mz.reactivedemo.common.aggregate.Aggregate; 5 | 6 | import java.util.function.Function; 7 | 8 | public class AggregateFactoryImpl implements AggregateFactory { 9 | 10 | private final Function> createAggregateById; 11 | 12 | private final Function> createAggregateByState; 13 | 14 | public AggregateFactoryImpl(Function> createAggregateById, 15 | Function> createAggregateByState) { 16 | this.createAggregateById = createAggregateById; 17 | this.createAggregateByState = createAggregateByState; 18 | } 19 | 20 | @Override 21 | public Aggregate of(String id) { 22 | return createAggregateById.apply(id); 23 | } 24 | 25 | @Override 26 | public Aggregate of(S state) { 27 | return createAggregateByState.apply(state); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/impl/AggregateRepositoryImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance.impl; 2 | 3 | import akka.actor.ActorRef; 4 | import akka.actor.ActorSystem; 5 | import com.mz.reactivedemo.adapter.persistance.AggregateFactory; 6 | import com.mz.reactivedemo.adapter.persistance.AggregateRepository; 7 | import com.mz.reactivedemo.adapter.persistance.actor.RecoveryActor; 8 | import com.mz.reactivedemo.adapter.persistance.actor.RepositoryActor; 9 | import com.mz.reactivedemo.common.CommandResult; 10 | import com.mz.reactivedemo.common.api.events.Command; 11 | import org.apache.commons.logging.Log; 12 | import org.apache.commons.logging.LogFactory; 13 | import reactor.core.publisher.Mono; 14 | import reactor.core.scheduler.Schedulers; 15 | 16 | import java.time.Duration; 17 | 18 | import static akka.pattern.Patterns.ask; 19 | 20 | public class AggregateRepositoryImpl implements AggregateRepository { 21 | 22 | private static String REPOSITORY_NAME = "persistence_repository"; 23 | 24 | private static final Log log = LogFactory.getLog(AggregateRepositoryImpl.class); 25 | 26 | private final ActorSystem system; 27 | 28 | private ActorRef repositoryActor; 29 | 30 | public AggregateRepositoryImpl(ActorSystem system) { 31 | this.system = system; 32 | this.repositoryActor = getRepositoryActor(system); 33 | } 34 | 35 | private ActorRef getRepositoryActor(ActorSystem system) { 36 | return system.actorOf(RepositoryActor.props(), String.format("%s_%s", system.name(), 37 | REPOSITORY_NAME)); 38 | } 39 | 40 | private Mono recoverRepositoryActor() { 41 | log.info("recoverRepositoryActor() -> is going to recover RepositoryActor"); 42 | return Mono.fromCompletionStage(ask(system.actorOf(RecoveryActor.props()), 43 | new RecoveryActor.RecoverActor(this.repositoryActor.path(), 44 | () -> getRepositoryActor(system)), 45 | Duration.ofMillis(5000))) 46 | .cast(ActorRef.class); 47 | } 48 | 49 | private Mono> sendCommand(ActorRef repActor, String aggregateId, Command cmd, 50 | AggregateFactory aggregateFactory) { 51 | return Mono.fromCompletionStage(ask(repActor, new RepositoryActor.CommandMsg(cmd, aggregateId, 52 | aggregateFactory), Duration.ofMillis(5000))) 53 | .publishOn(Schedulers.elastic()) 54 | .map(r -> (CommandResult) r); 55 | } 56 | 57 | @Override 58 | public Mono> execute(String aggregateId, Command cmd, 59 | AggregateFactory aggregateFactory) { 60 | return sendCommand(this.repositoryActor, aggregateId, cmd, aggregateFactory) 61 | .onErrorResume(error -> recoverRepositoryActor() 62 | .flatMap(actorRef -> { 63 | this.repositoryActor = actorRef; 64 | return sendCommand(actorRef, aggregateId, cmd, aggregateFactory); 65 | }) 66 | ); 67 | 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/impl/AggregateServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance.impl; 2 | 3 | import com.mz.reactivedemo.adapter.persistance.AggregateFactory; 4 | import com.mz.reactivedemo.adapter.persistance.AggregateRepository; 5 | import com.mz.reactivedemo.adapter.persistance.AggregateService; 6 | import com.mz.reactivedemo.common.CommandResult; 7 | import com.mz.reactivedemo.common.api.events.Command; 8 | import com.mz.reactivedemo.common.api.events.DomainEvent; 9 | import reactor.core.publisher.Mono; 10 | 11 | import java.util.function.Consumer; 12 | import java.util.function.Function; 13 | 14 | public class AggregateServiceImpl implements AggregateService { 15 | 16 | protected final Function> updateView; 17 | 18 | protected final Consumer publishChangedEvent; 19 | 20 | protected final Consumer publishDocumentMessage; 21 | 22 | protected final AggregateRepository repository; 23 | 24 | protected final AggregateFactory aggregateFactory; 25 | 26 | public AggregateServiceImpl( 27 | AggregateRepository repository, 28 | AggregateFactory aggregateFactory, 29 | Function> updateView, 30 | Consumer publishChangedEvent, 31 | Consumer publishDocumentMessage 32 | ) { 33 | this.updateView = updateView; 34 | this.publishChangedEvent = publishChangedEvent; 35 | this.publishDocumentMessage = publishDocumentMessage; 36 | this.repository = repository; 37 | this.aggregateFactory = aggregateFactory; 38 | } 39 | 40 | @Override 41 | public Mono execute(String aggregateId, Command cmd) { 42 | return repository.execute(aggregateId, cmd, aggregateFactory) 43 | .flatMap(this::processResult); 44 | } 45 | 46 | private Mono processResult(CommandResult result) { 47 | switch (result.status()) { 48 | case MODIFIED: 49 | if (result.state().isPresent()) { 50 | return updateView.apply(result.state().get()) 51 | .doOnSuccess(s -> result.domainEvents().forEach(publishChangedEvent)) 52 | .doOnSuccess(publishDocumentMessage); 53 | } else { 54 | return Mono.empty(); 55 | } 56 | case ERROR: 57 | return Mono.error(result.error().orElseGet(() -> new RuntimeException("Generic error"))); 58 | case NOT_MODIFIED: 59 | default: 60 | return Mono.empty(); 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /common-persistence/src/test/java/com/mz/reactivedemo/adapter/persistance/actor/RecoveryActorTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.adapter.persistance.actor; 2 | 3 | import akka.actor.ActorPath; 4 | import akka.actor.ActorRef; 5 | import akka.actor.ActorSystem; 6 | import akka.testkit.javadsl.TestKit; 7 | import org.junit.jupiter.api.AfterAll; 8 | import org.junit.jupiter.api.BeforeAll; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertEquals; 12 | 13 | public class RecoveryActorTest { 14 | 15 | static ActorSystem system; 16 | 17 | @BeforeAll 18 | public static void setup() { 19 | system = ActorSystem.create(); 20 | } 21 | 22 | @Test 23 | void recoverActorNotEquals() { 24 | final var testMsg = "test"; 25 | final var testProbe = new TestKit(system); 26 | final var repositoryActor = new TestKit(system); 27 | var repoActorPath = ActorPath.fromString("akka://default/user/testNotEquals"); 28 | var repoActorNew = new TestKit(system); 29 | final var recoveryActor = system.actorOf(RecoveryActor.props()); 30 | recoveryActor.tell(new RecoveryActor.RecoverActor(repoActorPath, () -> repoActorNew.getRef()), testProbe.getRef()); 31 | 32 | ActorRef result = testProbe.expectMsgClass(ActorRef.class); 33 | result.tell(testMsg, testProbe.getRef()); 34 | 35 | repoActorNew.expectMsg(testMsg); 36 | } 37 | 38 | @Test 39 | void recoverActorEquals() { 40 | final var testProbe = new TestKit(system); 41 | 42 | final var repositoryActor = system.actorOf(RepositoryActor.props()); 43 | 44 | var repoActorPath = ActorPath.fromString("akka://default/user/testEquals"); 45 | final var recoveryActor = system.actorOf(RecoveryActor.props()); 46 | 47 | recoveryActor.tell(new RecoveryActor.RecoverActor(repoActorPath, () -> repositoryActor), testProbe.getRef()); 48 | var repoActorNew = testProbe.expectMsgClass(ActorRef.class); 49 | assertEquals(repoActorNew, repositoryActor); 50 | } 51 | 52 | @AfterAll 53 | public static void teardown() { 54 | TestKit.shutdownActorSystem(system); 55 | system = null; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /common/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-reactive-microservices 7 | com.mz 8 | 0.0.1-SNAPSHOT 9 | 10 | 11 | jar 12 | 13 | 4.0.0 14 | 15 | common 16 | com.mz 17 | 18 | 19 | 20 | com.mz 21 | common-api 22 | 23 | 24 | 25 | org.eclipse.collections 26 | eclipse-collections-api 27 | 28 | 29 | 30 | org.eclipse.collections 31 | eclipse-collections 32 | 33 | 34 | 35 | 36 | org.immutables 37 | value 38 | provided 39 | 40 | 41 | 42 | org.springframework 43 | spring-web 44 | compile 45 | 46 | 47 | org.springframework 48 | spring-webflux 49 | compile 50 | 51 | 52 | 53 | org.springframework.boot 54 | spring-boot-starter-data-mongodb-reactive 55 | 56 | 57 | 58 | com.fasterxml.jackson.core 59 | jackson-databind 60 | compile 61 | 62 | 63 | 64 | javax.annotation 65 | javax.annotation-api 66 | 67 | 68 | 69 | io.projectreactor.kafka 70 | reactor-kafka 71 | compile 72 | 73 | 74 | 75 | 76 | org.junit.jupiter 77 | junit-jupiter-api 78 | test 79 | 80 | 81 | 82 | org.junit.jupiter 83 | junit-jupiter-engine 84 | test 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | org.apache.maven.plugins 94 | maven-failsafe-plugin 95 | 96 | 97 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/CommandResult.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common; 2 | 3 | import com.mz.reactivedemo.common.api.events.DomainEvent; 4 | import org.eclipse.collections.api.list.ImmutableList; 5 | import org.eclipse.collections.impl.factory.Lists; 6 | import org.immutables.value.Value; 7 | 8 | import java.util.Optional; 9 | 10 | @Value.Immutable 11 | public interface CommandResult { 12 | 13 | enum StatusCode { 14 | NOT_MODIFIED, 15 | MODIFIED, 16 | BAD_COMMAND, 17 | ERROR 18 | } 19 | 20 | StatusCode status(); 21 | 22 | Optional error(); 23 | 24 | Optional state(); 25 | 26 | ImmutableList domainEvents(); 27 | 28 | static CommandResult of(S state, ImmutableList domainEvents) { 29 | return ImmutableCommandResult.builder() 30 | .state(state) 31 | .status(domainEvents.size() > 0 ? StatusCode.MODIFIED : StatusCode.NOT_MODIFIED) 32 | .domainEvents(domainEvents) 33 | .build(); 34 | } 35 | 36 | static CommandResult error(Throwable error) { 37 | return ImmutableCommandResult.builder() 38 | .status(StatusCode.ERROR) 39 | .domainEvents(Lists.immutable.empty()) 40 | .error(error) 41 | .build(); 42 | } 43 | 44 | static CommandResult none() { 45 | return ImmutableCommandResult.builder() 46 | .status(StatusCode.NOT_MODIFIED) 47 | .domainEvents(Lists.immutable.empty()) 48 | .build(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/CommandResultState.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common; 2 | 3 | public enum CommandResultState { 4 | CHANGED, NONE, ERROR 5 | } 6 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/KafkaMapper.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common; 2 | 3 | import com.fasterxml.jackson.core.JsonProcessingException; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | import org.apache.kafka.clients.producer.ProducerRecord; 6 | import reactor.kafka.sender.SenderRecord; 7 | 8 | import java.util.function.Function; 9 | 10 | public enum KafkaMapper { 11 | 12 | FN; 13 | 14 | public Function mapFromJson(ObjectMapper mapper, Class clazz) { 15 | return json -> { 16 | try { 17 | return mapper.readValue(json, clazz); 18 | } catch (JsonProcessingException e) { 19 | throw new RuntimeException(e); 20 | } 21 | }; 22 | } 23 | 24 | public Function> mapToRecord( 25 | String topic, 26 | ObjectMapper mapper, 27 | Function idMapper 28 | ) { 29 | return value -> { 30 | try { 31 | final var producerRecord = new ProducerRecord<>( 32 | topic, idMapper.apply(value), 33 | mapper.writeValueAsString(value) 34 | ); 35 | return SenderRecord.create(producerRecord, value); 36 | } catch (JsonProcessingException e) { 37 | throw new RuntimeException(e); 38 | } 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/ValidateResult.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common; 2 | 3 | import com.mz.reactivedemo.common.api.events.DomainEvent; 4 | import org.eclipse.collections.api.list.ImmutableList; 5 | import org.immutables.value.Value; 6 | 7 | @Value.Immutable 8 | public interface ValidateResult { 9 | 10 | ImmutableList events(); 11 | 12 | static ImmutableValidateResult.Builder builder() { 13 | return ImmutableValidateResult.builder(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/aggregate/AbstractRootAggregate.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.aggregate; 2 | 3 | import com.mz.reactivedemo.common.ValidateResult; 4 | import com.mz.reactivedemo.common.api.events.Command; 5 | import com.mz.reactivedemo.common.api.events.DomainEvent; 6 | import com.mz.reactivedemo.common.util.Try; 7 | import org.eclipse.collections.api.list.ImmutableList; 8 | 9 | /** 10 | * Created by zemi on 04/01/2019. 11 | */ 12 | public abstract class AbstractRootAggregate implements Aggregate { 13 | 14 | protected AggregateStatus status; 15 | 16 | protected abstract ImmutableList behavior(Command cmd); 17 | 18 | protected abstract String getRootEntityId(); 19 | 20 | public abstract Aggregate apply(DomainEvent event); 21 | 22 | @Override 23 | public Try validate(Command cmd) { 24 | return Try.of(() -> behavior(cmd)) 25 | .map(events -> ValidateResult.builder() 26 | .events(events) 27 | .build()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/aggregate/Aggregate.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.aggregate; 2 | 3 | import com.mz.reactivedemo.common.ValidateResult; 4 | import com.mz.reactivedemo.common.api.events.Command; 5 | import com.mz.reactivedemo.common.api.events.DomainEvent; 6 | import com.mz.reactivedemo.common.util.Try; 7 | 8 | public interface Aggregate { 9 | 10 | Aggregate apply(DomainEvent event); 11 | 12 | Try validate(Command cmd); 13 | 14 | S state(); 15 | } 16 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/aggregate/AggregateStatus.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.aggregate; 2 | 3 | public enum AggregateStatus { 4 | NEW, EXISTING 5 | } 6 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/aggregate/Id.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.aggregate; 2 | 3 | /** 4 | * Created by zemi on 30/09/2018. 5 | */ 6 | public class Id extends StringValue { 7 | 8 | public Id(java.lang.String value) { 9 | super(value); 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/aggregate/StringValue.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.aggregate; 2 | 3 | import java.util.Objects; 4 | 5 | /** 6 | * Created by zemi on 30/09/2018. 7 | */ 8 | public class StringValue { 9 | public final java.lang.String value; 10 | 11 | public StringValue(java.lang.String value) { 12 | if (Objects.isNull(value) || value.isEmpty()) { 13 | throw new RuntimeException("String value is null or empty!"); 14 | } 15 | this.value = value; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/http/ErrorMessage.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.http; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import org.immutables.value.Value; 6 | 7 | /** 8 | * Created by zemi on 02/10/2018. 9 | */ 10 | @Value.Immutable 11 | @JsonSerialize(as = ImmutableErrorMessage.class) 12 | @JsonDeserialize(as = ImmutableErrorMessage.class) 13 | public interface ErrorMessage { 14 | 15 | String error(); 16 | 17 | static ImmutableErrorMessage.Builder builder() { 18 | return ImmutableErrorMessage.builder(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/http/HttpErrorHandler.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.http; 2 | 3 | import org.springframework.http.HttpStatus; 4 | import org.springframework.http.MediaType; 5 | import org.springframework.web.reactive.function.server.ServerRequest; 6 | import org.springframework.web.reactive.function.server.ServerResponse; 7 | import reactor.core.publisher.Mono; 8 | 9 | import static org.springframework.web.reactive.function.BodyInserters.fromValue; 10 | 11 | public enum HttpErrorHandler { 12 | FN; 13 | 14 | public Mono onError(E e, ServerRequest req) { 15 | return Mono.just(ErrorMessage.builder().error(e.getMessage()).build()) 16 | .flatMap(error -> ServerResponse.status(HttpStatus.BAD_REQUEST) 17 | .contentType(MediaType.APPLICATION_JSON) 18 | .body(fromValue(error))); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/http/HttpHandler.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.http; 2 | 3 | import org.springframework.http.MediaType; 4 | import org.springframework.web.reactive.function.server.RouterFunction; 5 | import org.springframework.web.reactive.function.server.ServerResponse; 6 | import reactor.core.publisher.Mono; 7 | 8 | import static org.springframework.web.reactive.function.BodyInserters.fromValue; 9 | 10 | /** 11 | * Created by zemi on 02/10/2018. 12 | */ 13 | public interface HttpHandler { 14 | 15 | default Mono mapToResponse(T result) { 16 | return ServerResponse.ok() 17 | .contentType(MediaType.APPLICATION_JSON).body(fromValue(result)); 18 | } 19 | 20 | RouterFunction route(); 21 | } 22 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/util/AbstractMatch.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.util; 2 | 3 | import java.util.Optional; 4 | 5 | abstract class AbstractMatch { 6 | 7 | protected final Object o; 8 | 9 | protected AbstractMatch(Object o) { 10 | this.o = o; 11 | } 12 | 13 | protected boolean casePattern(Object obj, Class type) { 14 | return Optional.ofNullable(type) 15 | .flatMap(t -> Optional.ofNullable(obj).map(t::isInstance)).orElse(false); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/util/CaseMatch.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.util; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Consumer; 5 | 6 | public class CaseMatch extends AbstractMatch { 7 | 8 | private boolean executed = false; 9 | 10 | private CaseMatch(Object o) { 11 | super(o); 12 | } 13 | 14 | public CaseMatch when(Class type, Runnable statement) { 15 | Objects.requireNonNull(type); 16 | Objects.requireNonNull(statement); 17 | if (casePattern(o, type) && !executed) { 18 | statement.run(); 19 | this.executed = true; 20 | } 21 | return this; 22 | } 23 | 24 | public CaseMatch when(Class type, Consumer statement) { 25 | Objects.requireNonNull(type); 26 | Objects.requireNonNull(statement); 27 | if (casePattern(o, type) && !executed) { 28 | statement.accept(type.cast(o)); 29 | this.executed = true; 30 | } 31 | return this; 32 | } 33 | 34 | public CaseMatch orElse(Runnable statement) { 35 | Objects.requireNonNull(statement); 36 | if (!executed) { 37 | statement.run(); 38 | this.executed = true; 39 | } 40 | return this; 41 | } 42 | 43 | static public CaseMatch match(Object o) { 44 | return new CaseMatch(o); 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/util/Logger.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.util; 2 | 3 | import org.apache.commons.logging.Log; 4 | 5 | import java.util.function.Supplier; 6 | 7 | /** 8 | * Created by zemi on 22/06/2018. 9 | */ 10 | public class Logger { 11 | 12 | private final Log logger; 13 | 14 | public Logger(Log logger) { 15 | this.logger = logger; 16 | } 17 | 18 | public void debug(Supplier sp) { 19 | if (logger.isDebugEnabled()) logger.debug(sp.get()); 20 | } 21 | 22 | public void debug(Supplier sp, Throwable error) { 23 | if (logger.isDebugEnabled()) logger.debug(sp.get(), error); 24 | } 25 | 26 | public void warning(Supplier sp) { 27 | if (logger.isWarnEnabled()) logger.warn(sp.get()); 28 | } 29 | 30 | public void warning(Supplier sp, Throwable error) { 31 | if (logger.isWarnEnabled()) logger.warn(sp.get(), error); 32 | } 33 | 34 | public void info(Supplier sp) { 35 | if (logger.isInfoEnabled()) logger.info(sp.get()); 36 | } 37 | 38 | public void info(Supplier sp, Throwable error) { 39 | if (logger.isInfoEnabled()) logger.info(sp.get(), error); 40 | } 41 | 42 | public Log log() { 43 | return logger; 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/util/Match.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.util; 2 | 3 | import java.util.Objects; 4 | import java.util.Optional; 5 | import java.util.function.Function; 6 | import java.util.function.Supplier; 7 | 8 | public class Match extends AbstractMatch { 9 | 10 | private Optional result = Optional.empty(); 11 | 12 | private Match(Object o) { 13 | super(o); 14 | } 15 | 16 | public Match when(Class type, Function statement) { 17 | Objects.requireNonNull(type); 18 | Objects.requireNonNull(statement); 19 | if (casePattern(o, type) && !this.result.isPresent()) { 20 | this.result = Optional.ofNullable(statement.apply(type.cast(o))); 21 | } 22 | return this; 23 | } 24 | 25 | public Optional get() { 26 | return this.result; 27 | } 28 | 29 | public R orElseGet(Supplier other) { 30 | return result.orElseGet(other::get); 31 | } 32 | 33 | static public Match match(Object o) { 34 | return new Match<>(o); 35 | } 36 | } 37 | 38 | -------------------------------------------------------------------------------- /common/src/main/java/com/mz/reactivedemo/common/util/Try.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.util; 2 | 3 | import java.util.NoSuchElementException; 4 | import java.util.Objects; 5 | import java.util.Optional; 6 | import java.util.function.Consumer; 7 | import java.util.function.Function; 8 | import java.util.function.Supplier; 9 | 10 | public interface Try { 11 | 12 | static Try of(SupplierThrowable f) { 13 | try { 14 | Objects.requireNonNull(f); 15 | return new Success<>(f.get()); 16 | } catch (Throwable error) { 17 | return new Failure<>(error); 18 | } 19 | } 20 | 21 | static Try error(Throwable error) { 22 | return new Failure<>(error); 23 | } 24 | 25 | boolean isSuccess(); 26 | 27 | boolean isFailure(); 28 | 29 | Optional toOptional(); 30 | 31 | Try map(FunctionThrowable f); 32 | 33 | Try flatMap(FunctionThrowable> f); 34 | 35 | Try onFailure(Consumer f); 36 | 37 | Try onSuccess(Consumer f); 38 | 39 | T getOrElse(Supplier f); 40 | 41 | T get(); 42 | 43 | @FunctionalInterface 44 | interface FunctionThrowable extends Function { 45 | 46 | @Override 47 | default R apply(T t) { 48 | try { 49 | return this.applyWithThrow(t); 50 | } catch (Throwable e) { 51 | throw new RuntimeException(e); 52 | } 53 | } 54 | 55 | R applyWithThrow(T t) throws Throwable; 56 | 57 | } 58 | 59 | @FunctionalInterface 60 | interface SupplierThrowable extends Supplier { 61 | 62 | default T get() { 63 | try { 64 | return this.getThrowable(); 65 | } catch (Throwable e) { 66 | throw new RuntimeException(e); 67 | } 68 | } 69 | 70 | T getThrowable() throws Throwable; 71 | 72 | } 73 | 74 | class Success implements Try { 75 | 76 | private final T result; 77 | 78 | private Success(T result) { 79 | this.result = result; 80 | } 81 | 82 | @Override 83 | public T get() { 84 | return this.result; 85 | } 86 | 87 | @Override 88 | public boolean isSuccess() { 89 | return true; 90 | } 91 | 92 | @Override 93 | public boolean isFailure() { 94 | return false; 95 | } 96 | 97 | @Override 98 | public Optional toOptional() { 99 | return Optional.of(result); 100 | } 101 | 102 | @Override 103 | public Try map(FunctionThrowable f) { 104 | try { 105 | Objects.requireNonNull(result); 106 | R resultMap = f.apply(result); 107 | Objects.requireNonNull(resultMap); 108 | return new Success<>(resultMap); 109 | } catch (Throwable error) { 110 | return new Failure<>(error); 111 | } 112 | } 113 | 114 | @Override 115 | public Try flatMap(FunctionThrowable> f) { 116 | try { 117 | Objects.requireNonNull(f); 118 | return f.apply(this.result); 119 | } catch (Throwable error) { 120 | return new Failure<>(error); 121 | } 122 | } 123 | 124 | @Override 125 | public Try onFailure(Consumer f) { 126 | return this; 127 | } 128 | 129 | @Override 130 | public Try onSuccess(Consumer f) { 131 | Objects.requireNonNull(f); 132 | f.accept(result); 133 | return this; 134 | } 135 | 136 | @Override 137 | public T getOrElse(Supplier f) { 138 | return result; 139 | } 140 | } 141 | 142 | class Failure implements Try { 143 | 144 | private final Throwable error; 145 | 146 | private Failure(Throwable error) { 147 | this.error = error; 148 | } 149 | 150 | public Throwable error() { 151 | return this.error; 152 | } 153 | 154 | @Override 155 | public boolean isSuccess() { 156 | return false; 157 | } 158 | 159 | @Override 160 | public boolean isFailure() { 161 | return true; 162 | } 163 | 164 | @Override 165 | public Optional toOptional() { 166 | return Optional.empty(); 167 | } 168 | 169 | @Override 170 | public Try map(FunctionThrowable f) { 171 | return (Try) this; 172 | } 173 | 174 | @Override 175 | public Try flatMap(FunctionThrowable> f) { 176 | return (Try) this; 177 | } 178 | 179 | @Override 180 | public Try onFailure(Consumer f) { 181 | Objects.requireNonNull(f); 182 | f.accept(error); 183 | return this; 184 | } 185 | 186 | @Override 187 | public Try onSuccess(Consumer f) { 188 | return this; 189 | } 190 | 191 | @Override 192 | public T getOrElse(Supplier f) { 193 | return f.get(); 194 | } 195 | 196 | @Override 197 | public T get() { 198 | throw new NoSuchElementException(error.getMessage()); 199 | } 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /common/src/test/java/com/mz/reactivedemo/common/util/CaseMatchTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.util; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.*; 7 | 8 | class CaseMatchTest { 9 | 10 | @Test 11 | void when() { 12 | List param = Arrays.asList("String", "Two"); 13 | List testing = new ArrayList<>(); 14 | 15 | 16 | CaseMatch.match(param) 17 | .when(ArrayList.class, () -> testing.add(1)) 18 | .when(Set.class, () -> testing.add(2)) 19 | .when(List.class, () -> testing.add(3)); 20 | 21 | Assertions.assertTrue(testing.size() ==1); 22 | Assertions.assertTrue(testing.get(0) == 3); 23 | } 24 | 25 | @Test 26 | void whenConsumer() { 27 | List param = Arrays.asList("String", "Two"); 28 | List testing = new ArrayList<>(); 29 | 30 | 31 | CaseMatch.match(param) 32 | .when(ArrayList.class, c -> testing.add(1)) 33 | .when(Set.class, () -> testing.add(2)) 34 | .when(List.class, () -> testing.add(3)); 35 | 36 | Assertions.assertTrue(testing.size() ==1); 37 | Assertions.assertTrue(testing.get(0) == 3); 38 | } 39 | 40 | @Test 41 | void orElse() { 42 | List param = Arrays.asList("String", "Two"); 43 | List testing = new ArrayList<>(); 44 | 45 | 46 | CaseMatch.match(param) 47 | .when(Map.class, c -> testing.add(1)) 48 | .when(Set.class, () -> testing.add(2)) 49 | .orElse(() -> testing.add(-1)); 50 | 51 | Assertions.assertTrue(testing.size() ==1); 52 | Assertions.assertTrue(testing.get(0) == -1); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /common/src/test/java/com/mz/reactivedemo/common/util/MatchTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.util; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | class MatchTest { 12 | 13 | @Test 14 | void build() { 15 | 16 | var list = List.of("list"); 17 | 18 | String result = Match.match(list) 19 | .when(List.class, type -> "List") 20 | .when(ArrayList.class, type -> "ArrayList") 21 | .when(String.class, type -> "String") 22 | .when(List.class, type -> "List2") 23 | .get().get(); 24 | 25 | assertTrue(result.equals("List")); 26 | } 27 | 28 | @Test 29 | void orElseGet() { 30 | var list = List.of("list"); 31 | 32 | var result = Match.match(list) 33 | .when(String.class, type -> "String") 34 | .when(Map.class, type -> "List2") 35 | .orElseGet(() -> "orElseGet"); 36 | 37 | assertTrue(result.equals("orElseGet")); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /common/src/test/java/com/mz/reactivedemo/common/util/TryTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.common.util; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | import java.util.NoSuchElementException; 7 | 8 | class TryTest { 9 | 10 | @Test 11 | void of() { 12 | Assertions.assertTrue(Try.of(() -> { 13 | throw new Exception(); 14 | }).isFailure()); 15 | 16 | Assertions.assertTrue(Try.of(() -> "success").isSuccess()); 17 | } 18 | 19 | @Test 20 | void map() { 21 | Try resultS = Try.of(() -> 2).map(n -> Integer.toString(n)).map(s -> !s.isEmpty()); 22 | Assertions.assertTrue(resultS.isSuccess()); 23 | Assertions.assertFalse(resultS.isFailure()); 24 | Assertions.assertTrue(resultS.get()); 25 | Assertions.assertTrue(resultS.toOptional().isPresent()); 26 | 27 | Try resultF = Try.of(() -> "Ano").map(Integer::valueOf).map(s -> s > 0); 28 | Assertions.assertFalse(resultF.isSuccess()); 29 | Assertions.assertTrue(resultF.isFailure()); 30 | Assertions.assertThrows(NoSuchElementException.class, resultF::get); 31 | Assertions.assertFalse(resultF.toOptional().isPresent()); 32 | Assertions.assertTrue(resultF.getOrElse(() -> true)); 33 | } 34 | 35 | @Test 36 | void map_withThrowExp() { 37 | Try resultF = Try.of(() -> "Ano").map(s -> { 38 | if ("Ano".equals(s)) { 39 | throw new Exception(); 40 | } 41 | return true; 42 | }); 43 | Assertions.assertFalse(resultF.isSuccess()); 44 | Assertions.assertTrue(resultF.isFailure()); 45 | Assertions.assertThrows(NoSuchElementException.class, resultF::get); 46 | Assertions.assertFalse(resultF.toOptional().isPresent()); 47 | Assertions.assertTrue(resultF.getOrElse(() -> true)); 48 | } 49 | 50 | @Test 51 | void flatMap() { 52 | Try resultS = Try.of(() -> 2).map(n -> Integer.toString(n)).flatMap((s -> Try.of(() -> !s.isEmpty()))); 53 | Assertions.assertTrue(resultS.isSuccess()); 54 | Assertions.assertFalse(resultS.isFailure()); 55 | Assertions.assertTrue(resultS.get()); 56 | Assertions.assertTrue(resultS.toOptional().isPresent()); 57 | 58 | Try resultF = Try.of(() -> "Ano").flatMap(n -> Try.of(() -> Integer.valueOf(n))).map(s -> s > 0); 59 | Assertions.assertFalse(resultF.isSuccess()); 60 | Assertions.assertTrue(resultF.isFailure()); 61 | Assertions.assertThrows(NoSuchElementException.class, () -> resultF.get()); 62 | Assertions.assertFalse(resultF.toOptional().isPresent()); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docker/create-kafka-topics.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | KAFKA_BROKER=localhost 4 | 5 | kafka-topics --create \ 6 | --bootstrap-server "$KAFKA_BROKER":9092 \ 7 | --replication-factor 1 \ 8 | --partitions 1 \ 9 | --topic shortener-changed \ 10 | && 11 | kafka-topics --create \ 12 | --bootstrap-server "$KAFKA_BROKER":9092 \ 13 | --replication-factor 1 \ 14 | --partitions 1 \ 15 | --topic shortener-document \ 16 | && 17 | kafka-topics --create \ 18 | --bootstrap-server "$KAFKA_BROKER":9092 \ 19 | --replication-factor 1 \ 20 | --partitions 1 \ 21 | --topic shortener-viewed \ 22 | && 23 | kafka-topics --create \ 24 | --bootstrap-server "$KAFKA_BROKER":9092 \ 25 | --replication-factor 1 \ 26 | --partitions 1 \ 27 | --topic user-document \ 28 | && 29 | kafka-topics --create \ 30 | --bootstrap-server "$KAFKA_BROKER":9092 \ 31 | --replication-factor 1 \ 32 | --partitions 1 \ 33 | --topic user-changed -------------------------------------------------------------------------------- /docker/docker-compose-api-gateway.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | api-gateway: 5 | build: 6 | context: ../ 7 | dockerfile: api-gateway/Dockerfile 8 | container_name: api-gateway 9 | ports: 10 | - 8080:8080 11 | environment: 12 | STATISTIC_SERVICE_DOMAIN: statistic-service 13 | SHORTENER_SERVICE_DOMAIN: shortener-service 14 | USER_SERVICE_DOMAIN: user-service 15 | depends_on: 16 | - zookeeper 17 | - kafka 18 | - schema-registry 19 | - kafka-connect 20 | - ksqldb-server 21 | - mongo-db 22 | - shortener-service -------------------------------------------------------------------------------- /docker/docker-compose-shortener-service.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | shortener-service: 5 | build: 6 | context: ../ 7 | dockerfile: shortener-impl/Dockerfile 8 | container_name: shortener-service 9 | ports: 10 | - "8092:8092" 11 | environment: 12 | - BROKERS=kafka:29092 13 | - ZK_NODES=zookeeper 14 | - MONGO_DB_HOST=mongo-db 15 | - MONGO_URI=mongodb://mongo-db:27017/shortener-ms-db 16 | depends_on: 17 | - zookeeper 18 | - kafka 19 | - schema-registry 20 | - kafka-connect 21 | - ksqldb-server 22 | - mongo-db 23 | 24 | -------------------------------------------------------------------------------- /docker/docker-compose-statistic-service.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | statistic-service: 5 | build: 6 | context: ../ 7 | dockerfile: statistic-impl/Dockerfile 8 | container_name: statistic-service 9 | ports: 10 | - 8091:8091 11 | environment: 12 | - BROKERS=kafka:29092 13 | - ZK_NODES=zookeeper 14 | - MONGO_DB_HOST=mongo-db 15 | depends_on: 16 | - zookeeper 17 | - kafka 18 | - schema-registry 19 | - kafka-connect 20 | - ksqldb-server 21 | - mongo-db -------------------------------------------------------------------------------- /docker/docker-compose-user-service.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | user-service: 5 | build: 6 | context: ../ 7 | dockerfile: user-impl/Dockerfile 8 | container_name: user-service 9 | ports: 10 | - 8093:8093 11 | environment: 12 | - BROKERS=kafka:29092 13 | - ZK_NODES=zookeeper 14 | - MONGO_DB_HOST=mongo-db 15 | - MONGO_URI=mongodb://mongo-db:27017/user-ms-db 16 | depends_on: 17 | - zookeeper 18 | - kafka 19 | - schema-registry 20 | - kafka-connect 21 | - ksqldb-server 22 | - mongo-db 23 | 24 | -------------------------------------------------------------------------------- /docker/docker-compose.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | docker-compose -f docker-compose.yml -f docker-compose-api-gateway.yml -f docker-compose-shortener-service.yml -f docker-compose-statistic-service.yml -f docker-compose-user-service.yml "$@" -------------------------------------------------------------------------------- /shortener-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-reactive-microservices 7 | com.mz 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | shortener-api 13 | 14 | 15 | com.mz 16 | common-api 17 | 18 | 19 | 20 | com.mz 21 | user-api 22 | 23 | 24 | 25 | org.springframework 26 | spring-context 27 | compile 28 | 29 | 30 | 31 | org.immutables 32 | value 33 | provided 34 | 35 | 36 | 37 | org.springframework 38 | spring-webflux 39 | compile 40 | 41 | 42 | com.fasterxml.jackson.core 43 | jackson-databind 44 | compile 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | org.apache.maven.plugins 53 | maven-failsafe-plugin 54 | 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /shortener-api/src/main/java/com/mz/reactivedemo/shortener/api/command/CreateShortener.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.api.command; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import com.mz.reactivedemo.common.api.events.Command; 6 | import org.immutables.value.Value; 7 | 8 | /** 9 | * Created by zemi on 29/05/2018. 10 | */ 11 | @Value.Immutable 12 | @JsonSerialize(as = ImmutableCreateShortener.class) 13 | @JsonDeserialize(as = ImmutableCreateShortener.class) 14 | public interface CreateShortener extends Command { 15 | String url(); 16 | 17 | String userId(); 18 | 19 | static ImmutableCreateShortener.Builder builder() { 20 | return ImmutableCreateShortener.builder(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shortener-api/src/main/java/com/mz/reactivedemo/shortener/api/command/UpdateShortener.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.api.command; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import com.mz.reactivedemo.common.api.events.Command; 6 | import org.immutables.value.Value; 7 | 8 | /** 9 | * Created by zemi on 30/09/2018. 10 | */ 11 | @Value.Immutable 12 | @JsonSerialize(as = ImmutableUpdateShortener.class) 13 | @JsonDeserialize(as = ImmutableUpdateShortener.class) 14 | public interface UpdateShortener extends Command { 15 | 16 | String id(); 17 | 18 | String url(); 19 | 20 | static ImmutableUpdateShortener.Builder builder() { 21 | return ImmutableUpdateShortener.builder(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /shortener-api/src/main/java/com/mz/reactivedemo/shortener/api/dto/ShortenerDto.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.api.dto; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import org.immutables.value.Value; 6 | 7 | import java.io.Serializable; 8 | import java.time.Instant; 9 | import java.util.Optional; 10 | 11 | /** 12 | * Created by zemi on 07/10/2018. 13 | */ 14 | @Value.Immutable 15 | @JsonSerialize(as = ImmutableShortenerDto.class) 16 | @JsonDeserialize(as = ImmutableShortenerDto.class) 17 | public interface ShortenerDto extends Serializable { 18 | 19 | String id(); 20 | 21 | Optional userId(); 22 | 23 | String key(); 24 | 25 | String url(); 26 | 27 | String shortUrl(); 28 | 29 | Instant createdAt(); 30 | 31 | Long version(); 32 | 33 | static ImmutableShortenerDto.Builder builder() { 34 | return ImmutableShortenerDto.builder(); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /shortener-api/src/main/java/com/mz/reactivedemo/shortener/api/event/ShortenerChangedEvent.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.api.event; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import org.immutables.value.Value; 6 | 7 | /** 8 | * Created by zemi on 20/10/2018. 9 | */ 10 | @Value.Immutable 11 | @JsonSerialize(as = ImmutableShortenerChangedEvent.class) 12 | @JsonDeserialize(as = ImmutableShortenerChangedEvent.class) 13 | public interface ShortenerChangedEvent extends ShortenerEvent { 14 | 15 | ShortenerEventType type(); 16 | 17 | ShortenerPayload payload(); 18 | 19 | static ImmutableShortenerChangedEvent.Builder builder() { 20 | return ImmutableShortenerChangedEvent.builder(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /shortener-api/src/main/java/com/mz/reactivedemo/shortener/api/event/ShortenerEvent.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.api.event; 2 | 3 | import com.mz.reactivedemo.common.api.events.DomainEvent; 4 | 5 | /** 6 | * Created by zemi on 29/05/2018. 7 | */ 8 | public interface ShortenerEvent extends DomainEvent { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /shortener-api/src/main/java/com/mz/reactivedemo/shortener/api/event/ShortenerEventType.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.api.event; 2 | 3 | /** 4 | * Created by zemi on 20/10/2018. 5 | */ 6 | public enum ShortenerEventType { 7 | CREATED, UPDATED, DELETED 8 | } 9 | -------------------------------------------------------------------------------- /shortener-api/src/main/java/com/mz/reactivedemo/shortener/api/event/ShortenerPayload.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.api.event; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 5 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 6 | import org.immutables.value.Value; 7 | 8 | import java.util.Optional; 9 | 10 | @Value.Immutable 11 | @JsonSerialize(as = ImmutableShortenerPayload.class) 12 | @JsonDeserialize(as = ImmutableShortenerPayload.class) 13 | public interface ShortenerPayload { 14 | String id(); 15 | 16 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 17 | Optional userId(); 18 | 19 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 20 | Optional key(); 21 | 22 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 23 | Optional url(); 24 | 25 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 26 | Optional shortUrl(); 27 | 28 | Long version(); 29 | 30 | static ImmutableShortenerPayload.Builder builder() { 31 | return ImmutableShortenerPayload.builder(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /shortener-api/src/main/java/com/mz/reactivedemo/shortener/api/event/ShortenerViewed.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.api.event; 2 | 3 | 4 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 5 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 6 | import com.mz.reactivedemo.common.api.events.Event; 7 | import org.immutables.value.Value; 8 | 9 | /** 10 | * Created by zemi on 29/05/2018. 11 | */ 12 | @Value.Immutable 13 | @JsonSerialize(as = ImmutableShortenerViewed.class) 14 | @JsonDeserialize(as = ImmutableShortenerViewed.class) 15 | public interface ShortenerViewed extends Event { 16 | 17 | String key(); 18 | 19 | Long number(); 20 | 21 | String aggregateId(); 22 | 23 | static ImmutableShortenerViewed.Builder builder() { 24 | return ImmutableShortenerViewed.builder(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /shortener-api/src/main/java/com/mz/reactivedemo/shortener/api/topics/ShortenerTopics.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.api.topics; 2 | 3 | /** 4 | * Created by zemi on 14/10/2018. 5 | */ 6 | public interface ShortenerTopics { 7 | String SHORTENER_VIEWED = "shortener-viewed"; 8 | 9 | String SHORTENER_DOCUMENT = "shortener-document"; 10 | 11 | String SHORTENER_CHANGED = "shortener-changed"; 12 | 13 | } 14 | -------------------------------------------------------------------------------- /shortener-impl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11-jdk 2 | VOLUME /tmp 3 | 4 | ENV BROKERS=localhost:9092 5 | ENV ZK_NODES=localhost 6 | ENV MONGO_URI=mongodb://localhost:27017/shortener-ms-db 7 | ENV AUTO_CREATE_TOPICS=false 8 | 9 | COPY shortener-impl/target/*SNAPSHOT.jar app.jar 10 | 11 | ENTRYPOINT ["java","-jar","/app.jar",""] 12 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/ShortenerApplication.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class ShortenerApplication { 8 | 9 | public static void main(String[] args) { 10 | SpringApplication.run(ShortenerApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/ShortenerHandler.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener; 2 | 3 | import com.mz.reactivedemo.common.http.HttpHandler; 4 | import com.mz.reactivedemo.shortener.api.command.CreateShortener; 5 | import com.mz.reactivedemo.shortener.api.command.UpdateShortener; 6 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 7 | import com.mz.reactivedemo.shortener.view.ShortenerQuery; 8 | import org.apache.commons.logging.Log; 9 | import org.apache.commons.logging.LogFactory; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.stereotype.Component; 13 | import org.springframework.web.reactive.function.server.RouterFunction; 14 | import org.springframework.web.reactive.function.server.RouterFunctions; 15 | import org.springframework.web.reactive.function.server.ServerRequest; 16 | import org.springframework.web.reactive.function.server.ServerResponse; 17 | import reactor.core.publisher.Mono; 18 | import reactor.core.scheduler.Schedulers; 19 | 20 | import java.net.URI; 21 | 22 | import static org.springframework.web.reactive.function.BodyInserters.fromValue; 23 | import static org.springframework.web.reactive.function.server.RequestPredicates.*; 24 | 25 | /** 26 | * Created by zemi on 27/09/2018. 27 | */ 28 | @Component 29 | public class ShortenerHandler implements HttpHandler { 30 | 31 | private static final Log log = LogFactory.getLog(ShortenerHandler.class); 32 | 33 | private final ShortenerService shortenerService; 34 | 35 | private final ShortenerQuery shortenerQuery; 36 | 37 | public ShortenerHandler(ShortenerService shortenerService, ShortenerQuery shortenerQuery) { 38 | this.shortenerService = shortenerService; 39 | this.shortenerQuery = shortenerQuery; 40 | } 41 | 42 | Mono tick(ServerRequest req) { 43 | log.info("tick() ->"); 44 | return ServerResponse.ok() 45 | .contentType(MediaType.APPLICATION_JSON) 46 | .body(Mono.just("Tick"), String.class); 47 | } 48 | 49 | Mono map(ServerRequest request) { 50 | log.info("map() -> key:" + request.pathVariable("key")); 51 | return shortenerQuery.map(request.pathVariable("key")) 52 | .flatMap(url -> ServerResponse.status(HttpStatus.SEE_OTHER) 53 | .headers(headers -> headers.setLocation(URI 54 | .create(url))).build()); 55 | } 56 | 57 | Mono getById(ServerRequest request) { 58 | log.info("getById() -> "); 59 | return shortenerQuery.get(request.pathVariable("eventId")) 60 | .flatMap(this::mapToResponse); 61 | } 62 | 63 | Mono getAll(ServerRequest request) { 64 | log.info("getAll() -> "); 65 | return ServerResponse.ok() 66 | .contentType(MediaType.APPLICATION_JSON) 67 | .body(shortenerQuery.getAll(), ShortenerDto.class); 68 | } 69 | 70 | Mono create(ServerRequest request) { 71 | log.info("execute() -> "); 72 | return request.bodyToMono(CreateShortener.class) 73 | .flatMap(shortenerService::create) 74 | .flatMap(this::mapToResponse); 75 | } 76 | 77 | Mono update(ServerRequest request) { 78 | log.info("update() -> "); 79 | return Mono.fromCallable(() -> request.pathVariable("eventId")) 80 | .publishOn(Schedulers.parallel()) 81 | .flatMap(id -> request.bodyToMono(UpdateShortener.class) 82 | .flatMap(shortenerService::update) 83 | .flatMap(this::mapToResponse)); 84 | } 85 | 86 | Mono getError(ServerRequest request) { 87 | log.info("getError() -> "); 88 | return Mono.error(new RuntimeException("Error")).flatMap(r -> ServerResponse.ok() 89 | .contentType(MediaType 90 | .APPLICATION_JSON).body(fromValue(r))); 91 | } 92 | 93 | @Override 94 | public RouterFunction route() { 95 | return RouterFunctions 96 | .route(GET("/").and(accept(MediaType.APPLICATION_JSON)), this::getAll) 97 | .andRoute(GET("/errors").and(accept(MediaType.APPLICATION_JSON)), this::getError) 98 | .andRoute(POST("").and(accept(MediaType.APPLICATION_JSON)), this::create) 99 | .andRoute(PUT("/{eventId}").and(accept(MediaType.APPLICATION_JSON)), this::update) 100 | .andRoute(GET("/{eventId}").and(accept(MediaType.APPLICATION_JSON)), this::getById) 101 | .andRoute(GET("/map/{key}") 102 | .and(accept(MediaType.APPLICATION_JSON)), this::map) 103 | .andRoute(GET("/health/ticks") 104 | .and(accept(MediaType.APPLICATION_JSON)), this::tick); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/ShortenerMapper.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener; 2 | 3 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 4 | import com.mz.reactivedemo.shortener.api.event.ShortenerChangedEvent; 5 | import com.mz.reactivedemo.shortener.api.event.ShortenerEventType; 6 | import com.mz.reactivedemo.shortener.api.event.ShortenerPayload; 7 | import com.mz.reactivedemo.shortener.domain.event.ShortenerUpdated; 8 | import com.mz.reactivedemo.shortener.view.ShortenerDocument; 9 | 10 | import java.util.Optional; 11 | import java.util.function.Function; 12 | 13 | public enum ShortenerMapper { 14 | 15 | FN; 16 | 17 | public final Function mapToDTO = document -> ShortenerDto.builder() 18 | .id(document.getId()) 19 | .key(document.getKey()) 20 | .url(document.getUrl()) 21 | .shortUrl(document.getShortUrl()) 22 | .userId(Optional.ofNullable(document.getUserId())) 23 | .createdAt(document.getCreatedAt()) 24 | .version(document.getVersion()) 25 | .build(); 26 | 27 | public final Function mapToDocument = dto -> { 28 | var document = 29 | new ShortenerDocument(dto.key(), dto.url(), dto.shortUrl(), dto 30 | .createdAt(), dto.version()); 31 | document.setId(dto.id()); 32 | dto.userId().ifPresent(document::setUserId); 33 | return document; 34 | }; 35 | 36 | public final Function mapUpdatedToChangedEvent = updated -> 37 | ShortenerChangedEvent.builder() 38 | .aggregateId(updated.aggregateId()) 39 | .payload(ShortenerPayload.builder() 40 | .id(updated.aggregateId()) 41 | .url(updated.url()) 42 | .version(updated.version()) 43 | .build()) 44 | .type(ShortenerEventType.UPDATED) 45 | .build(); 46 | 47 | public final Function mapDtoToPayload = dto -> ShortenerPayload.builder() 48 | .id(dto.id()) 49 | .key(dto.key()) 50 | .url(dto.url()) 51 | .userId(dto.userId()) 52 | .shortUrl(dto.shortUrl()) 53 | .version(dto.version()) 54 | .build(); 55 | 56 | } 57 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/ShortenerRepository.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener; 2 | 3 | import com.mz.reactivedemo.shortener.view.ShortenerDocument; 4 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 5 | import org.springframework.stereotype.Repository; 6 | import reactor.core.publisher.Mono; 7 | 8 | /** 9 | * Created by zemi on 29/05/2018. 10 | */ 11 | @Repository 12 | public interface ShortenerRepository extends ReactiveCrudRepository { 13 | Mono findByKey(String key); 14 | } 15 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/ShortenerService.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener; 2 | 3 | import com.mz.reactivedemo.shortener.api.command.CreateShortener; 4 | import com.mz.reactivedemo.shortener.api.command.UpdateShortener; 5 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 6 | import reactor.core.publisher.Mono; 7 | 8 | /** 9 | * Created by zemi on 29/05/2018. 10 | */ 11 | public interface ShortenerService { 12 | 13 | Mono create(CreateShortener shortener); 14 | 15 | Mono update(UpdateShortener shortener); 16 | 17 | // Flux getAll(); 18 | // 19 | // Flux events(); 20 | // 21 | // Flux documents(); 22 | // 23 | // Mono get(String eventId); 24 | // 25 | // Mono map(String key); 26 | } 27 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/adapter/user/UserAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.adapter.user; 2 | 3 | import org.springframework.stereotype.Component; 4 | import reactor.kafka.receiver.KafkaReceiver; 5 | import reactor.kafka.receiver.ReceiverOptions; 6 | 7 | import static java.util.Objects.requireNonNull; 8 | 9 | @Component 10 | public class UserAdapterImpl { 11 | 12 | private final ReceiverOptions userChangedReceiverOptions; 13 | 14 | public UserAdapterImpl(ReceiverOptions userChangedReceiverOptions) { 15 | this.userChangedReceiverOptions = requireNonNull(userChangedReceiverOptions, "userChangedReceiverOptions is required"); 16 | init(); 17 | } 18 | 19 | private void init() { 20 | KafkaReceiver.create(userChangedReceiverOptions).receive() 21 | .retry() 22 | .subscribe(record -> System.out.println(record.value())); 23 | 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/domain/aggregate/ShortUrl.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.domain.aggregate; 2 | 3 | import java.net.URI; 4 | 5 | /** 6 | * Created by zemi on 22/10/2018. 7 | */ 8 | public class ShortUrl { 9 | 10 | public static final String HTTP_LOCALHOST_8080_SHORTENERS = "http://localhost:8080/shorteners/map/"; 11 | 12 | public final String value; 13 | 14 | public ShortUrl(String value) { 15 | this.value = HTTP_LOCALHOST_8080_SHORTENERS.concat(value); 16 | URI.create(value); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/domain/aggregate/ShortenerAggregate.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.domain.aggregate; 2 | 3 | import com.mz.reactivedemo.common.aggregate.*; 4 | import com.mz.reactivedemo.common.api.events.Command; 5 | import com.mz.reactivedemo.common.api.events.DomainEvent; 6 | import com.mz.reactivedemo.common.util.CaseMatch; 7 | import com.mz.reactivedemo.common.util.Match; 8 | import com.mz.reactivedemo.shortener.api.command.CreateShortener; 9 | import com.mz.reactivedemo.shortener.api.command.UpdateShortener; 10 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 11 | import com.mz.reactivedemo.shortener.domain.event.ShortenerCreated; 12 | import com.mz.reactivedemo.shortener.domain.event.ShortenerUpdated; 13 | import org.eclipse.collections.api.list.ImmutableList; 14 | import org.eclipse.collections.impl.factory.Lists; 15 | 16 | import java.time.Instant; 17 | import java.util.UUID; 18 | 19 | /** 20 | * Created by zemi on 29/09/2018. 21 | */ 22 | public class ShortenerAggregate extends AbstractRootAggregate { 23 | 24 | private Id id; 25 | 26 | private Id userId; 27 | 28 | private ShortUrl shortUrl; 29 | 30 | private StringValue key; 31 | 32 | private Url url; 33 | 34 | private Instant createdAt; 35 | 36 | private Long version; 37 | 38 | private ShortenerAggregate(String id) { 39 | this.id = new Id(id); 40 | this.version = 0L; 41 | this.status = AggregateStatus.NEW; 42 | } 43 | 44 | private ShortenerAggregate(ShortenerDto shortenerState) { 45 | this(shortenerState.id()); 46 | this.shortUrl = new ShortUrl(shortenerState.shortUrl()); 47 | this.url = new Url(shortenerState.url()); 48 | this.key = new StringValue(shortenerState.key()); 49 | this.createdAt = shortenerState.createdAt(); 50 | this.version = shortenerState.version(); 51 | shortenerState.userId().ifPresent(userId -> this.userId = new Id(userId)); 52 | this.status = AggregateStatus.EXISTING; 53 | } 54 | 55 | private ShortenerCreated validateCreate(CreateShortener cmd) { 56 | StringValue key = new StringValue(UUID.randomUUID().toString()); 57 | Url url = new Url(cmd.url()); 58 | ShortUrl shortUrl = new ShortUrl(key.value); 59 | Id userId = new Id(cmd.userId()); 60 | ShortenerDto state = ShortenerDto.builder() 61 | .id(this.id.value) 62 | .version(0L) 63 | .createdAt(Instant.now()) 64 | .key(key.value) 65 | .shortUrl(shortUrl.value) 66 | .url(url.value) 67 | .userId(userId.value) 68 | .build(); 69 | return ShortenerCreated.builder().shortener(state).aggregateId(this.id.value).build(); 70 | } 71 | 72 | private ShortenerUpdated validateUpdate(UpdateShortener cmd) { 73 | if (status == AggregateStatus.NEW) { 74 | throw new RuntimeException("Wrong aggregate status"); 75 | } 76 | Url url = new Url(cmd.url()); 77 | return ShortenerUpdated.builder().aggregateId(this.id.value).url(url.value).version(this.version).build(); 78 | } 79 | 80 | private void applyShortenerUpdated(ShortenerUpdated evt) { 81 | this.url = new Url(evt.url()); 82 | ++this.version; 83 | } 84 | 85 | private void applyShortenerCreated(ShortenerCreated evt) { 86 | this.url = new Url(evt.shortener().url()); 87 | this.key = new StringValue(evt.shortener().key()); 88 | this.shortUrl = new ShortUrl(this.key.value); 89 | this.createdAt = evt.eventCreatedAt(); 90 | evt.shortener().userId().ifPresent(userId -> this.userId = new Id(userId)); 91 | this.status = AggregateStatus.EXISTING; 92 | } 93 | 94 | @Override 95 | protected ImmutableList behavior(Command cmd) { 96 | return Match.match(cmd) 97 | .when(CreateShortener.class, this::validateCreate) 98 | .when(UpdateShortener.class, this::validateUpdate) 99 | .get().map(Lists.immutable::of).orElseGet(Lists.immutable::empty); 100 | } 101 | 102 | @Override 103 | protected String getRootEntityId() { 104 | return id.value; 105 | } 106 | 107 | @Override 108 | public Aggregate apply(DomainEvent event) { 109 | CaseMatch.match(event) 110 | .when(ShortenerCreated.class, this::applyShortenerCreated) 111 | .when(ShortenerUpdated.class, this::applyShortenerUpdated); 112 | return this; 113 | } 114 | 115 | @Override 116 | public ShortenerDto state() { 117 | return ShortenerDto.builder() 118 | .id(id.value) 119 | .key(this.key.value) 120 | .url(this.url.value) 121 | .shortUrl(this.shortUrl.value) 122 | .version(this.version) 123 | .userId(this.userId.value) 124 | .createdAt(this.createdAt).build(); 125 | } 126 | 127 | public static ShortenerAggregate of(ShortenerDto shortenerDto) { 128 | return new ShortenerAggregate(shortenerDto); 129 | } 130 | 131 | public static ShortenerAggregate of(String id) { 132 | return new ShortenerAggregate(id); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/domain/aggregate/Url.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.domain.aggregate; 2 | 3 | import java.net.URI; 4 | 5 | /** 6 | * Created by zemi on 30/09/2018. 7 | */ 8 | public class Url { 9 | 10 | public final String value; 11 | 12 | public Url(String value) { 13 | URI.create(value); 14 | this.value = value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/domain/event/ShortenerChanged.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.domain.event; 2 | 3 | import com.mz.reactivedemo.shortener.api.event.ShortenerEvent; 4 | 5 | /** 6 | * Created by zemi on 21/10/2018. 7 | */ 8 | public interface ShortenerChanged extends ShortenerEvent { 9 | } 10 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/domain/event/ShortenerCreated.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.domain.event; 2 | 3 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 4 | import org.immutables.value.Value; 5 | 6 | /** 7 | * Created by zemi on 29/05/2018. 8 | */ 9 | @Value.Immutable 10 | public interface ShortenerCreated extends ShortenerChanged { 11 | 12 | ShortenerDto shortener(); 13 | 14 | static ImmutableShortenerCreated.Builder builder() { 15 | return ImmutableShortenerCreated.builder(); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/domain/event/ShortenerUpdated.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.domain.event; 2 | 3 | import org.immutables.value.Value; 4 | 5 | /** 6 | * Created by zemi on 30/09/2018. 7 | */ 8 | @Value.Immutable 9 | public interface ShortenerUpdated extends ShortenerChanged { 10 | 11 | String url(); 12 | 13 | Long version(); 14 | 15 | static ImmutableShortenerUpdated.Builder builder() { 16 | return ImmutableShortenerUpdated.builder(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/impl/ShortenerApplicationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.impl; 2 | 3 | import com.mz.reactivedemo.adapter.persistance.AggregateService; 4 | import com.mz.reactivedemo.adapter.persistance.document.DocumentReadOnlyRepository; 5 | import com.mz.reactivedemo.shortener.ShortenerService; 6 | import com.mz.reactivedemo.shortener.api.command.CreateShortener; 7 | import com.mz.reactivedemo.shortener.api.command.UpdateShortener; 8 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 9 | import com.mz.user.dto.UserDto; 10 | import org.apache.commons.logging.Log; 11 | import org.apache.commons.logging.LogFactory; 12 | import org.springframework.stereotype.Service; 13 | import reactor.core.publisher.Mono; 14 | 15 | import java.util.UUID; 16 | 17 | import static java.util.Objects.requireNonNull; 18 | 19 | /** 20 | * Created by zemi on 29/05/2018. 21 | */ 22 | @Service 23 | public class ShortenerApplicationServiceImpl implements ShortenerService { 24 | 25 | private static final Log log = LogFactory.getLog(ShortenerApplicationServiceImpl.class); 26 | 27 | private final AggregateService aggregateService; 28 | 29 | private final DocumentReadOnlyRepository userReadOnlyRepository; 30 | 31 | public ShortenerApplicationServiceImpl( 32 | AggregateService aggregateService, 33 | DocumentReadOnlyRepository userReadOnlyRepository 34 | ) { 35 | this.aggregateService = requireNonNull(aggregateService, "aggregateService is required"); 36 | this.userReadOnlyRepository = requireNonNull(userReadOnlyRepository, "userReadOnlyRepository is required"); 37 | } 38 | 39 | @Override 40 | public Mono create(CreateShortener createShortener) { 41 | log.debug("execute() ->"); 42 | return userReadOnlyRepository.get(createShortener.userId()) 43 | .switchIfEmpty(Mono.error(new RuntimeException(String.format("User with id %s doesn't exist", createShortener.userId())))) 44 | .flatMap(user -> aggregateService.execute(UUID.randomUUID().toString(), createShortener)); 45 | } 46 | 47 | @Override 48 | public Mono update(UpdateShortener shortener) { 49 | log.debug("update() ->"); 50 | return aggregateService.execute(shortener.id(), shortener); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/impl/ShortenerFunctions.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.impl; 2 | 3 | import com.mz.reactivedemo.common.api.events.DomainEvent; 4 | import com.mz.reactivedemo.common.util.Match; 5 | import com.mz.reactivedemo.shortener.ShortenerMapper; 6 | import com.mz.reactivedemo.shortener.ShortenerRepository; 7 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 8 | import com.mz.reactivedemo.shortener.api.event.ShortenerChangedEvent; 9 | import com.mz.reactivedemo.shortener.api.event.ShortenerEventType; 10 | import com.mz.reactivedemo.shortener.domain.event.ShortenerCreated; 11 | import com.mz.reactivedemo.shortener.domain.event.ShortenerUpdated; 12 | import com.mz.reactivedemo.shortener.streams.ApplicationMessageBus; 13 | import org.springframework.stereotype.Component; 14 | import reactor.core.publisher.Mono; 15 | 16 | import java.util.function.Consumer; 17 | import java.util.function.Function; 18 | 19 | 20 | public interface ShortenerFunctions { 21 | 22 | @Component 23 | class UpdateView implements Function> { 24 | 25 | private final ShortenerRepository repository; 26 | 27 | public UpdateView(ShortenerRepository repository) { 28 | this.repository = repository; 29 | } 30 | 31 | @Override 32 | public Mono apply(ShortenerDto shortenerDto) { 33 | return repository.save(ShortenerMapper.FN.mapToDocument.apply(shortenerDto)).map(ShortenerMapper.FN.mapToDTO); 34 | } 35 | } 36 | 37 | @Component 38 | class PublishChangedEvent implements Consumer { 39 | 40 | private final ApplicationMessageBus applicationMessageBus; 41 | 42 | public PublishChangedEvent(ApplicationMessageBus applicationMessageBus) { 43 | this.applicationMessageBus = applicationMessageBus; 44 | } 45 | 46 | @Override 47 | public void accept(DomainEvent event) { 48 | Match.match(event) 49 | .when(ShortenerCreated.class, e -> ShortenerChangedEvent.builder() 50 | .aggregateId(e.aggregateId()) 51 | .payload(ShortenerMapper.FN.mapDtoToPayload.apply(e.shortener())) 52 | .type(ShortenerEventType.CREATED) 53 | .build()) 54 | .when(ShortenerUpdated.class, ShortenerMapper.FN.mapUpdatedToChangedEvent) 55 | .get().ifPresent(applicationMessageBus::publishEvent); 56 | } 57 | } 58 | 59 | @Component 60 | class PublishDocumentMessage implements Consumer { 61 | 62 | private final ApplicationMessageBus applicationMessageBus; 63 | 64 | public PublishDocumentMessage(ApplicationMessageBus applicationMessageBus) { 65 | this.applicationMessageBus = applicationMessageBus; 66 | } 67 | 68 | @Override 69 | public void accept(ShortenerDto shortenerDto) { 70 | this.applicationMessageBus.publishShortenerDto(shortenerDto); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/port/kafka/ShortenerProcessor.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.port.kafka; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.mz.reactivedemo.common.util.Logger; 5 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 6 | import com.mz.reactivedemo.shortener.api.event.ShortenerChangedEvent; 7 | import com.mz.reactivedemo.shortener.api.event.ShortenerViewed; 8 | import com.mz.reactivedemo.shortener.streams.ApplicationMessageBus; 9 | import org.apache.commons.logging.LogFactory; 10 | import org.springframework.stereotype.Service; 11 | import reactor.core.scheduler.Schedulers; 12 | import reactor.kafka.sender.KafkaSender; 13 | 14 | import javax.annotation.PostConstruct; 15 | 16 | import static com.mz.reactivedemo.common.KafkaMapper.FN; 17 | import static com.mz.reactivedemo.shortener.api.topics.ShortenerTopics.*; 18 | import static java.util.Objects.requireNonNull; 19 | 20 | /** 21 | * Created by zemi on 07/10/2018. 22 | */ 23 | @Service 24 | public class ShortenerProcessor { 25 | 26 | private final Logger logger = new Logger(LogFactory.getLog(ShortenerProcessor.class)); 27 | 28 | private final ApplicationMessageBus shortenerMessageBus; 29 | 30 | private final KafkaSender kafkaSender; 31 | 32 | private final ObjectMapper objectMapper; 33 | 34 | public ShortenerProcessor( 35 | ApplicationMessageBus shortenerMessageBus, 36 | KafkaSender kafkaSender, 37 | ObjectMapper objectMapper 38 | ) { 39 | this.shortenerMessageBus = requireNonNull(shortenerMessageBus, "shortenerMessageBus is required"); 40 | this.kafkaSender = requireNonNull(kafkaSender, "kafkaSender is required"); 41 | this.objectMapper = requireNonNull(objectMapper, "objectMapper is required"); 42 | } 43 | 44 | @PostConstruct 45 | private void onInit() { 46 | logger.debug(() -> "ShortenerProcessor.onInit() ->"); 47 | var events = shortenerMessageBus.events() 48 | .subscribeOn(Schedulers.parallel()); 49 | 50 | var shortenerViewedStream = events 51 | .filter(event -> event instanceof ShortenerViewed) 52 | .cast(ShortenerViewed.class) 53 | .map(FN.mapToRecord(SHORTENER_VIEWED, objectMapper, ShortenerViewed::aggregateId)); 54 | 55 | var shortenerChangedStream = events 56 | .filter(event -> event instanceof ShortenerChangedEvent) 57 | .cast(ShortenerChangedEvent.class) 58 | .map(FN.mapToRecord(SHORTENER_CHANGED, objectMapper, ShortenerChangedEvent::aggregateId)); 59 | 60 | var shortenerDocumentStream = shortenerMessageBus.documents() 61 | .subscribeOn(Schedulers.parallel()) 62 | .map(FN.mapToRecord(SHORTENER_DOCUMENT, objectMapper, ShortenerDto::id)); 63 | 64 | kafkaSender.send(shortenerChangedStream) 65 | .doOnError(this::processError) 66 | .retry() 67 | .subscribe(); 68 | 69 | kafkaSender.send(shortenerViewedStream) 70 | .doOnError(this::processError) 71 | .retry() 72 | .subscribe(); 73 | 74 | kafkaSender.send(shortenerDocumentStream) 75 | .doOnError(this::processError) 76 | .retry() 77 | .subscribe(); 78 | } 79 | 80 | private void processError(Throwable error) { 81 | logger.log().error(error); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/streams/ApplicationMessageBus.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.streams; 2 | 3 | import com.mz.reactivedemo.common.api.events.Event; 4 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 5 | import reactor.core.publisher.Flux; 6 | 7 | public interface ApplicationMessageBus { 8 | 9 | void publishEvent(Event event); 10 | 11 | void publishShortenerDto(ShortenerDto dto); 12 | 13 | Flux events(); 14 | 15 | Flux documents(); 16 | 17 | } 18 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/streams/ApplicationMessageBusImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.streams; 2 | 3 | import com.mz.reactivedemo.common.api.events.Event; 4 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 5 | import org.apache.commons.logging.Log; 6 | import org.apache.commons.logging.LogFactory; 7 | import org.springframework.stereotype.Service; 8 | import reactor.core.publisher.Flux; 9 | import reactor.core.publisher.FluxSink; 10 | import reactor.core.publisher.ReplayProcessor; 11 | import reactor.core.scheduler.Schedulers; 12 | 13 | import java.util.Optional; 14 | 15 | @Service 16 | public class ApplicationMessageBusImpl implements ApplicationMessageBus { 17 | 18 | private static final Log log = LogFactory.getLog(ApplicationMessageBusImpl.class); 19 | 20 | protected final ReplayProcessor events = ReplayProcessor.create(1); 21 | 22 | protected final FluxSink eventSink = events.sink(); 23 | 24 | protected final ReplayProcessor documents = ReplayProcessor.create(1); 25 | 26 | protected final FluxSink documentsSink = documents.sink(); 27 | 28 | @Override 29 | public void publishEvent(Event event) { 30 | Optional.ofNullable(event).ifPresent(eventSink::next); 31 | } 32 | 33 | @Override 34 | public void publishShortenerDto(ShortenerDto dto) { 35 | Optional.ofNullable(dto).ifPresent(documentsSink::next); 36 | } 37 | 38 | @Override 39 | public Flux events() { 40 | return events.publishOn(Schedulers.parallel()); 41 | } 42 | 43 | @Override 44 | public Flux documents() { 45 | return documents.publishOn(Schedulers.parallel()); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/view/ShortenerDocument.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.view; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.mapping.Document; 5 | 6 | import java.time.Instant; 7 | import java.util.Objects; 8 | 9 | /** 10 | * Created by zemi on 29/05/2018. 11 | */ 12 | @Document(collection = "shortener") 13 | public class ShortenerDocument { 14 | 15 | @Id 16 | private String id; 17 | 18 | private String key; 19 | 20 | private String url; 21 | 22 | private String shortUrl; 23 | 24 | private String userId; 25 | 26 | private Instant createdAt; 27 | 28 | private Long version; 29 | 30 | /** 31 | * 32 | * @param key 33 | * @param url 34 | * @param shortUrl 35 | * @param createdAt 36 | */ 37 | public ShortenerDocument(String key, String url, String shortUrl, Instant createdAt, Long version) { 38 | this.key = key; 39 | this.url = url; 40 | this.shortUrl = shortUrl; 41 | this.createdAt = createdAt; 42 | this.version = version; 43 | } 44 | 45 | public ShortenerDocument() { 46 | } 47 | 48 | public String getUserId() { 49 | return userId; 50 | } 51 | 52 | public void setUserId(String userId) { 53 | this.userId = userId; 54 | } 55 | 56 | public String getShortUrl() { 57 | return shortUrl; 58 | } 59 | 60 | public void setShortUrl(String shortUrl) { 61 | this.shortUrl = shortUrl; 62 | } 63 | 64 | public String getId() { 65 | return id; 66 | } 67 | 68 | public void setId(String id) { 69 | this.id = id; 70 | } 71 | 72 | public String getKey() { 73 | return key; 74 | } 75 | 76 | public void setKey(String key) { 77 | this.key = key; 78 | } 79 | 80 | public String getUrl() { 81 | return url; 82 | } 83 | 84 | public void setUrl(String url) { 85 | this.url = url; 86 | } 87 | 88 | public Instant getCreatedAt() { 89 | return createdAt; 90 | } 91 | 92 | public void setCreatedAt(Instant createdAt) { 93 | this.createdAt = createdAt; 94 | } 95 | 96 | public Long getVersion() { 97 | return version; 98 | } 99 | 100 | public void setVersion(Long version) { 101 | this.version = version; 102 | } 103 | 104 | @Override 105 | public boolean equals(Object o) { 106 | if (this == o) return true; 107 | if (!(o instanceof ShortenerDocument)) return false; 108 | ShortenerDocument that = (ShortenerDocument) o; 109 | return id.equals(that.id) && 110 | key.equals(that.key) && 111 | url.equals(that.url) && 112 | shortUrl.equals(that.shortUrl) && 113 | userId.equals(that.userId) && 114 | createdAt.equals(that.createdAt) && 115 | version.equals(that.version); 116 | } 117 | 118 | @Override 119 | public int hashCode() { 120 | return Objects.hash(id, key, url, shortUrl, userId, createdAt, version); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/view/ShortenerQuery.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.view; 2 | 3 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 4 | import reactor.core.publisher.Flux; 5 | import reactor.core.publisher.Mono; 6 | 7 | public interface ShortenerQuery { 8 | 9 | Flux getAll(); 10 | 11 | Mono get(String id); 12 | 13 | Mono map(String key); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /shortener-impl/src/main/java/com/mz/reactivedemo/shortener/view/impl/ShortenerQueryImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.view.impl; 2 | 3 | import com.mz.reactivedemo.shortener.ShortenerMapper; 4 | import com.mz.reactivedemo.shortener.ShortenerRepository; 5 | import com.mz.reactivedemo.shortener.api.event.ShortenerViewed; 6 | import com.mz.reactivedemo.shortener.impl.ShortenerApplicationServiceImpl; 7 | import com.mz.reactivedemo.shortener.streams.ApplicationMessageBus; 8 | import com.mz.reactivedemo.shortener.view.ShortenerDocument; 9 | import com.mz.reactivedemo.shortener.view.ShortenerQuery; 10 | import org.apache.commons.logging.Log; 11 | import org.apache.commons.logging.LogFactory; 12 | import org.springframework.stereotype.Service; 13 | import reactor.core.publisher.Flux; 14 | import reactor.core.publisher.Mono; 15 | 16 | import javax.validation.constraints.NotNull; 17 | import java.util.Optional; 18 | import java.util.UUID; 19 | 20 | @Service 21 | public class ShortenerQueryImpl implements ShortenerQuery { 22 | 23 | private static final Log log = LogFactory.getLog(ShortenerApplicationServiceImpl.class); 24 | 25 | private final ShortenerRepository repository; 26 | 27 | private final ApplicationMessageBus shortenerMessageBus; 28 | 29 | private String mapShortenerToValue(@NotNull ShortenerDocument shortenerDocument) { 30 | log.debug("mapShortenerToValue() ->"); 31 | return "https://" + shortenerDocument.getUrl(); 32 | } 33 | 34 | public ShortenerQueryImpl(ShortenerRepository repository, ApplicationMessageBus shortenerMessageBus) { 35 | this.repository = repository; 36 | this.shortenerMessageBus = shortenerMessageBus; 37 | } 38 | 39 | 40 | @Override 41 | public Flux getAll() { 42 | log.debug("getAll() ->"); 43 | return repository.findAll().map(ShortenerMapper.FN.mapToDTO); 44 | } 45 | 46 | @Override 47 | public Mono get(String id) { 48 | log.debug("get() ->"); 49 | return repository.findById(id) 50 | .map(ShortenerMapper.FN.mapToDTO); 51 | } 52 | 53 | @Override 54 | public Mono map(String key) { 55 | log.debug("mapToChangedEvent() -> key: " + key); 56 | return repository.findByKey(key) 57 | .doOnSuccess(shortener -> Optional.ofNullable(shortener) 58 | .ifPresent(s -> this.shortenerMessageBus.publishEvent(ShortenerViewed.builder() 59 | .aggregateId(shortener.getId()) 60 | .key(s.getKey()) 61 | .number(1L) 62 | .eventId(UUID.randomUUID().toString()) 63 | .build()))) 64 | .map(this::mapShortenerToValue); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /shortener-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | persistence { 3 | journal { 4 | plugin = "akka-contrib-mongodb-persistence-journal" 5 | leveldb.native = false 6 | } 7 | snapshot-store { 8 | plugin = "akka-contrib-mongodb-persistence-snapshot" 9 | } 10 | } 11 | 12 | contrib { 13 | persistence.mongodb { 14 | mongo { 15 | mongouri = "mongodb://localhost:27017/shortener-ms-db" 16 | mongouri = ${?MONGO_URI} 17 | journal-collection = "journal" 18 | journal-index = "journal_index" 19 | snaps-collection = "snapshots" 20 | snaps-index = "snaps_index" 21 | journal-write-concern = "Acknowledged" 22 | #use-legacy-serialization = false 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /shortener-impl/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8092 3 | 4 | spring: 5 | data: 6 | mongodb: 7 | host: ${MONGO_DB_HOST:localhost}:27017 8 | 9 | cloud: 10 | stream: 11 | kafka: 12 | binder: 13 | brokers: ${BROKERS:localhost:9092} 14 | kafka: 15 | bootstrap: 16 | servers: ${BROKERS:localhost:9092} 17 | 18 | consumer: 19 | group-id: shortener-ms-reactive 20 | 21 | #spring.cloud.stream.kafka.streams.binder.configuration.application.server: localhost:8080 22 | spring.application.name: shortener-ms 23 | spring.cloud.stream.bindings.aggregate-in-0: 24 | destination: user-document 25 | spring.cloud.stream.kafka.streams.binder: 26 | brokers: ${BROKERS:localhost} 27 | configuration: 28 | commit.interval.ms: 1000 29 | 30 | spring.cloud.stream.function.definition: aggregate 31 | 32 | 33 | 34 | #spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms: 1000 35 | -------------------------------------------------------------------------------- /shortener-impl/src/test/java/com/mz/reactivedemo/shortener/ShortenerMapperTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener; 2 | 3 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 4 | import com.mz.reactivedemo.shortener.api.event.ShortenerChangedEvent; 5 | import com.mz.reactivedemo.shortener.api.event.ShortenerEventType; 6 | import com.mz.reactivedemo.shortener.api.event.ShortenerPayload; 7 | import com.mz.reactivedemo.shortener.domain.event.ShortenerUpdated; 8 | import org.junit.jupiter.api.Test; 9 | 10 | import java.time.Instant; 11 | import java.util.UUID; 12 | 13 | import static org.junit.jupiter.api.Assertions.assertEquals; 14 | import static org.junit.jupiter.api.Assertions.assertTrue; 15 | 16 | class ShortenerMapperTest { 17 | 18 | @Test 19 | void mapUpdateToChangedEvent() { 20 | String shortenerId = UUID.randomUUID().toString(); 21 | ShortenerUpdated updated = ShortenerUpdated.builder() 22 | .aggregateId(shortenerId) 23 | .url("updatedUrl.com") 24 | .version(1L) 25 | .build(); 26 | 27 | ShortenerPayload dto = ShortenerPayload.builder() 28 | .id(shortenerId) 29 | .version(1L) 30 | .url(updated.url()) 31 | .build(); 32 | 33 | ShortenerChangedEvent changedEvent= ShortenerMapper.FN.mapUpdatedToChangedEvent.apply(updated); 34 | assertTrue(changedEvent.type() == ShortenerEventType.UPDATED); 35 | assertTrue(changedEvent.payload().id().equals(dto.id())); 36 | assertTrue(changedEvent.payload().version().equals(dto.version())); 37 | assertTrue(changedEvent.payload().url().equals(dto.url())); 38 | assertTrue(changedEvent.payload().url().equals(dto.url())); 39 | } 40 | 41 | @Test 42 | void mapDtoToPayload() { 43 | ShortenerDto dto = ShortenerDto.builder() 44 | .id(UUID.randomUUID().toString()) 45 | .url("url") 46 | .shortUrl("shortUrl") 47 | .createdAt(Instant.now()) 48 | .key(UUID.randomUUID().toString()) 49 | .version(1L) 50 | .build(); 51 | 52 | ShortenerPayload payload = ShortenerMapper.FN.mapDtoToPayload.apply(dto); 53 | assertEquals(payload.id(), dto.id()); 54 | assertEquals(payload.version(), dto.version()); 55 | assertEquals(payload.url().get(), dto.url()); 56 | assertEquals(payload.key().get(), dto.key()); 57 | assertEquals(payload.shortUrl().get(), dto.shortUrl()); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /shortener-impl/src/test/java/com/mz/reactivedemo/shortener/aggregate/IdValueTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.aggregate; 2 | 3 | import com.mz.reactivedemo.common.aggregate.Id; 4 | import org.junit.jupiter.api.Assertions; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.UUID; 8 | 9 | /** 10 | * Created by zemi on 06/10/2018. 11 | */ 12 | public class IdValueTest { 13 | 14 | @Test 15 | public void createTest() { 16 | Id id = new Id(UUID.randomUUID().toString()); 17 | Assertions.assertFalse(id.value.isEmpty()); 18 | } 19 | 20 | @Test 21 | public void createTest_Error() { 22 | Assertions.assertThrows(RuntimeException.class, () -> new Id("")); 23 | Assertions.assertThrows(RuntimeException.class, () -> new Id(null)); 24 | } 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /shortener-impl/src/test/java/com/mz/reactivedemo/shortener/domain/aggregate/ShortenerAggregateTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.domain.aggregate; 2 | 3 | import com.mz.reactivedemo.common.ValidateResult; 4 | import com.mz.reactivedemo.common.util.Try; 5 | import com.mz.reactivedemo.shortener.api.command.CreateShortener; 6 | import com.mz.reactivedemo.shortener.api.command.UpdateShortener; 7 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 8 | import com.mz.reactivedemo.shortener.domain.event.ShortenerCreated; 9 | import com.mz.reactivedemo.shortener.domain.event.ShortenerUpdated; 10 | import org.junit.jupiter.api.Assertions; 11 | import org.junit.jupiter.api.Test; 12 | 13 | import java.util.UUID; 14 | 15 | class ShortenerAggregateTest { 16 | 17 | @Test 18 | void create() { 19 | String id = UUID.randomUUID().toString(); 20 | String userId = UUID.randomUUID().toString(); 21 | ShortenerAggregate shortenerAggregate = ShortenerAggregate.of(id); 22 | CreateShortener createShortener = CreateShortener.builder() 23 | .url("www.test.url") 24 | .userId(userId) 25 | .build(); 26 | Try resultTry = shortenerAggregate.validate(createShortener); 27 | Assertions.assertTrue(resultTry.get().events().size() == 1); 28 | Assertions.assertTrue(resultTry.get().events().stream().allMatch(e -> e instanceof ShortenerCreated)); 29 | 30 | resultTry.get().events().forEach(e -> shortenerAggregate.apply(e)); 31 | ShortenerDto state = shortenerAggregate.state(); 32 | Assertions.assertEquals(state.url(), "www.test.url"); 33 | Assertions.assertEquals(state.id(), id); 34 | Assertions.assertEquals(state.version().longValue(), 0L); 35 | Assertions.assertEquals(state.userId().get(), userId); 36 | } 37 | 38 | @Test 39 | void update() { 40 | String id = UUID.randomUUID().toString(); 41 | String userId = UUID.randomUUID().toString(); 42 | ShortenerAggregate shortenerAggregate = ShortenerAggregate.of(id); 43 | CreateShortener createShortener = CreateShortener.builder() 44 | .url("www.test.url") 45 | .userId(userId) 46 | .build(); 47 | Try resultTry = shortenerAggregate.validate(createShortener); 48 | resultTry.get().events().forEach(e -> shortenerAggregate.apply(e)); 49 | ShortenerDto state = shortenerAggregate.state(); 50 | 51 | Assertions.assertEquals(state.url(), "www.test.url"); 52 | Assertions.assertEquals(state.id(), id); 53 | Assertions.assertEquals(state.version().longValue(), 0L); 54 | 55 | Try validateUpdate = shortenerAggregate.validate(UpdateShortener.builder() 56 | .id(id) 57 | .url("www.update.rl") 58 | .build()); 59 | validateUpdate.get().events().forEach(e -> shortenerAggregate.apply(e)); 60 | Assertions.assertTrue(validateUpdate.get().events().stream().allMatch(e -> e instanceof ShortenerUpdated)); 61 | 62 | ShortenerDto updatedState = shortenerAggregate.state(); 63 | Assertions.assertEquals(updatedState.url(), "www.update.rl"); 64 | Assertions.assertEquals(updatedState.id(), id); 65 | Assertions.assertEquals(updatedState.version().longValue(), 1L); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /shortener-impl/src/test/java/com/mz/reactivedemo/shortener/impl/ShortenerQueryImplTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.impl; 2 | 3 | import com.mz.reactivedemo.shortener.ShortenerRepository; 4 | import com.mz.reactivedemo.shortener.streams.ApplicationMessageBus; 5 | import com.mz.reactivedemo.shortener.view.ShortenerDocument; 6 | import com.mz.reactivedemo.shortener.view.impl.ShortenerQueryImpl; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.mockito.InjectMocks; 10 | import org.mockito.Mock; 11 | import org.mockito.Mockito; 12 | import org.mockito.junit.jupiter.MockitoExtension; 13 | import reactor.core.publisher.Mono; 14 | import reactor.test.StepVerifier; 15 | 16 | import java.util.UUID; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | class ShortenerQueryImplTest { 20 | 21 | @Mock 22 | ShortenerRepository repository; 23 | 24 | @Mock 25 | ApplicationMessageBus messageBus; 26 | 27 | @InjectMocks 28 | ShortenerQueryImpl shortenerQuery; 29 | 30 | @Test 31 | void map() { 32 | final String key = "14e9c9c8-e23d-406a-bab6-5566358300a9"; 33 | 34 | ShortenerDocument shortenerDocument = new ShortenerDocument(); 35 | shortenerDocument.setKey(key); 36 | shortenerDocument.setUrl("www.url.tst"); 37 | shortenerDocument.setShortUrl("www.url.tst"); 38 | shortenerDocument.setId(UUID.randomUUID().toString()); 39 | 40 | Mockito.when(repository.findByKey(key)).thenReturn(Mono.just(shortenerDocument)); 41 | Mono source = shortenerQuery.map(key); 42 | StepVerifier.create(source) 43 | .expectNext("https://www.url.tst") 44 | .expectComplete().verify(); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /shortener-impl/src/test/java/com/mz/reactivedemo/shortener/impl/ShortenerServiceImplTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.reactivedemo.shortener.impl; 2 | 3 | import com.mz.reactivedemo.adapter.persistance.AggregateService; 4 | import com.mz.reactivedemo.adapter.persistance.document.DocumentReadOnlyRepository; 5 | import com.mz.reactivedemo.shortener.api.command.CreateShortener; 6 | import com.mz.reactivedemo.shortener.api.dto.ShortenerDto; 7 | import com.mz.user.dto.UserDto; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.mockito.InjectMocks; 11 | import org.mockito.Mock; 12 | import org.mockito.Mockito; 13 | import org.mockito.junit.jupiter.MockitoExtension; 14 | import reactor.core.publisher.Mono; 15 | import reactor.test.StepVerifier; 16 | 17 | import java.time.Instant; 18 | import java.util.UUID; 19 | 20 | import static org.mockito.ArgumentMatchers.any; 21 | import static org.mockito.ArgumentMatchers.anyString; 22 | 23 | /** 24 | * Created by zemi on 29/05/2018. 25 | */ 26 | 27 | @ExtendWith(MockitoExtension.class) 28 | public class ShortenerServiceImplTest { 29 | 30 | @Mock 31 | AggregateService aggregateRepository; 32 | 33 | @Mock 34 | DocumentReadOnlyRepository userDocumentReadOnlyRepository; 35 | 36 | @InjectMocks 37 | ShortenerApplicationServiceImpl stub; 38 | 39 | @Test 40 | public void create() { 41 | 42 | String id = UUID.randomUUID().toString(); 43 | String userId = UUID.randomUUID().toString(); 44 | 45 | String url = "http://testlong.test"; 46 | CreateShortener createShortener = CreateShortener.builder() 47 | .url(url) 48 | .userId(userId) 49 | .build(); 50 | 51 | String key = UUID.randomUUID().toString(); 52 | String shortUrl = "http://testurl.org"; 53 | Instant now = Instant.now(); 54 | 55 | var state = ShortenerDto.builder() 56 | .id(id) 57 | .url(url) 58 | .key(key) 59 | .shortUrl(shortUrl) 60 | .createdAt(now) 61 | .version(1L) 62 | .build(); 63 | var user = UserDto.builder() 64 | .id(UUID.randomUUID().toString()) 65 | .version(1L) 66 | .createdAt(Instant.now()) 67 | .firstName("FirstName") 68 | .lastName("LastName") 69 | .build(); 70 | 71 | Mockito.when(aggregateRepository.execute(any(String.class), any(CreateShortener.class))).thenReturn(Mono.just(state)); 72 | Mockito.when(userDocumentReadOnlyRepository.get(anyString())).thenReturn(Mono.just(user)); 73 | 74 | Mono source = stub.create(createShortener); 75 | 76 | StepVerifier.create(source) 77 | .expectNextMatches(nextValue -> url.equals(nextValue.url())) 78 | .expectComplete().verify(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /statistic-impl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11-jdk 2 | VOLUME /tmp 3 | 4 | ENV BROKERS=localhost:9092 5 | ENV ZK_NODES=localhost 6 | ENV MONGO_DB_HOST=localhost 7 | ENV AUTO_CREATE_TOPICS=false 8 | 9 | COPY statistic-impl/target/*SNAPSHOT.jar app.jar 10 | 11 | ENTRYPOINT ["java","-jar","/app.jar",""] -------------------------------------------------------------------------------- /statistic-impl/src/main/java/com/mz/statistic/StatisticApplication.java: -------------------------------------------------------------------------------- 1 | package com.mz.statistic; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * Created by zemi on 06/10/2018. 8 | */ 9 | @SpringBootApplication 10 | public class StatisticApplication { 11 | 12 | public static void main(String... args) { 13 | SpringApplication.run(StatisticApplication.class, args); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /statistic-impl/src/main/java/com/mz/statistic/StatisticHandler.java: -------------------------------------------------------------------------------- 1 | package com.mz.statistic; 2 | 3 | import com.mz.reactivedemo.common.http.HttpHandler; 4 | import com.mz.statistic.model.EventType; 5 | import com.mz.statistic.model.StatisticDocument; 6 | import org.springframework.http.MediaType; 7 | import org.springframework.stereotype.Component; 8 | import org.springframework.web.reactive.function.server.RouterFunction; 9 | import org.springframework.web.reactive.function.server.RouterFunctions; 10 | import org.springframework.web.reactive.function.server.ServerRequest; 11 | import org.springframework.web.reactive.function.server.ServerResponse; 12 | import reactor.core.publisher.Mono; 13 | 14 | import static org.springframework.web.reactive.function.server.RequestPredicates.GET; 15 | import static org.springframework.web.reactive.function.server.RequestPredicates.accept; 16 | import static org.springframework.web.reactive.function.server.ServerResponse.ok; 17 | 18 | 19 | /** 20 | * Created by zemi on 26/09/2018. 21 | */ 22 | @Component 23 | public class StatisticHandler implements HttpHandler { 24 | 25 | private final StatisticService service; 26 | 27 | public StatisticHandler(StatisticService service) { 28 | this.service = service; 29 | } 30 | 31 | 32 | Mono getAll(ServerRequest request) { 33 | return ok() 34 | .contentType(MediaType.APPLICATION_JSON) 35 | .body(service.getAll(), StatisticDocument.class); 36 | } 37 | 38 | Mono eventsCount(ServerRequest request) { 39 | return Mono.just(EventType.valueOf(request.pathVariable("type"))) 40 | .flatMap(t -> ok().contentType(MediaType.APPLICATION_JSON) 41 | .body(service.eventsCount(t), Long.class)); 42 | } 43 | 44 | @Override 45 | public RouterFunction route() { 46 | return RouterFunctions 47 | .route(GET("/").and(accept(MediaType.APPLICATION_JSON)), this::getAll) 48 | .andRoute(GET("/shorteners/events/{type}/counts").and(accept(MediaType.APPLICATION_JSON)), 49 | this::eventsCount); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /statistic-impl/src/main/java/com/mz/statistic/StatisticRepository.java: -------------------------------------------------------------------------------- 1 | package com.mz.statistic; 2 | 3 | import com.mz.statistic.model.EventType; 4 | import com.mz.statistic.model.StatisticDocument; 5 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 6 | import org.springframework.stereotype.Repository; 7 | import reactor.core.publisher.Flux; 8 | 9 | /** 10 | * Created by zemi on 29/05/2018. 11 | */ 12 | @Repository 13 | public interface StatisticRepository extends ReactiveCrudRepository { 14 | 15 | // Flux findByUrlAndEventType(String url, EventType eventType); 16 | 17 | Flux findByEventType(EventType eventType); 18 | 19 | Flux findByEventId(String eventId); 20 | 21 | } 22 | -------------------------------------------------------------------------------- /statistic-impl/src/main/java/com/mz/statistic/StatisticService.java: -------------------------------------------------------------------------------- 1 | package com.mz.statistic; 2 | 3 | import com.mz.statistic.model.EventType; 4 | import com.mz.statistic.model.StatisticDocument; 5 | import reactor.core.publisher.Flux; 6 | import reactor.core.publisher.Mono; 7 | 8 | /** 9 | * Created by zemi on 29/05/2018. 10 | */ 11 | public interface StatisticService { 12 | 13 | Flux getAll(); 14 | 15 | Mono eventsCount(EventType type); 16 | 17 | 18 | } 19 | -------------------------------------------------------------------------------- /statistic-impl/src/main/java/com/mz/statistic/adapters/shortener/ShortenerSubscriber.java: -------------------------------------------------------------------------------- 1 | package com.mz.statistic.adapters.shortener; 2 | 3 | import com.mz.reactivedemo.shortener.api.event.ShortenerChangedEvent; 4 | import com.mz.reactivedemo.shortener.api.event.ShortenerViewed; 5 | import reactor.core.publisher.Flux; 6 | 7 | /** 8 | * Created by zemi on 07/10/2018. 9 | */ 10 | public interface ShortenerSubscriber { 11 | 12 | Flux eventsShortenerViewed(); 13 | 14 | Flux shortenerChanged(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /statistic-impl/src/main/java/com/mz/statistic/adapters/shortener/impl/ShortenerSubscriberImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.statistic.adapters.shortener.impl; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.mz.reactivedemo.common.util.Logger; 5 | import com.mz.reactivedemo.shortener.api.event.ShortenerChangedEvent; 6 | import com.mz.reactivedemo.shortener.api.event.ShortenerViewed; 7 | import com.mz.statistic.adapters.shortener.ShortenerSubscriber; 8 | import org.apache.commons.logging.LogFactory; 9 | import org.apache.kafka.clients.consumer.ConsumerRecord; 10 | import org.springframework.beans.factory.annotation.Qualifier; 11 | import org.springframework.stereotype.Component; 12 | import reactor.core.publisher.Flux; 13 | import reactor.core.publisher.FluxSink; 14 | import reactor.core.publisher.ReplayProcessor; 15 | import reactor.core.scheduler.Schedulers; 16 | import reactor.kafka.receiver.KafkaReceiver; 17 | import reactor.kafka.receiver.ReceiverOptions; 18 | 19 | import javax.annotation.PostConstruct; 20 | 21 | import static com.mz.reactivedemo.common.KafkaMapper.FN; 22 | import static java.util.Objects.requireNonNull; 23 | 24 | /** 25 | * Created by zemi on 14/10/2018. 26 | */ 27 | @Component 28 | public class ShortenerSubscriberImpl implements ShortenerSubscriber { 29 | 30 | private final Logger logger = new Logger(LogFactory.getLog(ShortenerSubscriberImpl.class)); 31 | 32 | private final ReplayProcessor events = ReplayProcessor.create(1); 33 | 34 | private final FluxSink eventSink = events.sink(); 35 | 36 | private final ReplayProcessor changedEvents = ReplayProcessor.create(1); 37 | 38 | private final FluxSink changedEventsSink = changedEvents.sink(); 39 | 40 | private final ReceiverOptions shortenerChangedReceiverOptions; 41 | 42 | private final ReceiverOptions kafkaReceiverOptionsShortenerDocumentTopic; 43 | 44 | private final ReceiverOptions kafkaReceiverOptionsShortenerViewedTopic; 45 | 46 | private final ObjectMapper objectMapper; 47 | 48 | public ShortenerSubscriberImpl( 49 | @Qualifier("kafkaReceiverOptionsShortenerChangedTopic") ReceiverOptions shortenerChangedReceiverOptions, 50 | @Qualifier("kafkaReceiverOptionsShortenerDocumentTopic") ReceiverOptions kafkaReceiverOptionsShortenerDocumentTopic, 51 | @Qualifier("kafkaReceiverOptionsShortenerViewedTopic") ReceiverOptions kafkaReceiverOptionsShortenerViewedTopic, 52 | ObjectMapper objectMapper 53 | ) { 54 | this.shortenerChangedReceiverOptions = requireNonNull(shortenerChangedReceiverOptions, "shortenerChangedReceiverOptions is required"); 55 | this.kafkaReceiverOptionsShortenerDocumentTopic = requireNonNull(kafkaReceiverOptionsShortenerDocumentTopic, "kafkaReceiverOptionsShortenerDocumentTopic is required"); 56 | this.kafkaReceiverOptionsShortenerViewedTopic = requireNonNull(kafkaReceiverOptionsShortenerViewedTopic, "kafkaReceiverOptionsShortenerViewedTopic is required"); 57 | this.objectMapper = requireNonNull(objectMapper, "objectMapper is required"); 58 | } 59 | 60 | @Override 61 | public Flux eventsShortenerViewed() { 62 | return events.publishOn(Schedulers.parallel()); 63 | } 64 | 65 | @Override 66 | public Flux shortenerChanged() { 67 | return changedEvents.publishOn(Schedulers.parallel()); 68 | } 69 | 70 | @PostConstruct 71 | private void subscribeToTopics() { 72 | KafkaReceiver.create(shortenerChangedReceiverOptions).receive() 73 | .map(ConsumerRecord::value) 74 | .map(FN.mapFromJson(objectMapper, ShortenerChangedEvent.class)) 75 | .retry() 76 | .subscribe(changedEventsSink::next, this::processError); 77 | 78 | KafkaReceiver.create(kafkaReceiverOptionsShortenerViewedTopic).receive() 79 | .map(ConsumerRecord::value) 80 | .map(FN.mapFromJson(objectMapper, ShortenerViewed.class)) 81 | .retry() 82 | .subscribe(eventSink::next, this::processError); 83 | 84 | KafkaReceiver.create(kafkaReceiverOptionsShortenerDocumentTopic).receive() 85 | .retry() 86 | .subscribe(record -> System.out.println(record.value()), this::processError); 87 | } 88 | 89 | private void processError(Throwable error) { 90 | logger.log().error(error); 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /statistic-impl/src/main/java/com/mz/statistic/adapters/user/UserSubscriber.java: -------------------------------------------------------------------------------- 1 | package com.mz.statistic.adapters.user; 2 | 3 | import com.mz.user.message.event.UserChangedEvent; 4 | import reactor.core.publisher.Flux; 5 | 6 | public interface UserSubscriber { 7 | Flux userChanged(); 8 | } 9 | -------------------------------------------------------------------------------- /statistic-impl/src/main/java/com/mz/statistic/adapters/user/impl/UserSubscriberImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.statistic.adapters.user.impl; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.mz.reactivedemo.common.KafkaMapper; 5 | import com.mz.reactivedemo.common.util.Logger; 6 | import com.mz.statistic.adapters.user.UserSubscriber; 7 | import com.mz.user.message.event.UserChangedEvent; 8 | import org.apache.commons.logging.LogFactory; 9 | import org.apache.kafka.clients.consumer.ConsumerRecord; 10 | import org.springframework.beans.factory.annotation.Qualifier; 11 | import org.springframework.stereotype.Component; 12 | import reactor.core.publisher.Flux; 13 | import reactor.core.publisher.FluxSink; 14 | import reactor.core.publisher.ReplayProcessor; 15 | import reactor.core.scheduler.Schedulers; 16 | import reactor.kafka.receiver.KafkaReceiver; 17 | import reactor.kafka.receiver.ReceiverOptions; 18 | 19 | import javax.annotation.PostConstruct; 20 | 21 | import static java.util.Objects.requireNonNull; 22 | 23 | @Component 24 | public class UserSubscriberImpl implements UserSubscriber { 25 | 26 | private final Logger logger = new Logger(LogFactory.getLog(UserSubscriberImpl.class)); 27 | 28 | private final ReplayProcessor changedEvents = ReplayProcessor.create(1); 29 | 30 | private final FluxSink changedEventsSink = changedEvents.sink(); 31 | 32 | private final ReceiverOptions kafkaReceiverOptionsUserChangedTopic; 33 | 34 | private final ObjectMapper objectMapper; 35 | 36 | public UserSubscriberImpl( 37 | @Qualifier("kafkaReceiverOptionsUserChangedTopic") ReceiverOptions kafkaReceiverOptionsUserChangedTopic, 38 | ObjectMapper objectMapper) { 39 | this.kafkaReceiverOptionsUserChangedTopic = requireNonNull(kafkaReceiverOptionsUserChangedTopic, "kafkaReceiverOptionsUserChangedTopic is required"); 40 | this.objectMapper = requireNonNull(objectMapper, "objectMapper is required"); 41 | } 42 | 43 | @PostConstruct 44 | private void subscribeToTopic() { 45 | KafkaReceiver.create(kafkaReceiverOptionsUserChangedTopic).receive() 46 | .map(ConsumerRecord::value) 47 | .map(KafkaMapper.FN.mapFromJson(objectMapper, UserChangedEvent.class)) 48 | .retry() 49 | .subscribe(changedEventsSink::next, this::processError); 50 | } 51 | 52 | private void processError(Throwable error) { 53 | logger.log().error(error); 54 | } 55 | 56 | @Override 57 | public Flux userChanged() { 58 | return changedEvents.publishOn(Schedulers.parallel()); 59 | } 60 | 61 | } 62 | -------------------------------------------------------------------------------- /statistic-impl/src/main/java/com/mz/statistic/impl/StatisticServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.statistic.impl; 2 | 3 | import com.mz.reactivedemo.common.util.Logger; 4 | import com.mz.reactivedemo.shortener.api.event.ShortenerChangedEvent; 5 | import com.mz.reactivedemo.shortener.api.event.ShortenerViewed; 6 | import com.mz.statistic.StatisticRepository; 7 | import com.mz.statistic.StatisticService; 8 | import com.mz.statistic.adapters.shortener.ShortenerSubscriber; 9 | import com.mz.statistic.adapters.user.UserSubscriber; 10 | import com.mz.statistic.model.EventType; 11 | import com.mz.statistic.model.StatisticDocument; 12 | import com.mz.user.message.event.UserChangedEvent; 13 | import org.apache.commons.logging.LogFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.stereotype.Service; 16 | import reactor.core.publisher.Flux; 17 | import reactor.core.publisher.Mono; 18 | 19 | /** 20 | * Created by zemi on 29/05/2018. 21 | */ 22 | @Service 23 | public class StatisticServiceImpl implements StatisticService { 24 | 25 | private static final Logger log = new Logger(LogFactory.getLog(StatisticServiceImpl.class)); 26 | 27 | private final StatisticRepository repository; 28 | 29 | private final ShortenerSubscriber shortenerSubscriber; 30 | 31 | private final UserSubscriber userSubscriber; 32 | 33 | 34 | @Autowired 35 | public StatisticServiceImpl(StatisticRepository repository, 36 | ShortenerSubscriber shortenerSubscriber, 37 | UserSubscriber userSubscriber) { 38 | this.repository = repository; 39 | this.shortenerSubscriber = shortenerSubscriber; 40 | this.userSubscriber = userSubscriber; 41 | subscribeToEvents(); 42 | } 43 | 44 | public void subscribeToEvents() { 45 | shortenerSubscriber.eventsShortenerViewed() 46 | .flatMap(this::processViewedEvent) 47 | .doOnError(exp -> log.log().error("eventsShortenerViewed event stream error", exp)) 48 | .retry() 49 | .subscribe(); 50 | shortenerSubscriber.shortenerChanged() 51 | .flatMap(this::processShortenerChangedEvent) 52 | .doOnError(exp -> log.log().error("shortenerChanged event stream error", exp)) 53 | .retry() 54 | .subscribe(); 55 | userSubscriber.userChanged() 56 | .flatMap(this::processUserChangedEvent) 57 | .doOnError(exp -> log.log().error("userChanged event stream error", exp)) 58 | .retry() 59 | .subscribe(); 60 | } 61 | 62 | private Mono processViewedEvent(ShortenerViewed event) { 63 | return repository.findByEventId(event.eventId()) 64 | .next() 65 | .switchIfEmpty(Mono.defer(() -> { 66 | StatisticDocument statisticDocument = new StatisticDocument(); 67 | statisticDocument.setCreatedAt(event.eventCreatedAt()); 68 | statisticDocument.setEventId(event.eventId()); 69 | statisticDocument.setEventType(EventType.SHORTENER_VIEWED); 70 | statisticDocument.setAggregateId(event.aggregateId()); 71 | return repository.save(statisticDocument); 72 | })); 73 | } 74 | 75 | private Mono processShortenerChangedEvent(ShortenerChangedEvent event) { 76 | StatisticDocument statisticDocument = new StatisticDocument(); 77 | statisticDocument.setCreatedAt(event.eventCreatedAt()); 78 | statisticDocument.setEventId(event.eventId()); 79 | statisticDocument.setAggregateId(event.aggregateId()); 80 | switch (event.type()) { 81 | case CREATED: 82 | statisticDocument.setEventType(EventType.SHORTENER_CREATED); 83 | break; 84 | case UPDATED: 85 | statisticDocument.setEventType(EventType.SHORTENER_UPDATED); 86 | break; 87 | case DELETED: 88 | statisticDocument.setEventType(EventType.SHORTENER_DELETED); 89 | break; 90 | } 91 | return repository.save(statisticDocument); 92 | } 93 | 94 | private Mono processUserChangedEvent(UserChangedEvent event) { 95 | StatisticDocument statisticDocument = new StatisticDocument(); 96 | statisticDocument.setCreatedAt(event.eventCreatedAt()); 97 | statisticDocument.setEventId(event.eventId()); 98 | statisticDocument.setAggregateId(event.aggregateId()); 99 | switch (event.type()) { 100 | case USER_CREATED: 101 | statisticDocument.setEventType(EventType.USER_CREATED); 102 | break; 103 | case USER_UPDATED: 104 | case CONTACT_INFO_CREATED: 105 | statisticDocument.setEventType(EventType.USER_UPDATED); 106 | break; 107 | } 108 | return repository.save(statisticDocument); 109 | } 110 | 111 | @Override 112 | public Flux getAll() { 113 | log.debug(() -> "getAll() ->"); 114 | return repository.findAll() 115 | .takeLast(100); 116 | } 117 | 118 | @Override 119 | public Mono eventsCount(EventType type) { 120 | return repository.findByEventType(type) 121 | .map(r -> 1) 122 | .reduce(0L, Long::sum); 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /statistic-impl/src/main/java/com/mz/statistic/model/EventType.java: -------------------------------------------------------------------------------- 1 | package com.mz.statistic.model; 2 | 3 | /** 4 | * Created by zemi on 21/10/2018. 5 | */ 6 | public enum EventType { 7 | SHORTENER_VIEWED, SHORTENER_CREATED, SHORTENER_UPDATED, SHORTENER_DELETED, 8 | USER_CREATED, USER_UPDATED 9 | } 10 | -------------------------------------------------------------------------------- /statistic-impl/src/main/java/com/mz/statistic/model/StatisticDocument.java: -------------------------------------------------------------------------------- 1 | package com.mz.statistic.model; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.mapping.Document; 5 | 6 | import java.time.Instant; 7 | import java.util.Objects; 8 | 9 | /** 10 | * Created by zemi on 29/05/2018. 11 | */ 12 | @Document(collection = "statistic") 13 | public class StatisticDocument { 14 | 15 | @Id 16 | private String id; 17 | 18 | private String eventId; 19 | 20 | private Instant createdAt; 21 | 22 | private EventType eventType; 23 | 24 | private String aggregateId; 25 | 26 | public StatisticDocument(String id, String eventId, Instant createdAt, EventType eventType, String aggregateId) { 27 | this.id = id; 28 | this.eventId = eventId; 29 | this.createdAt = createdAt; 30 | this.eventType = eventType; 31 | this.aggregateId = aggregateId; 32 | } 33 | 34 | public StatisticDocument() { 35 | } 36 | 37 | public String getEventId() { 38 | 39 | return eventId; 40 | } 41 | 42 | public void setEventId(String eventId) { 43 | this.eventId = eventId; 44 | } 45 | 46 | public String getId() { 47 | return id; 48 | } 49 | 50 | public void setId(String id) { 51 | this.id = id; 52 | } 53 | 54 | public Instant getCreatedAt() { 55 | return createdAt; 56 | } 57 | 58 | public void setCreatedAt(Instant createdAt) { 59 | this.createdAt = createdAt; 60 | } 61 | 62 | public EventType getEventType() { 63 | return eventType; 64 | } 65 | 66 | public void setEventType(EventType eventType) { 67 | this.eventType = eventType; 68 | } 69 | 70 | public String getAggregateId() { 71 | return aggregateId; 72 | } 73 | 74 | public void setAggregateId(String aggregateId) { 75 | this.aggregateId = aggregateId; 76 | } 77 | 78 | @Override 79 | public boolean equals(Object o) { 80 | if (this == o) return true; 81 | if (o == null || getClass() != o.getClass()) return false; 82 | StatisticDocument that = (StatisticDocument) o; 83 | return id.equals(that.id) && 84 | eventId.equals(that.eventId) && 85 | createdAt.equals(that.createdAt) && 86 | eventType == that.eventType && 87 | aggregateId.equals(that.aggregateId); 88 | } 89 | 90 | @Override 91 | public int hashCode() { 92 | return Objects.hash(id, eventId, createdAt, eventType, aggregateId); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /statistic-impl/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8091 3 | 4 | spring: 5 | data: 6 | mongodb: 7 | host: ${MONGO_DB_HOST:localhost}:27017 8 | 9 | kafka: 10 | bootstrap: 11 | servers: ${BROKERS:localhost:9092} 12 | 13 | consumer: 14 | group-id: statistic-service 15 | 16 | #spring.cloud.stream.bindings.input1.binder: kafka 17 | -------------------------------------------------------------------------------- /statistic-impl/src/test/java/com/mz/statistic/StatisticHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.statistic; 2 | 3 | import com.mz.statistic.model.EventType; 4 | import com.mz.statistic.model.StatisticDocument; 5 | import org.junit.jupiter.api.AfterEach; 6 | import org.junit.jupiter.api.Assertions; 7 | import org.junit.jupiter.api.Test; 8 | import org.junit.jupiter.api.extension.ExtendWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.test.context.junit.jupiter.SpringExtension; 13 | import org.springframework.test.web.reactive.server.WebTestClient; 14 | 15 | import java.time.Instant; 16 | import java.util.UUID; 17 | 18 | /** 19 | * Created by zemi on 22/10/2018. 20 | */ 21 | @ExtendWith(SpringExtension.class) 22 | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) 23 | public class StatisticHandlerTest { 24 | 25 | @Autowired 26 | WebTestClient webTestClient; 27 | 28 | @Autowired 29 | StatisticRepository repository; 30 | 31 | @AfterEach 32 | void afterEach() { 33 | repository.deleteAll().block(); 34 | } 35 | 36 | @Test 37 | public void getAllTest() { 38 | StatisticDocument statisticDocument1 = new StatisticDocument(UUID.randomUUID().toString(), 39 | UUID.randomUUID().toString(), Instant.now(), EventType.SHORTENER_VIEWED, UUID.randomUUID().toString()); 40 | repository.save(statisticDocument1).subscribe(); 41 | StatisticDocument statisticDocument2 = new StatisticDocument(UUID.randomUUID().toString(), UUID 42 | .randomUUID().toString(), Instant.now(), EventType.SHORTENER_VIEWED, UUID.randomUUID().toString()); 43 | repository.save(statisticDocument2).subscribe(); 44 | 45 | webTestClient.get().uri("/statistics").accept(MediaType 46 | .APPLICATION_JSON).exchange() 47 | .expectStatus().isOk() 48 | .expectHeader().contentType(MediaType.APPLICATION_JSON) 49 | .expectBody(); 50 | } 51 | 52 | @Test 53 | public void countsTest() { 54 | EventType eventType = EventType.SHORTENER_UPDATED; 55 | StatisticDocument statisticDocument1 = new StatisticDocument(UUID.randomUUID().toString(), UUID 56 | .randomUUID().toString(), Instant.now(), EventType.SHORTENER_UPDATED, UUID.randomUUID().toString()); 57 | 58 | Long resultBefore = 59 | webTestClient.get().uri("/statistics/shorteners/events/{eventType}/counts", eventType).accept(MediaType 60 | .APPLICATION_JSON).exchange() 61 | .expectStatus().isOk() 62 | .expectHeader().contentType(MediaType.APPLICATION_JSON) 63 | .expectBody(Long.class).returnResult().getResponseBody(); 64 | Assertions.assertNotNull(resultBefore); 65 | 66 | repository.save(statisticDocument1).subscribe(); 67 | StatisticDocument statisticDocument2 = new StatisticDocument(UUID.randomUUID().toString(), UUID 68 | .randomUUID().toString(), Instant.now(), EventType.SHORTENER_UPDATED, UUID.randomUUID().toString()); 69 | repository.save(statisticDocument2).subscribe(); 70 | 71 | StatisticDocument statisticDocument3 = new StatisticDocument(UUID.randomUUID().toString(), UUID 72 | .randomUUID().toString(), Instant.now(), EventType.SHORTENER_CREATED, UUID.randomUUID().toString()); 73 | repository.save(statisticDocument3).subscribe(); 74 | 75 | Long resultAfter = 76 | webTestClient.get().uri("/statistics/shorteners/events/{eventType}/counts", eventType).accept(MediaType 77 | .APPLICATION_JSON).exchange() 78 | .expectStatus().isOk() 79 | .expectHeader().contentType(MediaType.APPLICATION_JSON) 80 | .expectBody(Long.class).returnResult().getResponseBody(); 81 | Assertions.assertNotNull(resultAfter); 82 | Assertions.assertNotEquals(resultBefore, resultAfter); 83 | } 84 | 85 | 86 | } 87 | -------------------------------------------------------------------------------- /user-api/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | spring-reactive-microservices 7 | com.mz 8 | 0.0.1-SNAPSHOT 9 | 10 | 4.0.0 11 | 12 | user-api 13 | 14 | 15 | 16 | com.mz 17 | common-api 18 | 19 | 20 | 21 | org.immutables 22 | value 23 | provided 24 | 25 | 26 | com.fasterxml.jackson.core 27 | jackson-databind 28 | compile 29 | 30 | 31 | 32 | org.eclipse.collections 33 | eclipse-collections 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /user-api/src/main/java/com/mz/user/dto/BasicDto.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.dto; 2 | 3 | import java.time.Instant; 4 | 5 | /** 6 | * Created by zemi on 16/01/2019. 7 | */ 8 | public interface BasicDto { 9 | 10 | String id(); 11 | 12 | Instant createdAt(); 13 | 14 | Long version(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /user-api/src/main/java/com/mz/user/dto/ContactInfoDto.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 5 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 6 | import org.immutables.value.Value; 7 | 8 | import java.time.Instant; 9 | import java.util.Optional; 10 | 11 | /** 12 | * Created by zemi on 16/01/2019. 13 | */ 14 | @Value.Immutable 15 | @JsonSerialize(as = ImmutableContactInfoDto.class) 16 | @JsonDeserialize(as = ImmutableContactInfoDto.class) 17 | public interface ContactInfoDto { 18 | 19 | String userId(); 20 | 21 | Instant createdAt(); 22 | 23 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 24 | Optional email(); 25 | 26 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 27 | Optional phoneNumber(); 28 | 29 | static ImmutableContactInfoDto.Builder builder() { 30 | return ImmutableContactInfoDto.builder(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /user-api/src/main/java/com/mz/user/dto/UserDto.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 5 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 6 | import org.immutables.value.Value; 7 | 8 | import java.util.List; 9 | import java.util.Optional; 10 | 11 | /** 12 | * Created by zemi on 16/01/2019. 13 | */ 14 | @Value.Immutable 15 | @JsonSerialize(as = ImmutableUserDto.class) 16 | @JsonDeserialize(as = ImmutableUserDto.class) 17 | public interface UserDto extends BasicDto { 18 | 19 | String lastName(); 20 | 21 | String firstName(); 22 | 23 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 24 | List shortenerIds(); 25 | 26 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 27 | Optional contactInformation(); 28 | 29 | static ImmutableUserDto.Builder builder() { 30 | return ImmutableUserDto.builder(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /user-api/src/main/java/com/mz/user/message/ContactInfoPayload.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.message; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 5 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 6 | import org.immutables.value.Value; 7 | 8 | import java.io.Serializable; 9 | import java.util.Optional; 10 | 11 | @Value.Immutable 12 | @JsonSerialize(as = ImmutableContactInfoPayload.class) 13 | @JsonDeserialize(as = ImmutableContactInfoPayload.class) 14 | public interface ContactInfoPayload extends Serializable { 15 | 16 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 17 | Optional userId(); 18 | 19 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 20 | Optional email(); 21 | 22 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 23 | Optional phoneNumber(); 24 | 25 | static ImmutableContactInfoPayload.Builder builder() { 26 | return ImmutableContactInfoPayload.builder(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /user-api/src/main/java/com/mz/user/message/UserPayload.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.message; 2 | 3 | import com.fasterxml.jackson.annotation.JsonInclude; 4 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 5 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 6 | import org.immutables.value.Value; 7 | 8 | import java.io.Serializable; 9 | import java.time.Instant; 10 | import java.util.Optional; 11 | 12 | @Value.Immutable 13 | @JsonSerialize(as = ImmutableUserPayload.class) 14 | @JsonDeserialize(as = ImmutableUserPayload.class) 15 | public interface UserPayload extends Serializable { 16 | 17 | String id(); 18 | 19 | Instant createdAt(); 20 | 21 | Long version(); 22 | 23 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 24 | Optional shortenerId(); 25 | 26 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 27 | Optional lastName(); 28 | 29 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 30 | Optional firstName(); 31 | 32 | @JsonInclude(JsonInclude.Include.NON_EMPTY) 33 | Optional contactInfo(); 34 | 35 | static ImmutableUserPayload.Builder builder() { 36 | return ImmutableUserPayload.builder(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /user-api/src/main/java/com/mz/user/message/command/CreateContactInfo.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.message.command; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import com.mz.reactivedemo.common.api.events.Command; 6 | import org.immutables.value.Value; 7 | 8 | import java.util.Optional; 9 | 10 | /** 11 | * Created by zemi on 13/01/2019. 12 | */ 13 | @Value.Immutable 14 | @JsonSerialize(as = ImmutableCreateContactInfo.class) 15 | @JsonDeserialize(as = ImmutableCreateContactInfo.class) 16 | public interface CreateContactInfo extends Command { 17 | 18 | Optional email(); 19 | 20 | Optional phoneNumber(); 21 | 22 | static ImmutableCreateContactInfo.Builder builder() { return ImmutableCreateContactInfo.builder(); } 23 | } 24 | -------------------------------------------------------------------------------- /user-api/src/main/java/com/mz/user/message/command/CreateUser.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.message.command; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import com.mz.reactivedemo.common.api.events.Command; 6 | import com.mz.user.message.ContactInfoPayload; 7 | import org.immutables.value.Value; 8 | 9 | import java.util.Optional; 10 | 11 | /** 12 | * Created by zemi on 13/01/2019. 13 | */ 14 | @Value.Immutable 15 | @JsonSerialize(as = ImmutableCreateUser.class) 16 | @JsonDeserialize(as = ImmutableCreateUser.class) 17 | public interface CreateUser extends Command { 18 | 19 | String firstName(); 20 | 21 | String lastName(); 22 | 23 | Optional contactInformation(); 24 | 25 | static ImmutableCreateUser.Builder builder() { 26 | return ImmutableCreateUser.builder(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /user-api/src/main/java/com/mz/user/message/event/UserChangedEvent.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.message.event; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import com.mz.reactivedemo.common.api.events.DomainEvent; 6 | import com.mz.user.message.UserPayload; 7 | import org.immutables.value.Value; 8 | 9 | @Value.Immutable 10 | @JsonSerialize(as = ImmutableUserChangedEvent.class) 11 | @JsonDeserialize(as = ImmutableUserChangedEvent.class) 12 | public interface UserChangedEvent extends DomainEvent { 13 | 14 | UserPayload payload(); 15 | 16 | UserEventType type(); 17 | 18 | static ImmutableUserChangedEvent.Builder builder() { 19 | return ImmutableUserChangedEvent.builder(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /user-api/src/main/java/com/mz/user/message/event/UserEventType.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.message.event; 2 | 3 | public enum UserEventType { 4 | USER_CREATED, 5 | USER_UPDATED, 6 | CONTACT_INFO_CREATED 7 | } 8 | -------------------------------------------------------------------------------- /user-api/src/main/java/com/mz/user/topics/UserTopics.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.topics; 2 | 3 | public interface UserTopics { 4 | 5 | String USER_DOCUMENT = "user-document"; 6 | 7 | String USER_CHANGED = "user-changed"; 8 | } 9 | -------------------------------------------------------------------------------- /user-impl/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM openjdk:11-jdk 2 | VOLUME /tmp 3 | 4 | ENV BROKERS=localhost:9092 5 | ENV ZK_NODES=localhost 6 | ENV MONGO_DB_HOST=localhost 7 | ENV MONGO_URI=mongodb://localhost:27017/user-ms-db 8 | ENV AUTO_CREATE_TOPICS=false 9 | 10 | COPY user-impl/target/*SNAPSHOT.jar app.jar 11 | 12 | ENTRYPOINT ["java","-jar","/app.jar",""] 13 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/UserApplication.java: -------------------------------------------------------------------------------- 1 | package com.mz.user; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * Created by zemi on 03/01/2019. 8 | */ 9 | @SpringBootApplication 10 | public class UserApplication { 11 | public static void main(String[] args) { 12 | SpringApplication.run(UserApplication.class, args); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/UserApplicationConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.mz.user; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.SerializationFeature; 5 | import com.mz.reactivedemo.adapter.persistance.AggregateFactory; 6 | import com.mz.reactivedemo.adapter.persistance.AggregatePersistenceConfiguration; 7 | import com.mz.reactivedemo.adapter.persistance.AggregateRepository; 8 | import com.mz.reactivedemo.adapter.persistance.AggregateService; 9 | import com.mz.reactivedemo.common.http.HttpErrorHandler; 10 | import com.mz.user.domain.aggregate.UserAggregate; 11 | import com.mz.user.dto.UserDto; 12 | import com.mz.user.impl.UserFunctions.PublishUserChangedEvent; 13 | import com.mz.user.impl.UserFunctions.PublishUserDocumentMessage; 14 | import com.mz.user.impl.UserFunctions.UpdateUserView; 15 | import org.apache.kafka.clients.consumer.ConsumerConfig; 16 | import org.apache.kafka.clients.producer.ProducerConfig; 17 | import org.apache.kafka.common.serialization.StringDeserializer; 18 | import org.apache.kafka.common.serialization.StringSerializer; 19 | import org.springframework.beans.factory.annotation.Value; 20 | import org.springframework.context.annotation.Bean; 21 | import org.springframework.context.annotation.Configuration; 22 | import org.springframework.context.annotation.Import; 23 | import org.springframework.context.annotation.Primary; 24 | import org.springframework.http.MediaType; 25 | import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; 26 | import org.springframework.web.reactive.function.server.RequestPredicates; 27 | import org.springframework.web.reactive.function.server.RouterFunction; 28 | import org.springframework.web.reactive.function.server.RouterFunctions; 29 | import org.springframework.web.reactive.function.server.ServerResponse; 30 | import reactor.core.publisher.Mono; 31 | import reactor.kafka.receiver.ReceiverOptions; 32 | import reactor.kafka.sender.KafkaSender; 33 | import reactor.kafka.sender.SenderOptions; 34 | 35 | import java.util.Collections; 36 | import java.util.HashMap; 37 | import java.util.Map; 38 | 39 | import static com.mz.reactivedemo.shortener.api.topics.ShortenerTopics.SHORTENER_CHANGED; 40 | import static org.springframework.web.reactive.function.server.RequestPredicates.GET; 41 | import static org.springframework.web.reactive.function.server.RequestPredicates.accept; 42 | 43 | @Configuration 44 | @Import(AggregatePersistenceConfiguration.class) 45 | public class UserApplicationConfiguration { 46 | 47 | @Bean 48 | public AggregateService aggregateService( 49 | UpdateUserView updateUserView, 50 | PublishUserChangedEvent publishUserChanged, 51 | PublishUserDocumentMessage publishDocumentMessage, 52 | AggregateRepository aggregateRepository) { 53 | return AggregateService.of(aggregateRepository, 54 | AggregateFactory.build(UserAggregate::of, UserAggregate::of), 55 | updateUserView, publishUserChanged, 56 | publishDocumentMessage); 57 | } 58 | 59 | @Bean 60 | public RouterFunction userRoute(UserHandler handler) { 61 | return RouterFunctions.route() 62 | .add(RouterFunctions.nest(RequestPredicates.path("/users"),handler.route())) 63 | .add(RouterFunctions.route(GET("/health/ticks") 64 | .and(accept(MediaType.APPLICATION_JSON_UTF8)), req -> ServerResponse.ok() 65 | .contentType(MediaType.APPLICATION_JSON_UTF8) 66 | .body(Mono.just("Tick"), String.class)) 67 | ) 68 | .onError(Throwable.class, HttpErrorHandler.FN::onError) 69 | .build(); 70 | } 71 | 72 | @Bean("kafkaReceiverOptionsShortenerChangedTopic") 73 | public ReceiverOptions kafkaReceiverOptionsShortenerChangedTopic( 74 | @Value("${kafka.bootstrap.servers}") String bootstrapServers, 75 | @Value("${kafka.consumer.group-id}") String consumerGroupId 76 | ) { 77 | Map consumerProps = new HashMap<>(); 78 | consumerProps.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); 79 | consumerProps.put(ConsumerConfig.GROUP_ID_CONFIG, consumerGroupId); 80 | consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); 81 | consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class); 82 | 83 | return ReceiverOptions.create(consumerProps) 84 | .subscription(Collections.singleton(SHORTENER_CHANGED)); 85 | } 86 | 87 | @Bean 88 | public KafkaSender kafkaSender(@Value("${kafka.bootstrap.servers}") String bootstrapServers) { 89 | Map producerProps = new HashMap<>(); 90 | producerProps.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers); 91 | producerProps.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); 92 | producerProps.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class); 93 | 94 | return KafkaSender.create(SenderOptions.create(producerProps).maxInFlight(1024)); 95 | } 96 | 97 | @Bean 98 | @Primary 99 | public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { 100 | ObjectMapper objectMapper = builder.build(); 101 | objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); 102 | return objectMapper; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/UserApplicationMessageBus.java: -------------------------------------------------------------------------------- 1 | package com.mz.user; 2 | 3 | import com.mz.reactivedemo.common.api.events.Event; 4 | import com.mz.user.dto.UserDto; 5 | import reactor.core.publisher.Flux; 6 | 7 | public interface UserApplicationMessageBus { 8 | 9 | void publishEvent(Event event); 10 | 11 | void publishDocumentMessage(UserDto dto); 12 | 13 | Flux events(); 14 | 15 | Flux documents(); 16 | } 17 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/UserApplicationService.java: -------------------------------------------------------------------------------- 1 | package com.mz.user; 2 | 3 | import com.mz.reactivedemo.common.api.events.Command; 4 | import com.mz.user.dto.UserDto; 5 | import com.mz.user.message.command.CreateContactInfo; 6 | import com.mz.user.message.command.CreateUser; 7 | import reactor.core.publisher.Mono; 8 | 9 | public interface UserApplicationService { 10 | Mono execute(Command cmd); 11 | 12 | Mono createUser(CreateUser command); 13 | Mono createContactInfo(String userId, CreateContactInfo command); 14 | } 15 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/UserHandler.java: -------------------------------------------------------------------------------- 1 | package com.mz.user; 2 | 3 | import com.mz.reactivedemo.common.http.HttpHandler; 4 | import com.mz.user.dto.UserDto; 5 | import com.mz.user.message.command.CreateContactInfo; 6 | import com.mz.user.message.command.CreateUser; 7 | import com.mz.user.view.UserQuery; 8 | import org.apache.commons.logging.Log; 9 | import org.apache.commons.logging.LogFactory; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.stereotype.Component; 12 | import org.springframework.web.reactive.function.server.RouterFunction; 13 | import org.springframework.web.reactive.function.server.RouterFunctions; 14 | import org.springframework.web.reactive.function.server.ServerRequest; 15 | import org.springframework.web.reactive.function.server.ServerResponse; 16 | import reactor.core.publisher.Mono; 17 | 18 | import static org.springframework.web.reactive.function.server.RequestPredicates.*; 19 | 20 | @Component 21 | public class UserHandler implements HttpHandler { 22 | 23 | private static final Log log = LogFactory.getLog(UserHandler.class); 24 | 25 | private final UserApplicationService userApplicationService; 26 | 27 | private final UserQuery userQuery; 28 | 29 | public UserHandler(UserApplicationService userApplicationService, UserQuery userQuery) { 30 | this.userApplicationService = userApplicationService; 31 | this.userQuery = userQuery; 32 | } 33 | 34 | private void logOnError(Throwable e) { 35 | log.error(e); 36 | } 37 | 38 | Mono createUser(ServerRequest request) { 39 | log.info("createUser() -> "); 40 | return request.bodyToMono(CreateUser.class) 41 | .flatMap(userApplicationService::createUser) 42 | .flatMap(this::mapToResponse) 43 | .doOnError(this::logOnError); 44 | } 45 | 46 | Mono createContactInfo(ServerRequest request) { 47 | log.info("createContactInfo() -> "); 48 | return Mono.just(request.pathVariable("userId")) 49 | .flatMap(userId -> request.bodyToMono(CreateContactInfo.class) 50 | .flatMap(cmd -> userApplicationService.createContactInfo(userId, cmd))) 51 | .flatMap(this::mapToResponse) 52 | .doOnError(this::logOnError); 53 | } 54 | 55 | Mono getAll(ServerRequest request) { 56 | log.info("getAll() -> "); 57 | return ServerResponse.ok() 58 | .contentType(MediaType.APPLICATION_JSON) 59 | .body(userQuery.getAll(), UserDto.class) 60 | .doOnError(this::logOnError); 61 | } 62 | 63 | Mono getById(ServerRequest request) { 64 | log.info("getById() -> "); 65 | return userQuery.getById(request.pathVariable("id")) 66 | .flatMap(this::mapToResponse) 67 | .doOnError(this::logOnError); 68 | } 69 | 70 | public RouterFunction route() { 71 | return RouterFunctions 72 | .route(POST("").and(accept(MediaType.APPLICATION_JSON)), this::createUser) 73 | .andRoute(PUT("/{userId}/contactinformation").and(accept(MediaType.APPLICATION_JSON)), 74 | this::createContactInfo) 75 | .andRoute(GET("").and(accept(MediaType.APPLICATION_JSON)), this::getAll) 76 | .andRoute(GET("/{id}").and(accept(MediaType.APPLICATION_JSON)), this::getById); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/UserMapper.java: -------------------------------------------------------------------------------- 1 | package com.mz.user; 2 | 3 | import com.mz.user.domain.event.ContactInfoCreated; 4 | import com.mz.user.domain.event.UserCreated; 5 | import com.mz.user.dto.ContactInfoDto; 6 | import com.mz.user.dto.UserDto; 7 | import com.mz.user.message.ContactInfoPayload; 8 | import com.mz.user.message.UserPayload; 9 | import com.mz.user.view.ContactInfoDocument; 10 | import com.mz.user.view.UserDocument; 11 | import org.eclipse.collections.impl.factory.Lists; 12 | 13 | import java.util.Optional; 14 | import java.util.function.Function; 15 | 16 | public enum UserMapper { 17 | 18 | FN; 19 | 20 | public final Function mapToDto = doc -> 21 | UserDto.builder() 22 | .id(doc.getId()) 23 | .firstName(doc.getFirstName()) 24 | .lastName(doc.getLastName()) 25 | .createdAt(doc.getCreatedAt()) 26 | .version(doc.getVersion()) 27 | .shortenerIds(Lists.immutable.ofAll(doc.getShortenerIds())) 28 | .contactInformation(Optional.ofNullable(doc.getContactInformationDocument()) 29 | .map(c -> ContactInfoDto.builder() 30 | .userId(doc.getId()) 31 | .createdAt(c.getCreatedAt()) 32 | .email(Optional.ofNullable(c.getEmail())) 33 | .phoneNumber(Optional.ofNullable(c.getPhoneNumber())) 34 | .build())) 35 | .build(); 36 | 37 | public final Function mapToDocument = dto -> { 38 | UserDocument userDocument = new UserDocument(dto.id(), dto.firstName(), 39 | dto.lastName(), dto.version(), dto.createdAt(), 40 | dto.contactInformation().map(this.mapToContactInfoDocument).orElse(null)); 41 | userDocument.setShortenerIds(dto.shortenerIds()); 42 | return userDocument; 43 | }; 44 | 45 | public final Function mapToContactInfoDocument = dto -> 46 | new ContactInfoDocument(dto.email().orElse(null), 47 | dto.phoneNumber().orElse(null), dto.createdAt()); 48 | 49 | public final Function mapCreatedToPayload = (e) -> { 50 | ContactInfoPayload infoPayload = ContactInfoPayload.builder() 51 | .userId(e.aggregateId()) 52 | .email(e.email()) 53 | .phoneNumber(e.phoneNumber()) 54 | .build(); 55 | return UserPayload.builder() 56 | .id(e.aggregateId()) 57 | .version(e.version()) 58 | .createdAt(e.eventCreatedAt()) 59 | .firstName(e.firstName()) 60 | .lastName(e.lastName()) 61 | .contactInfo(infoPayload) 62 | .version(e.version()) 63 | .build(); 64 | }; 65 | 66 | public final Function mapContactCreatedToPayload = e -> 67 | ContactInfoPayload.builder() 68 | .userId(e.aggregateId()) 69 | .phoneNumber(e.phoneNumber()) 70 | .email(e.email()) 71 | .build(); 72 | 73 | } 74 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/adapter/shortener/ShortenerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.adapter.shortener; 2 | 3 | public interface ShortenerAdapter { 4 | } 5 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/adapter/shortener/impl/ShortenerAdapterImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.adapter.shortener.impl; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.mz.reactivedemo.common.util.Logger; 5 | import com.mz.reactivedemo.shortener.api.event.ShortenerChangedEvent; 6 | import com.mz.reactivedemo.shortener.api.event.ShortenerEventType; 7 | import com.mz.user.UserApplicationMessageBus; 8 | import com.mz.user.UserApplicationService; 9 | import com.mz.user.adapter.shortener.ShortenerAdapter; 10 | import com.mz.user.domain.command.AddShortener; 11 | import org.apache.commons.logging.LogFactory; 12 | import org.apache.kafka.clients.consumer.ConsumerRecord; 13 | import org.springframework.beans.factory.annotation.Qualifier; 14 | import org.springframework.stereotype.Component; 15 | import reactor.kafka.receiver.KafkaReceiver; 16 | import reactor.kafka.receiver.ReceiverOptions; 17 | 18 | import javax.annotation.PostConstruct; 19 | 20 | import static com.mz.reactivedemo.common.KafkaMapper.FN; 21 | import static java.util.Objects.requireNonNull; 22 | 23 | @Component 24 | public class ShortenerAdapterImpl implements ShortenerAdapter { 25 | 26 | private final Logger logger = new Logger(LogFactory.getLog(ShortenerAdapterImpl.class)); 27 | 28 | private final UserApplicationMessageBus messageBus; 29 | 30 | private final UserApplicationService userApplicationService; 31 | 32 | private final ReceiverOptions kafkaReceiverOptionsShortenerChangedTopic; 33 | 34 | private final ObjectMapper objectMapper; 35 | 36 | public ShortenerAdapterImpl( 37 | UserApplicationMessageBus messageBus, 38 | UserApplicationService userApplicationService, 39 | @Qualifier("kafkaReceiverOptionsShortenerChangedTopic") ReceiverOptions kafkaReceiverOptionsShortenerChangedTopic, 40 | ObjectMapper objectMapper 41 | ) { 42 | this.messageBus = requireNonNull(messageBus, "messageBus is required"); 43 | this.userApplicationService = requireNonNull(userApplicationService, "userApplicationService is required"); 44 | this.kafkaReceiverOptionsShortenerChangedTopic = requireNonNull(kafkaReceiverOptionsShortenerChangedTopic, "kafkaReceiverOptionsShortenerChangedTopic is required"); 45 | this.objectMapper = requireNonNull(objectMapper, "objectMapper is required"); 46 | } 47 | 48 | @PostConstruct 49 | private void subscribeToTopics() { 50 | KafkaReceiver.create(kafkaReceiverOptionsShortenerChangedTopic).receive() 51 | .map(ConsumerRecord::value) 52 | .map(FN.mapFromJson(objectMapper, ShortenerChangedEvent.class)) 53 | .filter(event -> ShortenerEventType.CREATED.equals(event.type())) 54 | .retry() 55 | .subscribe(this::processShortenerChanged, this::processError); 56 | } 57 | 58 | private void processShortenerChanged(ShortenerChangedEvent changedEvent) { 59 | logger.debug(() -> "shortenerChanged ->"); 60 | AddShortener addShortener = AddShortener.builder() 61 | .shortenerId(changedEvent.payload().id()) 62 | .userId(changedEvent.payload().userId().get()) 63 | .build(); 64 | userApplicationService.execute(addShortener).subscribe(); 65 | } 66 | 67 | private void processError(Throwable error) { 68 | logger.log().error(error); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/domain/aggregate/ContactInfo.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.domain.aggregate; 2 | 3 | import com.mz.reactivedemo.common.aggregate.Id; 4 | import com.mz.user.dto.ContactInfoDto; 5 | import org.immutables.value.Value; 6 | 7 | import java.time.Instant; 8 | import java.util.Optional; 9 | 10 | /** 11 | * Created by zemi on 2019-01-18. 12 | */ 13 | @Value.Immutable 14 | public interface ContactInfo { 15 | 16 | Id userId(); 17 | 18 | Optional email(); 19 | 20 | Optional phoneNumber(); 21 | 22 | @Value.Default 23 | default Instant createdAt() { 24 | return Instant.now(); 25 | } 26 | 27 | default ContactInfoDto toDto() { 28 | return ContactInfoDto 29 | .builder() 30 | .createdAt(createdAt()) 31 | .email(email().map(m -> m.value)) 32 | .phoneNumber(phoneNumber().map(n -> n.value)) 33 | // .userId(userId().map(eventId -> eventId.value)) 34 | .build(); 35 | } 36 | 37 | static ImmutableContactInfo.Builder builder() { 38 | return ImmutableContactInfo.builder(); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/domain/aggregate/Email.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.domain.aggregate; 2 | 3 | import com.mz.reactivedemo.common.aggregate.StringValue; 4 | 5 | import java.util.regex.Pattern; 6 | 7 | /** 8 | * Created by zemi on 02/01/2019. 9 | */ 10 | public class Email extends StringValue { 11 | 12 | private static String regex = "^[a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+$"; 13 | 14 | private static Pattern pattern = Pattern.compile(regex); 15 | 16 | public Email(String email) { 17 | super(email); 18 | validateEmail(email); 19 | } 20 | 21 | private void validateEmail(String email) { 22 | if (!pattern.matcher(email).matches()) throw new RuntimeException("Email validation error"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/domain/aggregate/FirstName.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.domain.aggregate; 2 | 3 | import com.mz.reactivedemo.common.aggregate.StringValue; 4 | 5 | /** 6 | * Created by zemi on 02/01/2019. 7 | */ 8 | public class FirstName extends StringValue { 9 | public FirstName(String value) { 10 | super(value); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/domain/aggregate/LastName.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.domain.aggregate; 2 | 3 | import com.mz.reactivedemo.common.aggregate.StringValue; 4 | 5 | /** 6 | * Created by zemi on 02/01/2019. 7 | */ 8 | public class LastName extends StringValue { 9 | 10 | public LastName(String value) { 11 | super(value); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/domain/aggregate/PhoneNumber.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.domain.aggregate; 2 | 3 | import com.mz.reactivedemo.common.aggregate.StringValue; 4 | 5 | import java.util.regex.Pattern; 6 | 7 | /** 8 | * Created by zemi on 02/01/2019. 9 | */ 10 | public class PhoneNumber extends StringValue { 11 | 12 | private static String regex = "^(\\+|00)(?:[0-9] ?){6,14}[0-9]$"; 13 | 14 | private static Pattern pattern = Pattern.compile(regex); 15 | 16 | public PhoneNumber(String phoneNumber) { 17 | super(phoneNumber); 18 | validate(phoneNumber); 19 | } 20 | 21 | private void validate(String phoneNumber) { 22 | if (!pattern.matcher(phoneNumber).matches()) throw new RuntimeException("Phone number validation error"); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/domain/command/AddShortener.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.domain.command; 2 | 3 | import com.mz.reactivedemo.common.api.events.Command; 4 | import org.immutables.value.Value; 5 | 6 | @Value.Immutable 7 | public interface AddShortener extends Command { 8 | 9 | String userId(); 10 | 11 | String shortenerId(); 12 | 13 | static ImmutableAddShortener.Builder builder() { 14 | return ImmutableAddShortener.builder(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/domain/event/ContactInfoCreated.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.domain.event; 2 | 3 | import com.mz.reactivedemo.common.api.events.DomainEvent; 4 | import org.immutables.value.Value; 5 | 6 | import java.time.Instant; 7 | import java.util.Optional; 8 | 9 | /** 10 | * Created by zemi on 16/01/2019. 11 | */ 12 | @Value.Immutable 13 | public interface ContactInfoCreated extends DomainEvent { 14 | 15 | Instant createdAt(); 16 | 17 | Optional email(); 18 | 19 | Optional phoneNumber(); 20 | 21 | Long userVersion(); 22 | 23 | static ImmutableContactInfoCreated.Builder builder() { 24 | return ImmutableContactInfoCreated.builder(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/domain/event/ShortenerAdded.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.domain.event; 2 | 3 | import com.mz.reactivedemo.common.api.events.DomainEvent; 4 | import org.immutables.value.Value; 5 | 6 | @Value.Immutable 7 | public interface ShortenerAdded extends DomainEvent { 8 | 9 | String shortenerId(); 10 | 11 | Long userVersion(); 12 | 13 | static ImmutableShortenerAdded.Builder builder() { 14 | return ImmutableShortenerAdded.builder(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/domain/event/UserCreated.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.domain.event; 2 | 3 | import com.mz.reactivedemo.common.api.events.DomainEvent; 4 | import org.immutables.value.Value; 5 | 6 | import java.util.Optional; 7 | 8 | /** 9 | * Created by zemi on 16/01/2019. 10 | */ 11 | @Value.Immutable 12 | public interface UserCreated extends DomainEvent { 13 | 14 | String lastName(); 15 | 16 | String firstName(); 17 | 18 | Long version(); 19 | 20 | Optional email(); 21 | 22 | Optional phoneNumber(); 23 | 24 | static ImmutableUserCreated.Builder builder() { 25 | return ImmutableUserCreated.builder(); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/impl/UserApplicationMessageBusImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.impl; 2 | 3 | import com.mz.reactivedemo.common.api.events.Event; 4 | import com.mz.user.UserApplicationMessageBus; 5 | import com.mz.user.dto.UserDto; 6 | import org.springframework.stereotype.Service; 7 | import reactor.core.publisher.Flux; 8 | import reactor.core.publisher.FluxSink; 9 | import reactor.core.publisher.ReplayProcessor; 10 | import reactor.core.scheduler.Schedulers; 11 | 12 | import java.util.Optional; 13 | 14 | @Service 15 | public class UserApplicationMessageBusImpl implements UserApplicationMessageBus { 16 | 17 | protected final ReplayProcessor events = ReplayProcessor.create(1); 18 | 19 | protected final FluxSink eventSink = events.sink(); 20 | 21 | protected final ReplayProcessor documents = ReplayProcessor.create(1); 22 | 23 | protected final FluxSink documentsSink = documents.sink(); 24 | 25 | @Override 26 | public void publishEvent(Event event) { 27 | Optional.ofNullable(event).ifPresent(e -> eventSink.next(e)); 28 | } 29 | 30 | @Override 31 | public void publishDocumentMessage(UserDto dto) { 32 | Optional.ofNullable(dto).ifPresent(d -> documentsSink.next(d)); 33 | } 34 | 35 | @Override 36 | public Flux events() { 37 | return events.retry().publishOn(Schedulers.parallel()); 38 | } 39 | 40 | @Override 41 | public Flux documents() { 42 | return documents.retry().publishOn(Schedulers.parallel()); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/impl/UserApplicationServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.impl; 2 | 3 | import com.mz.reactivedemo.adapter.persistance.AggregateService; 4 | import com.mz.reactivedemo.common.api.events.Command; 5 | import com.mz.reactivedemo.common.util.Logger; 6 | import com.mz.reactivedemo.common.util.Match; 7 | import com.mz.user.UserApplicationService; 8 | import com.mz.user.domain.command.AddShortener; 9 | import com.mz.user.dto.UserDto; 10 | import com.mz.user.message.command.CreateContactInfo; 11 | import com.mz.user.message.command.CreateUser; 12 | import org.apache.commons.logging.LogFactory; 13 | import org.springframework.stereotype.Component; 14 | import reactor.core.publisher.Mono; 15 | 16 | import java.util.UUID; 17 | 18 | @Component 19 | public class UserApplicationServiceImpl 20 | implements UserApplicationService { 21 | 22 | private Logger logger = new Logger(LogFactory.getLog(UserApplicationServiceImpl.class)); 23 | 24 | private final AggregateService aggregateService; 25 | 26 | public UserApplicationServiceImpl(AggregateService aggregateService) { 27 | this.aggregateService = aggregateService; 28 | } 29 | 30 | @Override 31 | public Mono execute(Command cmd) { 32 | logger.debug(() -> "execute() ->"); 33 | return Match.>match(cmd) 34 | .when(AddShortener.class, c -> aggregateService.execute(c.userId(), c)) 35 | .when(CreateUser.class, this::createUser) 36 | .orElseGet(() -> Mono.empty()); 37 | } 38 | 39 | @Override 40 | public Mono createUser(CreateUser command) { 41 | logger.debug(() -> "createUser() ->"); 42 | return aggregateService.execute(UUID.randomUUID().toString(), command); 43 | } 44 | 45 | @Override 46 | public Mono createContactInfo(String userId, CreateContactInfo command) { 47 | logger.debug(() -> "createContactInfo() ->"); 48 | return aggregateService.execute(userId, command); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/impl/UserFunctions.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.impl; 2 | 3 | import com.mz.reactivedemo.common.api.events.DomainEvent; 4 | import com.mz.reactivedemo.common.util.Match; 5 | import com.mz.user.UserApplicationMessageBus; 6 | import com.mz.user.domain.event.ContactInfoCreated; 7 | import com.mz.user.domain.event.ShortenerAdded; 8 | import com.mz.user.domain.event.UserCreated; 9 | import com.mz.user.dto.UserDto; 10 | import com.mz.user.message.UserPayload; 11 | import com.mz.user.message.event.UserChangedEvent; 12 | import com.mz.user.message.event.UserEventType; 13 | import com.mz.user.view.UserRepository; 14 | import org.springframework.stereotype.Component; 15 | import reactor.core.publisher.Mono; 16 | 17 | import java.util.function.Consumer; 18 | import java.util.function.Function; 19 | 20 | import static com.mz.user.UserMapper.FN; 21 | 22 | public final class UserFunctions { 23 | 24 | private UserFunctions() {} 25 | 26 | @Component 27 | public static class PublishUserChangedEvent implements Consumer { 28 | 29 | private final UserApplicationMessageBus messageBus; 30 | 31 | public PublishUserChangedEvent(UserApplicationMessageBus messageBus) { 32 | this.messageBus = messageBus; 33 | } 34 | 35 | @Override 36 | public void accept(DomainEvent event) { 37 | Match.match(event) 38 | .when(UserCreated.class, e -> UserChangedEvent.builder() 39 | .aggregateId(e.aggregateId()) 40 | .payload(FN.mapCreatedToPayload.apply(e)) 41 | .type(UserEventType.USER_CREATED) 42 | .build()) 43 | .when(ContactInfoCreated.class, e -> UserChangedEvent.builder() 44 | .type(UserEventType.CONTACT_INFO_CREATED) 45 | .aggregateId(e.aggregateId()) 46 | .payload(UserPayload.builder() 47 | .id(e.aggregateId()) 48 | .version(e.userVersion()) 49 | .createdAt(e.createdAt()) 50 | .contactInfo(FN.mapContactCreatedToPayload.apply(e)) 51 | .build()) 52 | .build()) 53 | .when(ShortenerAdded.class, e -> UserChangedEvent.builder() 54 | .type(UserEventType.USER_UPDATED) 55 | .aggregateId(e.aggregateId()) 56 | .payload(UserPayload.builder() 57 | .id(e.aggregateId()) 58 | .shortenerId(e.shortenerId()) 59 | .version(e.userVersion()) 60 | .createdAt(e.eventCreatedAt()) 61 | .build()) 62 | .build() 63 | ) 64 | .get().ifPresent(messageBus::publishEvent); 65 | } 66 | } 67 | 68 | @Component 69 | public static class UpdateUserView implements Function> { 70 | 71 | private final UserRepository repository; 72 | 73 | public UpdateUserView(UserRepository repository) { 74 | this.repository = repository; 75 | } 76 | 77 | @Override 78 | public Mono apply(UserDto userDto) { 79 | return repository.save(FN.mapToDocument.apply(userDto)).map(FN.mapToDto); 80 | } 81 | } 82 | 83 | @Component 84 | public static class PublishUserDocumentMessage implements Consumer { 85 | 86 | private final UserApplicationMessageBus messageBus; 87 | 88 | public PublishUserDocumentMessage(UserApplicationMessageBus messageBus) { 89 | this.messageBus = messageBus; 90 | } 91 | 92 | @Override 93 | public void accept(UserDto userDto) { 94 | this.messageBus.publishDocumentMessage(userDto); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/port/kafka/UserProcessor.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.port.kafka; 2 | 3 | 4 | import com.fasterxml.jackson.core.JsonProcessingException; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import com.mz.reactivedemo.common.util.Logger; 7 | import com.mz.user.UserApplicationMessageBus; 8 | import com.mz.user.dto.UserDto; 9 | import com.mz.user.message.event.UserChangedEvent; 10 | import org.apache.commons.logging.LogFactory; 11 | import org.apache.kafka.clients.producer.ProducerRecord; 12 | import org.springframework.stereotype.Service; 13 | import reactor.core.scheduler.Schedulers; 14 | import reactor.kafka.sender.KafkaSender; 15 | import reactor.kafka.sender.SenderRecord; 16 | 17 | import javax.annotation.PostConstruct; 18 | 19 | import static com.mz.reactivedemo.common.KafkaMapper.FN; 20 | import static com.mz.user.topics.UserTopics.USER_CHANGED; 21 | import static com.mz.user.topics.UserTopics.USER_DOCUMENT; 22 | import static java.util.Objects.requireNonNull; 23 | 24 | @Service 25 | public class UserProcessor { 26 | 27 | private final Logger logger = new Logger(LogFactory.getLog(UserProcessor.class)); 28 | 29 | private final UserApplicationMessageBus messageBus; 30 | 31 | private final KafkaSender kafkaSender; 32 | 33 | private final ObjectMapper objectMapper; 34 | 35 | public UserProcessor( 36 | UserApplicationMessageBus messageBus, 37 | KafkaSender kafkaSender, 38 | ObjectMapper objectMapper 39 | ) { 40 | this.messageBus = requireNonNull(messageBus, "messageBus is required"); 41 | this.kafkaSender = requireNonNull(kafkaSender, "kafkaSender is required"); 42 | this.objectMapper = requireNonNull(objectMapper, "objectMapper is required"); 43 | } 44 | 45 | @PostConstruct 46 | void onInit() { 47 | logger.debug(() -> "UserProcessor.onInit() ->"); 48 | var userChangedStream = messageBus.events() 49 | .subscribeOn(Schedulers.parallel()) 50 | .filter(event -> event instanceof UserChangedEvent) 51 | .cast(UserChangedEvent.class) 52 | .map(FN.mapToRecord(USER_CHANGED, objectMapper, UserChangedEvent::aggregateId)); 53 | 54 | var userDocumentStream = messageBus.documents() 55 | .subscribeOn(Schedulers.parallel()) 56 | .map(this::mapUserDocumentSenderRecord); 57 | 58 | kafkaSender.send(userChangedStream) 59 | .doOnError(this::processError) 60 | .retry() 61 | .subscribe(); 62 | 63 | kafkaSender.send(userDocumentStream) 64 | .doOnError(this::processError) 65 | .retry() 66 | .subscribe(); 67 | } 68 | 69 | private void processError(Throwable throwable) { 70 | logger.log().error(throwable); 71 | } 72 | 73 | private SenderRecord mapUserDocumentSenderRecord(UserDto document) { 74 | try { 75 | final var producerRecord = new ProducerRecord<>( 76 | USER_DOCUMENT, document.id(), 77 | objectMapper.writeValueAsString(document) 78 | ); 79 | return SenderRecord.create(producerRecord, document); 80 | } catch (JsonProcessingException e) { 81 | throw new RuntimeException(e); 82 | } 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/port/rest/CreateContactInfoRequest.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.port.rest; 2 | 3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize; 4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize; 5 | import org.immutables.value.Value; 6 | 7 | import java.util.Optional; 8 | 9 | @Value.Immutable 10 | @JsonSerialize(as = ImmutableCreateContactInfoRequest.class) 11 | @JsonDeserialize(as = ImmutableCreateContactInfoRequest.class) 12 | public interface CreateContactInfoRequest { 13 | 14 | 15 | Optional email(); 16 | 17 | Optional phoneNumber(); 18 | 19 | static ImmutableCreateContactInfoRequest.Builder builder() { 20 | return ImmutableCreateContactInfoRequest.builder(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/view/ContactInfoDocument.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.view; 2 | 3 | import java.time.Instant; 4 | import java.util.Objects; 5 | 6 | /** 7 | * Created by zemi on 02/01/2019. 8 | */ 9 | public class ContactInfoDocument { 10 | 11 | private String email; 12 | 13 | private String phoneNumber; 14 | 15 | private Instant createdAt; 16 | 17 | public ContactInfoDocument() { 18 | } 19 | 20 | public ContactInfoDocument(String email, String phoneNumber, Instant createdAt) { 21 | this.email = email; 22 | this.phoneNumber = phoneNumber; 23 | this.createdAt = createdAt; 24 | } 25 | 26 | public String getEmail() { 27 | return email; 28 | } 29 | 30 | public void setEmail(String email) { 31 | this.email = email; 32 | } 33 | 34 | public String getPhoneNumber() { 35 | return phoneNumber; 36 | } 37 | 38 | public void setPhoneNumber(String phoneNumber) { 39 | this.phoneNumber = phoneNumber; 40 | } 41 | 42 | public Instant getCreatedAt() { 43 | return createdAt; 44 | } 45 | 46 | public void setCreatedAt(Instant createdAt) { 47 | this.createdAt = createdAt; 48 | } 49 | 50 | @Override 51 | public boolean equals(Object o) { 52 | if (this == o) return true; 53 | if (!(o instanceof ContactInfoDocument)) return false; 54 | ContactInfoDocument that = (ContactInfoDocument) o; 55 | return Objects.equals(email, that.email) && 56 | Objects.equals(phoneNumber, that.phoneNumber) && 57 | Objects.equals(createdAt, that.createdAt); 58 | } 59 | 60 | @Override 61 | public int hashCode() { 62 | return Objects.hash(email, phoneNumber, createdAt); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/view/UserDocument.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.view; 2 | 3 | import org.springframework.data.annotation.Id; 4 | import org.springframework.data.mongodb.core.mapping.Document; 5 | 6 | import java.time.Instant; 7 | import java.util.List; 8 | import java.util.Objects; 9 | 10 | /** 11 | * Created by zemi on 02/01/2019. 12 | */ 13 | @Document(collection = "user") 14 | public class UserDocument { 15 | 16 | @Id 17 | private String id; 18 | 19 | private Long version; 20 | 21 | private String firstName; 22 | 23 | private String lastName; 24 | 25 | private Instant createdAt; 26 | 27 | private List shortenerIds; 28 | 29 | private ContactInfoDocument contactInformationDocument; 30 | 31 | public UserDocument() { 32 | super(); 33 | } 34 | 35 | public UserDocument(String id, String firstName, String lastName, Long version, Instant createdAt, 36 | ContactInfoDocument contactInformationDocument) { 37 | this.id = id; 38 | this.version = version; 39 | this.firstName = firstName; 40 | this.lastName = lastName; 41 | this.createdAt = createdAt; 42 | this.contactInformationDocument = contactInformationDocument; 43 | } 44 | 45 | public List getShortenerIds() { 46 | return shortenerIds; 47 | } 48 | 49 | public void setShortenerIds(List shortenerIds) { 50 | this.shortenerIds = shortenerIds; 51 | } 52 | 53 | public String getFirstName() { 54 | return firstName; 55 | } 56 | 57 | public void setFirstName(String firstName) { 58 | this.firstName = firstName; 59 | } 60 | 61 | public String getLastName() { 62 | return lastName; 63 | } 64 | 65 | public void setLastName(String lastName) { 66 | this.lastName = lastName; 67 | } 68 | 69 | public Instant getCreatedAt() { 70 | return createdAt; 71 | } 72 | 73 | public void setCreatedAt(Instant createdAt) { 74 | this.createdAt = createdAt; 75 | } 76 | 77 | public String getId() { 78 | return id; 79 | } 80 | 81 | public void setId(String id) { 82 | this.id = id; 83 | } 84 | 85 | public Long getVersion() { 86 | return version; 87 | } 88 | 89 | public void setVersion(Long version) { 90 | this.version = version; 91 | } 92 | 93 | public ContactInfoDocument getContactInformationDocument() { 94 | return contactInformationDocument; 95 | } 96 | 97 | public void setContactInformationDocument(ContactInfoDocument contactInformationDocument) { 98 | this.contactInformationDocument = contactInformationDocument; 99 | } 100 | 101 | @Override 102 | public boolean equals(Object o) { 103 | if (this == o) return true; 104 | if (!(o instanceof UserDocument)) return false; 105 | UserDocument that = (UserDocument) o; 106 | return id.equals(that.id) && 107 | version.equals(that.version) && 108 | firstName.equals(that.firstName) && 109 | lastName.equals(that.lastName) && 110 | createdAt.equals(that.createdAt) && 111 | shortenerIds.equals(that.shortenerIds) && 112 | contactInformationDocument.equals(that.contactInformationDocument); 113 | } 114 | 115 | @Override 116 | public int hashCode() { 117 | return Objects.hash(id, version, firstName, lastName, createdAt, shortenerIds, contactInformationDocument); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/view/UserQuery.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.view; 2 | 3 | import com.mz.user.dto.UserDto; 4 | import reactor.core.publisher.Flux; 5 | import reactor.core.publisher.Mono; 6 | 7 | public interface UserQuery { 8 | 9 | Mono getById(String id); 10 | 11 | Flux getAll(); 12 | } 13 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/view/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.view; 2 | 3 | import org.springframework.data.repository.reactive.ReactiveCrudRepository; 4 | 5 | public interface UserRepository extends ReactiveCrudRepository { 6 | } 7 | -------------------------------------------------------------------------------- /user-impl/src/main/java/com/mz/user/view/impl/UserQueryImpl.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.view.impl; 2 | 3 | import com.mz.user.dto.UserDto; 4 | import com.mz.user.view.UserQuery; 5 | import com.mz.user.view.UserRepository; 6 | import org.springframework.stereotype.Service; 7 | import reactor.core.publisher.Flux; 8 | import reactor.core.publisher.Mono; 9 | 10 | import static com.mz.user.UserMapper.FN; 11 | 12 | @Service 13 | public class UserQueryImpl implements UserQuery { 14 | 15 | private final UserRepository repository; 16 | 17 | public UserQueryImpl(UserRepository repository) { 18 | this.repository = repository; 19 | } 20 | 21 | @Override 22 | public Mono getById(String id) { 23 | return repository.findById(id).map(FN.mapToDto); 24 | } 25 | 26 | @Override 27 | public Flux getAll() { 28 | return repository.findAll().map(FN.mapToDto); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /user-impl/src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | akka { 2 | persistence { 3 | journal { 4 | plugin = "akka-contrib-mongodb-persistence-journal" 5 | leveldb.native = false 6 | } 7 | snapshot-store { 8 | plugin = "akka-contrib-mongodb-persistence-snapshot" 9 | } 10 | } 11 | 12 | contrib { 13 | persistence.mongodb { 14 | mongo { 15 | mongouri = "mongodb://localhost:27017/user-ms-db" 16 | mongouri = ${?MONGO_URI} 17 | journal-collection = "journal" 18 | journal-index = "journal_index" 19 | snaps-collection = "snapshots" 20 | snaps-index = "snaps_index" 21 | journal-write-concern = "Acknowledged" 22 | #use-legacy-serialization = false 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /user-impl/src/main/resources/application.yaml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8093 3 | 4 | spring: 5 | data: 6 | mongodb: 7 | host: ${MONGO_DB_HOST:localhost}:27017 8 | 9 | kafka: 10 | bootstrap: 11 | servers: ${BROKERS:localhost:9092} 12 | 13 | consumer: 14 | group-id: user-ms -------------------------------------------------------------------------------- /user-impl/src/test/java/com/mz/user/domain/aggregate/EmailTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.domain.aggregate; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | /** 7 | * Created by zemi on 03/01/2019. 8 | */ 9 | public class EmailTest { 10 | 11 | @Test 12 | public void testEmail_OK() { 13 | String emailValue = "test@test.org"; 14 | 15 | Email email = new Email(emailValue); 16 | 17 | Assertions.assertNotNull(email); 18 | Assertions.assertEquals(email.value, emailValue); 19 | } 20 | 21 | @Test 22 | public void testEmail_Failed() { 23 | String emailValue = "@test.org"; 24 | Assertions.assertThrows(RuntimeException.class, ()-> new Email(emailValue)); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /user-impl/src/test/java/com/mz/user/domain/aggregate/PhoneNumberTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.domain.aggregate; 2 | 3 | import org.junit.jupiter.api.Assertions; 4 | import org.junit.jupiter.api.Test; 5 | 6 | /** 7 | * Created by zemi on 03/01/2019. 8 | */ 9 | public class PhoneNumberTest { 10 | 11 | @Test 12 | public void testNumber_OK() { 13 | String phoneNum1 = "+421 911 888 222"; 14 | 15 | PhoneNumber phoneNumber1 = new PhoneNumber(phoneNum1); 16 | Assertions.assertEquals(phoneNumber1.value, phoneNum1); 17 | 18 | String phoneNum2 = "00421 911 888 222"; 19 | 20 | PhoneNumber phoneNumber2 = new PhoneNumber(phoneNum2); 21 | Assertions.assertEquals(phoneNumber2.value, phoneNum2); 22 | } 23 | 24 | @Test 25 | public void testNumber_Failed() { 26 | Assertions.assertThrows(RuntimeException.class, () -> new PhoneNumber("+421a911888")); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /user-impl/src/test/java/com/mz/user/port/kafka/UserProcessorTest.java: -------------------------------------------------------------------------------- 1 | package com.mz.user.port.kafka; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.mz.reactivedemo.common.api.events.Event; 5 | import com.mz.user.UserApplicationMessageBus; 6 | import com.mz.user.message.UserPayload; 7 | import com.mz.user.message.event.UserChangedEvent; 8 | import com.mz.user.message.event.UserEventType; 9 | import org.junit.jupiter.api.Disabled; 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.mockito.InjectMocks; 13 | import org.mockito.Mock; 14 | import org.mockito.Mockito; 15 | import org.mockito.junit.jupiter.MockitoExtension; 16 | import org.springframework.messaging.Message; 17 | import org.springframework.messaging.MessageChannel; 18 | import reactor.core.publisher.Flux; 19 | import reactor.kafka.sender.KafkaSender; 20 | 21 | import java.time.Instant; 22 | import java.util.UUID; 23 | 24 | import static org.mockito.ArgumentMatchers.any; 25 | import static org.mockito.Mockito.verify; 26 | import static org.mockito.Mockito.when; 27 | 28 | @ExtendWith(MockitoExtension.class) 29 | class UserProcessorTest { 30 | 31 | @Mock 32 | UserApplicationMessageBus messageBus; 33 | 34 | @Mock 35 | KafkaSender kafkaSender; 36 | 37 | @Mock 38 | ObjectMapper objectMapper; 39 | 40 | @InjectMocks 41 | UserProcessor userProcessor; 42 | 43 | @Disabled 44 | @Test 45 | void processUserChangedEvent() { 46 | MessageChannel messageChangedChannel = Mockito.mock(MessageChannel.class); 47 | String id = UUID.randomUUID().toString(); 48 | Flux events = Flux.just(UserChangedEvent.builder() 49 | .aggregateId(id) 50 | .payload(UserPayload.builder() 51 | .firstName("FirstNameTest") 52 | .lastName("LastNameTest") 53 | .version(0L) 54 | .createdAt(Instant.now()) 55 | .id(id) 56 | .build()) 57 | .type(UserEventType.USER_CREATED) 58 | .build()); 59 | when(messageBus.events()).thenReturn(events); 60 | when(messageBus.documents()).thenReturn(Flux.empty()); 61 | 62 | userProcessor.onInit(); 63 | verify(messageChangedChannel, Mockito.atMost(1)).send(any(Message.class)); 64 | } 65 | 66 | } 67 | --------------------------------------------------------------------------------