├── .gitignore ├── LICENSE ├── README.md ├── api ├── build.gradle ├── manifest_development.yml └── src │ ├── docs │ └── asciidoc │ │ └── index.adoc │ ├── main │ ├── java │ │ └── io │ │ │ └── pivotal │ │ │ └── dmfrey │ │ │ └── eventStoreDemo │ │ │ ├── Application.java │ │ │ ├── config │ │ │ ├── RabbitConfig.java │ │ │ └── SecurityConfig.java │ │ │ ├── domain │ │ │ ├── config │ │ │ │ ├── ApiConfig.java │ │ │ │ ├── RestConfig.java │ │ │ │ └── package-info.java │ │ │ ├── model │ │ │ │ ├── Board.java │ │ │ │ ├── Story.java │ │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ └── service │ │ │ │ └── BoardService.java │ │ │ └── endpoint │ │ │ ├── ApiController.java │ │ │ └── package-info.java │ └── resources │ │ ├── application.properties │ │ ├── application.yml │ │ └── bootstrap.yml │ └── test │ └── java │ └── io │ └── pivotal │ └── dmfrey │ └── eventStoreDemo │ ├── ApplicationTests.java │ ├── domain │ └── service │ │ └── BoardServiceTests.java │ └── endpoint │ ├── ApiControllerContractTests.java │ └── ApiControllerTests.java ├── build.gradle ├── command ├── build.gradle ├── manifest_development.yml └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── pivotal │ │ │ └── dmfrey │ │ │ └── eventStoreDemo │ │ │ ├── Application.java │ │ │ ├── config │ │ │ ├── EventStoreConfig.java │ │ │ ├── RabbitConfig.java │ │ │ ├── SecurityConfig.java │ │ │ └── package-info.java │ │ │ ├── domain │ │ │ ├── client │ │ │ │ ├── BoardClient.java │ │ │ │ ├── eventStore │ │ │ │ │ ├── config │ │ │ │ │ │ ├── EventStoreClientConfig.java │ │ │ │ │ │ ├── RestConfig.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── service │ │ │ │ │ │ ├── EventStoreBoardClient.java │ │ │ │ │ │ └── package-info.java │ │ │ │ ├── kafka │ │ │ │ │ ├── config │ │ │ │ │ │ ├── KafkaClientConfig.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── service │ │ │ │ │ │ ├── BoardEventsStreamsProcessor.java │ │ │ │ │ │ ├── DomainEventSink.java │ │ │ │ │ │ ├── DomainEventSinkImpl.java │ │ │ │ │ │ ├── DomainEventSource.java │ │ │ │ │ │ ├── DomainEventSourceImpl.java │ │ │ │ │ │ ├── KafkaBoardClient.java │ │ │ │ │ │ └── package-info.java │ │ │ │ └── package-info.java │ │ │ ├── config │ │ │ │ ├── CommandConfig.java │ │ │ │ └── package-info.java │ │ │ ├── events │ │ │ │ ├── BoardInitialized.java │ │ │ │ ├── BoardRenamed.java │ │ │ │ ├── DomainEvent.java │ │ │ │ ├── DomainEventIgnored.java │ │ │ │ ├── DomainEvents.java │ │ │ │ ├── StoryAdded.java │ │ │ │ ├── StoryDeleted.java │ │ │ │ ├── StoryUpdated.java │ │ │ │ └── package-info.java │ │ │ ├── model │ │ │ │ ├── Board.java │ │ │ │ ├── Story.java │ │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ └── service │ │ │ │ ├── BoardService.java │ │ │ │ └── package-info.java │ │ │ └── endpoint │ │ │ ├── CommandsController.java │ │ │ └── package-info.java │ └── resources │ │ ├── application.properties │ │ ├── application.yml │ │ └── bootstrap.yml │ └── test │ ├── java │ └── io │ │ └── pivotal │ │ └── dmfrey │ │ └── eventStoreDemo │ │ ├── ApplicationTests.java │ │ ├── config │ │ └── UnitTestConfig.java │ │ ├── contracts │ │ ├── ApiBase.java │ │ └── KafkaBase.java │ │ ├── domain │ │ ├── client │ │ │ ├── eventStore │ │ │ │ └── EventStoreBoardClientTests.java │ │ │ └── kafka │ │ │ │ ├── KafkaBoardClientEmbeddedKafkaTests.java │ │ │ │ └── KafkaBoardClientTests.java │ │ ├── model │ │ │ └── BoardTests.java │ │ └── service │ │ │ └── BoardServiceTests.java │ │ └── endpoint │ │ └── CommandsControllerTests.java │ └── resources │ ├── application.yml │ └── contracts │ ├── api │ ├── shouldDeleteExistingStoryOnExistingBoard.groovy │ ├── shouldPatchExistingStoryOnExistingBoard.groovy │ ├── shouldPostNewBoard.groovy │ ├── shouldPostNewStoryOnExistingBoar.groovy │ └── shouldPutExistingBoard.groovy │ └── kafka │ └── shouldPublishBoardInitialized.bak ├── event-store ├── build.gradle ├── manifest_development.yml └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── pivotal │ │ │ └── dmfrey │ │ │ └── eventStoreDemo │ │ │ ├── Application.java │ │ │ ├── config │ │ │ ├── DefaultConfig.java │ │ │ ├── RabbitConfig.java │ │ │ ├── SecurityConfig.java │ │ │ └── package-info.java │ │ │ ├── domain │ │ │ ├── config │ │ │ │ ├── CloudDataConfig.java │ │ │ │ ├── EventStoreConfig.java │ │ │ │ └── package-info.java │ │ │ ├── model │ │ │ │ ├── DomainEvents.java │ │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ ├── persistence │ │ │ │ ├── DomainEventEntity.java │ │ │ │ ├── DomainEventsEntity.java │ │ │ │ ├── DomainEventsRepository.java │ │ │ │ └── package-info.java │ │ │ └── service │ │ │ │ ├── DomainEventService.java │ │ │ │ ├── NotificationPublisher.java │ │ │ │ ├── NotificationPublisherImpl.java │ │ │ │ ├── converters │ │ │ │ ├── JsonStringToTupleConverter.java │ │ │ │ ├── TupleToJsonStringConverter.java │ │ │ │ └── package-info.java │ │ │ │ └── package-info.java │ │ │ └── endpoint │ │ │ ├── EventStoreController.java │ │ │ └── package-info.java │ └── resources │ │ ├── application.properties │ │ ├── application.yml │ │ └── bootstrap.yml │ └── test │ └── java │ └── io │ └── pivotal │ └── dmfrey │ └── eventStoreDemo │ ├── ApplicationTests.java │ ├── domain │ └── service │ │ ├── DomainEventServiceTests.java │ │ └── NotificationPublisherTests.java │ └── endpoint │ └── EventStoreControllerTests.java ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── images ├── Event Source Demo - Event Store.png └── Event Source Demo - Kafka.png ├── query ├── build.gradle ├── manifest_development.yml └── src │ ├── main │ ├── java │ │ └── io │ │ │ └── pivotal │ │ │ └── dmfrey │ │ │ └── eventStoreDemo │ │ │ ├── Application.java │ │ │ ├── config │ │ │ ├── RabbitConfig.java │ │ │ ├── SecurityConfig.java │ │ │ └── package-info.java │ │ │ ├── domain │ │ │ ├── client │ │ │ │ ├── BoardClient.java │ │ │ │ ├── eventStore │ │ │ │ │ ├── config │ │ │ │ │ │ ├── EventStoreClientConfig.java │ │ │ │ │ │ ├── RestConfig.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── service │ │ │ │ │ │ ├── EventStoreBoardClient.java │ │ │ │ │ │ └── package-info.java │ │ │ │ ├── kafka │ │ │ │ │ ├── config │ │ │ │ │ │ ├── KafkaClientConfig.java │ │ │ │ │ │ └── package-info.java │ │ │ │ │ ├── package-info.java │ │ │ │ │ └── service │ │ │ │ │ │ ├── BoardEventsStreamsProcessor.java │ │ │ │ │ │ ├── DomainEventSink.java │ │ │ │ │ │ ├── DomainEventSinkImpl.java │ │ │ │ │ │ ├── KafkaBoardClient.java │ │ │ │ │ │ └── package-info.java │ │ │ │ └── package-info.java │ │ │ ├── config │ │ │ │ ├── QueryConfig.java │ │ │ │ └── package-info.java │ │ │ ├── events │ │ │ │ ├── BoardInitialized.java │ │ │ │ ├── BoardRenamed.java │ │ │ │ ├── DomainEvent.java │ │ │ │ ├── DomainEventIgnored.java │ │ │ │ ├── DomainEvents.java │ │ │ │ ├── StoryAdded.java │ │ │ │ ├── StoryDeleted.java │ │ │ │ ├── StoryUpdated.java │ │ │ │ └── package-info.java │ │ │ ├── model │ │ │ │ ├── Board.java │ │ │ │ ├── Story.java │ │ │ │ └── package-info.java │ │ │ ├── package-info.java │ │ │ └── service │ │ │ │ ├── BoardEventNotificationSink.java │ │ │ │ ├── BoardService.java │ │ │ │ └── package-info.java │ │ │ └── endpoint │ │ │ ├── QueryController.java │ │ │ ├── model │ │ │ ├── BoardModel.java │ │ │ └── package-info.java │ │ │ └── package-info.java │ └── resources │ │ ├── application.properties │ │ ├── application.yml │ │ └── bootstrap.yml │ └── test │ ├── java │ └── io │ │ └── pivotal │ │ └── dmfrey │ │ └── eventStoreDemo │ │ ├── ApplicationTests.java │ │ ├── config │ │ └── UnitTestConfig.java │ │ ├── contracts │ │ └── ApiBase.java │ │ ├── domain │ │ ├── client │ │ │ ├── eventStore │ │ │ │ └── service │ │ │ │ │ └── EventStoreBoardClientTests.java │ │ │ └── kafka │ │ │ │ └── service │ │ │ │ └── KafkaBoardClientEmbeddedKafkaTests.java │ │ └── service │ │ │ ├── BoardEventNotificationSinkTests.java │ │ │ └── BoardServiceTests.java │ │ └── endpoint │ │ └── QueryControllerTests.java │ └── resources │ ├── application.yml │ └── contracts │ └── api │ └── shouldGetExistingBoard.groovy └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /build/ 3 | **/build/ 4 | /out/ 5 | **/out/* 6 | !gradle/wrapper/gradle-wrapper.jar 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | 16 | ### IntelliJ IDEA ### 17 | .idea 18 | *.iws 19 | *.iml 20 | *.ipr 21 | 22 | ### NetBeans ### 23 | nbproject/private/ 24 | build/ 25 | nbbuild/ 26 | dist/ 27 | nbdist/ 28 | .nb-gradle/ -------------------------------------------------------------------------------- /api/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | plugins { 3 | id "org.asciidoctor.convert" version "1.5.6" 4 | } 5 | 6 | ext { 7 | snippetsDir = file('build/generated-snippets') 8 | } 9 | 10 | dependencies { 11 | 12 | asciidoctor 'org.springframework.restdocs:spring-restdocs-asciidoctor' 13 | 14 | // Spring Cloud dependencies 15 | compile('org.springframework.cloud:spring-cloud-stream-binder-rabbit') 16 | 17 | compile('org.springframework.cloud:spring-cloud-starter-openfeign') 18 | compile('io.github.openfeign:feign-httpclient') 19 | 20 | 21 | // Test dependencies 22 | testCompile 'org.springframework.restdocs:spring-restdocs-mockmvc' 23 | 24 | } 25 | 26 | test { 27 | outputs.dir snippetsDir 28 | } 29 | 30 | asciidoctor { 31 | attributes 'snippets': snippetsDir 32 | inputs.dir snippetsDir 33 | dependsOn test 34 | } 35 | 36 | bootJar { 37 | dependsOn asciidoctor 38 | from ("${asciidoctor.outputDir}/html5") { 39 | into 'static/docs' 40 | } 41 | } -------------------------------------------------------------------------------- /api/manifest_development.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: esd-api 4 | host: esd-api-development 5 | buildpack: java_buildpack_offline 6 | memory: 1024M 7 | instances: 2 8 | path: build/libs/api-0.0.1-SNAPSHOT.jar 9 | services: 10 | - discovery-service 11 | - hystrix-dashboard 12 | - rabbitmq 13 | env: 14 | JAVA_OPTS: -Djava.security.egd=file:///dev/urandom 15 | SPRING_PROFILES_ACTIVE: event-store -------------------------------------------------------------------------------- /api/src/docs/asciidoc/index.adoc: -------------------------------------------------------------------------------- 1 | :toc: left 2 | = Kandban Board API Documentation 3 | 4 | == POST /boards - Create New Board 5 | 6 | === Request 7 | .curl 8 | include::{snippets}/create-board/curl-request.adoc[] 9 | 10 | .http 11 | include::{snippets}/create-board/http-request.adoc[] 12 | 13 | === Success Responses 14 | .http 15 | include::{snippets}/create-board/http-response.adoc[] 16 | 17 | 18 | == GET /boards/{boardUuid} - Get Board 19 | 20 | === Path Variables 21 | .http 22 | include::{snippets}/get-board/path-parameters.adoc[] 23 | 24 | === Request 25 | .curl 26 | include::{snippets}/get-board/curl-request.adoc[] 27 | 28 | .http 29 | include::{snippets}/get-board/http-request.adoc[] 30 | 31 | === Success Responses 32 | .http 33 | include::{snippets}/get-board/http-response.adoc[] 34 | 35 | 36 | == PATCH /boards/{boardUuid} - Rename Board 37 | 38 | === Path Variables 39 | .http 40 | include::{snippets}/rename-board/path-parameters.adoc[] 41 | 42 | === Request 43 | .curl 44 | include::{snippets}/rename-board/curl-request.adoc[] 45 | 46 | .http 47 | include::{snippets}/rename-board/http-request.adoc[] 48 | 49 | === Success Responses 50 | .http 51 | include::{snippets}/rename-board/http-response.adoc[] 52 | 53 | 54 | == POST /boards/{boardUuid}/stories - Add Story 55 | 56 | === Path Variables 57 | .http 58 | include::{snippets}/add-story/path-parameters.adoc[] 59 | 60 | === Request 61 | .curl 62 | include::{snippets}/add-story/curl-request.adoc[] 63 | 64 | .http 65 | include::{snippets}/add-story/http-request.adoc[] 66 | 67 | === Success Responses 68 | .http 69 | include::{snippets}/add-story/http-response.adoc[] 70 | 71 | 72 | == PUT /boards/{boardUuid}/stories/{storyUuid} - Update Story 73 | 74 | === Path Variables 75 | .http 76 | include::{snippets}/update-story/path-parameters.adoc[] 77 | 78 | === Request 79 | .curl 80 | include::{snippets}/update-story/curl-request.adoc[] 81 | 82 | .http 83 | include::{snippets}/update-story/http-request.adoc[] 84 | 85 | === Success Responses 86 | .http 87 | include::{snippets}/update-story/http-response.adoc[] 88 | 89 | 90 | == DELETE /boards/{boardUuid}/stories/{storyUuid} - Delete Story 91 | 92 | === Path Variables 93 | .http 94 | include::{snippets}/delete-story/path-parameters.adoc[] 95 | 96 | === Request 97 | .curl 98 | include::{snippets}/delete-story/curl-request.adoc[] 99 | 100 | .http 101 | include::{snippets}/delete-story/http-request.adoc[] 102 | 103 | === Success Responses 104 | .http 105 | include::{snippets}/delete-story/http-response.adoc[] 106 | -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/Application.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; 6 | 7 | @SpringBootApplication 8 | @EnableCircuitBreaker 9 | public class Application { 10 | 11 | public static void main( String[] args ) { 12 | 13 | SpringApplication.run( Application.class, args ); 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/RabbitConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; 2 | 3 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 4 | import org.springframework.cloud.config.java.AbstractCloudConfig; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | 9 | @Configuration 10 | public class RabbitConfig { 11 | 12 | 13 | @Configuration 14 | @Profile("cloud") 15 | public static class CloudRabbitConfig extends AbstractCloudConfig { 16 | 17 | @Bean 18 | public ConnectionFactory rabbitConnectionFactory() { 19 | 20 | return connectionFactory().rabbitConnectionFactory(); 21 | } 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 7 | 8 | @Configuration 9 | @EnableWebSecurity 10 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 11 | 12 | @Override 13 | protected void configure( HttpSecurity httpSecurity ) throws Exception { 14 | 15 | httpSecurity 16 | .authorizeRequests() 17 | .anyRequest().permitAll() 18 | .and() 19 | .csrf().disable(); 20 | 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/config/ApiConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.BoardService; 4 | import org.springframework.context.annotation.Bean; 5 | import org.springframework.context.annotation.Configuration; 6 | 7 | @Configuration 8 | public class ApiConfig { 9 | 10 | @Bean 11 | public BoardService boardService( final RestConfig.CommandClient commandClient, final RestConfig.QueryClient queryClient ) { 12 | 13 | return new BoardService( commandClient, queryClient ); 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/config/RestConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 4 | import org.springframework.cloud.openfeign.EnableFeignClients; 5 | import org.springframework.cloud.openfeign.FeignClient; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.util.UUID; 11 | 12 | @Configuration 13 | @EnableFeignClients 14 | public class RestConfig { 15 | 16 | @FeignClient( value = "esd-command" ) 17 | public interface CommandClient { 18 | 19 | @PostMapping( path = "/boards/" ) 20 | ResponseEntity createBoard(); 21 | 22 | @PatchMapping( path = "/boards/{boardUuid}" ) 23 | ResponseEntity renameBoard( @PathVariable( "boardUuid" ) UUID boardUuid, @RequestParam( name = "name", required = true ) String name ); 24 | 25 | @PostMapping( path = "/boards/{boardUuid}/stories" ) 26 | ResponseEntity addStory( @PathVariable( "boardUuid" ) UUID boardUuid, @RequestParam( name = "name", required = true ) String name ); 27 | 28 | @PutMapping( path = "/boards/{boardUuid}/stories/{storyUuid}" ) 29 | ResponseEntity updateStory( @PathVariable( "boardUuid" ) UUID boardUuid, @PathVariable( "storyUuid" ) UUID storyUuid, @RequestParam( "name" ) String name ); 30 | 31 | @DeleteMapping( path = "/boards/{boardUuid}/stories/{storyUuid}" ) 32 | ResponseEntity deleteStory( @PathVariable( "boardUuid" ) UUID boardUuid, @PathVariable( "storyUuid" ) UUID storyUuid ); 33 | 34 | } 35 | 36 | @FeignClient( value = "esd-query" ) 37 | public interface QueryClient { 38 | 39 | @GetMapping( path = "/boards/{boardUuid}" ) 40 | ResponseEntity board( @PathVariable( "boardUuid" ) UUID boardId ); 41 | 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/config/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.config; -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/model/Board.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.model; 2 | 3 | import lombok.Data; 4 | import lombok.extern.slf4j.Slf4j; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | 9 | @Data 10 | @Slf4j 11 | public class Board { 12 | 13 | private String name; 14 | private Collection backlog = new ArrayList<>(); 15 | 16 | } 17 | -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/model/Story.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Data; 5 | 6 | import java.util.UUID; 7 | 8 | @Data 9 | @JsonIgnoreProperties( ignoreUnknown = true ) 10 | public class Story { 11 | 12 | private UUID storyUuid; 13 | private String name; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/model/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.model; -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain; -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/BoardService.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; 2 | 3 | import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.config.RestConfig; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 6 | import org.springframework.http.ResponseEntity; 7 | 8 | import java.util.UUID; 9 | 10 | public class BoardService { 11 | 12 | private final RestConfig.CommandClient commandClient; 13 | private final RestConfig.QueryClient queryClient; 14 | 15 | public BoardService( final RestConfig.CommandClient commandClient, final RestConfig.QueryClient queryClient ) { 16 | 17 | this.commandClient = commandClient; 18 | this.queryClient = queryClient; 19 | 20 | } 21 | 22 | @HystrixCommand 23 | public ResponseEntity createBoard() { 24 | 25 | return this.commandClient.createBoard(); 26 | } 27 | 28 | @HystrixCommand 29 | public ResponseEntity renameBoard( final UUID boardUuid, final String name ) { 30 | 31 | return this.commandClient.renameBoard( boardUuid, name ); 32 | } 33 | 34 | @HystrixCommand 35 | public ResponseEntity addStory( final UUID boardUuid, final String name ) { 36 | 37 | return this.commandClient.addStory( boardUuid, name ); 38 | } 39 | 40 | @HystrixCommand 41 | public ResponseEntity updateStory( final UUID boardUuid, final UUID storyUuid, final String name ) { 42 | 43 | return this.commandClient.updateStory( boardUuid, storyUuid, name ); 44 | } 45 | 46 | @HystrixCommand 47 | public ResponseEntity deleteStory( final UUID boardUuid, final UUID storyUuid ) { 48 | 49 | return this.commandClient.deleteStory( boardUuid, storyUuid ); 50 | } 51 | 52 | @HystrixCommand 53 | public ResponseEntity board( final UUID boardUuid ) { 54 | 55 | return this.queryClient.board( boardUuid ); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/ApiController.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.BoardService; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.*; 8 | import org.springframework.web.util.UriComponentsBuilder; 9 | 10 | import java.util.UUID; 11 | 12 | @RestController 13 | @RequestMapping( "/boards" ) 14 | @Slf4j 15 | public class ApiController { 16 | 17 | private final BoardService service; 18 | 19 | public ApiController( final BoardService service ) { 20 | 21 | this.service = service; 22 | 23 | } 24 | 25 | @PostMapping 26 | public ResponseEntity createBoard() { 27 | log.info( "createBoard : enter" ); 28 | 29 | return this.service.createBoard(); 30 | } 31 | 32 | @GetMapping( path = "/{boardUuid}" ) 33 | public ResponseEntity board( @PathVariable( "boardUuid" ) UUID boardUuid ) { 34 | log.info( "board : enter" ); 35 | 36 | ResponseEntity responseEntity = this.service.board( boardUuid ); 37 | log.info( "board : responseEntity=" + responseEntity ); 38 | 39 | log.info( "board : exit" ); 40 | return responseEntity; 41 | } 42 | 43 | @PatchMapping( "/{boardUuid}" ) 44 | public ResponseEntity renameBoard( @PathVariable( "boardUuid" ) UUID boardUuid, @RequestParam( "name" ) String name, final UriComponentsBuilder uriComponentsBuilder ) { 45 | log.info( "renameBoard : enter" ); 46 | 47 | return this.service.renameBoard( boardUuid, name ); 48 | } 49 | 50 | @PostMapping( "/{boardUuid}/stories" ) 51 | public ResponseEntity addStoryToBoard( @PathVariable( "boardUuid" ) UUID boardUuid, @RequestParam( "name" ) String name, final UriComponentsBuilder uriComponentsBuilder ) { 52 | log.info( "addStoryToBoardBoard : enter" ); 53 | 54 | return this.service.addStory( boardUuid, name ); 55 | } 56 | 57 | @PutMapping( "/{boardUuid}/stories/{storyUuid}" ) 58 | public ResponseEntity updateStoryOnBoard( @PathVariable( "boardUuid" ) UUID boardUuid, @PathVariable( "storyUuid" ) UUID storyUuid, @RequestParam( "name" ) String name ) { 59 | log.info( "updateStoryOnBoard : enter" ); 60 | 61 | return this.service.updateStory( boardUuid, storyUuid, name ); 62 | } 63 | 64 | @DeleteMapping( "/{boardUuid}/stories/{storyUuid}" ) 65 | public ResponseEntity removeStoryFromBoard( @PathVariable( "boardUuid" ) UUID boardUuid, @PathVariable( "storyUuid" ) UUID storyUuid ) { 66 | log.info( "removeStoryFromBoard : enter" ); 67 | 68 | return this.service.deleteStory( boardUuid, storyUuid ); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /api/src/main/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint; -------------------------------------------------------------------------------- /api/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | management.endpoints.web.exposure.include=* 2 | -------------------------------------------------------------------------------- /api/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | debug: false 2 | 3 | logger.level: 4 | io.pivotal.dmfrey: DEBUG 5 | org.springframework.web: DEBUG 6 | com.netflix.feign: DEBUG 7 | 8 | server: 9 | port: ${PORT:8765} 10 | use-forward-headers: true 11 | tomcat: 12 | remote-ip-header: x-forwarded-for 13 | protocol-header: x-forwarded-proto 14 | 15 | spring: 16 | jackson: 17 | serialization: 18 | write_dates_as_timestamps: false 19 | -------------------------------------------------------------------------------- /api/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: esd-api 4 | 5 | eureka.instance.leaseRenewalIntervalInSeconds: 15 6 | 7 | --- 8 | spring: 9 | profiles: cloud 10 | 11 | cloud: 12 | services: 13 | registrationMethod: direct 14 | 15 | eureka.instance.leaseRenewalIntervalInSeconds: 30 16 | -------------------------------------------------------------------------------- /api/src/test/java/io/pivotal/dmfrey/eventStoreDemo/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo; 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( properties = { 10 | "--spring.cloud.service-registry.auto-registration.enabled=false" 11 | }) 12 | public class ApplicationTests { 13 | 14 | @Test 15 | public void contextLoads() { 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /api/src/test/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/ApiControllerContractTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint; 2 | 3 | 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; 8 | import org.springframework.boot.test.context.SpringBootTest; 9 | import org.springframework.cloud.contract.stubrunner.spring.AutoConfigureStubRunner; 10 | import org.springframework.http.MediaType; 11 | import org.springframework.test.annotation.DirtiesContext; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | import org.springframework.test.web.reactive.server.WebTestClient; 14 | 15 | import static org.springframework.cloud.contract.stubrunner.spring.StubRunnerProperties.StubsMode.LOCAL; 16 | import static org.springframework.http.HttpHeaders.LOCATION; 17 | 18 | @RunWith( SpringRunner.class ) 19 | @SpringBootTest( 20 | properties = { 21 | "stubrunner.idsToServiceIds.command=esd-command", 22 | "stubrunner.idsToServiceIds.query=esd-query", 23 | "spring.cloud.discovery.enabled=false", 24 | "spring.cloud.service-registry.auto-registration.enabled=false" 25 | } 26 | ) 27 | @AutoConfigureStubRunner( 28 | ids = { 29 | "io.pivotal.dmfrey:command:+:stubs:8095", 30 | "io.pivotal.dmfrey:query:+:stubs:8096" 31 | }, 32 | // stubsPerConsumer = true, 33 | stubsMode = LOCAL 34 | ) 35 | @AutoConfigureWebTestClient 36 | @DirtiesContext 37 | public class ApiControllerContractTests { 38 | 39 | @Autowired 40 | private ApiController controller; 41 | 42 | private String boardUuid = "11111111-90ab-cdef-1234-567890abcdef"; 43 | private String storyUuid = "10240df9-4a1e-4fa4-bbd1-0bb33d764603"; 44 | 45 | @Test 46 | public void testCreateBoard() throws Exception { 47 | 48 | WebTestClient.bindToController( this.controller ) 49 | .build() 50 | .post() 51 | .uri( "/boards" ) 52 | .exchange() 53 | .expectStatus().isCreated() 54 | .expectHeader().exists( LOCATION ); 55 | 56 | } 57 | 58 | @Test 59 | public void testRenameBoard() throws Exception { 60 | 61 | WebTestClient.bindToController( this.controller ) 62 | .build() 63 | .patch() 64 | .uri( "/boards/{boardUuid}?name={name}", boardUuid, "New Name" ) 65 | .exchange() 66 | .expectStatus().isAccepted(); 67 | 68 | } 69 | 70 | @Test 71 | public void testCreateStoryOnBoard() throws Exception { 72 | 73 | WebTestClient.bindToController( this.controller ) 74 | .build() 75 | .post() 76 | .uri( "/boards/{boardUuid}/stories?name={name}", boardUuid, "New Story Name" ) 77 | .exchange() 78 | .expectStatus().isCreated() 79 | .expectHeader().exists( LOCATION ); 80 | 81 | } 82 | 83 | @Test 84 | public void testUpdateStoryOnBoard() throws Exception { 85 | 86 | WebTestClient.bindToController( this.controller ) 87 | .build() 88 | .put() 89 | .uri( "/boards/{boardUuid}/stories/{storyUuid}?name={name}", boardUuid, storyUuid, "Updated Story Name" ) 90 | .exchange() 91 | .expectStatus().isAccepted(); 92 | 93 | } 94 | 95 | @Test 96 | public void testDeleteStoryOnBoard() throws Exception { 97 | 98 | WebTestClient.bindToController( this.controller ) 99 | .build() 100 | .delete() 101 | .uri( "/boards/{boardUuid}/stories/{storyUuid}", boardUuid, storyUuid ) 102 | .exchange() 103 | .expectStatus().isAccepted(); 104 | 105 | } 106 | 107 | @Test 108 | public void testGetExistingBoard() throws Exception { 109 | 110 | WebTestClient.bindToController( this.controller ) 111 | .build() 112 | .get() 113 | .uri( "/boards/{boardUuid}", boardUuid ) 114 | .accept( MediaType.APPLICATION_JSON ) 115 | .exchange() 116 | .expectStatus().isOk(); 117 | // .expectBody().jsonPath( "$.name", "New Board" ); 118 | 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '2.0.4.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | maven { url "https://repo.spring.io/snapshot" } 8 | maven { url "https://repo.spring.io/milestone" } 9 | } 10 | dependencies { 11 | classpath("io.spring.gradle:dependency-management-plugin:1.0.6.RELEASE") 12 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 13 | classpath "org.springframework.cloud:spring-cloud-contract-gradle-plugin:2.0.1.RELEASE" 14 | } 15 | } 16 | 17 | apply plugin: 'idea' 18 | apply plugin: 'eclipse' 19 | 20 | group 'io.pivotal.dmfrey' 21 | version '1.0-SNAPSHOT' 22 | 23 | subprojects { 24 | 25 | apply plugin: 'java' 26 | apply plugin: 'eclipse' 27 | apply plugin: 'maven' 28 | apply plugin: 'org.springframework.boot' 29 | apply plugin: 'spring-cloud-contract' 30 | apply plugin: 'io.spring.dependency-management' 31 | 32 | group = 'io.pivotal.dmfrey' 33 | version = '0.0.1-SNAPSHOT' 34 | sourceCompatibility = 1.8 35 | targetCompatibility = 1.8 36 | 37 | repositories { 38 | mavenCentral() 39 | maven { url "https://repo.spring.io/snapshot" } 40 | maven { url "https://repo.spring.io/milestone" } 41 | maven { url "https://repo.spring.io/plugins-release" } 42 | maven { url "https://repo.spring.io/plugins-milestone" } 43 | } 44 | 45 | ext { 46 | springCloudVersion = 'Finchley.SR1' 47 | } 48 | 49 | dependencies { 50 | 51 | // Spring dependencies 52 | 53 | 54 | // Spring Boot dependencies 55 | compile('org.springframework.boot:spring-boot-starter-actuator') 56 | compile('org.springframework.boot:spring-boot-starter-web') 57 | compile('org.springframework.boot:spring-boot-starter-webflux') 58 | compile('org.springframework.boot:spring-boot-starter-security') 59 | 60 | 61 | // Spring Cloud dependencies 62 | compile('org.springframework.cloud:spring-cloud-starter-netflix-eureka-client') 63 | compile('io.pivotal.spring.cloud:spring-cloud-services-starter-service-registry') 64 | 65 | compile('org.springframework.cloud:spring-cloud-starter-netflix-hystrix') 66 | compile('io.pivotal.spring.cloud:spring-cloud-services-starter-circuit-breaker') 67 | 68 | compile('org.springframework.cloud:spring-cloud-starter-sleuth') 69 | 70 | // Third-party dependencies 71 | compile('com.rabbitmq:amqp-client:5.2.0') 72 | compile('io.vavr:vavr:0.9.2') 73 | 74 | annotationProcessor( 75 | [group: 'org.projectlombok', name: 'lombok', version: '1.16.20'], 76 | ) 77 | compile('org.projectlombok:lombok:1.16.20') 78 | 79 | // Test dependencies 80 | testCompile('org.springframework.boot:spring-boot-starter-test') 81 | testCompile('org.springframework.cloud:spring-cloud-starter-contract-verifier') 82 | testCompile('org.springframework.cloud:spring-cloud-starter-contract-stub-runner') 83 | 84 | } 85 | 86 | dependencyManagement { 87 | 88 | imports { 89 | mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" 90 | mavenBom 'org.springframework.cloud:spring-cloud-contract-dependencies:2.0.1.RELEASE' 91 | mavenBom "io.pivotal.spring.cloud:spring-cloud-services-dependencies:2.0.1.RELEASE" 92 | } 93 | 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /command/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | 4 | // Spring dependencies 5 | compile('org.springframework.kafka:spring-kafka') 6 | 7 | 8 | // Spring Boot dependencies 9 | 10 | 11 | // Spring Cloud dependencies 12 | compile('org.springframework.cloud:spring-cloud-starter-openfeign') 13 | compile('io.github.openfeign:feign-httpclient') 14 | 15 | compile('org.springframework.cloud:spring-cloud-stream-binder-rabbit') 16 | compile('org.springframework.cloud:spring-cloud-stream-binder-kafka') 17 | compile('org.springframework.cloud:spring-cloud-stream-binder-kafka-core') 18 | compile('org.springframework.cloud:spring-cloud-stream-binder-kafka-streams') 19 | 20 | 21 | // Third-party dependencies 22 | 23 | 24 | // Test dependencies 25 | testCompile('org.springframework.kafka:spring-kafka-test') 26 | testCompile('org.springframework.cloud:spring-cloud-stream-binder-test') 27 | 28 | } 29 | 30 | contracts { 31 | 32 | basePackageForTests = 'io.pivotal.dmfrey.eventStoreDemo' 33 | packageWithBaseClasses = 'io.pivotal.dmfrey.eventStoreDemo.contracts' 34 | 35 | } -------------------------------------------------------------------------------- /command/manifest_development.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: esd-command 4 | host: esd-command-development 5 | buildpack: java_buildpack_offline 6 | memory: 1024M 7 | instances: 2 8 | path: build/libs/command-0.0.1-SNAPSHOT.jar 9 | services: 10 | - discovery-service 11 | - hystrix-dashboard 12 | - rabbitmq 13 | env: 14 | JAVA_OPTS: -Djava.security.egd=file:///dev/urandom 15 | SPRING_PROFILES_ACTIVE: event-store -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/Application.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; 6 | 7 | @SpringBootApplication 8 | @EnableCircuitBreaker 9 | public class Application { 10 | 11 | public static void main( String[] args ) { 12 | 13 | SpringApplication.run( Application.class, args ); 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/EventStoreConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.context.annotation.Profile; 5 | 6 | @Profile( "event-store" ) 7 | @Configuration 8 | public class EventStoreConfig { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/RabbitConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; 2 | 3 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 4 | import org.springframework.cloud.config.java.AbstractCloudConfig; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | 9 | @Configuration 10 | public class RabbitConfig { 11 | 12 | 13 | @Configuration 14 | @Profile("cloud") 15 | public static class CloudRabbitConfig extends AbstractCloudConfig { 16 | 17 | @Bean 18 | public ConnectionFactory rabbitConnectionFactory() { 19 | 20 | return connectionFactory().rabbitConnectionFactory(); 21 | } 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 7 | 8 | @Configuration 9 | @EnableWebSecurity 10 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 11 | 12 | @Override 13 | protected void configure(HttpSecurity httpSecurity) throws Exception { 14 | 15 | httpSecurity 16 | .authorizeRequests() 17 | .anyRequest().permitAll() 18 | .and() 19 | .csrf().disable(); 20 | 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/BoardClient.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 4 | 5 | import java.util.UUID; 6 | 7 | public interface BoardClient { 8 | 9 | void save( final Board board ); 10 | 11 | Board find( final UUID boardUuid ); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/config/EventStoreClientConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.service.EventStoreBoardClient; 5 | import org.springframework.beans.factory.annotation.Qualifier; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Primary; 9 | import org.springframework.context.annotation.Profile; 10 | 11 | @Profile( "event-store" ) 12 | @Configuration 13 | public class EventStoreClientConfig { 14 | 15 | @Bean 16 | @Primary 17 | public BoardClient boardClient( 18 | @Qualifier( "io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.config.RestConfig$EventStoreClient" ) final RestConfig.EventStoreClient eventStoreClient 19 | ) { 20 | 21 | return new EventStoreBoardClient( eventStoreClient ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/config/RestConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvent; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvents; 5 | import org.springframework.cloud.openfeign.EnableFeignClients; 6 | import org.springframework.cloud.openfeign.FeignClient; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Profile; 9 | import org.springframework.http.ResponseEntity; 10 | import org.springframework.web.bind.annotation.GetMapping; 11 | import org.springframework.web.bind.annotation.PathVariable; 12 | import org.springframework.web.bind.annotation.PostMapping; 13 | import org.springframework.web.bind.annotation.RequestBody; 14 | 15 | import java.util.UUID; 16 | 17 | @Profile( "event-store" ) 18 | @Configuration 19 | @EnableFeignClients 20 | public class RestConfig { 21 | 22 | @FeignClient( value = "esd-event-store" /*, fallback = HystrixFallbackEventStoreClient.class */ ) 23 | public interface EventStoreClient { 24 | 25 | @PostMapping( path = "/" ) 26 | ResponseEntity addNewDomainEvent( @RequestBody DomainEvent event ); 27 | 28 | @GetMapping( path = "/{boardUuid}" ) 29 | DomainEvents getDomainEventsForBoardUuid( @PathVariable( "boardUuid" ) UUID boardId ); 30 | 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/config/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.config; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/service/EventStoreBoardClient.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.service; 2 | 3 | import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.config.RestConfig; 6 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvents; 7 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvent; 8 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.http.HttpStatus; 11 | import org.springframework.http.ResponseEntity; 12 | 13 | import java.util.List; 14 | import java.util.UUID; 15 | 16 | @Slf4j 17 | public class EventStoreBoardClient implements BoardClient { 18 | 19 | private final RestConfig.EventStoreClient eventStoreClient; 20 | 21 | public EventStoreBoardClient( final RestConfig.EventStoreClient eventStoreClient ) { 22 | 23 | this.eventStoreClient = eventStoreClient; 24 | 25 | } 26 | 27 | @Override 28 | @HystrixCommand 29 | public void save( final Board board ) { 30 | log.debug( "save : enter" ); 31 | 32 | List newChanges = board.changes(); 33 | 34 | newChanges.forEach( domainEvent -> { 35 | log.debug( "save : domainEvent=" + domainEvent ); 36 | 37 | ResponseEntity accepted = this.eventStoreClient.addNewDomainEvent( domainEvent ); 38 | if( !accepted.getStatusCode().equals( HttpStatus.ACCEPTED ) ) { 39 | 40 | throw new IllegalStateException( "could not add DomainEvent to the Event Store" ); 41 | } 42 | }); 43 | board.flushChanges(); 44 | 45 | log.debug( "save : exit" ); 46 | } 47 | 48 | @Override 49 | @HystrixCommand 50 | public Board find( final UUID boardUuid ) { 51 | log.debug( "find : enter" ); 52 | 53 | DomainEvents domainEvents = this.eventStoreClient.getDomainEventsForBoardUuid( boardUuid ); 54 | if( domainEvents.getDomainEvents().isEmpty() ) { 55 | 56 | log.warn( "find : exit, target[" + boardUuid.toString() + "] not found" ); 57 | throw new IllegalArgumentException( "board[" + boardUuid.toString() + "] not found" ); 58 | } 59 | 60 | Board board = Board.createFrom( boardUuid, domainEvents.getDomainEvents() ); 61 | board.flushChanges(); 62 | 63 | log.debug( "find : exit" ); 64 | return board; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/service/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.service; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/config/KafkaClientConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service.DomainEventSource; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service.KafkaBoardClient; 6 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 7 | import org.springframework.cloud.stream.binder.kafka.streams.QueryableStoreRegistry; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | import org.springframework.context.annotation.Primary; 11 | import org.springframework.context.annotation.Profile; 12 | 13 | @Profile( "kafka" ) 14 | @Configuration 15 | @EnableAutoConfiguration 16 | public class KafkaClientConfig { 17 | 18 | public static final String BOARD_EVENTS_SNAPSHOTS = "board-events-snapshots"; 19 | 20 | @Bean 21 | @Primary 22 | public BoardClient boardClient( 23 | final DomainEventSource domainEventSource, 24 | final QueryableStoreRegistry queryableStoreRegistry 25 | ) { 26 | 27 | return new KafkaBoardClient( domainEventSource, queryableStoreRegistry ); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/config/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.config; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/BoardEventsStreamsProcessor.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; 2 | 3 | import org.apache.kafka.streams.kstream.KStream; 4 | import org.springframework.cloud.stream.annotation.Input; 5 | 6 | public interface BoardEventsStreamsProcessor { 7 | 8 | @Input( "input" ) 9 | KStream input(); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/DomainEventSink.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; 2 | 3 | import org.apache.kafka.streams.kstream.KStream; 4 | 5 | public interface DomainEventSink { 6 | 7 | void process( KStream input ); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/DomainEventSinkImpl.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvent; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.kafka.common.serialization.Serde; 8 | import org.apache.kafka.common.serialization.Serdes; 9 | import org.apache.kafka.common.utils.Bytes; 10 | import org.apache.kafka.streams.KeyValue; 11 | import org.apache.kafka.streams.kstream.KStream; 12 | import org.apache.kafka.streams.kstream.Materialized; 13 | import org.apache.kafka.streams.kstream.Serialized; 14 | import org.apache.kafka.streams.state.KeyValueStore; 15 | import org.springframework.cloud.stream.annotation.EnableBinding; 16 | import org.springframework.cloud.stream.annotation.StreamListener; 17 | import org.springframework.context.annotation.Profile; 18 | import org.springframework.kafka.support.serializer.JsonSerde; 19 | 20 | import java.io.IOException; 21 | 22 | import static io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.config.KafkaClientConfig.BOARD_EVENTS_SNAPSHOTS; 23 | 24 | @Profile( "kafka" ) 25 | @EnableBinding( BoardEventsStreamsProcessor.class ) 26 | @Slf4j 27 | public class DomainEventSinkImpl implements DomainEventSink { 28 | 29 | private final ObjectMapper mapper; 30 | private final Serde domainEventSerde; 31 | private final Serde boardSerde; 32 | 33 | public DomainEventSinkImpl( final ObjectMapper mapper ) { 34 | 35 | this.mapper = mapper; 36 | this.domainEventSerde = new JsonSerde<>( DomainEvent.class, mapper ); 37 | this.boardSerde = new JsonSerde<>( Board.class, mapper ); 38 | 39 | } 40 | 41 | @StreamListener( "input" ) 42 | public void process( KStream input ) { 43 | log.debug( "process : enter" ); 44 | 45 | input 46 | .map( (key, value) -> { 47 | 48 | try { 49 | 50 | DomainEvent domainEvent = mapper.readValue( value, DomainEvent.class ); 51 | log.debug( "process : domainEvent=" + domainEvent ); 52 | 53 | return new KeyValue<>( domainEvent.getBoardUuid().toString(), domainEvent ); 54 | 55 | } catch( IOException e ) { 56 | log.error( "process : error converting json to DomainEvent", e ); 57 | } 58 | 59 | return null; 60 | }) 61 | .groupBy( (s, domainEvent) -> s, Serialized.with( Serdes.String(), domainEventSerde ) ) 62 | .aggregate( 63 | Board::new, 64 | (key, domainEvent, board) -> board.handleEvent( domainEvent ), 65 | Materialized.>as( BOARD_EVENTS_SNAPSHOTS ) 66 | .withKeySerde( Serdes.String() ) 67 | .withValueSerde( boardSerde ) 68 | ); 69 | 70 | log.debug( "process : exit" ); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/DomainEventSource.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvent; 4 | 5 | public interface DomainEventSource { 6 | 7 | DomainEvent publish( final DomainEvent event ); 8 | } 9 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/DomainEventSourceImpl.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvent; 4 | import org.springframework.cloud.stream.annotation.EnableBinding; 5 | import org.springframework.cloud.stream.messaging.Source; 6 | import org.springframework.context.annotation.Profile; 7 | import org.springframework.integration.annotation.Publisher; 8 | 9 | @Profile( "kafka" ) 10 | @EnableBinding( Source.class ) 11 | public class DomainEventSourceImpl implements DomainEventSource { 12 | 13 | @Publisher( channel = Source.OUTPUT ) 14 | public DomainEvent publish( final DomainEvent event ) { 15 | 16 | return event; 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/KafkaBoardClient.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvent; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.kafka.streams.errors.InvalidStateStoreException; 8 | import org.apache.kafka.streams.state.QueryableStoreTypes; 9 | import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; 10 | import org.springframework.cloud.stream.binder.kafka.streams.QueryableStoreRegistry; 11 | 12 | import java.util.List; 13 | import java.util.UUID; 14 | 15 | import static io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.config.KafkaClientConfig.BOARD_EVENTS_SNAPSHOTS; 16 | 17 | @Slf4j 18 | public class KafkaBoardClient implements BoardClient { 19 | 20 | private final DomainEventSource domainEventSource; 21 | private final QueryableStoreRegistry queryableStoreRegistry; 22 | 23 | public KafkaBoardClient( 24 | final DomainEventSource domainEventSource, 25 | final QueryableStoreRegistry queryableStoreRegistry 26 | ) { 27 | 28 | this.domainEventSource = domainEventSource; 29 | this.queryableStoreRegistry = queryableStoreRegistry; 30 | 31 | } 32 | 33 | @Override 34 | public void save( final Board board ) { 35 | log.debug( "save : enter" ); 36 | 37 | List newChanges = board.changes(); 38 | 39 | newChanges.forEach( domainEvent -> { 40 | log.debug( "save : domainEvent=" + domainEvent ); 41 | 42 | this.domainEventSource.publish( domainEvent ); 43 | 44 | }); 45 | board.flushChanges(); 46 | 47 | log.debug( "save : exit" ); 48 | } 49 | 50 | @Override 51 | public Board find( final UUID boardUuid ) { 52 | log.debug( "find : enter" ); 53 | 54 | // while( true ) { 55 | 56 | try { 57 | 58 | ReadOnlyKeyValueStore store = queryableStoreRegistry.getQueryableStoreType( BOARD_EVENTS_SNAPSHOTS, QueryableStoreTypes.keyValueStore() ); 59 | 60 | Board board = store.get( boardUuid.toString() ); 61 | if( null != board ) { 62 | 63 | board.flushChanges(); 64 | log.debug( "find : board=" + board.toString() ); 65 | 66 | log.debug( "find : exit" ); 67 | return board; 68 | 69 | } else { 70 | 71 | throw new IllegalArgumentException( "board[" + boardUuid.toString() + "] not found!" ); 72 | } 73 | 74 | } catch( InvalidStateStoreException e ) { 75 | log.error( "find : error", e ); 76 | 77 | // try { 78 | // Thread.sleep( 100 ); 79 | // } catch( InterruptedException e1 ) { 80 | // log.error( "find : thread interrupted", e1 ); 81 | // } 82 | 83 | } 84 | 85 | // } 86 | 87 | throw new IllegalArgumentException( "board[" + boardUuid.toString() + "] not found!" ); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/config/CommandConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.BoardService; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | 9 | import java.util.UUID; 10 | 11 | @Configuration 12 | public class CommandConfig { 13 | 14 | @Bean 15 | public BoardService boardService( final BoardClient boardClient ) { 16 | 17 | return new BoardService( boardClient ); 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/config/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.config; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/BoardInitialized.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.ToString; 10 | 11 | import java.time.Instant; 12 | import java.util.UUID; 13 | 14 | @Data 15 | @EqualsAndHashCode( callSuper = true ) 16 | @ToString( callSuper = true ) 17 | @JsonPropertyOrder({ "eventType", "boardUuid", "occurredOn" }) 18 | public class BoardInitialized extends DomainEvent { 19 | 20 | @JsonCreator 21 | public BoardInitialized( 22 | @JsonProperty( "boardUuid" ) final UUID boardUuid, 23 | @JsonProperty( "occurredOn" ) final Instant when 24 | ) { 25 | super( boardUuid, when ); 26 | } 27 | 28 | @Override 29 | @JsonIgnore 30 | public String eventType() { 31 | 32 | return this.getClass().getSimpleName(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/BoardRenamed.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.ToString; 10 | 11 | import java.time.Instant; 12 | import java.util.UUID; 13 | 14 | @Data 15 | @EqualsAndHashCode( callSuper = true ) 16 | @ToString( callSuper = true ) 17 | @JsonPropertyOrder({ "eventType", "boardUuid", "occurredOn", "name" }) 18 | public class BoardRenamed extends DomainEvent { 19 | 20 | private final String name; 21 | 22 | @JsonCreator 23 | public BoardRenamed( 24 | @JsonProperty( "name" ) final String name, 25 | @JsonProperty( "boardUuid" ) final UUID boardUuid, 26 | @JsonProperty( "occurredOn" ) final Instant when 27 | ) { 28 | super( boardUuid, when ); 29 | 30 | this.name = name; 31 | 32 | } 33 | 34 | @Override 35 | @JsonIgnore 36 | public String eventType() { 37 | 38 | return this.getClass().getSimpleName(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/DomainEvent.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonSubTypes; 6 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 7 | import lombok.Data; 8 | import lombok.Getter; 9 | 10 | import java.time.Instant; 11 | import java.util.UUID; 12 | 13 | import static lombok.AccessLevel.NONE; 14 | 15 | @JsonTypeInfo( 16 | use = JsonTypeInfo.Id.NAME, 17 | include = JsonTypeInfo.As.PROPERTY, 18 | property = "eventType", 19 | defaultImpl = DomainEventIgnored.class 20 | ) 21 | @JsonSubTypes({ 22 | @JsonSubTypes.Type( value = BoardInitialized.class, name = "BoardInitialized" ), 23 | @JsonSubTypes.Type( value = BoardRenamed.class, name = "BoardRenamed" ), 24 | @JsonSubTypes.Type( value = StoryAdded.class, name = "StoryAdded" ), 25 | @JsonSubTypes.Type( value = StoryUpdated.class, name = "StoryUpdated" ), 26 | @JsonSubTypes.Type( value = StoryDeleted.class, name = "StoryDeleted" ) 27 | }) 28 | @Data 29 | public abstract class DomainEvent { 30 | 31 | private final UUID boardUuid; 32 | 33 | @Getter( NONE ) 34 | @JsonIgnore 35 | private final Instant when; 36 | 37 | DomainEvent( final UUID boardUuid, final Instant when ) { 38 | 39 | this.boardUuid = boardUuid; 40 | this.when = when; 41 | 42 | } 43 | 44 | @JsonProperty( "occurredOn" ) 45 | public Instant occurredOn() { 46 | 47 | return when; 48 | } 49 | 50 | @JsonProperty( "eventType" ) 51 | public abstract String eventType(); 52 | 53 | } 54 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/DomainEventIgnored.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.ToString; 10 | 11 | import java.time.Instant; 12 | import java.util.UUID; 13 | 14 | @Data 15 | @EqualsAndHashCode( callSuper = true ) 16 | @ToString( callSuper = true ) 17 | @JsonPropertyOrder({ "eventType", "boardUuid", "occurredOn" }) 18 | public class DomainEventIgnored extends DomainEvent { 19 | 20 | @JsonCreator 21 | public DomainEventIgnored( 22 | @JsonProperty( "boardUuid" ) final UUID boardUuid, 23 | @JsonProperty( "occurredOn" ) final Instant when 24 | ) { 25 | super( boardUuid, when ); 26 | } 27 | 28 | @Override 29 | @JsonIgnore 30 | public String eventType() { 31 | 32 | return this.getClass().getSimpleName(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/DomainEvents.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.UUID; 8 | 9 | @Data 10 | public class DomainEvents { 11 | 12 | private UUID boardUuid; 13 | private List domainEvents = new ArrayList<>(); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/StoryAdded.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Story; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | 12 | import java.time.Instant; 13 | import java.util.UUID; 14 | 15 | @Data 16 | @EqualsAndHashCode( callSuper = true ) 17 | @ToString( callSuper = true ) 18 | @JsonPropertyOrder({ "eventType", "boardUuid", "occurredOn", "storyUuid", "name" }) 19 | public class StoryAdded extends DomainEvent { 20 | 21 | private final UUID storyUuid; 22 | private final String name; 23 | 24 | @JsonCreator 25 | public StoryAdded( 26 | @JsonProperty( "storyUuid" ) final UUID storyUuid, 27 | @JsonProperty( "name" ) final String name, 28 | @JsonProperty( "boardUuid" ) final UUID boardUuid, 29 | @JsonProperty( "occurredOn" ) final Instant when 30 | ) { 31 | super( boardUuid, when ); 32 | 33 | this.storyUuid = storyUuid; 34 | this.name = name; 35 | 36 | } 37 | 38 | public Story getStory() { 39 | 40 | Story story = new Story(); 41 | story.setName( this.name ); 42 | 43 | return story; 44 | } 45 | 46 | @Override 47 | @JsonIgnore 48 | public String eventType() { 49 | 50 | return this.getClass().getSimpleName(); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/StoryDeleted.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Story; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | 12 | import java.time.Instant; 13 | import java.util.UUID; 14 | 15 | @Data 16 | @EqualsAndHashCode( callSuper = true ) 17 | @ToString( callSuper = true ) 18 | @JsonPropertyOrder({ "eventType", "boardUuid", "occurredOn", "storyUuid" }) 19 | public class StoryDeleted extends DomainEvent { 20 | 21 | private final UUID storyUuid; 22 | 23 | @JsonCreator 24 | public StoryDeleted( 25 | @JsonProperty( "storyUuid" ) final UUID storyUuid, 26 | @JsonProperty( "boardUuid" ) final UUID boardUuid, 27 | @JsonProperty( "occurredOn" ) final Instant when 28 | ) { 29 | super( boardUuid, when ); 30 | 31 | this.storyUuid = storyUuid; 32 | 33 | } 34 | 35 | @Override 36 | @JsonIgnore 37 | public String eventType() { 38 | 39 | return this.getClass().getSimpleName(); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/StoryUpdated.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Story; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | 12 | import java.time.Instant; 13 | import java.util.UUID; 14 | 15 | @Data 16 | @EqualsAndHashCode( callSuper = true ) 17 | @ToString( callSuper = true ) 18 | @JsonPropertyOrder({ "eventType", "boardUuid", "occurredOn", "storyUuid" }) 19 | public class StoryUpdated extends DomainEvent { 20 | 21 | private final UUID storyUuid; 22 | private final String name; 23 | 24 | @JsonCreator 25 | public StoryUpdated( 26 | @JsonProperty( "storyUuid" ) final UUID storyUuid, 27 | @JsonProperty( "name" ) final String name, 28 | @JsonProperty( "boardUuid" ) final UUID boardUuid, 29 | @JsonProperty( "occurredOn" ) final Instant when 30 | ) { 31 | super( boardUuid, when ); 32 | 33 | this.storyUuid = storyUuid; 34 | this.name = name; 35 | 36 | } 37 | 38 | public Story getStory() { 39 | 40 | Story story = new Story(); 41 | story.setName( this.name ); 42 | 43 | return story; 44 | } 45 | 46 | @Override 47 | @JsonIgnore 48 | public String eventType() { 49 | 50 | return this.getClass().getSimpleName(); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/model/Board.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.model; 2 | 3 | import com.google.common.collect.ImmutableList; 4 | import com.google.common.collect.ImmutableMap; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.*; 6 | import io.vavr.API; 7 | import lombok.AccessLevel; 8 | import lombok.Data; 9 | import lombok.Getter; 10 | import lombok.Setter; 11 | import lombok.extern.slf4j.Slf4j; 12 | 13 | import java.time.Instant; 14 | import java.util.*; 15 | 16 | import static io.vavr.API.$; 17 | import static io.vavr.API.Case; 18 | import static io.vavr.Predicates.instanceOf; 19 | import static io.vavr.collection.Stream.ofAll; 20 | import static lombok.AccessLevel.NONE; 21 | 22 | @Data 23 | @Slf4j 24 | public class Board { 25 | 26 | @Setter( NONE ) 27 | private UUID boardUuid; 28 | 29 | @Setter( NONE ) 30 | private String name = "New Board"; 31 | 32 | @Getter( NONE ) 33 | @Setter( NONE ) 34 | private Map stories = new HashMap<>(); 35 | 36 | @Getter( NONE ) 37 | @Setter( NONE ) 38 | private List changes = new ArrayList<>(); 39 | 40 | public Board() { } 41 | 42 | public Board( final UUID boardUuid ) { 43 | 44 | boardInitialized( new BoardInitialized( boardUuid, Instant.now() ) ); 45 | 46 | } 47 | 48 | private Board boardInitialized( final BoardInitialized event ) { 49 | log.debug( "boardInitialized : event=" + event ); 50 | 51 | flushChanges(); 52 | this.boardUuid = event.getBoardUuid(); 53 | this.changes.add( event ); 54 | 55 | return this; 56 | } 57 | 58 | public void renameBoard( final String name ) { 59 | 60 | boardRenamed( new BoardRenamed( name, this.boardUuid, Instant.now() ) ); 61 | 62 | } 63 | 64 | private Board boardRenamed( final BoardRenamed event ) { 65 | log.debug( "boardRenamed : event=" + event ); 66 | 67 | this.name = event.getName(); 68 | this.changes.add( event ); 69 | 70 | return this; 71 | } 72 | 73 | public String getName() { 74 | 75 | return this.name; 76 | } 77 | 78 | public void addStory( final UUID storyUuid, final String name ) { 79 | 80 | storyAdded( new StoryAdded( storyUuid, name, this.boardUuid, Instant.now() ) ); 81 | 82 | } 83 | 84 | private Board storyAdded( final StoryAdded event ) { 85 | log.debug( "storyAdded : event=" + event ); 86 | 87 | this.stories.put( event.getStoryUuid(), event.getStory() ); 88 | this.changes.add( event ); 89 | 90 | return this; 91 | } 92 | 93 | public void updateStory( final UUID storyUuid, final String name ) { 94 | 95 | storyUpdated( new StoryUpdated( storyUuid, name, this.boardUuid, Instant.now() ) ); 96 | 97 | } 98 | 99 | private Board storyUpdated( final StoryUpdated event ) { 100 | log.debug( "storyUpdated : event=" + event ); 101 | 102 | this.stories.replace( event.getStoryUuid(), event.getStory() ); 103 | this.changes.add( event ); 104 | 105 | return this; 106 | } 107 | 108 | public void deleteStory( final UUID storyUuid ) { 109 | 110 | storyDeleted( new StoryDeleted( storyUuid, this.boardUuid, Instant.now() ) ); 111 | 112 | } 113 | 114 | private Board storyDeleted( final StoryDeleted event ) { 115 | log.debug( "storyDeleted : event=" + event ); 116 | 117 | this.stories.remove( event.getStoryUuid() ); 118 | this.changes.add( event ); 119 | 120 | return this; 121 | } 122 | 123 | public Map getStories() { 124 | 125 | return ImmutableMap.copyOf( this.stories ); 126 | } 127 | 128 | public List changes() { 129 | 130 | return ImmutableList.copyOf( this.changes ); 131 | } 132 | 133 | public void flushChanges() { 134 | 135 | this.changes.clear(); 136 | 137 | } 138 | 139 | // Builder Methods 140 | public static Board createFrom( final UUID boardUuid, final Collection domainEvents ) { 141 | 142 | return ofAll( domainEvents ).foldLeft( new Board( boardUuid ), Board::handleEvent ); 143 | } 144 | 145 | public Board handleEvent( final DomainEvent domainEvent ) { 146 | 147 | return API.Match( domainEvent ).of( 148 | Case( $( instanceOf( BoardInitialized.class ) ), this::boardInitialized ), 149 | Case( $( instanceOf( BoardRenamed.class ) ), this::boardRenamed ), 150 | Case( $( instanceOf( StoryAdded.class ) ), this::storyAdded ), 151 | Case( $( instanceOf( StoryUpdated.class ) ), this::storyUpdated ), 152 | Case( $( instanceOf( StoryDeleted.class ) ), this::storyDeleted ), 153 | Case( $(), this ) 154 | ); 155 | } 156 | 157 | } 158 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/model/Story.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.model; 2 | 3 | import lombok.Data; 4 | 5 | @Data 6 | public class Story { 7 | 8 | private String name; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/model/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.model; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/BoardService.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 5 | import lombok.extern.slf4j.Slf4j; 6 | 7 | import java.util.UUID; 8 | 9 | @Slf4j 10 | public class BoardService { 11 | 12 | private final BoardClient client; 13 | 14 | public BoardService( final BoardClient client ) { 15 | 16 | this.client = client; 17 | 18 | } 19 | 20 | public UUID createBoard() { 21 | log.debug( "createBoard : enter" ); 22 | 23 | Board board = new Board( UUID.randomUUID() ); 24 | this.client.save( board ); 25 | 26 | return board.getBoardUuid(); 27 | } 28 | 29 | public void renameBoard( final UUID boardUuid, final String name ) { 30 | log.debug( "renameBoard : enter" ); 31 | 32 | Board board = this.client.find( boardUuid ); 33 | board.renameBoard( name ); 34 | this.client.save( board ); 35 | 36 | } 37 | 38 | public UUID addStory( final UUID boardUuid, final String name ) { 39 | log.debug( "addStory : enter" ); 40 | 41 | Board board = this.client.find( boardUuid ); 42 | 43 | UUID storyUuid = UUID.randomUUID(); 44 | board.addStory( storyUuid, name ); 45 | 46 | this.client.save( board ); 47 | 48 | return storyUuid; 49 | } 50 | 51 | public void updateStory( final UUID boardUuid, final UUID storyUuid, final String name ) { 52 | log.debug( "updateStory : enter" ); 53 | 54 | Board board = this.client.find( boardUuid ); 55 | board.updateStory( storyUuid, name ); 56 | 57 | this.client.save( board ); 58 | 59 | } 60 | 61 | public void deleteStory( final UUID boardUuid, final UUID storyUuid ) { 62 | log.debug( "deleteStory : enter" ); 63 | 64 | Board board = this.client.find( boardUuid ); 65 | board.deleteStory( storyUuid ); 66 | 67 | this.client.save( board ); 68 | 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/CommandsController.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.BoardService; 4 | import lombok.extern.slf4j.Slf4j; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.*; 7 | import org.springframework.web.util.UriComponentsBuilder; 8 | 9 | import java.util.UUID; 10 | 11 | @RestController 12 | @RequestMapping( "/boards" ) 13 | @Slf4j 14 | public class CommandsController { 15 | 16 | private final BoardService service; 17 | 18 | public CommandsController( final BoardService service ) { 19 | 20 | this.service = service; 21 | 22 | } 23 | 24 | @PostMapping( "/" ) 25 | public ResponseEntity createBoard( final UriComponentsBuilder uriComponentsBuilder ) { 26 | log.debug( "createBoard : enter" ); 27 | 28 | UUID boardUuid = this.service.createBoard(); 29 | 30 | return ResponseEntity 31 | .created( uriComponentsBuilder.path( "/boards/{boardUuid}" ).buildAndExpand( boardUuid ).toUri() ) 32 | .build(); 33 | } 34 | 35 | @PatchMapping( "/{boardUuid}" ) 36 | public ResponseEntity renameBoard( @PathVariable( "boardUuid" ) UUID boardUuid, @RequestParam( "name" ) String name, final UriComponentsBuilder uriComponentsBuilder ) { 37 | log.debug( "renameBoard : enter" ); 38 | 39 | this.service.renameBoard( boardUuid, name ); 40 | 41 | return ResponseEntity 42 | .accepted() 43 | .build(); 44 | } 45 | 46 | @PostMapping( "/{boardUuid}/stories" ) 47 | public ResponseEntity addStoryToBoard( @PathVariable( "boardUuid" ) UUID boardUuid, @RequestParam( "name" ) String name, final UriComponentsBuilder uriComponentsBuilder ) { 48 | log.debug( "addStoryToBoard : enter" ); 49 | 50 | UUID storyUuid = this.service.addStory( boardUuid, name ); 51 | 52 | return ResponseEntity 53 | .created( uriComponentsBuilder.path( "/boards/{boardUuid}/stories/{storyUuid}" ).buildAndExpand( boardUuid, storyUuid ).toUri() ) 54 | .build(); 55 | } 56 | 57 | @PutMapping( "/{boardUuid}/stories/{storyUuid}" ) 58 | public ResponseEntity updateStoryOnBoard( @PathVariable( "boardUuid" ) UUID boardUuid, @PathVariable( "storyUuid" ) UUID storyUuid, @RequestParam( "name" ) String name ) { 59 | log.debug( "updateStoryOnBoard : enter" ); 60 | 61 | this.service.updateStory( boardUuid, storyUuid, name ); 62 | 63 | return ResponseEntity 64 | .accepted() 65 | .build(); 66 | } 67 | 68 | @DeleteMapping( "/{boardUuid}/stories/{storyUuid}" ) 69 | public ResponseEntity removeStoryFromBoard( @PathVariable( "boardUuid" ) UUID boardUuid, @PathVariable( "storyUuid" ) UUID storyUuid ) { 70 | log.debug( "removeStoryFromBoard : enter" ); 71 | 72 | this.service.deleteStory( boardUuid, storyUuid ); 73 | 74 | return ResponseEntity 75 | .accepted() 76 | .build(); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /command/src/main/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint; -------------------------------------------------------------------------------- /command/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | management.endpoints.web.exposure.include=* 2 | -------------------------------------------------------------------------------- /command/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | debug: false 2 | 3 | logger.level: 4 | io.pivotal.dmfrey: DEBUG 5 | 6 | server: 7 | port: ${PORT:9080} 8 | use-forward-headers: true 9 | tomcat: 10 | remote-ip-header: x-forwarded-for 11 | protocol-header: x-forwarded-proto 12 | 13 | spring: 14 | jackson: 15 | serialization: 16 | write_dates_as_timestamps: false 17 | 18 | 19 | --- 20 | spring: 21 | profiles: event-store 22 | 23 | cloud: 24 | stream: 25 | bindings: 26 | input: 27 | binder: rabbitmq 28 | destination: board-events 29 | group: command-board-events-group 30 | consumer: 31 | useNativeDecoding: true 32 | headerMode: raw 33 | kafka: 34 | streams: 35 | binder: 36 | brokers: localhost 37 | zkNodes: localhost 38 | 39 | --- 40 | spring: 41 | profiles: kafka 42 | 43 | cloud: 44 | stream: 45 | bindings: 46 | output: 47 | binder: kafka 48 | destination: board-events 49 | contentType: application/json 50 | producer: 51 | headerMode: raw # Outbound data has no embedded headers 52 | input: 53 | destination: board-events 54 | contentType: application/json 55 | group: command-board-events-group 56 | consumer: 57 | useNativeDecoding: true 58 | headerMode: raw 59 | kafka: 60 | streams: 61 | binder: 62 | brokers: localhost 63 | zkNodes: localhost 64 | 65 | logger.level: 66 | org.apache.kafka: DEBUG 67 | org.apache.kafka.clients: ERROR -------------------------------------------------------------------------------- /command/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: esd-command 4 | 5 | eureka.instance.leaseRenewalIntervalInSeconds: 15 6 | 7 | --- 8 | spring: 9 | profiles: cloud 10 | 11 | cloud: 12 | services: 13 | registrationMethod: direct 14 | 15 | eureka.instance.leaseRenewalIntervalInSeconds: 30 16 | -------------------------------------------------------------------------------- /command/src/test/java/io/pivotal/dmfrey/eventStoreDemo/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.config.UnitTestConfig; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.context.annotation.Import; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | 10 | @RunWith( SpringRunner.class ) 11 | @SpringBootTest( properties = { 12 | "--spring.cloud.service-registry.auto-registration.enabled=false" 13 | }) 14 | @Import( UnitTestConfig.class ) 15 | public class ApplicationTests { 16 | 17 | @Test 18 | public void contextLoads() { 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /command/src/test/java/io/pivotal/dmfrey/eventStoreDemo/config/UnitTestConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 5 | import org.springframework.boot.test.context.TestConfiguration; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Primary; 8 | 9 | import java.util.UUID; 10 | 11 | @TestConfiguration 12 | public class UnitTestConfig { 13 | 14 | @Bean 15 | @Primary 16 | public BoardClient boardClient() { 17 | 18 | return new BoardClient() { 19 | 20 | @Override 21 | public void save( Board board ) { 22 | 23 | throw new UnsupportedOperationException( "client call not implemented yet" ); 24 | } 25 | 26 | @Override 27 | public Board find( UUID boardUuid ) { 28 | 29 | throw new UnsupportedOperationException( "client call not implemented yet" ); 30 | } 31 | 32 | }; 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /command/src/test/java/io/pivotal/dmfrey/eventStoreDemo/contracts/ApiBase.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.contracts; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.BoardService; 4 | import io.pivotal.dmfrey.eventStoreDemo.endpoint.CommandsController; 5 | import io.restassured.module.mockmvc.RestAssuredMockMvc; 6 | import org.junit.Before; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | import java.util.UUID; 14 | 15 | import static org.mockito.ArgumentMatchers.any; 16 | import static org.mockito.ArgumentMatchers.anyString; 17 | import static org.mockito.Mockito.verifyNoMoreInteractions; 18 | import static org.mockito.Mockito.when; 19 | import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; 20 | 21 | @RunWith( SpringRunner.class ) 22 | @SpringBootTest( 23 | webEnvironment = NONE, 24 | classes = CommandsController.class, 25 | properties = { 26 | "--spring.cloud.service-registry.auto-registration.enabled=false" 27 | } 28 | ) 29 | public abstract class ApiBase { 30 | 31 | @Autowired 32 | private CommandsController controller; 33 | 34 | @MockBean 35 | private BoardService service; 36 | 37 | private UUID boardUuid = UUID.fromString( "11111111-90ab-cdef-1234-567890abcdef" ); 38 | private UUID storyUuid = UUID.fromString( "10240df9-4a1e-4fa4-bbd1-0bb33d764603" ); 39 | 40 | @Before 41 | public void setup() { 42 | 43 | when( this.service.createBoard() ).thenReturn( this.boardUuid ); 44 | when( this.service.addStory( any( UUID.class ), anyString() ) ).thenReturn( this.storyUuid ); 45 | 46 | RestAssuredMockMvc.standaloneSetup( this.controller ); 47 | 48 | verifyNoMoreInteractions( this.service ); 49 | 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /command/src/test/java/io/pivotal/dmfrey/eventStoreDemo/contracts/KafkaBase.java: -------------------------------------------------------------------------------- 1 | //package io.pivotal.dmfrey.eventStoreDemo.contracts; 2 | // 3 | //import io.pivotal.dmfrey.eventStoreDemo.Application; 4 | //import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 5 | //import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 6 | //import org.apache.kafka.clients.consumer.Consumer; 7 | //import org.apache.kafka.clients.consumer.ConsumerConfig; 8 | //import org.junit.AfterClass; 9 | //import org.junit.BeforeClass; 10 | //import org.junit.ClassRule; 11 | //import org.springframework.boot.SpringApplication; 12 | //import org.springframework.boot.WebApplicationType; 13 | //import org.springframework.cloud.contract.verifier.messaging.amqp.ContractVerifierAmqpAutoConfiguration; 14 | //import org.springframework.cloud.contract.verifier.messaging.amqp.RabbitMockConnectionFactoryAutoConfiguration; 15 | //import org.springframework.cloud.contract.verifier.messaging.boot.AutoConfigureMessageVerifier; 16 | //import org.springframework.cloud.contract.verifier.messaging.integration.ContractVerifierIntegrationConfiguration; 17 | //import org.springframework.cloud.contract.verifier.messaging.noop.NoOpContractVerifierAutoConfiguration; 18 | //import org.springframework.cloud.contract.verifier.messaging.stream.ContractVerifierStreamAutoConfiguration; 19 | //import org.springframework.context.ConfigurableApplicationContext; 20 | //import org.springframework.kafka.core.DefaultKafkaConsumerFactory; 21 | //import org.springframework.kafka.test.rule.KafkaEmbedded; 22 | //import org.springframework.kafka.test.utils.KafkaTestUtils; 23 | // 24 | //import java.time.Instant; 25 | //import java.util.Map; 26 | //import java.util.UUID; 27 | // 28 | //import static io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.config.KafkaClientConfig.BOARD_EVENTS_SNAPSHOTS; 29 | // 30 | ////@RunWith( SpringRunner.class ) 31 | ////@SpringBootTest( 32 | //// webEnvironment = NONE, 33 | //// properties = { 34 | //// "--spring.cloud.service-registry.auto-registration.enabled=false" 35 | //// } 36 | ////) 37 | ////@ActiveProfiles( "kafka" ) 38 | //public abstract class KafkaBase { 39 | // 40 | // private static String RECEIVER_TOPIC = "board-events"; 41 | // 42 | // private static final String KAFKA_BROKERS_PROPERTY = "spring.kafka.bootstrap-servers"; 43 | // 44 | // @ClassRule 45 | // public static KafkaEmbedded kafkaEmbedded = new KafkaEmbedded( 1, true, RECEIVER_TOPIC, BOARD_EVENTS_SNAPSHOTS ); 46 | // 47 | // @BeforeClass 48 | // public static void setup() { 49 | // System.setProperty( KAFKA_BROKERS_PROPERTY, kafkaEmbedded.getBrokersAsString() ); 50 | // } 51 | // 52 | // @AfterClass 53 | // public static void clean() { 54 | // System.clearProperty( KAFKA_BROKERS_PROPERTY ); 55 | // } 56 | // 57 | // private static Consumer consumer; 58 | // 59 | // @BeforeClass 60 | // public static void setUp() throws Exception { 61 | // 62 | // Map consumerProps = KafkaTestUtils.consumerProps("command-board-events-group", "false", kafkaEmbedded); 63 | // consumerProps.put( ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest" ); 64 | // 65 | // DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>( consumerProps ); 66 | // 67 | // consumer = cf.createConsumer(); 68 | // 69 | // kafkaEmbedded.consumeFromAnEmbeddedTopic( consumer, RECEIVER_TOPIC ); 70 | // 71 | // 72 | // } 73 | // 74 | // @AfterClass 75 | // public static void tearDown() { 76 | // 77 | // consumer.close(); 78 | // 79 | // } 80 | // 81 | // private UUID boardUuid = UUID.fromString( "11111111-90ab-cdef-1234-567890abcdef" ); 82 | // private Instant boardInstant = Instant.parse( "2018-08-16T16:45:32.123Z" ); 83 | // 84 | // private UUID storyUuid = UUID.fromString( "10240df9-4a1e-4fa4-bbd1-0bb33d764603" ); 85 | // 86 | // public void publishBoardInitialized() { 87 | // 88 | // SpringApplication app = 89 | // new SpringApplication( 90 | // Application.class, AutoConfigureMessageVerifier.class, 91 | // ContractVerifierStreamAutoConfiguration.class, ContractVerifierIntegrationConfiguration.class, 92 | // ContractVerifierAmqpAutoConfiguration.class, RabbitMockConnectionFactoryAutoConfiguration.class, 93 | // NoOpContractVerifierAutoConfiguration.class 94 | // ); 95 | // app.setWebApplicationType( WebApplicationType.NONE ); 96 | // ConfigurableApplicationContext context = app.run("--server.port=0", 97 | // "--spring.cloud.service-registry.auto-registration.enabled=false", 98 | // "--spring.jmx.enabled=false", 99 | // "--spring.cloud.stream.bindings.input.destination=board-events", 100 | // "--spring.cloud.stream.bindings.output.binder=kafka", 101 | // "--spring.cloud.stream.bindings.output.destination=board-events", 102 | // "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", 103 | // "--spring.cloud.stream.bindings.output.producer.headerMode=raw", 104 | // "--spring.cloud.stream.bindings.input.consumer.headerMode=raw", 105 | // "--spring.cloud.stream.kafka.streams.binder.brokers=" + kafkaEmbedded.getBrokersAsString(), 106 | // "--spring.cloud.stream.kafka.streams.binder.zkNodes=" + kafkaEmbedded.getZookeeperConnectionString(), 107 | // "--spring.profiles.active=kafka", 108 | // "--spring.jackson.serialization.write_dates_as_timestamps=false", 109 | // "--logger.level.io.pivotal.dmfrey=DEBUG"); 110 | // 111 | // BoardClient boardClient = context.getBean( BoardClient.class ); 112 | // boardClient.save( new Board( boardUuid ) ); 113 | // 114 | // } 115 | // 116 | //} 117 | -------------------------------------------------------------------------------- /command/src/test/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/EventStoreBoardClientTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.config.EventStoreClientConfig; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.config.RestConfig; 6 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.BoardInitialized; 7 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvent; 8 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvents; 9 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 10 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Story; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.boot.test.context.SpringBootTest; 15 | import org.springframework.boot.test.mock.mockito.MockBean; 16 | import org.springframework.http.ResponseEntity; 17 | import org.springframework.test.context.ActiveProfiles; 18 | import org.springframework.test.context.junit4.SpringRunner; 19 | 20 | import java.time.Instant; 21 | import java.util.UUID; 22 | 23 | import static org.assertj.core.api.Assertions.assertThat; 24 | import static org.mockito.Mockito.*; 25 | 26 | @RunWith( SpringRunner.class ) 27 | @SpringBootTest( classes = { EventStoreClientConfig.class, RestConfig.class } ) 28 | @ActiveProfiles( "event-store" ) 29 | public class EventStoreBoardClientTests { 30 | 31 | @Autowired 32 | BoardClient boardClient; 33 | 34 | @MockBean 35 | RestConfig.EventStoreClient eventStoreClient; 36 | 37 | @Test 38 | public void testSave() throws Exception { 39 | 40 | UUID boardUuid = UUID.randomUUID(); 41 | Board board = createTestBoard( boardUuid ); 42 | 43 | when( this.eventStoreClient.addNewDomainEvent( any( DomainEvent.class ) ) ).thenReturn( ResponseEntity.accepted().build() ); 44 | 45 | this.boardClient.save( board ); 46 | 47 | verify( this.eventStoreClient, times( 1 ) ).addNewDomainEvent( any( DomainEvent.class ) ); 48 | 49 | } 50 | 51 | @Test( expected = IllegalStateException.class ) 52 | public void testSaveNotAccepted() throws Exception { 53 | 54 | UUID boardUuid = UUID.randomUUID(); 55 | Board board = createTestBoard( boardUuid ); 56 | 57 | when( this.eventStoreClient.addNewDomainEvent( any( DomainEvent.class ) ) ).thenReturn( ResponseEntity.unprocessableEntity().build() ); 58 | 59 | this.boardClient.save( board ); 60 | 61 | verify( this.eventStoreClient, times( 1 ) ).addNewDomainEvent( any( DomainEvent.class ) ); 62 | 63 | } 64 | 65 | @Test 66 | public void testFind() throws Exception { 67 | 68 | UUID boardUuid = UUID.randomUUID(); 69 | DomainEvents domainEvents = new DomainEvents(); 70 | domainEvents.setBoardUuid( boardUuid ); 71 | domainEvents.getDomainEvents().add( createTestBoardInitializedEvent( boardUuid ) ); 72 | 73 | when( this.eventStoreClient.getDomainEventsForBoardUuid( any( UUID.class ) ) ).thenReturn( domainEvents ); 74 | 75 | Board board = this.boardClient.find( boardUuid ); 76 | assertThat( board ).isNotNull(); 77 | assertThat( board.getBoardUuid() ).isEqualTo( boardUuid ); 78 | assertThat( board.getName() ).isEqualTo( "New Board" ); 79 | assertThat( board.getStories() ).hasSize( 0 ); 80 | assertThat( board.changes() ).hasSize( 0 ); 81 | 82 | verify( this.eventStoreClient, times( 1 ) ).getDomainEventsForBoardUuid( any( UUID.class ) ); 83 | 84 | } 85 | 86 | @Test( expected = IllegalArgumentException.class ) 87 | public void testFindNotFound() throws Exception { 88 | 89 | UUID boardUuid = UUID.randomUUID(); 90 | DomainEvents domainEvents = new DomainEvents(); 91 | domainEvents.setBoardUuid( boardUuid ); 92 | 93 | when( this.eventStoreClient.getDomainEventsForBoardUuid( any( UUID.class ) ) ).thenReturn( domainEvents ); 94 | 95 | this.boardClient.find( boardUuid ); 96 | 97 | verify( this.eventStoreClient, times( 1 ) ).addNewDomainEvent( any( DomainEvent.class ) ); 98 | 99 | } 100 | 101 | private BoardInitialized createTestBoardInitializedEvent( final UUID boardUuid ) { 102 | 103 | return new BoardInitialized( boardUuid, Instant.now() ); 104 | } 105 | 106 | private Board createTestBoard( final UUID boardUuid ) { 107 | 108 | Board board = new Board( boardUuid ); 109 | assertThat( board ).isNotNull(); 110 | assertThat( board.getBoardUuid() ).isEqualTo( boardUuid ); 111 | assertThat( board.getName() ).isEqualTo( "New Board" ); 112 | assertThat( board.changes() ).hasSize( 1 ); 113 | 114 | return board; 115 | } 116 | 117 | private Story createTestStory( final String name ) { 118 | 119 | Story story = new Story(); 120 | story.setName( name ); 121 | 122 | return story; 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /command/src/test/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/KafkaBoardClientTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.config.KafkaClientConfig; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service.DomainEventSourceImpl; 6 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 7 | import org.junit.AfterClass; 8 | import org.junit.BeforeClass; 9 | import org.junit.ClassRule; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | import org.springframework.boot.test.context.SpringBootTest; 14 | import org.springframework.boot.test.mock.mockito.MockBean; 15 | import org.springframework.cloud.stream.binder.kafka.streams.QueryableStoreRegistry; 16 | import org.springframework.kafka.test.rule.KafkaEmbedded; 17 | import org.springframework.test.annotation.DirtiesContext; 18 | import org.springframework.test.context.ActiveProfiles; 19 | import org.springframework.test.context.junit4.SpringRunner; 20 | 21 | import java.util.UUID; 22 | 23 | import static org.hamcrest.Matchers.*; 24 | import static org.junit.Assert.assertThat; 25 | import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; 26 | 27 | @RunWith( SpringRunner.class ) 28 | @SpringBootTest( 29 | webEnvironment = NONE, 30 | classes = { KafkaClientConfig.class, DomainEventSourceImpl.class }, 31 | properties = { 32 | "--spring.cloud.service-registry.auto-registration.enabled=false", 33 | "--spring.cloud.stream.bindings.output.binder=kafka" 34 | } 35 | ) 36 | @ActiveProfiles( "kafka" ) 37 | @DirtiesContext 38 | //@Ignore 39 | public class KafkaBoardClientTests { 40 | 41 | private static final String KAFKA_BROKERS_PROPERTY = "spring.kafka.bootstrap-servers"; 42 | 43 | @ClassRule 44 | public static KafkaEmbedded kafkaEmbedded = new KafkaEmbedded( 1, true ); 45 | 46 | @BeforeClass 47 | public static void setup() { 48 | System.setProperty( KAFKA_BROKERS_PROPERTY, kafkaEmbedded.getBrokersAsString() ); 49 | } 50 | 51 | @AfterClass 52 | public static void clean() { 53 | System.clearProperty( KAFKA_BROKERS_PROPERTY ); 54 | } 55 | 56 | @Autowired 57 | BoardClient boardClient; 58 | 59 | @MockBean 60 | QueryableStoreRegistry queryableStoreRegistry; 61 | 62 | @Test 63 | public void testSave() throws Exception { 64 | 65 | UUID boardUuid = UUID.randomUUID(); 66 | Board board = createTestBoard( boardUuid ); 67 | 68 | this.boardClient.save( board ); 69 | 70 | } 71 | 72 | private Board createTestBoard( final UUID boardUuid ) { 73 | 74 | Board board = new Board( boardUuid ); 75 | assertThat( board, is( not( nullValue() )) ); 76 | assertThat( board.getBoardUuid(), is( equalTo( boardUuid ) ) ); 77 | assertThat( board.getName(), is( equalTo( "New Board" ) ) ); 78 | assertThat( board.changes(), hasSize( 1 ) ); 79 | 80 | return board; 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /command/src/test/java/io/pivotal/dmfrey/eventStoreDemo/domain/model/BoardTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.model; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.*; 4 | import org.junit.Test; 5 | 6 | import java.time.Instant; 7 | import java.util.Arrays; 8 | import java.util.UUID; 9 | 10 | import static org.assertj.core.api.Assertions.assertThat; 11 | 12 | public class BoardTests { 13 | 14 | @Test 15 | public void testBoardCreateFrom() throws Exception { 16 | 17 | UUID boardUuid = UUID.randomUUID(); 18 | UUID storyUuid = UUID.randomUUID(); 19 | 20 | BoardInitialized boardInitialized = new BoardInitialized( boardUuid, Instant.now() ); 21 | BoardRenamed boardRenamed = new BoardRenamed( "Test Board", boardUuid, Instant.now() ); 22 | StoryAdded storyAdded = new StoryAdded( storyUuid, "Test Story", boardUuid, Instant.now() ); 23 | 24 | Board board = Board.createFrom( boardUuid, Arrays.asList( boardInitialized, boardRenamed, storyAdded ) ); 25 | assertThat( board ).isNotNull(); 26 | assertThat( board.changes() ).hasSize( 3 ) 27 | .containsSequence( boardInitialized, boardRenamed, storyAdded ); 28 | assertThat( board.getBoardUuid() ).isEqualTo( boardUuid ); 29 | assertThat( board.getName() ).isEqualTo( "Test Board" ); 30 | assertThat( board.getStories() ).hasSize( 1 ) 31 | .containsKey( storyUuid ) 32 | .containsValue( createTestStory( "Test Story" ) ); 33 | 34 | StoryUpdated storyUpdated = new StoryUpdated( storyUuid, "Test Story Updated", boardUuid, Instant.now() ); 35 | 36 | board = Board.createFrom( boardUuid, Arrays.asList( boardInitialized, boardRenamed, storyAdded, storyUpdated ) ); 37 | assertThat( board.changes() ).hasSize( 4 ) 38 | .containsSequence( boardInitialized, boardRenamed, storyAdded, storyUpdated ); 39 | assertThat( board.getStories() ).hasSize( 1 ) 40 | .containsKey( storyUuid ) 41 | .containsValue( createTestStory( "Test Story Updated" ) ); 42 | 43 | 44 | StoryDeleted storyDeleted = new StoryDeleted( storyUuid, boardUuid, Instant.now() ); 45 | 46 | board = Board.createFrom( boardUuid, Arrays.asList( boardInitialized, boardRenamed, storyAdded, storyUpdated, storyDeleted ) ); 47 | assertThat( board.changes() ).hasSize( 5 ) 48 | .containsSequence( boardInitialized, boardRenamed, storyAdded, storyUpdated, storyDeleted ); 49 | assertThat( board.getStories() ).hasSize( 0 ) 50 | .doesNotContainKey( storyUuid ); 51 | 52 | } 53 | 54 | private Story createTestStory( final String name ) { 55 | 56 | Story story = new Story(); 57 | story.setName( name ); 58 | 59 | return story; 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /command/src/test/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/BoardServiceTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Story; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | import java.util.UUID; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.mockito.ArgumentMatchers.any; 17 | import static org.mockito.Mockito.times; 18 | import static org.mockito.Mockito.verify; 19 | import static org.mockito.Mockito.when; 20 | 21 | @RunWith( SpringRunner.class ) 22 | @SpringBootTest( classes = { BoardService.class }, 23 | properties = { 24 | "spring.cloud.service-registry.auto-registration.enabled=false" 25 | } 26 | ) 27 | public class BoardServiceTests { 28 | 29 | @Autowired 30 | BoardService service; 31 | 32 | @MockBean 33 | BoardClient client; 34 | 35 | @Test 36 | public void testCreateBoard() throws Exception { 37 | 38 | UUID boardUuid = this.service.createBoard(); 39 | assertThat( boardUuid ).isNotNull(); 40 | 41 | verify( this.client, times( 1 ) ).save( any( Board.class ) ); 42 | 43 | } 44 | 45 | @Test 46 | public void testRenameBoard() throws Exception { 47 | 48 | UUID boardUuid = UUID.randomUUID(); 49 | Board board = createTestBoard( boardUuid ); 50 | 51 | when( this.client.find( any( UUID.class ) ) ).thenReturn( board ); 52 | 53 | this.service.renameBoard( boardUuid, "Test Board" ); 54 | assertThat( board.getName() ).isEqualTo( "Test Board" ); 55 | assertThat( board.changes() ).hasSize( 2 ); 56 | 57 | verify( this.client, times( 1 ) ).find( any( UUID.class ) ); 58 | verify( this.client, times( 1 ) ).save( any( Board.class ) ); 59 | 60 | } 61 | 62 | @Test 63 | public void testAddStoryToBoard() throws Exception { 64 | 65 | UUID boardUuid = UUID.randomUUID(); 66 | Board board = createTestBoard( boardUuid ); 67 | 68 | when( this.client.find( any( UUID.class ) ) ).thenReturn( board ); 69 | 70 | UUID storyUuid = this.service.addStory( boardUuid, "Test Story" ); 71 | assertThat( storyUuid ).isNotNull(); 72 | assertThat( board.getStories() ).hasSize( 1 ) 73 | .containsKey( storyUuid ) 74 | .containsValue( createTestStory( "Test Story" ) ); 75 | assertThat( board.changes() ).hasSize( 2 ); 76 | 77 | verify( this.client, times( 1 ) ).find( any( UUID.class ) ); 78 | verify( this.client, times( 1 ) ).save( any( Board.class ) ); 79 | 80 | } 81 | 82 | @Test 83 | public void testUpdateStoryOnBoard() throws Exception { 84 | 85 | UUID boardUuid = UUID.randomUUID(); 86 | UUID storyUuid = UUID.randomUUID(); 87 | Board board = createTestBoard( boardUuid ); 88 | board.addStory( storyUuid, "Test Story" ); 89 | assertThat( board.getStories() ).hasSize( 1 ) 90 | .containsKey( storyUuid ) 91 | .containsValue( createTestStory( "Test Story" ) ); 92 | 93 | when( this.client.find( any( UUID.class ) ) ).thenReturn( board ); 94 | 95 | this.service.updateStory( boardUuid, storyUuid,"Test Story Updated" ); 96 | assertThat( board.getStories() ).hasSize( 1 ) 97 | .containsKey( storyUuid ) 98 | .containsValue( createTestStory( "Test Story Updated" ) ); 99 | assertThat( board.changes() ).hasSize( 3 ); 100 | 101 | verify( this.client, times( 1 ) ).find( any( UUID.class ) ); 102 | verify( this.client, times( 1 ) ).save( any( Board.class ) ); 103 | 104 | } 105 | 106 | @Test 107 | public void testDeleteStoryFromBoard() throws Exception { 108 | 109 | UUID boardUuid = UUID.randomUUID(); 110 | UUID storyUuid = UUID.randomUUID(); 111 | Board board = createTestBoard( boardUuid ); 112 | board.addStory( storyUuid, "Test Story" ); 113 | assertThat( board.getStories() ).hasSize( 1 ) 114 | .containsKey( storyUuid ) 115 | .containsValue( createTestStory( "Test Story" ) ); 116 | 117 | when( this.client.find( any( UUID.class ) ) ).thenReturn( board ); 118 | 119 | this.service.deleteStory( boardUuid, storyUuid ); 120 | assertThat( board.getStories() ).hasSize( 0 ) 121 | .doesNotContainKey( storyUuid ); 122 | assertThat( board.changes() ).hasSize( 3 ); 123 | 124 | verify( this.client, times( 1 ) ).find( any( UUID.class ) ); 125 | verify( this.client, times( 1 ) ).save( any( Board.class ) ); 126 | 127 | } 128 | 129 | private Board createTestBoard( final UUID boardUuid ) { 130 | 131 | Board board = new Board( boardUuid ); 132 | assertThat( board ).isNotNull(); 133 | assertThat( board.getBoardUuid() ).isEqualTo( boardUuid ); 134 | assertThat( board.getName() ).isEqualTo( "New Board" ); 135 | assertThat( board.changes() ).hasSize( 1 ); 136 | 137 | return board; 138 | } 139 | 140 | private Story createTestStory( final String name ) { 141 | 142 | Story story = new Story(); 143 | story.setName( name ); 144 | 145 | return story; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /command/src/test/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/CommandsControllerTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.BoardService; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 8 | import org.springframework.boot.test.mock.mockito.MockBean; 9 | import org.springframework.http.HttpHeaders; 10 | import org.springframework.test.context.junit4.SpringRunner; 11 | import org.springframework.test.web.servlet.MockMvc; 12 | 13 | import java.util.UUID; 14 | 15 | import static org.hamcrest.Matchers.equalTo; 16 | import static org.hamcrest.Matchers.is; 17 | import static org.mockito.ArgumentMatchers.any; 18 | import static org.mockito.ArgumentMatchers.anyString; 19 | import static org.mockito.Mockito.times; 20 | import static org.mockito.Mockito.verify; 21 | import static org.mockito.Mockito.when; 22 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 24 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 26 | 27 | @RunWith( SpringRunner.class ) 28 | @WebMvcTest( value = CommandsController.class, secure = false ) 29 | public class CommandsControllerTests { 30 | 31 | @Autowired 32 | MockMvc mockMvc; 33 | 34 | @MockBean 35 | BoardService service; 36 | 37 | @Test 38 | public void testCreateBoard() throws Exception { 39 | 40 | UUID boardUuid = UUID.randomUUID(); 41 | when( this.service.createBoard() ).thenReturn( boardUuid ); 42 | 43 | this.mockMvc.perform( post( "/boards/" ).param( "name", "Test Board" ) ) 44 | .andDo( print() ) 45 | .andExpect( status().isCreated() ) 46 | .andExpect( header().string( HttpHeaders.LOCATION, is( equalTo( "http://localhost/boards/" + boardUuid.toString() ) ) ) ); 47 | 48 | verify( this.service, times( 1 ) ).createBoard(); 49 | 50 | } 51 | 52 | @Test 53 | public void testRenameBoard() throws Exception { 54 | 55 | UUID boardUuid = UUID.randomUUID(); 56 | 57 | this.mockMvc.perform( patch( "/boards/{boardUuid}", boardUuid ).param( "name", "Test Board" ) ) 58 | .andDo( print() ) 59 | .andExpect( status().isAccepted() );; 60 | 61 | verify( this.service, times( 1 ) ).renameBoard( any( UUID.class ), anyString() ); 62 | 63 | } 64 | 65 | @Test 66 | public void testCreateStoryOnBoard() throws Exception { 67 | 68 | UUID boardUuid = UUID.randomUUID(); 69 | UUID storyUuid = UUID.randomUUID(); 70 | when( this.service.addStory( any( UUID.class ), anyString() ) ).thenReturn( storyUuid ); 71 | 72 | this.mockMvc.perform( post( "/boards/{boardUuid}/stories", boardUuid ).param( "name", "Test Story" ) ) 73 | .andDo( print() ) 74 | .andExpect( status().isCreated() ) 75 | .andExpect( header().string( HttpHeaders.LOCATION, is( equalTo( "http://localhost/boards/" + boardUuid.toString() + "/stories/" + storyUuid.toString() ) ) ) ); 76 | 77 | verify( this.service, times( 1 ) ).addStory( any( UUID.class ), anyString() ); 78 | 79 | } 80 | 81 | @Test 82 | public void testUpdateStoryOnBoard() throws Exception { 83 | 84 | UUID boardUuid = UUID.randomUUID(); 85 | UUID storyUuid = UUID.randomUUID(); 86 | 87 | this.mockMvc.perform( put( "/boards/{boardUuid}/stories/{storyUuid}", boardUuid, storyUuid ).param( "name", "Test Story Updated" ) ) 88 | .andDo( print() ) 89 | .andExpect( status().isAccepted() ); 90 | 91 | verify( this.service, times( 1 ) ).updateStory( any( UUID.class ), any( UUID.class ), anyString() ); 92 | 93 | } 94 | 95 | @Test 96 | public void testDeleteStoryOnBoard() throws Exception { 97 | 98 | UUID boardUuid = UUID.randomUUID(); 99 | UUID storyUuid = UUID.randomUUID(); 100 | 101 | this.mockMvc.perform( delete( "/boards/{boardUuid}/stories/{storyUuid}", boardUuid, storyUuid ) ) 102 | .andDo( print() ) 103 | .andExpect( status().isAccepted() ); 104 | 105 | verify( this.service, times( 1 ) ).deleteStory( any( UUID.class ), any( UUID.class ) ); 106 | 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /command/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | spring: 3 | profiles: kafka 4 | 5 | kafka: 6 | bootstrap-servers: ${spring.embedded.kafka.brokers} 7 | topic: 8 | receiver: board-events 9 | -------------------------------------------------------------------------------- /command/src/test/resources/contracts/api/shouldDeleteExistingStoryOnExistingBoard.groovy: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | org.springframework.cloud.contract.spec.Contract.make { 4 | 5 | request { 6 | method 'DELETE' 7 | url( '/boards/11111111-90ab-cdef-1234-567890abcdef/stories/10240df9-4a1e-4fa4-bbd1-0bb33d764603' ) 8 | 9 | } 10 | 11 | response { 12 | 13 | status 202 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /command/src/test/resources/contracts/api/shouldPatchExistingStoryOnExistingBoard.groovy: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | org.springframework.cloud.contract.spec.Contract.make { 4 | 5 | request { 6 | method 'PUT' 7 | url( '/boards/11111111-90ab-cdef-1234-567890abcdef/stories/10240df9-4a1e-4fa4-bbd1-0bb33d764603' ) { 8 | 9 | queryParameters { 10 | 11 | parameter 'name': 'Updated Story Name' 12 | } 13 | 14 | } 15 | 16 | } 17 | 18 | response { 19 | 20 | status 202 21 | 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /command/src/test/resources/contracts/api/shouldPostNewBoard.groovy: -------------------------------------------------------------------------------- 1 | 2 | org.springframework.cloud.contract.spec.Contract.make { 3 | 4 | request { 5 | 6 | method 'POST' 7 | 8 | url( '/boards/' ) 9 | 10 | } 11 | 12 | response { 13 | 14 | status 201 15 | 16 | headers { 17 | header( 'LOCATION','http://localhost/boards/11111111-90ab-cdef-1234-567890abcdef' ) 18 | } 19 | 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /command/src/test/resources/contracts/api/shouldPostNewStoryOnExistingBoar.groovy: -------------------------------------------------------------------------------- 1 | 2 | org.springframework.cloud.contract.spec.Contract.make { 3 | 4 | request { 5 | 6 | method 'POST' 7 | 8 | url( '/boards/11111111-90ab-cdef-1234-567890abcdef/stories' ) { 9 | 10 | queryParameters { 11 | 12 | parameter 'name': 'New Story Name' 13 | } 14 | 15 | } 16 | 17 | } 18 | 19 | response { 20 | 21 | status 201 22 | 23 | headers { 24 | header( 'LOCATION','http://localhost/boards/11111111-90ab-cdef-1234-567890abcdef/stories/10240df9-4a1e-4fa4-bbd1-0bb33d764603' ) 25 | } 26 | 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /command/src/test/resources/contracts/api/shouldPutExistingBoard.groovy: -------------------------------------------------------------------------------- 1 | 2 | org.springframework.cloud.contract.spec.Contract.make { 3 | 4 | request { 5 | 6 | method 'PATCH' 7 | 8 | url( '/boards/11111111-90ab-cdef-1234-567890abcdef' ) { 9 | 10 | queryParameters { 11 | 12 | parameter 'name': 'New Name' 13 | } 14 | 15 | } 16 | 17 | } 18 | 19 | response { 20 | 21 | status 202 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /command/src/test/resources/contracts/kafka/shouldPublishBoardInitialized.bak: -------------------------------------------------------------------------------- 1 | //package kafka 2 | // 3 | //org.springframework.cloud.contract.spec.Contract.make { 4 | // 5 | // label 'publish_board_initialised' 6 | // 7 | // input { 8 | // 9 | // triggeredBy( 'publishBoardInitialized()' ) 10 | // 11 | // } 12 | // 13 | // outputMessage { 14 | // 15 | // sentTo( 'board-events' ) 16 | // 17 | // body {''' 18 | // "eventType": "BoardInitialized", 19 | // "boardUuid": "11111111-90ab-cdef-1234-567890abcdef", 20 | // "occurredOn" "2018-08-16T16:45:32.123Z" 21 | // '''} 22 | // } 23 | // 24 | //} 25 | -------------------------------------------------------------------------------- /event-store/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | 4 | // Spring Boot dependencies 5 | compile('org.springframework.boot:spring-boot-starter-data-jpa') 6 | // compile('org.springframework.boot:spring-boot-starter-data-mongodb') 7 | 8 | 9 | // Spring Cloud dependencies 10 | compile('org.springframework.cloud:spring-cloud-stream-binder-rabbit') 11 | 12 | 13 | // Third-party dependencies 14 | runtime('com.h2database:h2') 15 | 16 | 17 | // Test dependencies 18 | compile('org.springframework.cloud:spring-cloud-stream-test-support') 19 | 20 | 21 | } 22 | 23 | -------------------------------------------------------------------------------- /event-store/manifest_development.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: esd-event-store 4 | host: esd-event-store-development 5 | buildpack: java_buildpack_offline 6 | memory: 1024M 7 | instances: 2 8 | path: build/libs/event-store-0.0.1-SNAPSHOT.jar 9 | services: 10 | - discovery-service 11 | - hystrix-dashboard 12 | - rabbitmq 13 | - mysql 14 | env: 15 | JAVA_OPTS: -Djava.security.egd=file:///dev/urandom 16 | SPRING_PROFILES_ACTIVE: event-store -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/Application.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; 6 | 7 | @SpringBootApplication 8 | @EnableCircuitBreaker 9 | public class Application { 10 | 11 | public static void main( String[] args ) { 12 | 13 | SpringApplication.run( Application.class, args ); 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/DefaultConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; 2 | 3 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 4 | import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.context.annotation.Profile; 7 | 8 | @Profile( "default" ) 9 | @Configuration 10 | @EnableAutoConfiguration( exclude = {MongoDataAutoConfiguration.class } ) 11 | public class DefaultConfig { 12 | } 13 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/RabbitConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; 2 | 3 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 4 | import org.springframework.cloud.config.java.AbstractCloudConfig; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | 9 | @Configuration 10 | public class RabbitConfig { 11 | 12 | 13 | @Configuration 14 | @Profile("cloud") 15 | public static class CloudRabbitConfig extends AbstractCloudConfig { 16 | 17 | @Bean 18 | public ConnectionFactory rabbitConnectionFactory() { 19 | 20 | return connectionFactory().rabbitConnectionFactory(); 21 | } 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 7 | 8 | @Configuration 9 | @EnableWebSecurity 10 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 11 | 12 | @Override 13 | protected void configure(HttpSecurity httpSecurity) throws Exception { 14 | 15 | httpSecurity 16 | .authorizeRequests() 17 | .anyRequest().permitAll() 18 | .and() 19 | .csrf().disable(); 20 | 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/config/CloudDataConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.persistence.DomainEventEntity; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.persistence.DomainEventsEntity; 5 | import org.springframework.boot.autoconfigure.domain.EntityScan; 6 | import org.springframework.cloud.config.java.AbstractCloudConfig; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Profile; 10 | import org.springframework.data.jpa.convert.threeten.Jsr310JpaConverters; 11 | 12 | import javax.sql.DataSource; 13 | 14 | @Configuration 15 | @EntityScan( 16 | basePackageClasses = { DomainEventsEntity.class, DomainEventEntity.class, Jsr310JpaConverters.class } 17 | ) 18 | @Profile({ "cloud" }) 19 | public class CloudDataConfig extends AbstractCloudConfig { 20 | 21 | @Bean 22 | public DataSource dataSource() { 23 | 24 | return connectionFactory().dataSource(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/config/EventStoreConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.persistence.DomainEventsRepository; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.DomainEventService; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.NotificationPublisher; 6 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.converters.JsonStringToTupleConverter; 7 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.converters.TupleToJsonStringConverter; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.context.annotation.Configuration; 10 | 11 | @Configuration 12 | public class EventStoreConfig { 13 | 14 | @Bean 15 | public DomainEventService domainEventService( 16 | final DomainEventsRepository domainEventsRepository, final NotificationPublisher publisher, 17 | final TupleToJsonStringConverter tupleToJsonStringConverter, final JsonStringToTupleConverter jsonStringToTupleConverter 18 | ) { 19 | 20 | return new DomainEventService( domainEventsRepository, publisher, tupleToJsonStringConverter, jsonStringToTupleConverter ); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/config/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.config; -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/model/DomainEvents.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonRawValue; 4 | import lombok.Data; 5 | 6 | import java.util.List; 7 | 8 | 9 | @Data 10 | public class DomainEvents { 11 | 12 | private String boardUuid; 13 | 14 | @JsonRawValue 15 | private List domainEvents; 16 | 17 | } 18 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/model/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.model; -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain; -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/persistence/DomainEventEntity.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.persistence; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import javax.persistence.*; 8 | import java.time.LocalDateTime; 9 | 10 | @Entity( name = "domainEvent" ) 11 | @Table( name = "domain_event" ) 12 | @Data 13 | @EqualsAndHashCode( exclude = { "id", "occurredOn" }) 14 | @JsonIgnoreProperties( ignoreUnknown = true ) 15 | public class DomainEventEntity { 16 | 17 | @Id 18 | private String id; 19 | 20 | @Column( columnDefinition = "TIMESTAMP" ) 21 | private LocalDateTime occurredOn; 22 | 23 | @Lob 24 | private String data; 25 | 26 | @Column( name = "board_uuid" ) 27 | private String boardUuid; 28 | 29 | } 30 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/persistence/DomainEventsEntity.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.persistence; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.DomainEvents; 4 | import lombok.Data; 5 | import lombok.EqualsAndHashCode; 6 | 7 | import javax.persistence.*; 8 | import java.util.LinkedHashSet; 9 | import java.util.Set; 10 | 11 | import static java.util.stream.Collectors.toList; 12 | import static javax.persistence.CascadeType.ALL; 13 | 14 | @Entity( name = "domainEvents" ) 15 | @Table( name = "domain_events" ) 16 | @Data 17 | @EqualsAndHashCode( exclude = { "domainEvents" }) 18 | public class DomainEventsEntity { 19 | 20 | @Id 21 | private String boardUuid; 22 | 23 | @OneToMany( cascade = ALL ) 24 | @JoinColumn( name = "board_uuid" ) 25 | @OrderBy( "occurredOn ASC" ) 26 | private Set domainEvents; 27 | 28 | public DomainEventsEntity() { 29 | 30 | this.domainEvents = new LinkedHashSet<>(); 31 | 32 | } 33 | 34 | public DomainEventsEntity( final String boardUuid ) { 35 | this(); 36 | 37 | this.boardUuid = boardUuid; 38 | 39 | } 40 | 41 | public DomainEvents toModel() { 42 | 43 | DomainEvents model = new DomainEvents(); 44 | model.setBoardUuid( boardUuid ); 45 | model.setDomainEvents( domainEvents.stream() 46 | .map( DomainEventEntity::getData ) 47 | .collect( toList() ) ); 48 | 49 | return model; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/persistence/DomainEventsRepository.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.persistence; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | 5 | import java.util.List; 6 | 7 | 8 | public interface DomainEventsRepository extends CrudRepository { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/persistence/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.persistence; -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/DomainEventService.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.DomainEvents; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.persistence.DomainEventEntity; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.persistence.DomainEventsEntity; 6 | import io.pivotal.dmfrey.eventStoreDemo.domain.persistence.DomainEventsRepository; 7 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.converters.JsonStringToTupleConverter; 8 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.converters.TupleToJsonStringConverter; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.springframework.transaction.annotation.Transactional; 11 | import org.springframework.tuple.Tuple; 12 | 13 | import java.time.Instant; 14 | import java.time.LocalDateTime; 15 | import java.time.ZoneOffset; 16 | import java.util.UUID; 17 | 18 | @Slf4j 19 | @Transactional( readOnly = true ) 20 | public class DomainEventService { 21 | 22 | private final DomainEventsRepository domainEventsRepository; 23 | private final NotificationPublisher publisher; 24 | private final TupleToJsonStringConverter toJsonStringConverter; 25 | private final JsonStringToTupleConverter toTupleConverter; 26 | 27 | 28 | public DomainEventService( 29 | final DomainEventsRepository domainEventsRepository, final NotificationPublisher publisher, 30 | final TupleToJsonStringConverter toJsonStringConverter, final JsonStringToTupleConverter toTupleConverter) { 31 | 32 | this.domainEventsRepository = domainEventsRepository; 33 | this.publisher = publisher; 34 | this.toJsonStringConverter = toJsonStringConverter; 35 | this.toTupleConverter = toTupleConverter; 36 | 37 | } 38 | 39 | public DomainEvents getDomainEvents( final String boardUuid ) { 40 | log.debug( "processDomainEvent : enter" ); 41 | 42 | log.debug( "processDomainEvent : boardUuid=" + boardUuid ); 43 | 44 | return domainEventsRepository.findById( boardUuid ) 45 | .map( DomainEventsEntity::toModel ) 46 | .orElseThrow( IllegalArgumentException::new ); 47 | } 48 | 49 | @Transactional 50 | public void processDomainEvent( final Tuple event ) { 51 | log.debug( "processDomainEvent : enter" ); 52 | 53 | log.debug( "processDomainEvent : event[{}] ", event ); 54 | 55 | String eventType = event.getString( "eventType" ); 56 | switch ( eventType ) { 57 | 58 | case "BoardInitialized": 59 | processBoardInitialized( event ); 60 | break; 61 | 62 | default: 63 | processBoardEvent( event ); 64 | break; 65 | } 66 | 67 | log.debug( "processDomainEvent : calling publisher.sendNotification( event )" ); 68 | this.publisher.sendNotification( event ); 69 | 70 | log.debug( "processDomainEvent : exit" ); 71 | } 72 | 73 | private void processBoardInitialized( final Tuple event ) { 74 | log.debug( "processBoardInitialized : enter " ); 75 | 76 | String boardUuid = event.getString( "boardUuid" ); 77 | 78 | DomainEventsEntity domainEventsEntity = new DomainEventsEntity( boardUuid ); 79 | 80 | DomainEventEntity domainEventEntity = new DomainEventEntity(); 81 | domainEventEntity.setId( UUID.randomUUID().toString() ); 82 | 83 | Instant occurredOn = Instant.parse( event.getString( "occurredOn" ) ); 84 | domainEventEntity.setOccurredOn( LocalDateTime.ofInstant( occurredOn, ZoneOffset.UTC ) ); 85 | 86 | domainEventEntity.setData( this.toJsonStringConverter.convert( event ) ); 87 | 88 | domainEventsEntity.getDomainEvents().add( domainEventEntity ); 89 | 90 | this.domainEventsRepository.save( domainEventsEntity ); 91 | 92 | } 93 | 94 | private void processBoardEvent( final Tuple event ) { 95 | log.debug( "processBoardEvent : enter " ); 96 | 97 | String boardUuid = event.getString( "boardUuid" ); 98 | 99 | this.domainEventsRepository.findById( boardUuid ) 100 | .ifPresent( found -> { 101 | log.debug( "processBoardEvent : a DomainEventsEntity[{}] was found for boardUuid[{}]. ", found, boardUuid ); 102 | 103 | DomainEventEntity domainEventEntity = new DomainEventEntity(); 104 | domainEventEntity.setId( UUID.randomUUID().toString() ); 105 | 106 | Instant occurredOn = Instant.parse( event.getString( "occurredOn" ) ); 107 | domainEventEntity.setOccurredOn( LocalDateTime.ofInstant( occurredOn, ZoneOffset.UTC ) ); 108 | 109 | domainEventEntity.setData( toJsonStringConverter.convert( event ) ); 110 | 111 | found.getDomainEvents().add( domainEventEntity ); 112 | this.domainEventsRepository.save( found ); 113 | 114 | }); 115 | 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/NotificationPublisher.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; 2 | 3 | import org.springframework.messaging.Message; 4 | import org.springframework.tuple.Tuple; 5 | 6 | public interface NotificationPublisher { 7 | 8 | Message sendNotification( final Tuple event ); 9 | 10 | } 11 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/NotificationPublisherImpl.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.converters.TupleToJsonStringConverter; 4 | import org.springframework.cloud.stream.annotation.EnableBinding; 5 | import org.springframework.cloud.stream.messaging.Source; 6 | import org.springframework.integration.annotation.Publisher; 7 | import org.springframework.messaging.Message; 8 | import org.springframework.messaging.support.MessageBuilder; 9 | import org.springframework.tuple.Tuple; 10 | 11 | @EnableBinding( Source.class ) 12 | public class NotificationPublisherImpl implements NotificationPublisher { 13 | 14 | private final TupleToJsonStringConverter converter; 15 | 16 | public NotificationPublisherImpl( final TupleToJsonStringConverter tupleToJsonStringConverter ) { 17 | 18 | this.converter = tupleToJsonStringConverter; 19 | 20 | } 21 | 22 | @Publisher( channel = Source.OUTPUT ) 23 | public Message sendNotification( Tuple event ) { 24 | 25 | String payload = converter.convert( event ); 26 | 27 | return MessageBuilder 28 | .withPayload( payload ) 29 | .setHeader( "x-delay", 1000 ) 30 | .build(); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/converters/JsonStringToTupleConverter.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service.converters; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import org.springframework.core.convert.converter.Converter; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.tuple.JsonNodeToTupleConverter; 7 | import org.springframework.tuple.Tuple; 8 | 9 | @Component 10 | public class JsonStringToTupleConverter implements Converter { 11 | 12 | private final ObjectMapper mapper; 13 | 14 | private final JsonNodeToTupleConverter jsonNodeToTupleConverter = new JsonNodeToTupleConverter(); 15 | 16 | public JsonStringToTupleConverter( ObjectMapper mapper ) { 17 | this.mapper = mapper; 18 | 19 | } 20 | 21 | @Override 22 | public Tuple convert(byte[] source) { 23 | if (source == null) { 24 | return null; 25 | } 26 | try { 27 | return jsonNodeToTupleConverter.convert( mapper.readTree( source ) ); 28 | } 29 | catch (Exception e) { 30 | throw new IllegalArgumentException(e.getMessage(), e); 31 | } 32 | } 33 | 34 | } -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/converters/TupleToJsonStringConverter.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service.converters; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.node.ArrayNode; 5 | import com.fasterxml.jackson.databind.node.BaseJsonNode; 6 | import com.fasterxml.jackson.databind.node.ObjectNode; 7 | import org.springframework.core.convert.converter.Converter; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.tuple.Tuple; 10 | 11 | import java.util.List; 12 | 13 | @Component 14 | public class TupleToJsonStringConverter implements Converter { 15 | 16 | private final ObjectMapper mapper; 17 | 18 | public TupleToJsonStringConverter( final ObjectMapper mapper ) { 19 | 20 | this.mapper = mapper; 21 | 22 | } 23 | 24 | @Override 25 | public String convert( Tuple source ) { 26 | ObjectNode root = toObjectNode(source); 27 | String json = null; 28 | try { 29 | json = mapper.writeValueAsString(root); 30 | } 31 | catch (Exception e) { 32 | throw new IllegalArgumentException("Tuple to string conversion failed", e); 33 | } 34 | return json; 35 | } 36 | 37 | private ObjectNode toObjectNode(Tuple source) { 38 | ObjectNode root = mapper.createObjectNode(); 39 | for (int i = 0; i < source.size(); i++) { 40 | Object value = source.getValues().get(i); 41 | String name = source.getFieldNames().get(i); 42 | if (value == null) { 43 | root.putNull(name); 44 | } else { 45 | root.putPOJO(name, toNode(value)); 46 | } 47 | } 48 | return root; 49 | } 50 | 51 | private ArrayNode toArrayNode(List source) { 52 | ArrayNode array = mapper.createArrayNode(); 53 | for (Object value : source) { 54 | if (value != null) { 55 | array.add(toNode(value)); 56 | } 57 | } 58 | return array; 59 | } 60 | 61 | private BaseJsonNode toNode(Object value) { 62 | if (value != null) { 63 | if (value instanceof Tuple) { 64 | return toObjectNode((Tuple) value); 65 | } 66 | else if (value instanceof List) { 67 | return toArrayNode((List) value); 68 | } 69 | else if (!value.getClass().isPrimitive()) { 70 | return mapper.getNodeFactory().pojoNode(value); 71 | } 72 | else { 73 | return mapper.valueToTree(value); 74 | } 75 | } 76 | return null; 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/converters/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service.converters; -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/EventStoreController.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.DomainEventService; 4 | import org.springframework.http.ResponseEntity; 5 | import org.springframework.tuple.Tuple; 6 | import org.springframework.tuple.TupleBuilder; 7 | import org.springframework.util.Assert; 8 | import org.springframework.web.bind.annotation.*; 9 | 10 | import java.util.UUID; 11 | 12 | @RestController 13 | public class EventStoreController { 14 | 15 | private final DomainEventService service; 16 | 17 | public EventStoreController( final DomainEventService service ) { 18 | 19 | this.service = service; 20 | 21 | } 22 | 23 | @PostMapping( "/" ) 24 | public ResponseEntity saveEvent( @RequestBody String json ) { 25 | 26 | Tuple event = TupleBuilder.fromString( json ); 27 | 28 | Assert.isTrue( event.hasFieldName( "eventType" ), "eventType is required" ); 29 | Assert.isTrue( event.hasFieldName( "boardUuid" ), "boardUuid is required" ); 30 | Assert.isTrue( event.hasFieldName( "occurredOn" ), "occurredOn is required" ); 31 | 32 | this.service.processDomainEvent( event ); 33 | 34 | return ResponseEntity 35 | .accepted() 36 | .build(); 37 | } 38 | 39 | @GetMapping( "/{boardUuid}" ) 40 | public ResponseEntity domainEvents( @PathVariable( "boardUuid" ) UUID boardUuid ) { 41 | 42 | return ResponseEntity 43 | .ok( this.service.getDomainEvents( boardUuid.toString() ) ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /event-store/src/main/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint; -------------------------------------------------------------------------------- /event-store/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | management.endpoints.web.exposure.include=* 2 | -------------------------------------------------------------------------------- /event-store/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | debug: false 2 | 3 | logging.level: 4 | io.pivotal.dmfrey: DEBUG 5 | 6 | management.endpoints.web.expose: health, info, env, metrics 7 | 8 | server: 9 | port: ${PORT:9082} 10 | use-forward-headers: true 11 | tomcat: 12 | remote-ip-header: x-forwarded-for 13 | protocol-header: x-forwarded-proto 14 | 15 | spring: 16 | h2: 17 | console: 18 | enabled: true 19 | path: /h2-console 20 | 21 | jackson: 22 | serialization: 23 | write_dates_as_timestamps: false 24 | 25 | cloud: 26 | stream: 27 | bindings: 28 | output: 29 | destination: board-event-notifications 30 | contentType: application/json 31 | producer: 32 | headerMode: raw # Outbound data has no embedded headers 33 | 34 | --- 35 | spring: 36 | profiles: cloud 37 | 38 | h2: 39 | console: 40 | enabled: false 41 | 42 | jpa: 43 | hibernate: 44 | ddl-auto: update -------------------------------------------------------------------------------- /event-store/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: esd-event-store 4 | 5 | eureka.instance.leaseRenewalIntervalInSeconds: 15 6 | 7 | --- 8 | spring: 9 | profiles: cloud 10 | 11 | cloud: 12 | services: 13 | registrationMethod: direct 14 | 15 | eureka.instance.leaseRenewalIntervalInSeconds: 30 16 | -------------------------------------------------------------------------------- /event-store/src/test/java/io/pivotal/dmfrey/eventStoreDemo/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo; 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( properties = { 10 | "spring.cloud.service-registry.auto-registration.enabled=false" 11 | }) 12 | public class ApplicationTests { 13 | 14 | @Test 15 | public void contextLoads() { 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /event-store/src/test/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/DomainEventServiceTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.DomainEvents; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.persistence.DomainEventEntity; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.persistence.DomainEventsEntity; 6 | import io.pivotal.dmfrey.eventStoreDemo.domain.persistence.DomainEventsRepository; 7 | import org.junit.Test; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.boot.test.mock.mockito.MockBean; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | import org.springframework.tuple.Tuple; 14 | import org.springframework.tuple.TupleBuilder; 15 | 16 | import java.time.LocalDateTime; 17 | import java.util.Optional; 18 | import java.util.UUID; 19 | 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | import static org.mockito.Mockito.*; 22 | 23 | @RunWith( SpringRunner.class ) 24 | @SpringBootTest 25 | public class DomainEventServiceTests { 26 | 27 | private static final String BOARD_INITIALIZED_EVENT = "{\"eventType\":\"BoardInitialized\",\"boardUuid\":\"ff4795e1-2514-4f5a-90e2-cd33dfadfbf2\",\"occurredOn\":\"2018-02-23T03:49:52.313Z\"}"; 28 | private static final String BOARD_RENAMED_EVENT = "{\"eventType\":\"BoardRenamed\",\"boardUuid\":\"ff4795e1-2514-4f5a-90e2-cd33dfadfbf2\",\"occurredOn\":\"2018-02-23T03:51:36.520Z\",\"name\":\"My Board\"}"; 29 | 30 | @Autowired 31 | private DomainEventService service; 32 | 33 | @MockBean 34 | private DomainEventsRepository repository; 35 | 36 | @MockBean 37 | private NotificationPublisher notificationPublisher; 38 | 39 | @Test 40 | public void testGetDomainEvents() throws Exception { 41 | 42 | DomainEventsEntity domainEventsEntity = createDomainEventsEntity(); 43 | when( this.repository.findById( anyString() ) ).thenReturn( Optional.of( domainEventsEntity ) ); 44 | 45 | DomainEvents domainEvents = this.service.getDomainEvents( "ff4795e1-2514-4f5a-90e2-cd33dfadfbf2" ); 46 | assertThat( domainEvents ).isNotNull(); 47 | assertThat( domainEvents.getBoardUuid() ).isEqualTo( "ff4795e1-2514-4f5a-90e2-cd33dfadfbf2" ); 48 | assertThat( domainEvents.getDomainEvents() ) 49 | .hasSize( 1 ) 50 | .containsExactly( BOARD_INITIALIZED_EVENT ); 51 | 52 | verify( this.repository, times( 1 ) ).findById( anyString() ); 53 | 54 | } 55 | 56 | @Test( expected = IllegalArgumentException.class ) 57 | public void testGetDomainEventsNotFound() throws Exception { 58 | 59 | when( this.repository.findById( anyString() ) ).thenThrow( new IllegalArgumentException() ); 60 | 61 | this.service.getDomainEvents( "ff4795e1-2514-4f5a-90e2-cd33dfadfbf2" ); 62 | 63 | verify( this.repository, times( 1 ) ).findById( anyString() ); 64 | 65 | } 66 | 67 | @Test 68 | public void testProcessBoardInitializedEvent() throws Exception { 69 | 70 | this.service.processDomainEvent( TupleBuilder.fromString( BOARD_INITIALIZED_EVENT ) ); 71 | 72 | verify( this.repository, times( 1 ) ).save( any( DomainEventsEntity.class ) ); 73 | verify( this.notificationPublisher, times( 1 ) ).sendNotification( any( Tuple.class ) ); 74 | 75 | } 76 | 77 | @Test 78 | public void testProcessBoardRenamedEvent() throws Exception { 79 | 80 | DomainEventsEntity domainEventsEntity = createDomainEventsEntity(); 81 | when( this.repository.findById( anyString() ) ).thenReturn( Optional.of( domainEventsEntity ) ); 82 | 83 | this.service.processDomainEvent( TupleBuilder.fromString( BOARD_RENAMED_EVENT ) ); 84 | 85 | verify( this.repository, times( 1 ) ).findById( anyString() ); 86 | verify( this.repository, times( 1 ) ).save( any( DomainEventsEntity.class ) ); 87 | verify( this.notificationPublisher, times( 1 ) ).sendNotification( any( Tuple.class ) ); 88 | 89 | } 90 | 91 | private DomainEventsEntity createDomainEventsEntity() { 92 | 93 | DomainEventEntity domainEvent = new DomainEventEntity(); 94 | domainEvent.setId( UUID.randomUUID().toString() ); 95 | domainEvent.setData( BOARD_INITIALIZED_EVENT ); 96 | domainEvent.setOccurredOn( LocalDateTime.now() ); 97 | domainEvent.setBoardUuid( "ff4795e1-2514-4f5a-90e2-cd33dfadfbf2" ); 98 | 99 | DomainEventsEntity domainEvents = new DomainEventsEntity(); 100 | domainEvents.setBoardUuid( "ff4795e1-2514-4f5a-90e2-cd33dfadfbf2" ); 101 | domainEvents.getDomainEvents().add( domainEvent ); 102 | 103 | return domainEvents; 104 | } 105 | 106 | } 107 | -------------------------------------------------------------------------------- /event-store/src/test/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/NotificationPublisherTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.cloud.stream.messaging.Source; 8 | import org.springframework.cloud.stream.test.binder.MessageCollector; 9 | import org.springframework.messaging.Message; 10 | import org.springframework.test.annotation.DirtiesContext; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | import org.springframework.tuple.Tuple; 13 | import org.springframework.tuple.TupleBuilder; 14 | 15 | import java.util.concurrent.BlockingQueue; 16 | 17 | import static org.hamcrest.Matchers.is; 18 | import static org.junit.Assert.assertThat; 19 | import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; 20 | import static org.springframework.cloud.stream.test.matcher.MessageQueueMatcher.receivesPayloadThat; 21 | 22 | @RunWith( SpringRunner.class ) 23 | @SpringBootTest( 24 | webEnvironment = NONE, 25 | properties = { 26 | "--spring.cloud.service-registry.auto-registration.enabled=false" 27 | } 28 | ) 29 | @DirtiesContext 30 | public class NotificationPublisherTests { 31 | 32 | private static final String BOARD_INITIALIZED_EVENT = "{\"eventType\":\"BoardInitialized\",\"boardUuid\":\"ff4795e1-2514-4f5a-90e2-cd33dfadfbf2\",\"occurredOn\":\"2018-02-23T03:49:52.313Z\"}"; 33 | 34 | @Autowired 35 | private Source source; 36 | 37 | @Autowired 38 | private MessageCollector collector; 39 | 40 | @Autowired 41 | private NotificationPublisher notificationPublisher; 42 | 43 | @Test 44 | public void testSendNotification() throws Exception { 45 | 46 | BlockingQueue> messages = collector.forChannel( source.output() ); 47 | 48 | Tuple event = TupleBuilder.fromString( BOARD_INITIALIZED_EVENT ); 49 | this.notificationPublisher.sendNotification( event ); 50 | 51 | assertThat( messages, receivesPayloadThat( is( BOARD_INITIALIZED_EVENT ) ) ); 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /event-store/src/test/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/EventStoreControllerTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.DomainEvents; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.DomainEventService; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | import org.springframework.test.web.servlet.MockMvc; 13 | import org.springframework.tuple.Tuple; 14 | 15 | import static java.util.Collections.singletonList; 16 | import static org.mockito.ArgumentMatchers.any; 17 | import static org.mockito.ArgumentMatchers.anyString; 18 | import static org.mockito.Mockito.*; 19 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 20 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 24 | 25 | @RunWith( SpringRunner.class ) 26 | @WebMvcTest( value = EventStoreController.class, secure = false ) 27 | public class EventStoreControllerTests { 28 | 29 | private static final String BOARD_INITIALIZED_EVENT = "{\"eventType\":\"BoardInitialized\",\"boardUuid\":\"ff4795e1-2514-4f5a-90e2-cd33dfadfbf2\",\"occurredOn\":\"2018-02-23T03:49:52.313Z\"}"; 30 | 31 | @Autowired 32 | ObjectMapper mapper; 33 | 34 | @Autowired 35 | MockMvc mockMvc; 36 | 37 | @MockBean 38 | DomainEventService service; 39 | 40 | @Test 41 | public void testSaveEvents() throws Exception { 42 | 43 | this.mockMvc.perform( post( "/" ).content( BOARD_INITIALIZED_EVENT ) ) 44 | .andDo( print() ) 45 | .andExpect( status().isAccepted() ); 46 | 47 | verify( this.service, times( 1 ) ).processDomainEvent( any( Tuple.class ) ); 48 | 49 | } 50 | 51 | @Test 52 | public void testDomainEvents() throws Exception { 53 | 54 | DomainEvents domainEvents = createDomainEvents(); 55 | String domainEventsJson = mapper.writeValueAsString( domainEvents ); 56 | 57 | when( this.service.getDomainEvents( anyString() ) ).thenReturn( domainEvents ); 58 | 59 | this.mockMvc.perform( get( "/{boardUuid}", domainEvents.getBoardUuid() ) ) 60 | .andDo( print() ) 61 | .andExpect( status().isOk() ) 62 | .andExpect( content().json( domainEventsJson ) ); 63 | 64 | verify( this.service, times( 1 ) ).getDomainEvents( anyString() ); 65 | 66 | } 67 | 68 | private DomainEvents createDomainEvents() { 69 | 70 | DomainEvents domainEvents = new DomainEvents(); 71 | domainEvents.setBoardUuid( "ff4795e1-2514-4f5a-90e2-cd33dfadfbf2" ); 72 | domainEvents.setDomainEvents( singletonList( BOARD_INITIALIZED_EVENT ) ); 73 | 74 | return domainEvents; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmfrey/event-store-demo/084a9220204c2769a2ba2834f1079dd8f85ceac9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Feb 20 08:45:55 EST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.9-all.zip 7 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /images/Event Source Demo - Event Store.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmfrey/event-store-demo/084a9220204c2769a2ba2834f1079dd8f85ceac9/images/Event Source Demo - Event Store.png -------------------------------------------------------------------------------- /images/Event Source Demo - Kafka.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dmfrey/event-store-demo/084a9220204c2769a2ba2834f1079dd8f85ceac9/images/Event Source Demo - Kafka.png -------------------------------------------------------------------------------- /query/build.gradle: -------------------------------------------------------------------------------- 1 | 2 | dependencies { 3 | 4 | // Spring dependencies 5 | compile('org.springframework.kafka:spring-kafka') 6 | 7 | 8 | // Spring Boot dependencies 9 | 10 | 11 | // Spring Cloud dependencies 12 | compile('org.springframework.cloud:spring-cloud-starter-openfeign') 13 | compile('org.springframework.cloud:spring-cloud-stream-binder-rabbit') 14 | compile('org.springframework.cloud:spring-cloud-stream-binder-kafka') 15 | compile('org.springframework.cloud:spring-cloud-stream-binder-kafka-core') 16 | compile('org.springframework.cloud:spring-cloud-stream-binder-kafka-streams') 17 | 18 | 19 | // Third-party dependencies 20 | 21 | 22 | // Test dependencies 23 | testCompile('org.springframework.kafka:spring-kafka-test') 24 | testCompile('org.springframework.cloud:spring-cloud-stream-binder-test') 25 | 26 | } 27 | 28 | contracts { 29 | 30 | basePackageForTests = 'io.pivotal.dmfrey.eventStoreDemo' 31 | packageWithBaseClasses = 'io.pivotal.dmfrey.eventStoreDemo.contracts' 32 | 33 | } -------------------------------------------------------------------------------- /query/manifest_development.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: esd-query 4 | host: esd-query-development 5 | buildpack: java_buildpack_offline 6 | memory: 1024M 7 | instances: 2 8 | path: build/libs/query-0.0.1-SNAPSHOT.jar 9 | services: 10 | - discovery-service 11 | - hystrix-dashboard 12 | - rabbitmq 13 | env: 14 | JAVA_OPTS: -Djava.security.egd=file:///dev/urandom 15 | SPRING_PROFILES_ACTIVE: event-store -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/Application.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; 6 | 7 | @SpringBootApplication 8 | @EnableCircuitBreaker 9 | public class Application { 10 | 11 | public static void main( String[] args ) { 12 | 13 | SpringApplication.run( Application.class, args ); 14 | 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/RabbitConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; 2 | 3 | import org.springframework.amqp.rabbit.connection.ConnectionFactory; 4 | import org.springframework.cloud.config.java.AbstractCloudConfig; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | 9 | @Configuration 10 | public class RabbitConfig { 11 | 12 | 13 | @Configuration 14 | @Profile("cloud") 15 | public static class CloudRabbitConfig extends AbstractCloudConfig { 16 | 17 | @Bean 18 | public ConnectionFactory rabbitConnectionFactory() { 19 | 20 | return connectionFactory().rabbitConnectionFactory(); 21 | } 22 | 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/SecurityConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 5 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 6 | import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; 7 | 8 | @Configuration 9 | @EnableWebSecurity 10 | public class SecurityConfig extends WebSecurityConfigurerAdapter { 11 | 12 | @Override 13 | protected void configure(HttpSecurity httpSecurity) throws Exception { 14 | 15 | httpSecurity 16 | .authorizeRequests() 17 | .anyRequest().permitAll() 18 | .and() 19 | .csrf().disable(); 20 | 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/config/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/BoardClient.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 4 | 5 | import java.util.UUID; 6 | 7 | public interface BoardClient { 8 | 9 | Board find( final UUID boardUuid ); 10 | 11 | void removeFromCache( final UUID boardUuid ); 12 | 13 | } 14 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/config/EventStoreClientConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.service.EventStoreBoardClient; 5 | import org.springframework.beans.factory.annotation.Qualifier; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Configuration; 8 | import org.springframework.context.annotation.Primary; 9 | import org.springframework.context.annotation.Profile; 10 | 11 | @Profile( "event-store" ) 12 | @Configuration 13 | public class EventStoreClientConfig { 14 | 15 | @Bean 16 | @Primary 17 | public BoardClient boardClient( 18 | @Qualifier( "io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.config.RestConfig$EventStoreClient" ) final RestConfig.EventStoreClient eventStoreClient 19 | ) { 20 | 21 | return new EventStoreBoardClient( eventStoreClient ); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/config/RestConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvents; 4 | import org.springframework.cloud.openfeign.EnableFeignClients; 5 | import org.springframework.cloud.openfeign.FeignClient; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.Profile; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | 12 | import java.util.UUID; 13 | 14 | @Profile( "event-store" ) 15 | @Configuration 16 | @EnableFeignClients 17 | public class RestConfig { 18 | 19 | @FeignClient( value = "esd-event-store", fallback = HystrixFallbackEventStoreClient.class ) 20 | public interface EventStoreClient { 21 | 22 | @GetMapping( path = "/{boardUuid}" ) 23 | DomainEvents getDomainEventsForBoardUuid( @PathVariable( "boardUuid" ) UUID boardId ); 24 | 25 | } 26 | 27 | // @Bean 28 | // public HystrixFallbackEventStoreClient hystrixFallbackEventStoreClient() { 29 | // 30 | // return new HystrixFallbackEventStoreClient(); 31 | // } 32 | 33 | @Component 34 | static class HystrixFallbackEventStoreClient implements EventStoreClient { 35 | 36 | @Override 37 | public DomainEvents getDomainEventsForBoardUuid( final UUID boardUuid) { 38 | 39 | return new DomainEvents(); 40 | } 41 | 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/config/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.config; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/service/EventStoreBoardClient.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.service; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.config.RestConfig; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvent; 6 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvents; 7 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.springframework.cache.annotation.CacheEvict; 10 | import org.springframework.cache.annotation.Cacheable; 11 | import org.springframework.http.HttpStatus; 12 | import org.springframework.http.ResponseEntity; 13 | 14 | import java.util.List; 15 | import java.util.UUID; 16 | 17 | @Slf4j 18 | public class EventStoreBoardClient implements BoardClient { 19 | 20 | private final RestConfig.EventStoreClient eventStoreClient; 21 | 22 | public EventStoreBoardClient( final RestConfig.EventStoreClient eventStoreClient ) { 23 | 24 | this.eventStoreClient = eventStoreClient; 25 | 26 | } 27 | 28 | @Override 29 | @Cacheable( "boards" ) 30 | public Board find( final UUID boardUuid ) { 31 | log.debug( "find : enter" ); 32 | 33 | DomainEvents domainEvents = this.eventStoreClient.getDomainEventsForBoardUuid( boardUuid ); 34 | if( null == domainEvents || null == domainEvents.getDomainEvents() || domainEvents.getDomainEvents().isEmpty() ) { 35 | 36 | log.warn( "find : exit, target[" + boardUuid.toString() + "] not found" ); 37 | throw new IllegalArgumentException( "board[" + boardUuid.toString() + "] not found" ); 38 | } 39 | 40 | Board board = Board.createFrom( boardUuid, domainEvents.getDomainEvents() ); 41 | 42 | log.debug( "find : exit" ); 43 | return board; 44 | } 45 | 46 | @Override 47 | @CacheEvict( value = "boards", key = "#boardUuid" ) 48 | public void removeFromCache( final UUID boardUuid ) { 49 | log.debug( "removeFromCache : enter" ); 50 | 51 | // this method is intentionally left blank 52 | 53 | log.debug( "removeFromCache : exit" ); 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/eventStore/service/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.eventStore.service; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/config/KafkaClientConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service.KafkaBoardClient; 5 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 6 | import org.springframework.cloud.stream.binder.kafka.streams.QueryableStoreRegistry; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.context.annotation.Primary; 10 | import org.springframework.context.annotation.Profile; 11 | 12 | @Profile( "kafka" ) 13 | @Configuration 14 | @EnableAutoConfiguration 15 | public class KafkaClientConfig { 16 | 17 | public static final String BOARD_EVENTS_SNAPSHOTS = "query-board-snapshots"; 18 | 19 | @Bean 20 | @Primary 21 | public BoardClient boardClient( 22 | final QueryableStoreRegistry queryableStoreRegistry 23 | ) { 24 | 25 | return new KafkaBoardClient( queryableStoreRegistry ); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/config/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.config; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/BoardEventsStreamsProcessor.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; 2 | 3 | import org.apache.kafka.streams.kstream.KStream; 4 | import org.springframework.cloud.stream.annotation.Input; 5 | 6 | public interface BoardEventsStreamsProcessor { 7 | 8 | @Input( "input" ) 9 | KStream input(); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/DomainEventSink.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; 2 | 3 | import org.apache.kafka.streams.kstream.KStream; 4 | 5 | public interface DomainEventSink { 6 | 7 | void process( KStream input ); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/DomainEventSinkImpl.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvent; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.kafka.common.serialization.Serde; 8 | import org.apache.kafka.common.serialization.Serdes; 9 | import org.apache.kafka.common.utils.Bytes; 10 | import org.apache.kafka.streams.KeyValue; 11 | import org.apache.kafka.streams.kstream.KStream; 12 | import org.apache.kafka.streams.kstream.Materialized; 13 | import org.apache.kafka.streams.kstream.Serialized; 14 | import org.apache.kafka.streams.state.KeyValueStore; 15 | import org.springframework.cloud.stream.annotation.EnableBinding; 16 | import org.springframework.cloud.stream.annotation.StreamListener; 17 | import org.springframework.context.annotation.Profile; 18 | import org.springframework.kafka.support.serializer.JsonSerde; 19 | 20 | import java.io.IOException; 21 | 22 | import static io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.config.KafkaClientConfig.BOARD_EVENTS_SNAPSHOTS; 23 | 24 | @Profile( "kafka" ) 25 | @EnableBinding( BoardEventsStreamsProcessor.class ) 26 | @Slf4j 27 | public class DomainEventSinkImpl implements DomainEventSink { 28 | 29 | private final ObjectMapper mapper; 30 | private final Serde domainEventSerde; 31 | private final Serde boardSerde; 32 | 33 | public DomainEventSinkImpl( final ObjectMapper mapper ) { 34 | 35 | this.mapper = mapper; 36 | this.domainEventSerde = new JsonSerde<>( DomainEvent.class, mapper ); 37 | this.boardSerde = new JsonSerde<>( Board.class, mapper ); 38 | 39 | } 40 | 41 | @StreamListener( "input" ) 42 | public void process( KStream input ) { 43 | log.debug( "process : enter" ); 44 | 45 | input 46 | .map( (key, value) -> { 47 | 48 | try { 49 | 50 | DomainEvent domainEvent = mapper.readValue( value, DomainEvent.class ); 51 | log.debug( "process : domainEvent=" + domainEvent ); 52 | 53 | return new KeyValue<>( domainEvent.getBoardUuid().toString(), domainEvent ); 54 | 55 | } catch( IOException e ) { 56 | log.error( "process : error converting json to DomainEvent", e ); 57 | } 58 | 59 | return null; 60 | }) 61 | .groupBy( (s, domainEvent) -> s, Serialized.with( Serdes.String(), domainEventSerde ) ) 62 | .aggregate( 63 | Board::new, 64 | (key, domainEvent, board) -> board.handleEvent( domainEvent ), 65 | Materialized.>as( BOARD_EVENTS_SNAPSHOTS ) 66 | .withKeySerde( Serdes.String() ) 67 | .withValueSerde( boardSerde ) 68 | ); 69 | 70 | log.debug( "process : exit" ); 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/KafkaBoardClient.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.DomainEvent; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.apache.kafka.streams.errors.InvalidStateStoreException; 8 | import org.apache.kafka.streams.state.QueryableStoreTypes; 9 | import org.apache.kafka.streams.state.ReadOnlyKeyValueStore; 10 | import org.springframework.cloud.stream.binder.kafka.streams.QueryableStoreRegistry; 11 | 12 | import java.util.List; 13 | import java.util.UUID; 14 | 15 | import static io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.config.KafkaClientConfig.BOARD_EVENTS_SNAPSHOTS; 16 | 17 | @Slf4j 18 | public class KafkaBoardClient implements BoardClient { 19 | 20 | private final QueryableStoreRegistry queryableStoreRegistry; 21 | 22 | public KafkaBoardClient( 23 | final QueryableStoreRegistry queryableStoreRegistry 24 | ) { 25 | 26 | this.queryableStoreRegistry = queryableStoreRegistry; 27 | 28 | } 29 | 30 | @Override 31 | public Board find( final UUID boardUuid ) { 32 | log.debug( "find : enter" ); 33 | 34 | try { 35 | 36 | ReadOnlyKeyValueStore store = queryableStoreRegistry.getQueryableStoreType( BOARD_EVENTS_SNAPSHOTS, QueryableStoreTypes.keyValueStore() ); 37 | 38 | Board board = store.get( boardUuid.toString() ); 39 | if( null != board ) { 40 | 41 | log.debug( "find : board=" + board.toString() ); 42 | 43 | log.debug( "find : exit" ); 44 | return board; 45 | 46 | } else { 47 | 48 | throw new IllegalArgumentException( "board[" + boardUuid.toString() + "] not found!" ); 49 | } 50 | 51 | } catch( InvalidStateStoreException e ) { 52 | log.error( "find : error", e ); 53 | 54 | } 55 | 56 | throw new IllegalArgumentException( "board[" + boardUuid.toString() + "] not found!" ); 57 | } 58 | 59 | @Override 60 | public void removeFromCache(UUID boardUuid) { 61 | 62 | throw new UnsupportedOperationException( "this method is not implemented in kafka client" ); 63 | } 64 | 65 | } 66 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/config/QueryConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.BoardService; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | 8 | @Configuration 9 | public class QueryConfig { 10 | 11 | @Bean 12 | public BoardService boardService( final BoardClient boardClient ) { 13 | 14 | return new BoardService( boardClient ); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/config/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.config; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/BoardInitialized.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.ToString; 10 | 11 | import java.time.Instant; 12 | import java.util.UUID; 13 | 14 | @Data 15 | @EqualsAndHashCode( callSuper = true ) 16 | @ToString( callSuper = true ) 17 | @JsonPropertyOrder({ "eventType", "boardUuid", "occurredOn" }) 18 | public class BoardInitialized extends DomainEvent { 19 | 20 | @JsonCreator 21 | public BoardInitialized( 22 | @JsonProperty( "boardUuid" ) final UUID boardUuid, 23 | @JsonProperty( "occurredOn" ) final Instant when 24 | ) { 25 | super( boardUuid, when ); 26 | } 27 | 28 | @Override 29 | @JsonIgnore 30 | public String eventType() { 31 | 32 | return this.getClass().getSimpleName(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/BoardRenamed.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.ToString; 10 | 11 | import java.time.Instant; 12 | import java.util.UUID; 13 | 14 | @Data 15 | @EqualsAndHashCode( callSuper = true ) 16 | @ToString( callSuper = true ) 17 | @JsonPropertyOrder({ "eventType", "boardUuid", "occurredOn", "name" }) 18 | public class BoardRenamed extends DomainEvent { 19 | 20 | private final String name; 21 | 22 | @JsonCreator 23 | public BoardRenamed( 24 | @JsonProperty( "name" ) final String name, 25 | @JsonProperty( "boardUuid" ) final UUID boardUuid, 26 | @JsonProperty( "occurredOn" ) final Instant when 27 | ) { 28 | super( boardUuid, when ); 29 | 30 | this.name = name; 31 | 32 | } 33 | 34 | @Override 35 | @JsonIgnore 36 | public String eventType() { 37 | 38 | return this.getClass().getSimpleName(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/DomainEvent.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | import com.fasterxml.jackson.annotation.JsonSubTypes; 6 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 7 | import lombok.Data; 8 | import lombok.Getter; 9 | 10 | import java.time.Instant; 11 | import java.util.UUID; 12 | 13 | import static lombok.AccessLevel.NONE; 14 | 15 | @JsonTypeInfo( 16 | use = JsonTypeInfo.Id.NAME, 17 | include = JsonTypeInfo.As.PROPERTY, 18 | property = "eventType", 19 | defaultImpl = DomainEventIgnored.class 20 | ) 21 | @JsonSubTypes({ 22 | @JsonSubTypes.Type( value = BoardInitialized.class, name = "BoardInitialized" ), 23 | @JsonSubTypes.Type( value = BoardRenamed.class, name = "BoardRenamed" ), 24 | @JsonSubTypes.Type( value = StoryAdded.class, name = "StoryAdded" ), 25 | @JsonSubTypes.Type( value = StoryUpdated.class, name = "StoryUpdated" ), 26 | @JsonSubTypes.Type( value = StoryDeleted.class, name = "StoryDeleted" ) 27 | }) 28 | @Data 29 | public abstract class DomainEvent { 30 | 31 | private final UUID boardUuid; 32 | 33 | @Getter( NONE ) 34 | @JsonIgnore 35 | private final Instant when; 36 | 37 | DomainEvent( final UUID boardUuid, final Instant when ) { 38 | 39 | this.boardUuid = boardUuid; 40 | this.when = when; 41 | 42 | } 43 | 44 | @JsonProperty( "occurredOn" ) 45 | public Instant occurredOn() { 46 | 47 | return when; 48 | } 49 | 50 | @JsonProperty( "eventType" ) 51 | public abstract String eventType(); 52 | 53 | } 54 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/DomainEventIgnored.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.ToString; 10 | 11 | import java.time.Instant; 12 | import java.util.UUID; 13 | 14 | @Data 15 | @EqualsAndHashCode( callSuper = true ) 16 | @ToString( callSuper = true ) 17 | @JsonPropertyOrder({ "eventType", "boardUuid", "occurredOn" }) 18 | public class DomainEventIgnored extends DomainEvent { 19 | 20 | @JsonCreator 21 | public DomainEventIgnored( 22 | @JsonProperty( "boardUuid" ) final UUID boardUuid, 23 | @JsonProperty( "occurredOn" ) final Instant when 24 | ) { 25 | super( boardUuid, when ); 26 | } 27 | 28 | @Override 29 | @JsonIgnore 30 | public String eventType() { 31 | 32 | return this.getClass().getSimpleName(); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/DomainEvents.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import lombok.Data; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.UUID; 8 | 9 | @Data 10 | public class DomainEvents { 11 | 12 | private UUID boardUuid; 13 | private List domainEvents = new ArrayList<>(); 14 | 15 | } 16 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/StoryAdded.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Story; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | 12 | import java.time.Instant; 13 | import java.util.UUID; 14 | 15 | @Data 16 | @EqualsAndHashCode( callSuper = true ) 17 | @ToString( callSuper = true ) 18 | @JsonPropertyOrder({ "eventType", "boardUuid", "occurredOn", "storyUuid", "name" }) 19 | public class StoryAdded extends DomainEvent { 20 | 21 | private final UUID storyUuid; 22 | private final String name; 23 | 24 | @JsonCreator 25 | public StoryAdded( 26 | @JsonProperty( "storyUuid" ) final UUID storyUuid, 27 | @JsonProperty( "name" ) final String name, 28 | @JsonProperty( "boardUuid" ) final UUID boardUuid, 29 | @JsonProperty( "occurredOn" ) final Instant when 30 | ) { 31 | super( boardUuid, when ); 32 | 33 | this.storyUuid = storyUuid; 34 | this.name = name; 35 | 36 | } 37 | 38 | public Story getStory() { 39 | 40 | Story story = new Story(); 41 | story.setStoryUuid( this.storyUuid ); 42 | story.setName( this.name ); 43 | 44 | return story; 45 | } 46 | 47 | @Override 48 | @JsonIgnore 49 | public String eventType() { 50 | 51 | return this.getClass().getSimpleName(); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/StoryDeleted.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import lombok.Data; 8 | import lombok.EqualsAndHashCode; 9 | import lombok.ToString; 10 | 11 | import java.time.Instant; 12 | import java.util.UUID; 13 | 14 | @Data 15 | @EqualsAndHashCode( callSuper = true ) 16 | @ToString( callSuper = true ) 17 | @JsonPropertyOrder({ "eventType", "boardUuid", "occurredOn", "storyUuid" }) 18 | public class StoryDeleted extends DomainEvent { 19 | 20 | private final UUID storyUuid; 21 | 22 | @JsonCreator 23 | public StoryDeleted( 24 | @JsonProperty( "storyUuid" ) final UUID storyUuid, 25 | @JsonProperty( "boardUuid" ) final UUID boardUuid, 26 | @JsonProperty( "occurredOn" ) final Instant when 27 | ) { 28 | super( boardUuid, when ); 29 | 30 | this.storyUuid = storyUuid; 31 | 32 | } 33 | 34 | @Override 35 | @JsonIgnore 36 | public String eventType() { 37 | 38 | return this.getClass().getSimpleName(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/StoryUpdated.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; 2 | 3 | import com.fasterxml.jackson.annotation.JsonCreator; 4 | import com.fasterxml.jackson.annotation.JsonIgnore; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 7 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Story; 8 | import lombok.Data; 9 | import lombok.EqualsAndHashCode; 10 | import lombok.ToString; 11 | 12 | import java.time.Instant; 13 | import java.util.UUID; 14 | 15 | @Data 16 | @EqualsAndHashCode( callSuper = true ) 17 | @ToString( callSuper = true ) 18 | @JsonPropertyOrder({ "eventType", "boardUuid", "occurredOn", "storyUuid" }) 19 | public class StoryUpdated extends DomainEvent { 20 | 21 | private final UUID storyUuid; 22 | private final String name; 23 | 24 | @JsonCreator 25 | public StoryUpdated( 26 | @JsonProperty( "storyUuid" ) final UUID storyUuid, 27 | @JsonProperty( "name" ) final String name, 28 | @JsonProperty( "boardUuid" ) final UUID boardUuid, 29 | @JsonProperty( "occurredOn" ) final Instant when 30 | ) { 31 | super( boardUuid, when ); 32 | 33 | this.storyUuid = storyUuid; 34 | this.name = name; 35 | 36 | } 37 | 38 | public Story getStory() { 39 | 40 | Story story = new Story(); 41 | story.setStoryUuid( this.storyUuid ); 42 | story.setName( this.name ); 43 | 44 | return story; 45 | } 46 | 47 | @Override 48 | @JsonIgnore 49 | public String eventType() { 50 | 51 | return this.getClass().getSimpleName(); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/events/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.events; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/model/Board.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnore; 4 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 5 | import com.fasterxml.jackson.annotation.JsonProperty; 6 | import com.google.common.collect.ImmutableList; 7 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.*; 8 | import io.vavr.API; 9 | import lombok.Data; 10 | import lombok.Getter; 11 | import lombok.Setter; 12 | import lombok.extern.slf4j.Slf4j; 13 | 14 | import java.time.Instant; 15 | import java.util.*; 16 | 17 | import static io.vavr.API.$; 18 | import static io.vavr.API.Case; 19 | import static io.vavr.Predicates.instanceOf; 20 | import static io.vavr.collection.Stream.ofAll; 21 | import static lombok.AccessLevel.NONE; 22 | 23 | @Data 24 | @Slf4j 25 | @JsonIgnoreProperties( ignoreUnknown = true ) 26 | public class Board { 27 | 28 | private UUID boardUuid; 29 | private String name = "New Board"; 30 | private Map stories = new HashMap<>(); 31 | 32 | public Board() { } 33 | 34 | public Board( final UUID boardUuid ) { 35 | 36 | boardInitialized( new BoardInitialized( boardUuid, Instant.now() ) ); 37 | 38 | } 39 | 40 | private Board boardInitialized( final BoardInitialized event ) { 41 | log.debug( "boardInitialized : event=" + event ); 42 | 43 | this.boardUuid = event.getBoardUuid(); 44 | 45 | return this; 46 | } 47 | 48 | private Board boardRenamed( final BoardRenamed event ) { 49 | log.debug( "boardRenamed : event=" + event ); 50 | 51 | this.name = event.getName(); 52 | 53 | return this; 54 | } 55 | 56 | private Board storyAdded( final StoryAdded event ) { 57 | log.debug( "storyAdded : event=" + event ); 58 | 59 | this.stories.put( event.getStoryUuid(), event.getStory() ); 60 | 61 | return this; 62 | } 63 | 64 | private Board storyUpdated( final StoryUpdated event ) { 65 | log.debug( "storyUpdated : event=" + event ); 66 | 67 | this.stories.replace( event.getStoryUuid(), event.getStory() ); 68 | 69 | return this; 70 | } 71 | 72 | private Board storyDeleted( final StoryDeleted event ) { 73 | log.debug( "storyDeleted : event=" + event ); 74 | 75 | this.stories.remove( event.getStoryUuid() ); 76 | 77 | return this; 78 | } 79 | 80 | // Builder Methods 81 | public static Board createFrom( final UUID boardUuid, final Collection domainEvents ) { 82 | 83 | return ofAll( domainEvents ).foldLeft( new Board( boardUuid ), Board::handleEvent ); 84 | } 85 | 86 | public Board handleEvent( final DomainEvent domainEvent ) { 87 | 88 | return API.Match( domainEvent ).of( 89 | Case( $( instanceOf( BoardInitialized.class ) ), this::boardInitialized ), 90 | Case( $( instanceOf( BoardRenamed.class ) ), this::boardRenamed ), 91 | Case( $( instanceOf( StoryAdded.class ) ), this::storyAdded ), 92 | Case( $( instanceOf( StoryUpdated.class ) ), this::storyUpdated ), 93 | Case( $( instanceOf( StoryDeleted.class ) ), this::storyDeleted ), 94 | Case( $(), this ) 95 | ); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/model/Story.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.model; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import lombok.Data; 5 | 6 | import java.util.UUID; 7 | 8 | @Data 9 | @JsonIgnoreProperties( ignoreUnknown = true ) 10 | public class Story { 11 | 12 | private UUID storyUuid; 13 | private String name; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/model/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.model; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/BoardEventNotificationSink.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; 2 | 3 | import lombok.extern.slf4j.Slf4j; 4 | import org.springframework.cloud.stream.annotation.EnableBinding; 5 | import org.springframework.cloud.stream.annotation.StreamListener; 6 | import org.springframework.cloud.stream.messaging.Sink; 7 | import org.springframework.context.annotation.Profile; 8 | import org.springframework.tuple.Tuple; 9 | import org.springframework.tuple.TupleBuilder; 10 | import org.springframework.util.Assert; 11 | 12 | import java.util.UUID; 13 | 14 | @Profile( "event-store" ) 15 | @EnableBinding( Sink.class ) 16 | @Slf4j 17 | public class BoardEventNotificationSink { 18 | 19 | private final BoardService service; 20 | 21 | public BoardEventNotificationSink( final BoardService service ) { 22 | this.service = service; 23 | 24 | } 25 | 26 | @StreamListener( Sink.INPUT ) 27 | public void processNotification( final String json ) { 28 | log.debug( "processNotification : enter" ); 29 | 30 | Tuple event = TupleBuilder.fromString( json ); 31 | 32 | Assert.hasText( event.getString( "eventType" ), "eventType not set" ); 33 | Assert.hasText( event.getString( "boardUuid" ), "boardUuid not set" ); 34 | Assert.hasText( event.getString( "occurredOn" ), "occurredOn not set" ); 35 | 36 | String eventType = event.getString( "eventType" ); 37 | if( eventType.equals( "BoardInitialized" ) ) { 38 | log.debug( "processNotification : exit, no board should exist in cache if 'BoardInitialized' event is received" ); 39 | 40 | return; 41 | } 42 | 43 | this.service.uncacheTarget( UUID.fromString( event.getString( "boardUuid" ) ) ); 44 | 45 | log.debug( "processNotification : exit" ); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/BoardService.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; 2 | 3 | import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | import java.util.UUID; 9 | 10 | @Slf4j 11 | public class BoardService { 12 | 13 | private final BoardClient client; 14 | 15 | public BoardService( final BoardClient client ) { 16 | 17 | this.client = client; 18 | 19 | } 20 | 21 | // @Cacheable( "boards" ) 22 | @HystrixCommand 23 | public Board find( final UUID boardUuid ) { 24 | log.debug( "find : enter" ); 25 | 26 | Board board = this.client.find( boardUuid ); 27 | log.debug( "find : board=" + board ); 28 | 29 | log.debug( "find : exit" ); 30 | return board; 31 | } 32 | 33 | // @CacheEvict( value = "boards", key = "#boardUuid" ) 34 | @HystrixCommand 35 | public void uncacheTarget( final UUID boardUuid ) { 36 | log.debug( "uncacheTarget : enter" ); 37 | 38 | this.client.removeFromCache( boardUuid ); 39 | 40 | log.debug( "uncacheTarget : exit" ); 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/QueryController.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.BoardService; 5 | import io.pivotal.dmfrey.eventStoreDemo.endpoint.model.BoardModel; 6 | import lombok.extern.slf4j.Slf4j; 7 | import org.springframework.http.ResponseEntity; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PathVariable; 10 | import org.springframework.web.bind.annotation.RestController; 11 | 12 | import java.util.UUID; 13 | 14 | @RestController 15 | @Slf4j 16 | public class QueryController { 17 | 18 | private final BoardService service; 19 | 20 | public QueryController( final BoardService service ) { 21 | 22 | this.service = service; 23 | 24 | } 25 | 26 | @GetMapping( "/boards/{boardUuid}" ) 27 | public ResponseEntity board( @PathVariable( "boardUuid" ) UUID boardUuid ) { 28 | log.debug( "board : enter" ); 29 | 30 | Board board = this.service.find( boardUuid ); 31 | log.debug( "board : board=" + board.toString() ); 32 | 33 | return ResponseEntity 34 | .ok( BoardModel.fromBoard( board ) ); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/model/BoardModel.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint.model; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Story; 5 | import lombok.Data; 6 | import lombok.extern.slf4j.Slf4j; 7 | 8 | import java.util.Collection; 9 | import java.util.UUID; 10 | 11 | @Data 12 | @Slf4j 13 | public class BoardModel { 14 | 15 | private String name; 16 | private Collection backlog; 17 | 18 | public static BoardModel fromBoard( final Board board ) { 19 | 20 | BoardModel model = new BoardModel(); 21 | model.setName( board.getName() ); 22 | 23 | if( null != board.getStories() && !board.getStories().isEmpty() ) { 24 | 25 | model.setBacklog( board.getStories().values() ); 26 | } 27 | 28 | return model; 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/model/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint.model; -------------------------------------------------------------------------------- /query/src/main/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/package-info.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint; -------------------------------------------------------------------------------- /query/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | management.endpoints.web.exposure.include=* 2 | -------------------------------------------------------------------------------- /query/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | debug: false 2 | 3 | logger.level: 4 | io.pivotal.dmfrey: DEBUG 5 | org.springframework.web: DEBUG 6 | 7 | server: 8 | port: ${PORT:9081} 9 | use-forward-headers: true 10 | tomcat: 11 | remote-ip-header: x-forwarded-for 12 | protocol-header: x-forwarded-proto 13 | 14 | spring: 15 | jackson: 16 | serialization: 17 | write_dates_as_timestamps: false 18 | 19 | --- 20 | spring: 21 | profiles: event-store 22 | 23 | cloud: 24 | stream: 25 | bindings: 26 | input: 27 | binder: rabbit 28 | destination: board-event-notifications 29 | contentType: application/json 30 | consumer: 31 | headerMode: raw # Outbound data has no embedded headers 32 | 33 | --- 34 | spring: 35 | profiles: kafka 36 | 37 | cloud: 38 | stream: 39 | bindings: 40 | input: 41 | destination: board-events 42 | contentType: application/json 43 | group: query-board-events-group 44 | consumer: 45 | useNativeDecoding: true 46 | headerMode: raw 47 | kafka: 48 | streams: 49 | binder: 50 | brokers: localhost 51 | zkNodes: localhost 52 | 53 | logger.level: 54 | org.apache.kafka: DEBUG 55 | org.apache.kafka.clients: ERROR -------------------------------------------------------------------------------- /query/src/main/resources/bootstrap.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | application: 3 | name: esd-query 4 | 5 | eureka.instance.leaseRenewalIntervalInSeconds: 15 6 | 7 | --- 8 | spring: 9 | profiles: cloud 10 | 11 | cloud: 12 | services: 13 | registrationMethod: direct 14 | 15 | eureka.instance.leaseRenewalIntervalInSeconds: 30 16 | -------------------------------------------------------------------------------- /query/src/test/java/io/pivotal/dmfrey/eventStoreDemo/ApplicationTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.config.UnitTestConfig; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.context.annotation.Import; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | 10 | @RunWith( SpringRunner.class ) 11 | @SpringBootTest( properties = { 12 | "spring.cloud.service-registry.auto-registration.enabled=false" 13 | }) 14 | @Import( UnitTestConfig.class ) 15 | public class ApplicationTests { 16 | 17 | @Test 18 | public void contextLoads() { 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /query/src/test/java/io/pivotal/dmfrey/eventStoreDemo/config/UnitTestConfig.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.config; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 5 | import org.springframework.boot.test.context.TestConfiguration; 6 | import org.springframework.context.annotation.Bean; 7 | import org.springframework.context.annotation.Primary; 8 | 9 | import java.util.UUID; 10 | 11 | @TestConfiguration 12 | public class UnitTestConfig { 13 | 14 | @Bean 15 | @Primary 16 | public BoardClient boardClient() { 17 | 18 | return new BoardClient() { 19 | 20 | @Override 21 | public Board find( UUID boardUuid ) { 22 | 23 | throw new UnsupportedOperationException( "client call not implemented yet" ); 24 | } 25 | 26 | @Override 27 | public void removeFromCache(UUID boardUuid) { 28 | 29 | throw new UnsupportedOperationException( "client call not implemented yet" ); 30 | } 31 | 32 | }; 33 | 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /query/src/test/java/io/pivotal/dmfrey/eventStoreDemo/contracts/ApiBase.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.contracts; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.BoardService; 5 | import io.pivotal.dmfrey.eventStoreDemo.endpoint.QueryController; 6 | import io.restassured.module.mockmvc.RestAssuredMockMvc; 7 | import org.junit.Before; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.context.SpringBootTest; 11 | import org.springframework.boot.test.mock.mockito.MockBean; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | 14 | import java.util.UUID; 15 | 16 | import static org.mockito.ArgumentMatchers.any; 17 | import static org.mockito.Mockito.verifyNoMoreInteractions; 18 | import static org.mockito.Mockito.when; 19 | import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE; 20 | 21 | @RunWith( SpringRunner.class ) 22 | @SpringBootTest( 23 | webEnvironment = NONE, 24 | classes = QueryController.class, 25 | properties = { 26 | "--spring.cloud.service-registry.auto-registration.enabled=false" 27 | } 28 | ) 29 | public abstract class ApiBase { 30 | 31 | @Autowired 32 | private QueryController controller; 33 | 34 | @MockBean 35 | private BoardService service; 36 | 37 | private UUID boardUuid = UUID.fromString( "11111111-90ab-cdef-1234-567890abcdef" ); 38 | private UUID storyUuid = UUID.fromString( "10240df9-4a1e-4fa4-bbd1-0bb33d764603" ); 39 | 40 | @Before 41 | public void setup() { 42 | 43 | when( this.service.find( any( UUID.class ) ) ).thenReturn( new Board( boardUuid ) ); 44 | 45 | RestAssuredMockMvc.standaloneSetup( this.controller ); 46 | 47 | verifyNoMoreInteractions( this.service ); 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /query/src/test/java/io/pivotal/dmfrey/eventStoreDemo/domain/client/kafka/service/KafkaBoardClientEmbeddedKafkaTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.service; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import io.pivotal.dmfrey.eventStoreDemo.Application; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 6 | import io.pivotal.dmfrey.eventStoreDemo.domain.events.BoardInitialized; 7 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.apache.kafka.clients.consumer.Consumer; 10 | import org.apache.kafka.clients.consumer.ConsumerConfig; 11 | import org.junit.AfterClass; 12 | import org.junit.BeforeClass; 13 | import org.junit.ClassRule; 14 | import org.junit.Test; 15 | import org.springframework.boot.SpringApplication; 16 | import org.springframework.boot.WebApplicationType; 17 | import org.springframework.context.ConfigurableApplicationContext; 18 | import org.springframework.kafka.core.DefaultKafkaConsumerFactory; 19 | import org.springframework.kafka.core.DefaultKafkaProducerFactory; 20 | import org.springframework.kafka.core.KafkaTemplate; 21 | import org.springframework.kafka.test.rule.KafkaEmbedded; 22 | import org.springframework.kafka.test.utils.KafkaTestUtils; 23 | 24 | import java.time.Instant; 25 | import java.util.Map; 26 | import java.util.UUID; 27 | 28 | import static io.pivotal.dmfrey.eventStoreDemo.domain.client.kafka.config.KafkaClientConfig.BOARD_EVENTS_SNAPSHOTS; 29 | import static org.hamcrest.Matchers.*; 30 | import static org.junit.Assert.assertThat; 31 | 32 | @Slf4j 33 | public class KafkaBoardClientEmbeddedKafkaTests { 34 | 35 | private static String RECEIVER_TOPIC = "board-events"; 36 | 37 | @ClassRule 38 | public static KafkaEmbedded embeddedKafka = new KafkaEmbedded( 1, true, RECEIVER_TOPIC, BOARD_EVENTS_SNAPSHOTS ); 39 | 40 | private static Consumer consumer; 41 | 42 | @BeforeClass 43 | public static void setUp() throws Exception { 44 | 45 | Map consumerProps = KafkaTestUtils.consumerProps("query-board-events-group", "false", embeddedKafka ); 46 | consumerProps.put( ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest" ); 47 | 48 | DefaultKafkaConsumerFactory cf = new DefaultKafkaConsumerFactory<>( consumerProps ); 49 | 50 | consumer = cf.createConsumer(); 51 | 52 | embeddedKafka.consumeFromAnEmbeddedTopic( consumer, RECEIVER_TOPIC ); 53 | 54 | } 55 | 56 | @AfterClass 57 | public static void tearDown() { 58 | 59 | consumer.close(); 60 | 61 | } 62 | 63 | @Test 64 | public void testFind() throws Exception { 65 | 66 | log.debug( "testFind : --spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString() ); 67 | log.debug( "testFind : --spring.cloud.stream.kafka.streams.binder.zkNodes=" + embeddedKafka.getZookeeperConnectionString() ); 68 | 69 | SpringApplication app = new SpringApplication( Application.class ); 70 | app.setWebApplicationType( WebApplicationType.NONE ); 71 | ConfigurableApplicationContext context = app.run("--server.port=0", 72 | "--spring.cloud.service-registry.auto-registration.enabled=false", 73 | "--spring.jmx.enabled=false", 74 | "--spring.cloud.stream.bindings.input.destination=board-events", 75 | "--spring.cloud.stream.bindings.output.destination=board-events", 76 | "--spring.cloud.stream.kafka.streams.binder.configuration.commit.interval.ms=1000", 77 | "--spring.cloud.stream.bindings.output.producer.headerMode=raw", 78 | "--spring.cloud.stream.bindings.input.consumer.headerMode=raw", 79 | "--spring.cloud.stream.kafka.streams.binder.brokers=" + embeddedKafka.getBrokersAsString(), 80 | "--spring.cloud.stream.kafka.streams.binder.zkNodes=" + embeddedKafka.getZookeeperConnectionString(), 81 | "--spring.profiles.active=kafka", 82 | "--spring.jackson.serialization.write_dates_as_timestamps=false", 83 | "--logger.level.io.pivotal.dmfrey=DEBUG"); 84 | try { 85 | 86 | receiveAndValidateBoard( context ); 87 | 88 | } finally { 89 | 90 | context.close(); 91 | 92 | } 93 | 94 | } 95 | 96 | private void receiveAndValidateBoard( ConfigurableApplicationContext context ) throws Exception { 97 | 98 | Map senderProps = KafkaTestUtils.producerProps( embeddedKafka ); 99 | DefaultKafkaProducerFactory pf = new DefaultKafkaProducerFactory<>( senderProps ); 100 | KafkaTemplate template = new KafkaTemplate<>( pf, true ); 101 | template.setDefaultTopic( RECEIVER_TOPIC ); 102 | 103 | ObjectMapper mapper = context.getBean( ObjectMapper.class ); 104 | BoardClient boardClient = context.getBean( BoardClient.class ); 105 | 106 | UUID boardUuid = UUID.randomUUID(); 107 | BoardInitialized boardInitialized = createTestBoardInitializedEvent( boardUuid ); 108 | String event = mapper.writeValueAsString( boardInitialized ); 109 | template.sendDefault( event ); 110 | 111 | Thread.sleep( 1000 ); 112 | 113 | Board board = boardClient.find( boardUuid ); 114 | assertThat( board, is( notNullValue() ) ); 115 | assertThat( board.getBoardUuid(), is( equalTo( boardUuid ) ) ); 116 | assertThat( board.getName(), is( equalTo( "New Board" ) ) ); 117 | assertThat( board.getStories().isEmpty(), is( equalTo( true ) ) ); 118 | 119 | } 120 | 121 | private BoardInitialized createTestBoardInitializedEvent( final UUID boardUuid ) { 122 | 123 | return new BoardInitialized( boardUuid, Instant.now() ); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /query/src/test/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/BoardEventNotificationSinkTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.boot.test.context.SpringBootTest; 7 | import org.springframework.boot.test.mock.mockito.MockBean; 8 | import org.springframework.cloud.stream.messaging.Sink; 9 | import org.springframework.messaging.Message; 10 | import org.springframework.messaging.MessageHeaders; 11 | import org.springframework.messaging.support.GenericMessage; 12 | import org.springframework.messaging.support.MessageBuilder; 13 | import org.springframework.test.context.ActiveProfiles; 14 | import org.springframework.test.context.junit4.SpringRunner; 15 | 16 | import java.util.UUID; 17 | 18 | import static org.mockito.ArgumentMatchers.any; 19 | import static org.mockito.Mockito.times; 20 | import static org.mockito.Mockito.verify; 21 | 22 | @RunWith( SpringRunner.class ) 23 | @SpringBootTest( 24 | properties = { 25 | "--spring.cloud.service-registry.auto-registration.enabled=false" 26 | } 27 | ) 28 | @ActiveProfiles( "event-store" ) 29 | public class BoardEventNotificationSinkTests { 30 | 31 | private static final String BOARD_INITIALIZED_EVENT = "{\"eventType\":\"BoardInitialized\",\"boardUuid\":\"ff4795e1-2514-4f5a-90e2-cd33dfadfbf2\",\"occurredOn\":\"2018-02-23T03:49:52.313Z\"}"; 32 | private static final String BOARD_RENAMED_EVENT = "{\"eventType\":\"BoardRenamed\",\"boardUuid\":\"ff4795e1-2514-4f5a-90e2-cd33dfadfbf2\",\"occurredOn\":\"2018-02-23T03:51:36.520Z\",\"name\":\"My Board\"}"; 33 | private static final String STORY_ADDED_EVENT = "{\"eventType\":\"StoryAdded\",\"boardUuid\":\"ff4795e1-2514-4f5a-90e2-cd33dfadfbf2\",\"occurredOn\":\"2018-02-23T03:52:15.876Z\",\"storyUuid\":\"242500df-373e-4e70-90bc-3c8cd54c81d8\",\"name\":\"My Story 1\",\"story\":{\"name\":\"My Story 1\"}}"; 34 | private static final String STORY_UPDATED_EVENT = "{\"eventType\":\"StoryUpdated\",\"boardUuid\":\"ff4795e1-2514-4f5a-90e2-cd33dfadfbf2\",\"occurredOn\":\"2018-02-23T03:52:15.876Z\",\"storyUuid\":\"242500df-373e-4e70-90bc-3c8cd54c81d8\",\"name\":\"My Story 1 Updated\",\"story\":{\"name\":\"My Story 1 Updated\"}}"; 35 | private static final String STORY_DELETED_EVENT = "{\"eventType\":\"StoryDeleted\",\"boardUuid\":\"ff4795e1-2514-4f5a-90e2-cd33dfadfbf2\",\"occurredOn\":\"2018-02-23T03:52:15.876Z\",\"storyUuid\":\"242500df-373e-4e70-90bc-3c8cd54c81d8\"}"; 36 | 37 | @Autowired 38 | private Sink channels; 39 | 40 | @MockBean 41 | private BoardService service; 42 | 43 | @Test 44 | public void testProcessNotificationBoardInitialized() throws Exception { 45 | 46 | Message message = MessageBuilder.withPayload( BOARD_INITIALIZED_EVENT ).setHeader( MessageHeaders.CONTENT_TYPE,"application/x-spring-tuple" ).build(); 47 | this.channels.input().send( message ); 48 | 49 | verify( this.service, times( 0 ) ).uncacheTarget( any( UUID.class ) ); 50 | 51 | } 52 | 53 | @Test 54 | public void testProcessNotificationBoardRenamed() throws Exception { 55 | 56 | this.channels.input().send( new GenericMessage<>( BOARD_RENAMED_EVENT ) ); 57 | 58 | verify( this.service, times( 1 ) ).uncacheTarget( any( UUID.class ) ); 59 | 60 | } 61 | 62 | @Test 63 | public void testProcessNotificationStoryAdded() throws Exception { 64 | 65 | this.channels.input().send( new GenericMessage<>( STORY_ADDED_EVENT ) ); 66 | 67 | verify( this.service, times( 1 ) ).uncacheTarget( any( UUID.class ) ); 68 | 69 | } 70 | 71 | @Test 72 | public void testProcessNotificationStoryUpdated() throws Exception { 73 | 74 | this.channels.input().send( new GenericMessage<>( STORY_UPDATED_EVENT ) ); 75 | 76 | verify( this.service, times( 1 ) ).uncacheTarget( any( UUID.class ) ); 77 | 78 | } 79 | 80 | @Test 81 | public void testProcessNotificationStoryDeleted() throws Exception { 82 | 83 | this.channels.input().send( new GenericMessage<>( STORY_DELETED_EVENT ) ); 84 | 85 | verify( this.service, times( 1 ) ).uncacheTarget( any( UUID.class ) ); 86 | 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /query/src/test/java/io/pivotal/dmfrey/eventStoreDemo/domain/service/BoardServiceTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.domain.service; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.client.BoardClient; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.config.QueryConfig; 5 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.test.context.junit4.SpringRunner; 12 | 13 | import java.util.UUID; 14 | 15 | import static org.assertj.core.api.Assertions.assertThat; 16 | import static org.mockito.Mockito.*; 17 | 18 | @RunWith( SpringRunner.class ) 19 | @SpringBootTest( classes = { QueryConfig.class } ) 20 | public class BoardServiceTests { 21 | 22 | @Autowired 23 | private BoardService service; 24 | 25 | @MockBean 26 | private BoardClient client; 27 | 28 | @Test 29 | public void testFind() throws Exception { 30 | 31 | Board board = createBoard(); 32 | when( this.client.find( any( UUID.class ) ) ).thenReturn( board ); 33 | 34 | Board found = this.service.find( board.getBoardUuid() ); 35 | assertThat( found ).isNotNull(); 36 | assertThat( found.getBoardUuid() ).isEqualTo( board.getBoardUuid() ); 37 | assertThat( found.getName() ).isEqualTo( found.getName() ); 38 | assertThat( found.getStories() ).hasSize( 0 ); 39 | 40 | verify( this.client, times( 1 ) ).find( any( UUID.class ) ); 41 | 42 | } 43 | 44 | @Test( expected = IllegalArgumentException.class ) 45 | public void testFindNotFound() throws Exception { 46 | 47 | when( this.client.find( any( UUID.class ) ) ).thenThrow( new IllegalArgumentException() ); 48 | 49 | this.service.find( UUID.randomUUID() ); 50 | 51 | verify( this.client, times( 1 ) ).find( any( UUID.class ) ); 52 | 53 | } 54 | 55 | private Board createBoard() { 56 | 57 | Board board = new Board(); 58 | board.setBoardUuid( UUID.fromString( "ff4795e1-2514-4f5a-90e2-cd33dfadfbf2" ) ); 59 | 60 | return board; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /query/src/test/java/io/pivotal/dmfrey/eventStoreDemo/endpoint/QueryControllerTests.java: -------------------------------------------------------------------------------- 1 | package io.pivotal.dmfrey.eventStoreDemo.endpoint; 2 | 3 | import io.pivotal.dmfrey.eventStoreDemo.domain.model.Board; 4 | import io.pivotal.dmfrey.eventStoreDemo.domain.service.BoardService; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 9 | import org.springframework.boot.test.mock.mockito.MockBean; 10 | import org.springframework.http.HttpHeaders; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | import org.springframework.test.web.servlet.MockMvc; 14 | 15 | import java.util.UUID; 16 | 17 | import static org.hamcrest.Matchers.*; 18 | import static org.mockito.Mockito.any; 19 | import static org.mockito.Mockito.times; 20 | import static org.mockito.Mockito.verify; 21 | import static org.mockito.Mockito.when; 22 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 24 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.header; 25 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 27 | 28 | @RunWith( SpringRunner.class ) 29 | @WebMvcTest( value = QueryController.class, secure = false ) 30 | public class QueryControllerTests { 31 | 32 | @Autowired 33 | MockMvc mockMvc; 34 | 35 | @MockBean 36 | BoardService service; 37 | 38 | @Test 39 | public void testBoard() throws Exception { 40 | 41 | Board board = createBoard(); 42 | when( this.service.find( any( UUID.class ) ) ).thenReturn( board ); 43 | 44 | this.mockMvc.perform( get( "/boards/{boardUuid}", board.getBoardUuid() ) ) 45 | .andExpect( status().isOk() ) 46 | .andExpect( header().string(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_UTF8_VALUE ) ) 47 | .andDo( print() ) 48 | .andExpect( jsonPath( "$.name", is( equalTo( board.getName() ) ) ) ) 49 | .andExpect( jsonPath( "$.backlog", is( nullValue() ) ) ); 50 | 51 | verify( this.service, times( 1 ) ).find( any( UUID.class ) ); 52 | 53 | } 54 | 55 | private Board createBoard() { 56 | 57 | Board board = new Board(); 58 | board.setBoardUuid( UUID.fromString( "ff4795e1-2514-4f5a-90e2-cd33dfadfbf2" ) ); 59 | 60 | return board; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /query/src/test/resources/application.yml: -------------------------------------------------------------------------------- 1 | --- 2 | spring: 3 | profiles: event-store 4 | 5 | cloud: 6 | stream: 7 | default-binder: rabbit 8 | 9 | 10 | --- 11 | spring: 12 | profiles: kafka 13 | 14 | kafka: 15 | bootstrap-servers: ${spring.embedded.kafka.brokers} 16 | topic: 17 | receiver: board-events 18 | -------------------------------------------------------------------------------- /query/src/test/resources/contracts/api/shouldGetExistingBoard.groovy: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | org.springframework.cloud.contract.spec.Contract.make { 4 | 5 | request { 6 | 7 | method 'GET' 8 | 9 | url( '/boards/11111111-90ab-cdef-1234-567890abcdef' ) 10 | 11 | } 12 | 13 | response { 14 | 15 | status 200 16 | 17 | body([ 18 | name: "New Board", 19 | backlog: null 20 | ]) 21 | 22 | headers { 23 | contentType( applicationJsonUtf8() ) 24 | } 25 | 26 | bodyMatchers { 27 | 28 | jsonPath( '$.name', byEquality() ) 29 | // jsonPath( '$.backlog', byNull() ) 30 | } 31 | 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'event-store-demo' 2 | 3 | include ':api', ':command', ':query', ':event-store' 4 | 5 | --------------------------------------------------------------------------------