diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/.editorconfig b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/.editorconfig new file mode 100644 index 0000000..e6e6a12 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.java] +indent_size = 4 +indent_style = tab diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/.env b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/.env new file mode 100644 index 0000000..b934e03 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/.env @@ -0,0 +1 @@ +# See apps/main/resources/.env diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/.github/workflows/ci.yml b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/.github/workflows/ci.yml new file mode 100644 index 0000000..8dcdf5b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: 🐳 Start all the environment + run: make start + + - name: 🔦 Lint + run: make lint + + - name: 🦭 Wait for the environment to get up + run: | + while ! make ping-mysql &>/dev/null; do + echo "Waiting for database connection..." + sleep 2 + done + + - name: ✅ Run the tests + run: make test diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/.gitignore b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/.gitignore new file mode 100644 index 0000000..8d89f10 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/.gitignore @@ -0,0 +1,12 @@ +# Gradle +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +/var/log/* +!/var/log/.gitkeep + +.env.local +.env.*.local diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/Dockerfile b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/Dockerfile new file mode 100644 index 0000000..98f0b27 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/Dockerfile @@ -0,0 +1,15 @@ +FROM openjdk:21-slim-buster +WORKDIR /app + +RUN apt update && apt install -y curl git + +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION 18.0.0 + +RUN mkdir -p $NVM_DIR +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash + +RUN . $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && nvm alias default $NODE_VERSION && nvm use default + +ENV NODE_PATH $NVM_DIR/versions/node/v$NODE_VERSION/lib/node_modules +ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/Makefile b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/Makefile new file mode 100644 index 0000000..4855cd1 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/Makefile @@ -0,0 +1,29 @@ +all: build + +start: + @docker compose -f docker-compose.ci.yml up -d + +build: + @./gradlew build --warning-mode all + +lint: + @docker exec codely-java_ddd_example-test_server ./gradlew spotlessCheck + +run-tests: + @./gradlew test --warning-mode all + +test: + @docker exec codely-java_ddd_example-test_server ./gradlew test --warning-mode all + +run: + @./gradlew :run + +ping-mysql: + @docker exec codely-java_ddd_example-mysql mysqladmin --user=root --password= --host "127.0.0.1" ping --silent + +# Start the app +start-mooc_backend: + @./gradlew bootRun --args='mooc_backend server' + +start-backoffice_frontend: + @./gradlew bootRun --args='backoffice_frontend server' diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/README.md b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/README.md new file mode 100644 index 0000000..6c6bb26 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/README.md @@ -0,0 +1,52 @@ +# ☕🚀 Java DDD example: Save the boilerplate in your new projects + + + + +> ⚡ Start your Java projects as fast as possible + +[![CodelyTV](https://img.shields.io/badge/codely-tv-green.svg?style=flat-square)](https://codely.tv) +[![CI pipeline status](https://github.com/CodelyTV/java-ddd-example/workflows/CI/badge.svg)](https://github.com/CodelyTV/java-ddd-example/actions) + +## ℹ️ Introduction + +This is a repository intended to serve as a starting point if you want to bootstrap a Java project with JUnit and Gradle. + +Here you have the [course on CodelyTV Pro where we explain step by step all this](https://pro.codely.tv/library/ddd-en-java/about/?utm_source=github&utm_medium=social&utm_campaign=readme) (Spanish) + +## 🏁 How To Start + +1. Install Java 11: `brew cask install corretto` +2. Set it as your default JVM: `export JAVA_HOME='/Library/Java/JavaVirtualMachines/amazon-corretto-11.jdk/Contents/Home'` +3. Clone this repository: `git clone https://github.com/CodelyTV/java-ddd-example`. +4. Bring up the Docker environment: `make up`. +5. Execute some [Gradle lifecycle tasks](https://docs.gradle.org/current/userguide/java_plugin.html#lifecycle_tasks) in order to check everything is OK: + 1. Create [the project JAR](https://docs.gradle.org/current/userguide/java_plugin.html#sec:jar): `make build` + 2. Run the tests and plugins verification tasks: `make test` +6. Start developing! + +## ☝️ How to update dependencies + +* Gradle ([releases](https://gradle.org/releases/)): `./gradlew wrapper --gradle-version=WANTED_VERSION --distribution-type=bin` + +## 💡 Related repositories + +### ☕ Java + +* 📂 [Java Basic example](https://github.com/CodelyTV/java-basic-example) +* ⚛ [Java OOP Examples](https://github.com/CodelyTV/java-oop-examples) +* 🧱 [Java SOLID Examples](https://github.com/CodelyTV/java-solid-examples) +* 🥦 [Java DDD Example](https://github.com/CodelyTV/java-ddd-example) + +### 🐘 PHP + +* 📂 [PHP Basic example](https://github.com/CodelyTV/php-basic-example) +* 🎩 [PHP DDD example](https://github.com/CodelyTV/php-ddd-example) +* 🥦 [PHP DDD Example](https://github.com/CodelyTV/php-ddd-example) + +### 🧬 Scala + +* 📂 [Scala Basic example](https://github.com/CodelyTV/scala-basic-example) +* ⚡ [Scala Basic example (g8 template)](https://github.com/CodelyTV/scala-basic-example.g8) +* ⚛ [Scala Examples](https://github.com/CodelyTV/scala-examples) +* 🥦 [Scala DDD Example](https://github.com/CodelyTV/scala-ddd-example) diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/.env b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/.env new file mode 100644 index 0000000..cd58c25 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/.env @@ -0,0 +1,34 @@ +# MOOC # +#--------------------------------# +MOOC_BACKEND_SERVER_PORT=8030 +# MySql +MOOC_DATABASE_HOST=codely-java_ddd_example-mysql +MOOC_DATABASE_PORT=3306 +MOOC_DATABASE_NAME=mooc +MOOC_DATABASE_USER=root +MOOC_DATABASE_PASSWORD= + +# BACKOFFICE # +#--------------------------------# +BACKOFFICE_BACKEND_SERVER_PORT=8040 +BACKOFFICE_FRONTEND_SERVER_PORT=8041 +# MySql +BACKOFFICE_DATABASE_HOST=codely-java_ddd_example-mysql +BACKOFFICE_DATABASE_PORT=3306 +BACKOFFICE_DATABASE_NAME=backoffice +BACKOFFICE_DATABASE_USER=root +BACKOFFICE_DATABASE_PASSWORD= +# Elasticsearch +BACKOFFICE_ELASTICSEARCH_HOST=codely-java_ddd_example-elasticsearch +BACKOFFICE_ELASTICSEARCH_PORT=9200 +BACKOFFICE_ELASTICSEARCH_INDEX_PREFIX=backoffice + +# COMMON # +#--------------------------------# +# RabbitMQ +RABBITMQ_HOST=codely-java_ddd_example-rabbitmq +RABBITMQ_PORT=5672 +RABBITMQ_LOGIN=codely +RABBITMQ_PASSWORD=c0d3ly +RABBITMQ_EXCHANGE=domain_events +RABBITMQ_MAX_RETRIES=5 diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/.no.env.local b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/.no.env.local new file mode 100644 index 0000000..8f100bc --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/.no.env.local @@ -0,0 +1,34 @@ +# MOOC # +#--------------------------------# +MOOC_BACKEND_SERVER_PORT=8030 +# MySql +MOOC_DATABASE_HOST=localhost +MOOC_DATABASE_PORT=3306 +MOOC_DATABASE_NAME=mooc +MOOC_DATABASE_USER=root +MOOC_DATABASE_PASSWORD= + +# BACKOFFICE # +#--------------------------------# +BACKOFFICE_BACKEND_SERVER_PORT=8040 +BACKOFFICE_FRONTEND_SERVER_PORT=8041 +# MySql +BACKOFFICE_DATABASE_HOST=localhost +BACKOFFICE_DATABASE_PORT=3306 +BACKOFFICE_DATABASE_NAME=backoffice +BACKOFFICE_DATABASE_USER=root +BACKOFFICE_DATABASE_PASSWORD= +# Elasticsearch +BACKOFFICE_ELASTICSEARCH_HOST=localhost +BACKOFFICE_ELASTICSEARCH_PORT=9200 +BACKOFFICE_ELASTICSEARCH_INDEX_PREFIX=backoffice + +# COMMON # +#--------------------------------# +# RabbitMQ +RABBITMQ_HOST=localhost +RABBITMQ_PORT=5672 +RABBITMQ_LOGIN=codely +RABBITMQ_PASSWORD=c0d3ly +RABBITMQ_EXCHANGE=domain_events +RABBITMQ_MAX_RETRIES=5 diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/application.properties b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/application.properties new file mode 100644 index 0000000..e439ebd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/application.properties @@ -0,0 +1 @@ +spring.main.allow-bean-definition-overriding=true diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/public/images/logo.png b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/public/images/logo.png new file mode 100644 index 0000000..7593959 Binary files /dev/null and b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/public/images/logo.png differ diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/master.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/master.ftl new file mode 100644 index 0000000..0a8bc23 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/master.ftl @@ -0,0 +1,27 @@ +<#import "spring.ftl" as spring /> + + + + + + + + + + ${title} + ${description} + + +<#include "partials/header.ftl"> + +
+

<@page_title/>

+ <@main/> +
+ +
+ +<#include "partials/footer.ftl"> + + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/pages/courses/courses.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/pages/courses/courses.ftl new file mode 100644 index 0000000..8b4c99c --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/pages/courses/courses.ftl @@ -0,0 +1,20 @@ +<#include "../../master.ftl"> + +<#macro page_title>Cursos + +<#macro main> +
+ Sunset in the mountains +
+
Cursos
+

+ Actualmente CodelyTV Pro cuenta con ${courses_counter} cursos. +

+
+
+ + <#include "partials/new_course_form.ftl"> +
+
+ <#include "partials/list_courses.ftl"> + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl new file mode 100644 index 0000000..aa64bee --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl @@ -0,0 +1,153 @@ +

Cursos existentes

+ + +
+
+ +
+
+
+ + + +
+
+ + + + + + + + + + + +
+ Id + + Nombre + + Duración +
+ + + + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/new_course_form.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/new_course_form.ftl new file mode 100644 index 0000000..b7ecd5f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/new_course_form.ftl @@ -0,0 +1,54 @@ +
+

Crear curso

+
+
+ + + + <#if errors['id']?? > + <#list errors['id'] as errorMessage> +

${errorMessage}

+ + +
+
+
+
+ + + + <#if errors['name']?? > + <#list errors['name'] as errorMessage> +

${errorMessage}

+ + +
+
+ + + <#if errors['duration']?? > + <#list errors['duration'] as errorMessage> +

${errorMessage}

+ + +
+
+
+ +
+
diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/pages/home.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/pages/home.ftl new file mode 100644 index 0000000..8ee5f2b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/pages/home.ftl @@ -0,0 +1,7 @@ +<#include "../master.ftl"> + +<#macro page_title>HOME + +<#macro main> + Estamos en la home! + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl new file mode 100644 index 0000000..27a640f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl @@ -0,0 +1,7 @@ + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/partials/header.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/partials/header.ftl new file mode 100644 index 0000000..3f2f738 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/backoffice_frontend/templates/partials/header.ftl @@ -0,0 +1,28 @@ +
+ +
diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/log4j2.properties b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/log4j2.properties new file mode 100644 index 0000000..b577541 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/resources/log4j2.properties @@ -0,0 +1,34 @@ +name = CodelyTvJavaDddExample +property.filename = logs +appenders = console, file + +status = warn + +appender.console.name = CONSOLE +appender.console.type = CONSOLE +appender.console.target = SYSTEM_OUT + +appender.console.logstash.type = LogstashLayout +appender.console.logstash.dateTimeFormatPattern = yyyy-MM-dd'T'HH:mm:ss.SSSZZZ +appender.console.logstash.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.json +appender.console.logstash.prettyPrintEnabled = true +appender.console.logstash.stackTraceEnabled = true + +appender.file.type = File +appender.file.name = LOGFILE +appender.file.fileName = var/log/java-ddd-example.log +appender.file.logstash.type = LogstashLayout +appender.file.logstash.dateTimeFormatPattern = yyyy-MM-dd'T'HH:mm:ss.SSSZZZ +appender.file.logstash.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.json +appender.file.logstash.prettyPrintEnabled = false +appender.file.logstash.stackTraceEnabled = true + +loggers = file +logger.file.name = tv.codely.java_ddd_example +logger.file.level = info +logger.file.appenderRefs = file +logger.file.appenderRef.file.ref = LOGFILE + +rootLogger.level = info +rootLogger.appenderRefs = stdout +rootLogger.appenderRef.stdout.ref = CONSOLE diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/Starter.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/Starter.java new file mode 100644 index 0000000..64ad963 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/Starter.java @@ -0,0 +1,96 @@ +package tv.codely.apps; + +import java.util.Arrays; +import java.util.HashMap; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.context.ConfigurableApplicationContext; + +import tv.codely.apps.backoffice.backend.BackofficeBackendApplication; +import tv.codely.apps.backoffice.frontend.BackofficeFrontendApplication; +import tv.codely.apps.mooc.backend.MoocBackendApplication; +import tv.codely.shared.infrastructure.cli.ConsoleCommand; + +public class Starter { + + public static void main(String[] args) { + if (args.length < 2) { + throw new RuntimeException("There are not enough arguments"); + } + + String applicationName = args[0]; + String commandName = args[1]; + boolean isServerCommand = commandName.equals("server"); + + ensureApplicationExist(applicationName); + ensureCommandExist(applicationName, commandName); + + Class applicationClass = applications().get(applicationName); + + SpringApplication app = new SpringApplication(applicationClass); + + if (!isServerCommand) { + app.setWebApplicationType(WebApplicationType.NONE); + } + + ConfigurableApplicationContext context = app.run(args); + + if (!isServerCommand) { + ConsoleCommand command = (ConsoleCommand) context.getBean(commands().get(applicationName).get(commandName)); + + command.execute(Arrays.copyOfRange(args, 2, args.length)); + } + } + + private static void ensureApplicationExist(String applicationName) { + if (!applications().containsKey(applicationName)) { + throw new RuntimeException( + String.format( + "The application <%s> doesn't exist. Valids:\n- %s", + applicationName, + String.join("\n- ", applications().keySet()) + ) + ); + } + } + + private static void ensureCommandExist(String applicationName, String commandName) { + if (!"server".equals(commandName) && !existCommand(applicationName, commandName)) { + throw new RuntimeException( + String.format( + "The command <%s> for application <%s> doesn't exist. Valids (application.command):\n- api\n- %s", + commandName, + applicationName, + String.join("\n- ", commands().get(applicationName).keySet()) + ) + ); + } + } + + private static HashMap> applications() { + HashMap> applications = new HashMap<>(); + + applications.put("mooc_backend", MoocBackendApplication.class); + applications.put("backoffice_backend", BackofficeBackendApplication.class); + applications.put("backoffice_frontend", BackofficeFrontendApplication.class); + + return applications; + } + + private static HashMap>> commands() { + HashMap>> commands = new HashMap<>(); + + commands.put("mooc_backend", MoocBackendApplication.commands()); + commands.put("backoffice_backend", BackofficeBackendApplication.commands()); + commands.put("backoffice_frontend", BackofficeFrontendApplication.commands()); + + return commands; + } + + private static Boolean existCommand(String applicationName, String commandName) { + HashMap>> commands = commands(); + + return commands.containsKey(applicationName) && commands.get(applicationName).containsKey(commandName); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java new file mode 100644 index 0000000..0a9374e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java @@ -0,0 +1,27 @@ +package tv.codely.apps.backoffice.backend; + +import java.util.HashMap; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +import tv.codely.apps.backoffice.backend.command.ConsumeRabbitMqDomainEventsCommand; +import tv.codely.shared.domain.Service; + +@SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class) +@ComponentScan( + includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Service.class), + value = { "tv.codely.shared", "tv.codely.backoffice", "tv.codely.apps.backoffice.backend" } +) +public class BackofficeBackendApplication { + + public static HashMap> commands() { + return new HashMap<>() { + { + put("domain-events:rabbitmq:consume", ConsumeRabbitMqDomainEventsCommand.class); + } + }; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java new file mode 100644 index 0000000..018972f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java @@ -0,0 +1,18 @@ +package tv.codely.apps.backoffice.backend.command; + +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqDomainEventsConsumer; +import tv.codely.shared.infrastructure.cli.ConsoleCommand; + +public final class ConsumeRabbitMqDomainEventsCommand extends ConsoleCommand { + + private final RabbitMqDomainEventsConsumer consumer; + + public ConsumeRabbitMqDomainEventsCommand(RabbitMqDomainEventsConsumer consumer) { + this.consumer = consumer; + } + + @Override + public void execute(String[] args) { + consumer.consume("backoffice"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerConfiguration.java new file mode 100644 index 0000000..80314c2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerConfiguration.java @@ -0,0 +1,28 @@ +package tv.codely.apps.backoffice.backend.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import tv.codely.apps.backoffice.backend.middleware.BasicHttpAuthMiddleware; +import tv.codely.shared.domain.bus.command.CommandBus; + +@Configuration +public class BackofficeBackendServerConfiguration { + + private final CommandBus bus; + + public BackofficeBackendServerConfiguration(CommandBus bus) { + this.bus = bus; + } + + @Bean + public FilterRegistrationBean basicHttpAuthMiddleware() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + + registrationBean.setFilter(new BasicHttpAuthMiddleware(bus)); + registrationBean.addUrlPatterns("/health-check"); + + return registrationBean; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerPortCustomizer.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerPortCustomizer.java new file mode 100644 index 0000000..8c46d55 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerPortCustomizer.java @@ -0,0 +1,28 @@ +package tv.codely.apps.backoffice.backend.config; + +import org.springframework.boot.web.server.ConfigurableWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; + +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; + +@Component +public final class BackofficeBackendServerPortCustomizer + implements WebServerFactoryCustomizer { + + private final Parameter param; + + public BackofficeBackendServerPortCustomizer(Parameter param) { + this.param = param; + } + + @Override + public void customize(ConfigurableWebServerFactory factory) { + try { + factory.setPort(param.getInt("BACKOFFICE_BACKEND_SERVER_PORT")); + } catch (ParameterNotExist parameterNotExist) { + parameterNotExist.printStackTrace(); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/controller/courses/CoursesGetController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/controller/courses/CoursesGetController.java new file mode 100644 index 0000000..48bb8b4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/controller/courses/CoursesGetController.java @@ -0,0 +1,85 @@ +package tv.codely.apps.backoffice.backend.controller.courses; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import tv.codely.backoffice.courses.application.BackofficeCoursesResponse; +import tv.codely.backoffice.courses.application.search_by_criteria.SearchBackofficeCoursesByCriteriaQuery; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +@CrossOrigin(origins = "*", methods = { RequestMethod.GET }) +public final class CoursesGetController extends ApiController { + + public CoursesGetController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @GetMapping("/courses") + public List> index(@RequestParam HashMap params) + throws QueryHandlerExecutionError { + BackofficeCoursesResponse courses = ask( + new SearchBackofficeCoursesByCriteriaQuery( + parseFilters(params), + Optional.ofNullable((String) params.get("order_by")), + Optional.ofNullable((String) params.get("order")), + Optional.ofNullable((Integer) params.get("limit")), + Optional.ofNullable((Integer) params.get("offset")) + ) + ); + + return courses + .courses() + .stream() + .map(response -> + new HashMap() { + { + put("id", response.id()); + put("name", response.name()); + put("duration", response.duration()); + } + } + ) + .collect(Collectors.toList()); + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return null; + } + + private List> parseFilters(HashMap params) { + int maxParams = params.size(); + + List> filters = new ArrayList<>(); + + for (int possibleFilterKey = 0; possibleFilterKey < maxParams; possibleFilterKey++) { + if (params.containsKey(String.format("filters[%s][field]", possibleFilterKey))) { + int key = possibleFilterKey; + + filters.add( + new HashMap() { + { + put("field", (String) params.get(String.format("filters[%s][field]", key))); + put("operator", (String) params.get(String.format("filters[%s][operator]", key))); + put("value", (String) params.get(String.format("filters[%s][value]", key))); + } + } + ); + } + } + + return filters; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetController.java new file mode 100644 index 0000000..bef9ec0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetController.java @@ -0,0 +1,34 @@ +package tv.codely.apps.backoffice.backend.controller.health_check; + +import java.util.HashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +public final class HealthCheckGetController extends ApiController { + + public HealthCheckGetController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @GetMapping("/health-check") + public HashMap index() { + HashMap status = new HashMap<>(); + status.put("application", "backoffice_backend"); + status.put("status", "ok"); + + return status; + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return null; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/middleware/BasicHttpAuthMiddleware.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/middleware/BasicHttpAuthMiddleware.java new file mode 100644 index 0000000..2ce6a90 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/backend/middleware/BasicHttpAuthMiddleware.java @@ -0,0 +1,77 @@ +package tv.codely.apps.backoffice.backend.middleware; + +import java.io.IOException; +import java.util.Base64; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import tv.codely.backoffice.auth.application.authenticate.AuthenticateUserCommand; +import tv.codely.backoffice.auth.domain.InvalidAuthCredentials; +import tv.codely.backoffice.auth.domain.InvalidAuthUsername; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; + +public final class BasicHttpAuthMiddleware implements Filter { + + private final CommandBus bus; + + public BasicHttpAuthMiddleware(CommandBus bus) { + this.bus = bus; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + String authorizationHeader = ((HttpServletRequest) request).getHeader("authorization"); + + if (hasIntroducedCredentials(authorizationHeader)) { + authenticate(authorizationHeader, chain, request, response); + } else { + askForCredentials(response); + } + } + + private boolean hasIntroducedCredentials(String authorizationHeader) { + return null != authorizationHeader; + } + + private void authenticate( + String authorizationHeader, + FilterChain chain, + ServletRequest request, + ServletResponse response + ) throws IOException, ServletException { + String[] auth = decodeAuth(authorizationHeader); + String user = auth[0]; + String pass = auth[1]; + + try { + bus.dispatch(new AuthenticateUserCommand(user, pass)); + + request.setAttribute("authentication_username", user); + + chain.doFilter(request, response); + } catch (InvalidAuthUsername | InvalidAuthCredentials | CommandHandlerExecutionError error) { + setInvalidCredentials(response); + } + } + + private String[] decodeAuth(String authString) { + return new String(Base64.getDecoder().decode(authString.split("\\s+")[1])).split(":"); + } + + private void setInvalidCredentials(ServletResponse response) { + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + httpServletResponse.reset(); + httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + + private void askForCredentials(ServletResponse response) { + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + httpServletResponse.reset(); + httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpServletResponse.setHeader("WWW-Authenticate", "Basic realm=\"CodelyTV\""); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/BackofficeFrontendApplication.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/BackofficeFrontendApplication.java new file mode 100644 index 0000000..ae42787 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/BackofficeFrontendApplication.java @@ -0,0 +1,24 @@ +package tv.codely.apps.backoffice.frontend; + +import java.util.HashMap; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +import tv.codely.shared.domain.Service; + +@SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class) +@ComponentScan( + includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Service.class), + value = { "tv.codely.shared", "tv.codely.backoffice", "tv.codely.mooc", "tv.codely.apps.backoffice.frontend" } +) +public class BackofficeFrontendApplication { + + public static HashMap> commands() { + return new HashMap>() { + {} + }; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendServerPortCustomizer.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendServerPortCustomizer.java new file mode 100644 index 0000000..6cfbd81 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendServerPortCustomizer.java @@ -0,0 +1,28 @@ +package tv.codely.apps.backoffice.frontend.config; + +import org.springframework.boot.web.server.ConfigurableWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; + +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; + +@Component +public final class BackofficeFrontendServerPortCustomizer + implements WebServerFactoryCustomizer { + + private final Parameter param; + + public BackofficeFrontendServerPortCustomizer(Parameter param) { + this.param = param; + } + + @Override + public void customize(ConfigurableWebServerFactory factory) { + try { + factory.setPort(param.getInt("BACKOFFICE_FRONTEND_SERVER_PORT")); + } catch (ParameterNotExist parameterNotExist) { + parameterNotExist.printStackTrace(); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java new file mode 100644 index 0000000..62749d1 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java @@ -0,0 +1,61 @@ +package tv.codely.apps.backoffice.frontend.config; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; +import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver; + +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqPublisher; + +@Configuration +@EnableWebMvc +public class BackofficeFrontendWebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/**").addResourceLocations("classpath:/backoffice_frontend/public/"); + } + + @Bean + public ViewResolver getViewResolver() { + FreeMarkerViewResolver resolver = new FreeMarkerViewResolver(); + resolver.setCache(false); + resolver.setSuffix(".ftl"); + return resolver; + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("classpath:/backoffice_frontend/templates/"); + configurer.setDefaultEncoding("UTF-8"); + // configurer.setFreemarkerVariables(new HashMap() {{ + // put("flash", new Flash()); + // }}); + + return configurer; + } + + @Primary + @Bean + public RabbitMqEventBus rabbitMqEventBus( + RabbitMqPublisher publisher, + @Qualifier("backofficeMysqlEventBus") MySqlEventBus failoverPublisher + ) { + return new RabbitMqEventBus(publisher, failoverPublisher); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesGetWebController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesGetWebController.java new file mode 100644 index 0000000..95ff91c --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesGetWebController.java @@ -0,0 +1,48 @@ +package tv.codely.apps.backoffice.frontend.controller.courses; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.ModelAndView; + +import tv.codely.mooc.courses_counter.application.find.CoursesCounterResponse; +import tv.codely.mooc.courses_counter.application.find.FindCoursesCounterQuery; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; + +@Controller +public final class CoursesGetWebController { + + private final QueryBus bus; + + public CoursesGetWebController(QueryBus bus) { + this.bus = bus; + } + + @GetMapping("/courses") + public ModelAndView index( + @ModelAttribute("inputs") HashMap inputs, + @ModelAttribute("errors") HashMap> errors + ) throws QueryHandlerExecutionError { + CoursesCounterResponse counterResponse = bus.ask(new FindCoursesCounterQuery()); + + return new ModelAndView( + "pages/courses/courses", + new HashMap() { + { + put("title", "Courses"); + put("description", "Courses CodelyTV - Backoffice"); + put("courses_counter", counterResponse.total()); + put("inputs", inputs); + put("errors", errors); + put("generated_uuid", UUID.randomUUID().toString()); + } + } + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesPostWebController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesPostWebController.java new file mode 100644 index 0000000..927dd41 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesPostWebController.java @@ -0,0 +1,67 @@ +package tv.codely.apps.backoffice.frontend.controller.courses; + +import java.io.Serializable; +import java.util.HashMap; + +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.web.servlet.view.RedirectView; + +import tv.codely.mooc.courses.application.create.CreateCourseCommand; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; +import tv.codely.shared.infrastructure.validation.ValidationResponse; +import tv.codely.shared.infrastructure.validation.Validator; + +@Controller +public final class CoursesPostWebController { + + private final CommandBus bus; + private final HashMap rules = new HashMap() { + { + put("id", "required|not_empty|uuid"); + put("name", "required|not_empty|string"); + put("duration", "required|not_empty|string"); + } + }; + + public CoursesPostWebController(CommandBus bus) { + this.bus = bus; + } + + @PostMapping(value = "/courses", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public RedirectView index(@RequestParam HashMap request, RedirectAttributes attributes) + throws Exception { + ValidationResponse validationResponse = Validator.validate(request, rules); + + return validationResponse.hasErrors() + ? redirectWithErrors(validationResponse, request, attributes) + : createCourse(request); + } + + private RedirectView redirectWithErrors( + ValidationResponse validationResponse, + HashMap request, + RedirectAttributes attributes + ) { + attributes.addFlashAttribute("errors", validationResponse.errors()); + attributes.addFlashAttribute("inputs", request); + + return new RedirectView("/courses"); + } + + private RedirectView createCourse(HashMap request) throws CommandHandlerExecutionError { + bus.dispatch( + new CreateCourseCommand( + request.get("id").toString(), + request.get("name").toString(), + request.get("duration").toString() + ) + ); + + return new RedirectView("/courses"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetController.java new file mode 100644 index 0000000..9c06c15 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetController.java @@ -0,0 +1,19 @@ +package tv.codely.apps.backoffice.frontend.controller.health_check; + +import java.util.HashMap; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public final class HealthCheckGetController { + + @GetMapping("/health-check") + public HashMap index() { + HashMap status = new HashMap<>(); + status.put("application", "backoffice_frontend"); + status.put("status", "ok"); + + return status; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/controller/home/HomeGetWebController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/controller/home/HomeGetWebController.java new file mode 100644 index 0000000..254fba9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/backoffice/frontend/controller/home/HomeGetWebController.java @@ -0,0 +1,25 @@ +package tv.codely.apps.backoffice.frontend.controller.home; + +import java.io.Serializable; +import java.util.HashMap; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.ModelAndView; + +@Controller +public final class HomeGetWebController { + + @GetMapping("/") + public ModelAndView index() { + return new ModelAndView( + "pages/home", + new HashMap() { + { + put("title", "Welcome"); + put("description", "CodelyTV - Backoffice"); + } + } + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/MoocBackendApplication.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/MoocBackendApplication.java new file mode 100644 index 0000000..b9e0448 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/MoocBackendApplication.java @@ -0,0 +1,29 @@ +package tv.codely.apps.mooc.backend; + +import java.util.HashMap; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +import tv.codely.apps.mooc.backend.command.ConsumeMySqlDomainEventsCommand; +import tv.codely.apps.mooc.backend.command.ConsumeRabbitMqDomainEventsCommand; +import tv.codely.shared.domain.Service; + +@SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class) +@ComponentScan( + includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Service.class), + value = { "tv.codely.shared", "tv.codely.mooc", "tv.codely.apps.mooc.backend" } +) +public class MoocBackendApplication { + + public static HashMap> commands() { + return new HashMap>() { + { + put("domain-events:mysql:consume", ConsumeMySqlDomainEventsCommand.class); + put("domain-events:rabbitmq:consume", ConsumeRabbitMqDomainEventsCommand.class); + } + }; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/command/ConsumeMySqlDomainEventsCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/command/ConsumeMySqlDomainEventsCommand.java new file mode 100644 index 0000000..78eaf41 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/command/ConsumeMySqlDomainEventsCommand.java @@ -0,0 +1,18 @@ +package tv.codely.apps.mooc.backend.command; + +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlDomainEventsConsumer; +import tv.codely.shared.infrastructure.cli.ConsoleCommand; + +public final class ConsumeMySqlDomainEventsCommand extends ConsoleCommand { + + private final MySqlDomainEventsConsumer consumer; + + public ConsumeMySqlDomainEventsCommand(MySqlDomainEventsConsumer consumer) { + this.consumer = consumer; + } + + @Override + public void execute(String[] args) { + consumer.consume(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java new file mode 100644 index 0000000..ce5d7bd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java @@ -0,0 +1,18 @@ +package tv.codely.apps.mooc.backend.command; + +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqDomainEventsConsumer; +import tv.codely.shared.infrastructure.cli.ConsoleCommand; + +public final class ConsumeRabbitMqDomainEventsCommand extends ConsoleCommand { + + private final RabbitMqDomainEventsConsumer consumer; + + public ConsumeRabbitMqDomainEventsCommand(RabbitMqDomainEventsConsumer consumer) { + this.consumer = consumer; + } + + @Override + public void execute(String[] args) { + consumer.consume("mooc"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java new file mode 100644 index 0000000..3041e7b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java @@ -0,0 +1,29 @@ +package tv.codely.apps.mooc.backend.config; + +import java.util.Optional; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import tv.codely.shared.infrastructure.spring.ApiExceptionMiddleware; + +@Configuration +public class MoocBackendServerConfiguration { + + private final Optional mapping; + + public MoocBackendServerConfiguration(Optional mapping) { + this.mapping = mapping; + } + + @Bean + public FilterRegistrationBean apiExceptionMiddleware() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + + mapping.ifPresent(map -> registrationBean.setFilter(new ApiExceptionMiddleware(map))); + + return registrationBean; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerPortCustomizer.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerPortCustomizer.java new file mode 100644 index 0000000..3d61c27 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerPortCustomizer.java @@ -0,0 +1,27 @@ +package tv.codely.apps.mooc.backend.config; + +import org.springframework.boot.web.server.ConfigurableWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; + +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; + +@Component +public final class MoocBackendServerPortCustomizer implements WebServerFactoryCustomizer { + + private final Parameter param; + + public MoocBackendServerPortCustomizer(Parameter param) { + this.param = param; + } + + @Override + public void customize(ConfigurableWebServerFactory factory) { + try { + factory.setPort(param.getInt("MOOC_BACKEND_SERVER_PORT")); + } catch (ParameterNotExist parameterNotExist) { + parameterNotExist.printStackTrace(); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/courses/CourseGetController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/courses/CourseGetController.java new file mode 100644 index 0000000..838783b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/courses/CourseGetController.java @@ -0,0 +1,54 @@ +package tv.codely.apps.mooc.backend.controller.courses; + +import java.io.Serializable; +import java.util.HashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.mooc.courses.application.CourseResponse; +import tv.codely.mooc.courses.application.find.FindCourseQuery; +import tv.codely.mooc.courses.domain.CourseNotExist; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +public final class CourseGetController extends ApiController { + + public CourseGetController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @GetMapping("/courses/{id}") + public ResponseEntity> index(@PathVariable String id) + throws QueryHandlerExecutionError { + CourseResponse course = ask(new FindCourseQuery(id)); + + return ResponseEntity + .ok() + .body( + new HashMap() { + { + put("id", course.id()); + put("name", course.name()); + put("duration", course.duration()); + } + } + ); + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return new HashMap, HttpStatus>() { + { + put(CourseNotExist.class, HttpStatus.NOT_FOUND); + } + }; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java new file mode 100644 index 0000000..54bc204 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java @@ -0,0 +1,60 @@ +package tv.codely.apps.mooc.backend.controller.courses; + +import java.util.HashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.mooc.courses.application.create.CreateCourseCommand; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +public final class CoursesPutController extends ApiController { + + public CoursesPutController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @PutMapping(value = "/courses/{id}") + public ResponseEntity index(@PathVariable String id, @RequestBody Request request) + throws CommandHandlerExecutionError { + dispatch(new CreateCourseCommand(id, request.name(), request.duration())); + + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return null; + } +} + +final class Request { + + private String name; + private String duration; + + public void setDuration(String duration) { + this.duration = duration; + } + + public void setName(String name) { + this.name = name; + } + + String name() { + return name; + } + + String duration() { + return duration; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetController.java new file mode 100644 index 0000000..88a932f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetController.java @@ -0,0 +1,39 @@ +package tv.codely.apps.mooc.backend.controller.courses_counter; + +import java.util.HashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.mooc.courses_counter.application.find.CoursesCounterResponse; +import tv.codely.mooc.courses_counter.application.find.FindCoursesCounterQuery; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +public final class CoursesCounterGetController extends ApiController { + + public CoursesCounterGetController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @GetMapping("/courses-counter") + public HashMap index() throws QueryHandlerExecutionError { + CoursesCounterResponse response = ask(new FindCoursesCounterQuery()); + + return new HashMap() { + { + put("total", response.total()); + } + }; + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return null; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetController.java new file mode 100644 index 0000000..43461f9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetController.java @@ -0,0 +1,34 @@ +package tv.codely.apps.mooc.backend.controller.health_check; + +import java.util.HashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +public final class HealthCheckGetController extends ApiController { + + public HealthCheckGetController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @GetMapping("/health-check") + public HashMap index() { + HashMap status = new HashMap<>(); + status.put("application", "mooc_backend"); + status.put("status", "ok"); + + return status; + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return null; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutController.java new file mode 100644 index 0000000..3fbcd68 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutController.java @@ -0,0 +1,36 @@ +package tv.codely.apps.mooc.backend.controller.notifications; + +import java.util.HashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.mooc.notifications.application.send_new_courses_newsletter.SendNewCoursesNewsletterCommand; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +public final class NewsletterNotificationPutController extends ApiController { + + public NewsletterNotificationPutController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @PutMapping(value = "/newsletter/{id}") + public ResponseEntity index(@PathVariable String id) throws CommandHandlerExecutionError { + dispatch(new SendNewCoursesNewsletterCommand(id)); + + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return null; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java new file mode 100644 index 0000000..dfc9657 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java @@ -0,0 +1,53 @@ +package tv.codely.apps.mooc.backend.controller.playground; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.shared.domain.Utils; + +@RestController +record DomainEventPostController(RabbitTemplate rabbitTemplate) { + @PostMapping(value = "/domain-events") + public ResponseEntity index(@RequestBody Request request) { + System.out.println(request.eventName()); + + var serializedEvent = Utils.jsonEncode(request.eventRaw()); + + Message message = new Message( + serializedEvent.getBytes(), + MessagePropertiesBuilder.newInstance().setContentEncoding("utf-8").setContentType("application/json").build() + ); + + rabbitTemplate.send("domain_events", request.eventName(), message); + + return new ResponseEntity<>(HttpStatus.CREATED); + } +} + +final class Request { + + private String eventName; + private Object eventRaw; + + public void setEventName(String eventName) { + this.eventName = eventName; + } + + public void setEventRaw(Object eventRaw) { + this.eventRaw = eventRaw; + } + + String eventName() { + return eventName; + } + + Object eventRaw() { + return eventRaw; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/resources/log4j2.properties b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/resources/log4j2.properties new file mode 100644 index 0000000..98427f2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/resources/log4j2.properties @@ -0,0 +1,33 @@ +name = CodelyTvJavaDddExample +property.filename = logs +appenders = console, file + +status = warn + +appender.console.name = CONSOLE +appender.console.type = CONSOLE +appender.console.target = SYSTEM_OUT + +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = [%level] [%date{HH:mm:ss.SSS}] [%class{0}#%method:%line] %message \(%mdc\) %n%throwable +appender.console.filter.threshold.type = ThresholdFilter +appender.console.filter.threshold.level = info + +appender.file.type = File +appender.file.name = LOGFILE +appender.file.fileName = var/log/java-ddd-example-test.log +appender.file.logstash.type = LogstashLayout +appender.file.logstash.dateTimeFormatPattern = yyyy-MM-dd'T'HH:mm:ss.SSSZZZ +appender.file.logstash.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.json +appender.file.logstash.prettyPrintEnabled = false +appender.file.logstash.stackTraceEnabled = true + +loggers = file +logger.file.name = tv.codely.java_ddd_example +logger.file.level = info +logger.file.appenderRefs = file +logger.file.appenderRef.file.ref = LOGFILE + +rootLogger.level = info +rootLogger.appenderRefs = stdout +rootLogger.appenderRef.stdout.ref = CONSOLE diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/ApplicationTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/ApplicationTestCase.java new file mode 100644 index 0000000..d356818 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/ApplicationTestCase.java @@ -0,0 +1,67 @@ +package tv.codely.apps; + +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Arrays; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; + +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.domain.bus.event.EventBus; + +@SpringBootTest +@AutoConfigureMockMvc +public abstract class ApplicationTestCase { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private EventBus eventBus; + + protected void assertResponse(String endpoint, Integer expectedStatusCode, String expectedResponse) throws Exception { + ResultMatcher response = expectedResponse.isEmpty() ? content().string("") : content().json(expectedResponse); + + mockMvc.perform(get(endpoint)).andExpect(status().is(expectedStatusCode)).andExpect(response); + } + + protected void assertResponse( + String endpoint, + Integer expectedStatusCode, + String expectedResponse, + HttpHeaders headers + ) throws Exception { + ResultMatcher response = expectedResponse.isEmpty() ? content().string("") : content().json(expectedResponse); + + mockMvc.perform(get(endpoint).headers(headers)).andExpect(status().is(expectedStatusCode)).andExpect(response); + } + + protected void assertRequestWithBody(String method, String endpoint, String body, Integer expectedStatusCode) + throws Exception { + mockMvc + .perform(request(HttpMethod.valueOf(method), endpoint).content(body).contentType(APPLICATION_JSON)) + .andExpect(status().is(expectedStatusCode)) + .andExpect(content().string("")); + } + + protected void assertRequest(String method, String endpoint, Integer expectedStatusCode) throws Exception { + mockMvc + .perform(request(HttpMethod.valueOf(method), endpoint)) + .andExpect(status().is(expectedStatusCode)) + .andExpect(content().string("")); + } + + protected void givenISendEventsToTheBus(DomainEvent... domainEvents) { + eventBus.publish(Arrays.asList(domainEvents)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/backoffice/BackofficeApplicationTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/backoffice/BackofficeApplicationTestCase.java new file mode 100644 index 0000000..195b553 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/backoffice/BackofficeApplicationTestCase.java @@ -0,0 +1,8 @@ +package tv.codely.apps.backoffice; + +import org.springframework.transaction.annotation.Transactional; + +import tv.codely.apps.ApplicationTestCase; + +@Transactional("backoffice-transaction_manager") +public abstract class BackofficeApplicationTestCase extends ApplicationTestCase {} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetControllerShould.java new file mode 100644 index 0000000..add71d8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetControllerShould.java @@ -0,0 +1,38 @@ +package tv.codely.apps.backoffice.backend.controller.health_check; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; + +import tv.codely.apps.ApplicationTestCase; + +final class HealthCheckGetControllerShould extends ApplicationTestCase { + + @Test + void check_the_app_is_working_ok_with_valid_credentials() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setBasicAuth("javi", "barbitas"); + + assertResponse("/health-check", 200, "{'application':'backoffice_backend','status':'ok'}", headers); + } + + @Test + void not_check_the_app_is_working_ok_with_invalid_credentials() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setBasicAuth("tipo_de_incognito", "homer.sampson"); + + assertResponse("/health-check", 403, "", headers); + } + + @Test + void not_check_the_app_is_working_ok_with_invalid_credentials_of_an_existing_user() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setBasicAuth("rafa", "incorrect.password"); + + assertResponse("/health-check", 403, "", headers); + } + + @Test + void not_check_the_app_is_working_ok_with_no_credentials() throws Exception { + assertResponse("/health-check", 401, ""); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetControllerShould.java new file mode 100644 index 0000000..c5a9747 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetControllerShould.java @@ -0,0 +1,13 @@ +package tv.codely.apps.backoffice.frontend.controller.health_check; + +import org.junit.jupiter.api.Test; + +import tv.codely.apps.ApplicationTestCase; + +final class HealthCheckGetControllerShould extends ApplicationTestCase { + + @Test + void check_the_app_is_working_ok() throws Exception { + assertResponse("/health-check", 200, "{'application':'backoffice_frontend','status':'ok'}"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/MoocApplicationTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/MoocApplicationTestCase.java new file mode 100644 index 0000000..90dc231 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/MoocApplicationTestCase.java @@ -0,0 +1,8 @@ +package tv.codely.apps.mooc; + +import org.springframework.transaction.annotation.Transactional; + +import tv.codely.apps.ApplicationTestCase; + +@Transactional("mooc-transaction_manager") +public abstract class MoocApplicationTestCase extends ApplicationTestCase {} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/courses/CourseGetControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/courses/CourseGetControllerShould.java new file mode 100644 index 0000000..0ebbcc4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/courses/CourseGetControllerShould.java @@ -0,0 +1,31 @@ +package tv.codely.apps.mooc.backend.controller.courses; + +import org.junit.jupiter.api.Test; + +import tv.codely.apps.mooc.MoocApplicationTestCase; + +final class CourseGetControllerShould extends MoocApplicationTestCase { + + @Test + void find_an_existing_course() throws Exception { + String id = "99ad55f5-6eab-4d73-b383-c63268e251e8"; + String body = "{\"name\": \"The best course\", \"duration\": \"5 hours\"}"; + + givenThereIsACourse(id, body); + + assertResponse(String.format("/courses/%s", id), 200, body); + } + + @Test + void no_find_a_non_existing_course() throws Exception { + String id = "4ecc0cb3-05b2-4238-b7e1-1fbb0d5d3661"; + String body = + "{\"error_code\": \"course_not_exist\", \"message\": \"The course <4ecc0cb3-05b2-4238-b7e1-1fbb0d5d3661> doesn't exist\"}"; + + assertResponse(String.format("/courses/%s", id), 404, body); + } + + private void givenThereIsACourse(String id, String body) throws Exception { + assertRequestWithBody("PUT", String.format("/courses/%s", id), body, 201); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/courses/CoursesPutControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/courses/CoursesPutControllerShould.java new file mode 100644 index 0000000..9dcbe3d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/courses/CoursesPutControllerShould.java @@ -0,0 +1,18 @@ +package tv.codely.apps.mooc.backend.controller.courses; + +import org.junit.jupiter.api.Test; + +import tv.codely.apps.mooc.MoocApplicationTestCase; + +public final class CoursesPutControllerShould extends MoocApplicationTestCase { + + @Test + void create_a_valid_non_existing_course() throws Exception { + assertRequestWithBody( + "PUT", + "/courses/1aab45ba-3c7a-4344-8936-78466eca77fa", + "{\"name\": \"The best course\", \"duration\": \"5 hours\"}", + 201 + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetControllerShould.java new file mode 100644 index 0000000..2927678 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetControllerShould.java @@ -0,0 +1,46 @@ +package tv.codely.apps.mooc.backend.controller.courses_counter; + +import org.junit.jupiter.api.Test; + +import tv.codely.apps.mooc.MoocApplicationTestCase; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +public final class CoursesCounterGetControllerShould extends MoocApplicationTestCase { + + @Test + void get_the_counter_with_one_course() throws Exception { + givenISendEventsToTheBus( + new CourseCreatedDomainEvent("8f34bc99-e0e2-4296-a008-75f51f03aeb4", "DDD en Java", "7 days") + ); + + assertResponse("/courses-counter", 200, "{'total': 1}"); + } + + @Test + void get_the_counter_with_more_than_one_course() throws Exception { + givenISendEventsToTheBus( + new CourseCreatedDomainEvent("8f34bc99-e0e2-4296-a008-75f51f03aeb4", "DDD en Java", "7 days"), + new CourseCreatedDomainEvent("3642f700-868a-4778-9317-a2d542d01785", "DDD en PHP", "6 days"), + new CourseCreatedDomainEvent("92dd8402-69f3-4900-b569-3f2c2797065f", "DDD en Cobol", "10 years") + ); + + assertResponse("/courses-counter", 200, "{'total': 3}"); + } + + @Test + void get_the_counter_with_more_than_one_course_having_duplicated_events() throws Exception { + givenISendEventsToTheBus( + new CourseCreatedDomainEvent("8f34bc99-e0e2-4296-a008-75f51f03aeb4", "DDD en Java", "7 days"), + new CourseCreatedDomainEvent("8f34bc99-e0e2-4296-a008-75f51f03aeb4", "DDD en Java", "7 days"), + new CourseCreatedDomainEvent("8f34bc99-e0e2-4296-a008-75f51f03aeb4", "DDD en Java", "7 days"), + new CourseCreatedDomainEvent("3642f700-868a-4778-9317-a2d542d01785", "DDD en PHP", "6 days"), + new CourseCreatedDomainEvent("3642f700-868a-4778-9317-a2d542d01785", "DDD en PHP", "6 days"), + new CourseCreatedDomainEvent("3642f700-868a-4778-9317-a2d542d01785", "DDD en PHP", "6 days"), + new CourseCreatedDomainEvent("3642f700-868a-4778-9317-a2d542d01785", "DDD en PHP", "6 days"), + new CourseCreatedDomainEvent("92dd8402-69f3-4900-b569-3f2c2797065f", "DDD en Cobol", "10 years"), + new CourseCreatedDomainEvent("92dd8402-69f3-4900-b569-3f2c2797065f", "DDD en Cobol", "10 years") + ); + + assertResponse("/courses-counter", 200, "{'total': 3}"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetControllerShould.java new file mode 100644 index 0000000..94069bf --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetControllerShould.java @@ -0,0 +1,13 @@ +package tv.codely.apps.mooc.backend.controller.health_check; + +import org.junit.jupiter.api.Test; + +import tv.codely.apps.mooc.MoocApplicationTestCase; + +final class HealthCheckGetControllerShould extends MoocApplicationTestCase { + + @Test + void check_the_app_is_working_ok() throws Exception { + assertResponse("/health-check", 200, "{'application':'mooc_backend','status':'ok'}"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutControllerShould.java new file mode 100644 index 0000000..8f011e9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/apps/test/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutControllerShould.java @@ -0,0 +1,13 @@ +package tv.codely.apps.mooc.backend.controller.notifications; + +import org.junit.jupiter.api.Test; + +import tv.codely.apps.mooc.MoocApplicationTestCase; + +final class NewsletterNotificationPutControllerShould extends MoocApplicationTestCase { + + @Test + void create_a_valid_non_existing_course() throws Exception { + assertRequest("PUT", "/newsletter/6eebbe60-50e7-400a-810c-3e0af0943ee6", 201); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/build.gradle b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/build.gradle new file mode 100644 index 0000000..508fb39 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/build.gradle @@ -0,0 +1,171 @@ +// Main project (located in apps/) +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath('org.springframework.boot:spring-boot-gradle-plugin:3.1.5') + } +} + +plugins { + id "com.diffplug.spotless" version "6.22.0" +} + +spotless { + java { + prettier(['prettier': '2.8.8', 'prettier-plugin-java': '2.2.0']) + .config(['parser': 'java', 'useTabs': true, 'printWidth': 120]) + + importOrder('\\#', 'java', '', 'tv.codely') + removeUnusedImports() + + endWithNewline() + + formatAnnotations() + } +} + +// Common for all projects +allprojects { + apply plugin: 'java' + apply plugin: 'io.spring.dependency-management' + apply plugin: 'org.springframework.boot' + + java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + + repositories { + mavenCentral() + maven { url 'https://repo.spring.io/milestone' } + } + + ext { + set('springCloudVersion', "Hoxton.M3") + set('elasticsearchVersion', '6.8.4') + } + + dependencies { + // Prod + implementation 'org.apache.logging.log4j:log4j-core:2.21.1' + implementation 'org.apache.logging.log4j:log4j-api:2.21.1' + implementation 'com.vlkan.log4j2:log4j2-logstash-layout:1.0.5' + implementation 'io.github.cdimascio:dotenv-java:3.0.0' + implementation 'org.hibernate.orm:hibernate-core:6.3.1.Final' + implementation 'jakarta.persistence:jakarta.persistence-api:3.2.0-B01' + implementation 'org.springframework:spring-orm:6.0.13' + implementation 'org.springframework:spring-context-support:6.0.13' + implementation 'org.apache.tomcat:tomcat-dbcp:10.1.15' + implementation 'com.sun.xml.bind:jaxb-impl:4.0.4' + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.1' + implementation 'org.freemarker:freemarker-gae:2.3.32' + implementation 'org.reflections:reflections:0.10.2' + implementation 'com.google.guava:guava:31.0.1-jre' + implementation 'org.springframework.boot:spring-boot-starter-amqp' + implementation "org.elasticsearch.client:elasticsearch-rest-client:${elasticsearchVersion}" + implementation "org.elasticsearch.client:elasticsearch-rest-high-level-client:${elasticsearchVersion}" + implementation 'mysql:mysql-connector-java:8.0.28' + + // Test + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' + testImplementation 'org.mockito:mockito-core:3.3.3' + testImplementation 'com.github.javafaker:javafaker:1.0.1' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.5.2' + } + + dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } + } + + test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + } + } + + task view_paths { + doLast { task -> + println "$task.project.name" + println "------------------" + println "Main: $sourceSets.main.java.srcDirTrees" + println " Resources: $sourceSets.main.resources.srcDirTrees" + println "Tests: $sourceSets.test.java.srcDirTrees" + println " Resources: $sourceSets.test.resources.srcDirTrees" + } + } +} + +// All subprojects (located in src/*) +subprojects { + group = "tv.codely.${rootProject.name}" + + sourceSets { + main { + java { srcDirs = ['main'] } + resources { srcDirs = ['main/resources'] } + } + + test { + java { srcDirs = ['test'] } + resources { srcDirs = ['test/resources'] } + } + } + + dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web:3.1.5' + + testImplementation rootProject.sourceSets.main.output + testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.5' + + if (project.name != "shared") { + implementation project(":shared") + testImplementation project(":shared").sourceSets.test.output + } + } + + bootJar { + enabled = false + } + + jar { + enabled = true + } +} + + +sourceSets { + main { + java { srcDirs = ['apps/main'] } + resources { srcDirs = ['apps/main/resources'] } + } + + test { + java { srcDirs = ['apps/test'] } + resources { srcDirs = ['apps/test/resources'] } + } +} + +bootJar { + archiveBaseName.set('java-ddd-example') + archiveVersion.set('0.0.1') + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web:3.1.5' + + implementation project(":shared") + implementation project(":backoffice") + implementation project(":mooc") + + testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.5' + testImplementation project(":shared").sourceSets.test.output +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/docker-compose.ci.yml b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/docker-compose.ci.yml new file mode 100755 index 0000000..d2c1c99 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/docker-compose.ci.yml @@ -0,0 +1,55 @@ +version: '3' + +services: + shared_mysql: + container_name: codely-java_ddd_example-mysql + image: mysql:8 + platform: linux/amd64 + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD= + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + entrypoint: + sh -c " + echo 'CREATE DATABASE IF NOT EXISTS mooc;CREATE DATABASE IF NOT EXISTS backoffice;' > /docker-entrypoint-initdb.d/init.sql; + /usr/local/bin/docker-entrypoint.sh --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + " + command: ["--default-authentication-plugin=mysql_native_password"] + + shared_rabbitmq: + container_name: codely-java_ddd_example-rabbitmq + image: 'rabbitmq:3.7-management' + platform: linux/amd64 + restart: unless-stopped + ports: + - "5672:5672" + - "8090:15672" + environment: + - RABBITMQ_DEFAULT_USER=codely + - RABBITMQ_DEFAULT_PASS=c0d3ly + + backoffice_elasticsearch: + container_name: codely-java_ddd_example-elasticsearch + image: 'elasticsearch:6.8.4' + platform: linux/amd64 + restart: unless-stopped + ports: + - "9300:9300" + - "9200:9200" + environment: + - discovery.type=single-node + + test_server_java: + container_name: codely-java_ddd_example-test_server + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + volumes: + - .:/app:delegated + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + tty: true diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/docker-compose.yml b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/docker-compose.yml new file mode 100755 index 0000000..216bacd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/docker-compose.yml @@ -0,0 +1,139 @@ +version: '3' + +services: + shared_mysql: + container_name: codely-java_ddd_example-mysql + image: mysql:8 + platform: linux/amd64 + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD= + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + entrypoint: + sh -c " + echo 'CREATE DATABASE IF NOT EXISTS mooc;CREATE DATABASE IF NOT EXISTS backoffice;' > /docker-entrypoint-initdb.d/init.sql; + /usr/local/bin/docker-entrypoint.sh --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + " + command: ["--default-authentication-plugin=mysql_native_password"] + + shared_rabbitmq: + container_name: codely-java_ddd_example-rabbitmq + image: 'rabbitmq:3.7-management' + platform: linux/amd64 + restart: unless-stopped + ports: + - "5672:5672" + - "8090:15672" + environment: + - RABBITMQ_DEFAULT_USER=codely + - RABBITMQ_DEFAULT_PASS=c0d3ly + + backoffice_elasticsearch: + container_name: codely-java_ddd_example-elasticsearch + image: 'elasticsearch:6.8.4' + platform: linux/amd64 + restart: unless-stopped + ports: + - "9300:9300" + - "9200:9200" + environment: + - discovery.type=single-node + + backoffice_backend_server_java: + container_name: codely-java_ddd_example-backoffice_backend_server + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "8040:8040" + volumes: + - .:/app:delegated + - backoffice_backend_gradle_cache:/app/.gradle + - backoffice_backend_build:/app/build + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + command: ["./gradlew", "bootRun", "--args", "backoffice_backend server"] + + backoffice_backend_consumers_java: + container_name: codely-java_ddd_example-backoffice_backend_consumers + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + volumes: + - .:/app:delegated + - backoffice_consumers_gradle_cache:/app/.gradle + - backoffice_consumers_build:/app/build + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + command: ["./gradlew", "bootRun", "--args", "backoffice_backend domain-events:rabbitmq:consume"] + + backoffice_frontend_server_java: + container_name: codely-java_ddd_example-backoffice_frontend_server + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "8041:8041" + volumes: + - .:/app:delegated + - backoffice_frontend_gradle_cache:/app/.gradle + - backoffice_frontend_build:/app/build + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + command: ["./gradlew", "bootRun", "--args", "backoffice_frontend server"] + + mooc_backend_server_java: + container_name: codely-java_ddd_example-mooc_backend_server + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "8030:8030" + volumes: + - .:/app:delegated + - mooc_backend_gradle_cache:/app/.gradle + - mooc_backend_build:/app/build + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + command: ["./gradlew", "bootRun", "--args", "mooc_backend server"] + + mooc_backend_consumers_java: + container_name: codely-java_ddd_example-mooc_backend_consumers + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + volumes: + - .:/app:delegated + - mooc_consumers_gradle_cache:/app/.gradle + - mooc_consumers_build:/app/build + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + command: ["./gradlew", "bootRun", "--args", "mooc_backend domain-events:rabbitmq:consume"] + +volumes: + backoffice_backend_gradle_cache: + backoffice_backend_build: + backoffice_consumers_gradle_cache: + backoffice_consumers_build: + backoffice_frontend_gradle_cache: + backoffice_frontend_build: + mooc_backend_gradle_cache: + mooc_backend_build: + mooc_consumers_gradle_cache: + mooc_consumers_build: diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/etc/http/backoffice_frontend.http b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/etc/http/backoffice_frontend.http new file mode 100644 index 0000000..119764f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/etc/http/backoffice_frontend.http @@ -0,0 +1,80 @@ +# ELASTIC - Search +POST localhost:9200/backoffice_courses/_search +Content-Type: application/json + +{ + "query": { + "term": { + "name": "git avanzado" + } + } +} + +### + +# ELASTIC - debug +POST localhost:9200/backoffice_courses/_search +Content-Type: application/json + +{ + "from": 0, + "size": 1000, + "query": { + "bool": { + "must": [ + { + "term": { + "name": { + "value": "pepe2" + } + } + } + ], + "adjust_pure_negative": true, + "boost": 1.0 + } + } +} + +### +# ELASTIC - Search +POST localhost:9200/backoffice_courses/_search +Content-Type: application/json + +### +# ELASTIC - Mapping +GET localhost:9200/backoffice_courses/_mapping +Content-Type: application/json + +### +# ELASTIC - Change mapping +PUT localhost:9200/backoffice_courses/_mapping/_doc +Content-Type: application/json + +{ + "properties": { + "name": { + "type": "text", + "fielddata": true + } + } +} + +### +# ELASTIC - DELETE +DELETE localhost:9200/backoffice_courses + +### + +PUT localhost:9200/backoffice_courses/_settings +Content-Type: application/json + +{ + "index": { + "blocks": { + "read_only_allow_delete": "false" + } + } +} + +### diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/etc/http/publish_domain_events.http b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/etc/http/publish_domain_events.http new file mode 100644 index 0000000..9b512e6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/etc/http/publish_domain_events.http @@ -0,0 +1,41 @@ +POST http://localhost:8030/domain-events +Content-Type: application/json + +{ + "eventName": "course.created", + "eventRaw": { + "data": { + "id": "{{$random.uuid}}", + "type": "course.created", + "occurred_on": "2023-11-14 10:00:00", + "attributes": { + "id": "9bd0c98a-92cc-4a56-a5a1-7d40839ddc83", + "name": "Demo course", + "duration": "2 days" + } + }, + "meta": { + } + } +} + +### +POST http://localhost:8030/domain-events +Content-Type: application/json + +{ + "eventName": "course.renamed", + "eventRaw": { + "data": { + "id": "{{$random.uuid}}", + "type": "course.renamed", + "occurred_on": "2023-11-14 10:00:00", + "attributes": { + "id": "9bd0c98a-92cc-4a56-a5a1-7d40839ddc83", + "name": "heeey 22222" + } + }, + "meta": { + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/gradle/wrapper/gradle-wrapper.jar b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7f93135 Binary files /dev/null and b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/gradle/wrapper/gradle-wrapper.jar differ diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/gradle/wrapper/gradle-wrapper.properties b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3fa8f86 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/gradlew b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/gradlew.bat b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/gradlew.bat new file mode 100755 index 0000000..93e3f59 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/settings.gradle b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/settings.gradle new file mode 100644 index 0000000..4823818 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/settings.gradle @@ -0,0 +1,13 @@ +rootProject.name = 'java-ddd-example' + +include ':shared' +project(':shared').projectDir = new File('src/shared') + +include ':analytics' +project(':analytics').projectDir = new File('src/analytics') + +include ':backoffice' +project(':backoffice').projectDir = new File('src/backoffice') + +include ':mooc' +project(':mooc').projectDir = new File('src/mooc') diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/application/store/DomainEventStorer.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/application/store/DomainEventStorer.java new file mode 100644 index 0000000..674c72e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/application/store/DomainEventStorer.java @@ -0,0 +1,22 @@ +package tv.codely.analytics.domain_events.application.store; + +import tv.codely.analytics.domain_events.domain.*; + +public final class DomainEventStorer { + private DomainEventsRepository repository; + + public DomainEventStorer(DomainEventsRepository repository) { + this.repository = repository; + } + + public void store( + AnalyticsDomainEventId id, + AnalyticsDomainEventAggregateId aggregateId, + AnalyticsDomainEventName name, + AnalyticsDomainEventBody body + ) { + AnalyticsDomainEvent domainEvent = new AnalyticsDomainEvent(id, aggregateId, name, body); + + repository.save(domainEvent); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/application/store/StoreDomainEventOnOccurred.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/application/store/StoreDomainEventOnOccurred.java new file mode 100644 index 0000000..23413ef --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/application/store/StoreDomainEventOnOccurred.java @@ -0,0 +1,28 @@ +package tv.codely.analytics.domain_events.application.store; + +import org.springframework.context.event.EventListener; +import tv.codely.analytics.domain_events.domain.AnalyticsDomainEventAggregateId; +import tv.codely.analytics.domain_events.domain.AnalyticsDomainEventBody; +import tv.codely.analytics.domain_events.domain.AnalyticsDomainEventId; +import tv.codely.analytics.domain_events.domain.AnalyticsDomainEventName; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; + +@DomainEventSubscriber({DomainEvent.class}) +public final class StoreDomainEventOnOccurred { + private final DomainEventStorer storer; + + public StoreDomainEventOnOccurred(DomainEventStorer storer) { + this.storer = storer; + } + + @EventListener + public void on(DomainEvent event) { + AnalyticsDomainEventId id = new AnalyticsDomainEventId(event.eventId()); + AnalyticsDomainEventAggregateId aggregateId = new AnalyticsDomainEventAggregateId(event.aggregateId()); + AnalyticsDomainEventName name = new AnalyticsDomainEventName(event.eventName()); + AnalyticsDomainEventBody body = new AnalyticsDomainEventBody(event.toPrimitives()); + + storer.store(id, aggregateId, name, body); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEvent.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEvent.java new file mode 100644 index 0000000..c2f310e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEvent.java @@ -0,0 +1,37 @@ +package tv.codely.analytics.domain_events.domain; + +public final class AnalyticsDomainEvent { + private final AnalyticsDomainEventId id; + private final AnalyticsDomainEventAggregateId aggregateId; + private final AnalyticsDomainEventName name; + private final AnalyticsDomainEventBody body; + + public AnalyticsDomainEvent( + AnalyticsDomainEventId id, + AnalyticsDomainEventAggregateId aggregateId, + AnalyticsDomainEventName name, + AnalyticsDomainEventBody body + ) { + + this.id = id; + this.aggregateId = aggregateId; + this.name = name; + this.body = body; + } + + public AnalyticsDomainEventId getId() { + return id; + } + + public AnalyticsDomainEventAggregateId getAggregateId() { + return aggregateId; + } + + public AnalyticsDomainEventName getName() { + return name; + } + + public AnalyticsDomainEventBody getBody() { + return body; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventAggregateId.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventAggregateId.java new file mode 100644 index 0000000..beef90d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventAggregateId.java @@ -0,0 +1,9 @@ +package tv.codely.analytics.domain_events.domain; + +import tv.codely.shared.domain.Identifier; + +public final class AnalyticsDomainEventAggregateId extends Identifier { + public AnalyticsDomainEventAggregateId(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventBody.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventBody.java new file mode 100644 index 0000000..a706b7a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventBody.java @@ -0,0 +1,16 @@ +package tv.codely.analytics.domain_events.domain; + +import java.io.Serializable; +import java.util.HashMap; + +public final class AnalyticsDomainEventBody { + private HashMap value; + + public HashMap getValue() { + return value; + } + + public AnalyticsDomainEventBody(HashMap value) { + this.value = value; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventId.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventId.java new file mode 100644 index 0000000..5f730dd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventId.java @@ -0,0 +1,9 @@ +package tv.codely.analytics.domain_events.domain; + +import tv.codely.shared.domain.Identifier; + +public final class AnalyticsDomainEventId extends Identifier { + public AnalyticsDomainEventId(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventName.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventName.java new file mode 100644 index 0000000..d4a018e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventName.java @@ -0,0 +1,9 @@ +package tv.codely.analytics.domain_events.domain; + +import tv.codely.shared.domain.StringValueObject; + +public final class AnalyticsDomainEventName extends StringValueObject { + public AnalyticsDomainEventName(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/DomainEventsRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/DomainEventsRepository.java new file mode 100644 index 0000000..c04ac90 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/analytics/main/tv/codely/analytics/domain_events/domain/DomainEventsRepository.java @@ -0,0 +1,5 @@ +package tv.codely.analytics.domain_events.domain; + +public interface DomainEventsRepository { + void save(AnalyticsDomainEvent event); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/build.gradle b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/build.gradle new file mode 100644 index 0000000..7d82dc7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/resources/database/backoffice.sql b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/resources/database/backoffice.sql new file mode 100644 index 0000000..58f92fa --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/resources/database/backoffice.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS `courses` ( + `id` CHAR(36) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `duration` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/resources/database/backoffice/backoffice_courses.json b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/resources/database/backoffice/backoffice_courses.json new file mode 100644 index 0000000..7891e8b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/resources/database/backoffice/backoffice_courses.json @@ -0,0 +1,22 @@ +{ + "mappings": { + "courses": { + "properties": { + "id": { + "type": "keyword", + "index": true + }, + "name": { + "type": "text", + "index": true, + "fielddata": true + }, + "duration": { + "type": "text", + "index": true, + "fielddata": true + } + } + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommand.java new file mode 100644 index 0000000..5e1e74c --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommand.java @@ -0,0 +1,21 @@ +package tv.codely.backoffice.auth.application.authenticate; + +import tv.codely.shared.domain.bus.command.Command; + +public final class AuthenticateUserCommand implements Command { + private final String username; + private final String password; + + public AuthenticateUserCommand(String username, String password) { + this.username = username; + this.password = password; + } + + public String username() { + return username; + } + + public String password() { + return password; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandler.java new file mode 100644 index 0000000..ec43db6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandler.java @@ -0,0 +1,23 @@ +package tv.codely.backoffice.auth.application.authenticate; + +import tv.codely.backoffice.auth.domain.AuthPassword; +import tv.codely.backoffice.auth.domain.AuthUsername; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.command.CommandHandler; + +@Service +public final class AuthenticateUserCommandHandler implements CommandHandler { + private final UserAuthenticator authenticator; + + public AuthenticateUserCommandHandler(UserAuthenticator authenticator) { + this.authenticator = authenticator; + } + + @Override + public void handle(AuthenticateUserCommand command) { + AuthUsername username = new AuthUsername(command.username()); + AuthPassword password = new AuthPassword(command.password()); + + authenticator.authenticate(username, password); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/UserAuthenticator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/UserAuthenticator.java new file mode 100644 index 0000000..1b43c79 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/UserAuthenticator.java @@ -0,0 +1,34 @@ +package tv.codely.backoffice.auth.application.authenticate; + +import tv.codely.backoffice.auth.domain.*; +import tv.codely.shared.domain.Service; + +import java.util.Optional; + +@Service +public final class UserAuthenticator { + private final AuthRepository repository; + + public UserAuthenticator(AuthRepository repository) { + this.repository = repository; + } + + public void authenticate(AuthUsername username, AuthPassword password) { + Optional auth = repository.search(username); + + ensureUserExist(auth, username); + ensureCredentialsAreValid(auth.get(), password); + } + + private void ensureUserExist(Optional auth, AuthUsername username) { + if (!auth.isPresent()) { + throw new InvalidAuthUsername(username); + } + } + + private void ensureCredentialsAreValid(AuthUser auth, AuthPassword password) { + if (!auth.passwordMatches(password)) { + throw new InvalidAuthCredentials(auth.username()); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthPassword.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthPassword.java new file mode 100644 index 0000000..331588f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthPassword.java @@ -0,0 +1,9 @@ +package tv.codely.backoffice.auth.domain; + +import tv.codely.shared.domain.StringValueObject; + +public final class AuthPassword extends StringValueObject { + public AuthPassword(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthRepository.java new file mode 100644 index 0000000..37152ec --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthRepository.java @@ -0,0 +1,7 @@ +package tv.codely.backoffice.auth.domain; + +import java.util.Optional; + +public interface AuthRepository { + Optional search(AuthUsername username); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUser.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUser.java new file mode 100644 index 0000000..af0d45e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUser.java @@ -0,0 +1,19 @@ +package tv.codely.backoffice.auth.domain; + +public final class AuthUser { + private final AuthUsername username; + private final AuthPassword password; + + public AuthUser(AuthUsername username, AuthPassword password) { + this.username = username; + this.password = password; + } + + public AuthUsername username() { + return username; + } + + public boolean passwordMatches(AuthPassword password) { + return this.password.equals(password); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUsername.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUsername.java new file mode 100644 index 0000000..6d1d48a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUsername.java @@ -0,0 +1,9 @@ +package tv.codely.backoffice.auth.domain; + +import tv.codely.shared.domain.StringValueObject; + +public final class AuthUsername extends StringValueObject { + public AuthUsername(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthCredentials.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthCredentials.java new file mode 100644 index 0000000..3092104 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthCredentials.java @@ -0,0 +1,7 @@ +package tv.codely.backoffice.auth.domain; + +public final class InvalidAuthCredentials extends RuntimeException { + public InvalidAuthCredentials(AuthUsername username) { + super(String.format("The credentials for <%s> are invalid", username.value())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthUsername.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthUsername.java new file mode 100644 index 0000000..857c10f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthUsername.java @@ -0,0 +1,7 @@ +package tv.codely.backoffice.auth.domain; + +public final class InvalidAuthUsername extends RuntimeException { + public InvalidAuthUsername(AuthUsername username) { + super(String.format("The user <%s> does not exist", username.value())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/infrastructure/persistence/InMemoryAuthRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/infrastructure/persistence/InMemoryAuthRepository.java new file mode 100644 index 0000000..e873c4f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/auth/infrastructure/persistence/InMemoryAuthRepository.java @@ -0,0 +1,25 @@ +package tv.codely.backoffice.auth.infrastructure.persistence; + +import tv.codely.backoffice.auth.domain.AuthPassword; +import tv.codely.backoffice.auth.domain.AuthRepository; +import tv.codely.backoffice.auth.domain.AuthUser; +import tv.codely.backoffice.auth.domain.AuthUsername; +import tv.codely.shared.domain.Service; + +import java.util.HashMap; +import java.util.Optional; + +@Service +public final class InMemoryAuthRepository implements AuthRepository { + private final HashMap users = new HashMap() {{ + put(new AuthUsername("javi"), new AuthPassword("barbitas")); + put(new AuthUsername("rafa"), new AuthPassword("pelazo")); + }}; + + @Override + public Optional search(AuthUsername username) { + return users.containsKey(username) + ? Optional.of(new AuthUser(username, users.get(username))) + : Optional.empty(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCourseResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCourseResponse.java new file mode 100644 index 0000000..5871ac9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCourseResponse.java @@ -0,0 +1,32 @@ +package tv.codely.backoffice.courses.application; + +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.shared.domain.bus.query.Response; + +public final class BackofficeCourseResponse implements Response { + private final String id; + private final String name; + private final String duration; + + public BackofficeCourseResponse(String id, String name, String duration) { + this.id = id; + this.name = name; + this.duration = duration; + } + + public static BackofficeCourseResponse fromAggregate(BackofficeCourse course) { + return new BackofficeCourseResponse(course.id(), course.name(), course.duration()); + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public String duration() { + return duration; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCoursesResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCoursesResponse.java new file mode 100644 index 0000000..1225864 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCoursesResponse.java @@ -0,0 +1,17 @@ +package tv.codely.backoffice.courses.application; + +import tv.codely.shared.domain.bus.query.Response; + +import java.util.List; + +public final class BackofficeCoursesResponse implements Response { + private final List courses; + + public BackofficeCoursesResponse(List courses) { + this.courses = courses; + } + + public List courses() { + return courses; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java new file mode 100644 index 0000000..f129d16 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java @@ -0,0 +1,18 @@ +package tv.codely.backoffice.courses.application.create; + +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; + +@Service +public final class BackofficeCourseCreator { + private final BackofficeCourseRepository repository; + + public BackofficeCourseCreator(BackofficeCourseRepository repository) { + this.repository = repository; + } + + public void create(String id, String name, String duration) { + this.repository.save(new BackofficeCourse(id, name, duration)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/create/CreateBackofficeCourseOnCourseCreated.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/create/CreateBackofficeCourseOnCourseCreated.java new file mode 100644 index 0000000..fd06214 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/create/CreateBackofficeCourseOnCourseCreated.java @@ -0,0 +1,21 @@ +package tv.codely.backoffice.courses.application.create; + +import org.springframework.context.event.EventListener; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +@Service +@DomainEventSubscriber({CourseCreatedDomainEvent.class}) +public final class CreateBackofficeCourseOnCourseCreated { + private final BackofficeCourseCreator creator; + + public CreateBackofficeCourseOnCourseCreated(BackofficeCourseCreator creator) { + this.creator = creator; + } + + @EventListener + public void on(CourseCreatedDomainEvent event) { + creator.create(event.aggregateId(), event.name(), event.duration()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java new file mode 100644 index 0000000..e50c42f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java @@ -0,0 +1,26 @@ +package tv.codely.backoffice.courses.application.rename; + +import tv.codely.backoffice.courses.domain.BackofficeCourseNotFound; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; + +@Service +public final class BackofficeCourseRenamer { + private final BackofficeCourseRepository repository; + + public BackofficeCourseRenamer(BackofficeCourseRepository repository) { + this.repository = repository; + } + + public void rename(String id, String name) { + this.repository.search(id) + .ifPresentOrElse(course -> { + course.rename(name); + + this.repository.save(course); + }, + () -> { + throw new BackofficeCourseNotFound(id); + }); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java new file mode 100644 index 0000000..c92e48d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java @@ -0,0 +1,21 @@ +package tv.codely.backoffice.courses.application.rename; + +import org.springframework.context.event.EventListener; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; +import tv.codely.shared.domain.course.CourseRenamedDomainEvent; + +@Service +@DomainEventSubscriber({CourseRenamedDomainEvent.class}) +public final class RenameBackofficeCourseOnCourseRenamed { + private final BackofficeCourseRenamer renamer; + + public RenameBackofficeCourseOnCourseRenamed(BackofficeCourseRenamer renamer) { + this.renamer = renamer; + } + + @EventListener + public void on(CourseRenamedDomainEvent event) { + renamer.rename(event.aggregateId(), event.name()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/AllBackofficeCoursesSearcher.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/AllBackofficeCoursesSearcher.java new file mode 100644 index 0000000..db65ef0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/AllBackofficeCoursesSearcher.java @@ -0,0 +1,23 @@ +package tv.codely.backoffice.courses.application.search_all; + +import tv.codely.backoffice.courses.application.BackofficeCourseResponse; +import tv.codely.backoffice.courses.application.BackofficeCoursesResponse; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; + +import java.util.stream.Collectors; + +@Service +public final class AllBackofficeCoursesSearcher { + private final BackofficeCourseRepository repository; + + public AllBackofficeCoursesSearcher(BackofficeCourseRepository repository) { + this.repository = repository; + } + + public BackofficeCoursesResponse search() { + return new BackofficeCoursesResponse( + repository.searchAll().stream().map(BackofficeCourseResponse::fromAggregate).collect(Collectors.toList()) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQuery.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQuery.java new file mode 100644 index 0000000..fc00a52 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQuery.java @@ -0,0 +1,6 @@ +package tv.codely.backoffice.courses.application.search_all; + +import tv.codely.shared.domain.bus.query.Query; + +public final class SearchAllBackofficeCoursesQuery implements Query { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQueryHandler.java new file mode 100644 index 0000000..b500126 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQueryHandler.java @@ -0,0 +1,19 @@ +package tv.codely.backoffice.courses.application.search_all; + +import tv.codely.backoffice.courses.application.BackofficeCoursesResponse; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.QueryHandler; + +@Service +public final class SearchAllBackofficeCoursesQueryHandler implements QueryHandler { + private final AllBackofficeCoursesSearcher searcher; + + public SearchAllBackofficeCoursesQueryHandler(AllBackofficeCoursesSearcher searcher) { + this.searcher = searcher; + } + + @Override + public BackofficeCoursesResponse handle(SearchAllBackofficeCoursesQuery query) { + return searcher.search(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/BackofficeCoursesByCriteriaSearcher.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/BackofficeCoursesByCriteriaSearcher.java new file mode 100644 index 0000000..1533079 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/BackofficeCoursesByCriteriaSearcher.java @@ -0,0 +1,37 @@ +package tv.codely.backoffice.courses.application.search_by_criteria; + +import tv.codely.backoffice.courses.application.BackofficeCourseResponse; +import tv.codely.backoffice.courses.application.BackofficeCoursesResponse; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.domain.criteria.Filters; +import tv.codely.shared.domain.criteria.Order; + +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +public final class BackofficeCoursesByCriteriaSearcher { + private final BackofficeCourseRepository repository; + + public BackofficeCoursesByCriteriaSearcher(BackofficeCourseRepository repository) { + this.repository = repository; + } + + public BackofficeCoursesResponse search( + Filters filters, + Order order, + Optional limit, + Optional offset + ) { + Criteria criteria = new Criteria(filters, order, limit, offset); + + return new BackofficeCoursesResponse( + repository.matching(criteria) + .stream() + .map(BackofficeCourseResponse::fromAggregate) + .collect(Collectors.toList()) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQuery.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQuery.java new file mode 100644 index 0000000..2671661 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQuery.java @@ -0,0 +1,49 @@ +package tv.codely.backoffice.courses.application.search_by_criteria; + +import tv.codely.shared.domain.bus.query.Query; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +public final class SearchBackofficeCoursesByCriteriaQuery implements Query { + private final List> filters; + private final Optional orderBy; + private final Optional orderType; + private final Optional limit; + private final Optional offset; + + public SearchBackofficeCoursesByCriteriaQuery( + List> filters, + Optional orderBy, + Optional orderType, + Optional limit, + Optional offset + ) { + this.filters = filters; + this.orderBy = orderBy; + this.orderType = orderType; + this.limit = limit; + this.offset = offset; + } + + public List> filters() { + return filters; + } + + public Optional orderBy() { + return orderBy; + } + + public Optional orderType() { + return orderType; + } + + public Optional limit() { + return limit; + } + + public Optional offset() { + return offset; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQueryHandler.java new file mode 100644 index 0000000..8bf9363 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQueryHandler.java @@ -0,0 +1,24 @@ +package tv.codely.backoffice.courses.application.search_by_criteria; + +import tv.codely.backoffice.courses.application.BackofficeCoursesResponse; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.QueryHandler; +import tv.codely.shared.domain.criteria.Filters; +import tv.codely.shared.domain.criteria.Order; + +@Service +public final class SearchBackofficeCoursesByCriteriaQueryHandler implements QueryHandler { + private final BackofficeCoursesByCriteriaSearcher searcher; + + public SearchBackofficeCoursesByCriteriaQueryHandler(BackofficeCoursesByCriteriaSearcher searcher) { + this.searcher = searcher; + } + + @Override + public BackofficeCoursesResponse handle(SearchBackofficeCoursesByCriteriaQuery query) { + Filters filters = Filters.fromValues(query.filters()); + Order order = Order.fromValues(query.orderBy(), query.orderType()); + + return searcher.search(filters, order, query.limit(), query.offset()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java new file mode 100644 index 0000000..05f88b1 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java @@ -0,0 +1,75 @@ +package tv.codely.backoffice.courses.domain; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public final class BackofficeCourse { + private final String id; + private String name; + private final String duration; + + public BackofficeCourse() { + id = null; + name = null; + duration = null; + } + + public BackofficeCourse(String id, String name, String duration) { + this.id = id; + this.name = name; + this.duration = duration; + } + + public static BackofficeCourse fromPrimitives(Map plainData) { + return new BackofficeCourse( + (String) plainData.get("id"), + (String) plainData.get("name"), + (String) plainData.get("duration") + ); + } + + public void rename(String newName) { + this.name = newName; + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public String duration() { + return duration; + } + + public HashMap toPrimitives() { + return new HashMap() {{ + put("id", id); + put("name", name); + put("duration", duration); + }}; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BackofficeCourse that = (BackofficeCourse) o; + return id.equals(that.id) && + name.equals(that.name) && + duration.equals(that.duration); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, duration); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java new file mode 100644 index 0000000..f5596ff --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java @@ -0,0 +1,7 @@ +package tv.codely.backoffice.courses.domain; + +public class BackofficeCourseNotFound extends RuntimeException { + public BackofficeCourseNotFound(String id) { + super(String.format("The course <%s> doesn't exist", id)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java new file mode 100644 index 0000000..3689352 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java @@ -0,0 +1,16 @@ +package tv.codely.backoffice.courses.domain; + +import tv.codely.shared.domain.criteria.Criteria; + +import java.util.List; +import java.util.Optional; + +public interface BackofficeCourseRepository { + void save(BackofficeCourse course); + + Optional search(String id); + + List searchAll(); + + List matching(Criteria criteria); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java new file mode 100644 index 0000000..1206e8f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java @@ -0,0 +1,45 @@ +package tv.codely.backoffice.courses.infrastructure.persistence; + +import org.springframework.context.annotation.Primary; +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.infrastructure.elasticsearch.ElasticsearchClient; +import tv.codely.shared.infrastructure.elasticsearch.ElasticsearchRepository; + +import java.util.List; +import java.util.Optional; + +@Primary +@Service +public final class ElasticsearchBackofficeCourseRepository extends ElasticsearchRepository implements BackofficeCourseRepository { + public ElasticsearchBackofficeCourseRepository(ElasticsearchClient client) { + super(client); + } + + @Override + public void save(BackofficeCourse course) { + persist(course.id(), course.toPrimitives()); + } + + @Override + public Optional search(String id) { + return this.searchById(id, BackofficeCourse::fromPrimitives); + } + + @Override + public List searchAll() { + return searchAllInElastic(BackofficeCourse::fromPrimitives); + } + + @Override + public List matching(Criteria criteria) { + return searchByCriteria(criteria, BackofficeCourse::fromPrimitives); + } + + @Override + protected String moduleName() { + return "courses"; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java new file mode 100644 index 0000000..b645f5b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java @@ -0,0 +1,64 @@ +package tv.codely.backoffice.courses.infrastructure.persistence; + +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.criteria.Criteria; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +public final class InMemoryCacheBackofficeCourseRepository implements BackofficeCourseRepository { + private final BackofficeCourseRepository repository; + private List courses = new ArrayList<>(); + private HashMap> matchingCourses = new HashMap<>(); + + public InMemoryCacheBackofficeCourseRepository(BackofficeCourseRepository repository) { + this.repository = repository; + } + + @Override + public void save(BackofficeCourse course) { + repository.save(course); + } + + @Override + public List searchAll() { + return courses.isEmpty() ? searchAndFillCache() : courses; + } + + public Optional search(String id) { + return courses.stream() + .filter(course -> course.id().equals(id)) + .findFirst() + .or(() -> { + Optional course = repository.search(id); + course.ifPresent(courses::add); + return course; + }); + } + + @Override + public List matching(Criteria criteria) { + return matchingCourses.containsKey(criteria.serialize()) + ? matchingCourses.get(criteria.serialize()) + : searchMatchingAndFillCache(criteria); + } + + private List searchMatchingAndFillCache(Criteria criteria) { + List courses = repository.matching(criteria); + + this.matchingCourses.put(criteria.serialize(), courses); + + return courses; + } + + private List searchAndFillCache() { + List courses = repository.searchAll(); + + this.courses = courses; + + return courses; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java new file mode 100644 index 0000000..a23fd77 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java @@ -0,0 +1,41 @@ +package tv.codely.backoffice.courses.infrastructure.persistence; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.transaction.annotation.Transactional; +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.infrastructure.hibernate.HibernateRepository; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional("backoffice-transaction_manager") +public class MySqlBackofficeCourseRepository extends HibernateRepository implements BackofficeCourseRepository { + public MySqlBackofficeCourseRepository(@Qualifier("backoffice-session_factory") SessionFactory sessionFactory) { + super(sessionFactory, BackofficeCourse.class); + } + + @Override + public void save(BackofficeCourse course) { + persist(course); + } + + @Override + public Optional search(String id) { + return byId(id); + } + + @Override + public List searchAll() { + return all(); + } + + @Override + public List matching(Criteria criteria) { + return byCriteria(criteria); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml new file mode 100644 index 0000000..a8592d0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeElasticsearchConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeElasticsearchConfiguration.java new file mode 100644 index 0000000..98c0f1a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeElasticsearchConfiguration.java @@ -0,0 +1,89 @@ +package tv.codely.backoffice.shared.infrastructure.persistence; + +import org.apache.http.HttpHost; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.indices.GetIndexRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; +import tv.codely.shared.domain.Utils; +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; +import tv.codely.shared.infrastructure.elasticsearch.ElasticsearchClient; + +import java.io.IOException; +import java.util.Objects; +import java.util.Scanner; + +@Configuration +public class BackofficeElasticsearchConfiguration { + private final Parameter config; + private final ResourcePatternResolver resourceResolver; + + public BackofficeElasticsearchConfiguration(Parameter config, ResourcePatternResolver resourceResolver) { + this.config = config; + this.resourceResolver = resourceResolver; + } + + @Bean + public ElasticsearchClient elasticsearchClient() throws ParameterNotExist, Exception { + ElasticsearchClient client = new ElasticsearchClient( + new RestHighLevelClient( + RestClient.builder( + new HttpHost( + config.get("BACKOFFICE_ELASTICSEARCH_HOST"), + config.getInt("BACKOFFICE_ELASTICSEARCH_PORT"), + "http" + ) + ) + ), + RestClient.builder( + new HttpHost( + config.get("BACKOFFICE_ELASTICSEARCH_HOST"), + config.getInt("BACKOFFICE_ELASTICSEARCH_PORT"), + "http" + )).build(), + config.get("BACKOFFICE_ELASTICSEARCH_INDEX_PREFIX") + ); + + Utils.retry(10, 10000, () -> { + try { + generateIndexIfNotExists(client, "backoffice"); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + return client; + } + + private void generateIndexIfNotExists(ElasticsearchClient client, String contextName) throws IOException { + Resource[] jsonsIndexes = resourceResolver.getResources( + String.format("classpath:database/%s/*.json", contextName) + ); + + for (Resource jsonIndex : jsonsIndexes) { + String indexName = Objects.requireNonNull(jsonIndex.getFilename()).replace(".json", ""); + + if (!indexExists(indexName, client)) { + String indexBody = new Scanner( + jsonIndex.getInputStream(), + "UTF-8" + ).useDelimiter("\\A").next(); + + Request request = new Request("PUT", indexName); + request.setJsonEntity(indexBody); + + client.lowLevelClient().performRequest(request); + } + } + } + + private boolean indexExists(String indexName, ElasticsearchClient client) throws IOException { + return client.highLevelClient().indices().exists(new GetIndexRequest(indexName), RequestOptions.DEFAULT); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeHibernateConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeHibernateConfiguration.java new file mode 100644 index 0000000..0615ff7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeHibernateConfiguration.java @@ -0,0 +1,47 @@ +package tv.codely.backoffice.shared.infrastructure.persistence; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; +import tv.codely.shared.infrastructure.hibernate.HibernateConfigurationFactory; + +import javax.sql.DataSource; +import java.io.IOException; + +@Configuration +@EnableTransactionManagement +public class BackofficeHibernateConfiguration { + private final HibernateConfigurationFactory factory; + private final Parameter config; + private final String CONTEXT_NAME = "backoffice"; + + public BackofficeHibernateConfiguration(HibernateConfigurationFactory factory, Parameter config) { + this.factory = factory; + this.config = config; + } + + @Bean("backoffice-transaction_manager") + public PlatformTransactionManager hibernateTransactionManager() throws IOException, ParameterNotExist { + return factory.hibernateTransactionManager(sessionFactory()); + } + + @Bean("backoffice-session_factory") + public LocalSessionFactoryBean sessionFactory() throws IOException, ParameterNotExist { + return factory.sessionFactory(CONTEXT_NAME, dataSource()); + } + + @Bean("backoffice-data_source") + public DataSource dataSource() throws IOException, ParameterNotExist { + return factory.dataSource( + config.get("BACKOFFICE_DATABASE_HOST"), + config.getInt("BACKOFFICE_DATABASE_PORT"), + config.get("BACKOFFICE_DATABASE_NAME"), + config.get("BACKOFFICE_DATABASE_USER"), + config.get("BACKOFFICE_DATABASE_PASSWORD") + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java new file mode 100644 index 0000000..1113298 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java @@ -0,0 +1,38 @@ +package tv.codely.backoffice.shared.infrastructure.persistence; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import tv.codely.shared.infrastructure.bus.event.DomainEventsInformation; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlDomainEventsConsumer; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; +import tv.codely.shared.infrastructure.bus.event.spring.SpringApplicationEventBus; + +@Configuration +public class BackofficeMySqlEventBusConfiguration { + private final SessionFactory sessionFactory; + private final DomainEventsInformation domainEventsInformation; + private final SpringApplicationEventBus bus; + + public BackofficeMySqlEventBusConfiguration( + @Qualifier("backoffice-session_factory") SessionFactory sessionFactory, + DomainEventsInformation domainEventsInformation, + SpringApplicationEventBus bus + ) { + this.sessionFactory = sessionFactory; + this.domainEventsInformation = domainEventsInformation; + this.bus = bus; + } + + @Bean + public MySqlEventBus backofficeMysqlEventBus() { + return new MySqlEventBus(sessionFactory); + } + + @Bean + public MySqlDomainEventsConsumer backofficeMySqlDomainEventsConsumer() { + return new MySqlDomainEventsConsumer(sessionFactory, domainEventsInformation, bus); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeRabbitMqEventBusConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeRabbitMqEventBusConfiguration.java new file mode 100644 index 0000000..b0fae3c --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeRabbitMqEventBusConfiguration.java @@ -0,0 +1,27 @@ +package tv.codely.backoffice.shared.infrastructure.persistence; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqPublisher; + +@Configuration +public class BackofficeRabbitMqEventBusConfiguration { + private final RabbitMqPublisher publisher; + private final MySqlEventBus failoverPublisher; + + public BackofficeRabbitMqEventBusConfiguration( + RabbitMqPublisher publisher, + @Qualifier("backofficeMysqlEventBus") MySqlEventBus failoverPublisher + ) { + this.publisher = publisher; + this.failoverPublisher = failoverPublisher; + } + + @Bean + public RabbitMqEventBus backofficeRabbitMqEventBus() { + return new RabbitMqEventBus(publisher, failoverPublisher); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/BackofficeContextInfrastructureTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/BackofficeContextInfrastructureTestCase.java new file mode 100644 index 0000000..efbad4e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/BackofficeContextInfrastructureTestCase.java @@ -0,0 +1,21 @@ +package tv.codely.backoffice; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import tv.codely.apps.backoffice.frontend.BackofficeFrontendApplication; +import tv.codely.backoffice.courses.ElasticsearchEnvironmentArranger; +import tv.codely.shared.infrastructure.InfrastructureTestCase; + +import java.io.IOException; + +@ContextConfiguration(classes = BackofficeFrontendApplication.class) +@SpringBootTest +public abstract class BackofficeContextInfrastructureTestCase extends InfrastructureTestCase { + @Autowired + private ElasticsearchEnvironmentArranger elasticsearchArranger; + + protected void clearElasticsearch() throws IOException { + elasticsearchArranger.arrange("backoffice", "backoffice_courses"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/AuthModuleUnitTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/AuthModuleUnitTestCase.java new file mode 100644 index 0000000..4ccc749 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/AuthModuleUnitTestCase.java @@ -0,0 +1,31 @@ +package tv.codely.backoffice.auth; + +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mockito; +import tv.codely.backoffice.auth.domain.AuthRepository; +import tv.codely.backoffice.auth.domain.AuthUser; +import tv.codely.backoffice.auth.domain.AuthUsername; +import tv.codely.shared.infrastructure.UnitTestCase; + +import java.util.Optional; + +import static org.mockito.Mockito.mock; + +public abstract class AuthModuleUnitTestCase extends UnitTestCase { + protected AuthRepository repository; + + @BeforeEach + protected void setUp() { + super.setUp(); + + repository = mock(AuthRepository.class); + } + + public void shouldSearch(AuthUsername username, AuthUser user) { + Mockito.when(repository.search(username)).thenReturn(Optional.of(user)); + } + + public void shouldSearch(AuthUsername username) { + Mockito.when(repository.search(username)).thenReturn(Optional.empty()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandlerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandlerShould.java new file mode 100644 index 0000000..a37020e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandlerShould.java @@ -0,0 +1,56 @@ +package tv.codely.backoffice.auth.application.authenticate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tv.codely.backoffice.auth.AuthModuleUnitTestCase; +import tv.codely.backoffice.auth.domain.AuthUser; +import tv.codely.backoffice.auth.domain.AuthUserMother; +import tv.codely.backoffice.auth.domain.InvalidAuthCredentials; +import tv.codely.backoffice.auth.domain.InvalidAuthUsername; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class AuthenticateUserCommandHandlerShould extends AuthModuleUnitTestCase { + private AuthenticateUserCommandHandler handler; + + @BeforeEach + protected void setUp() { + super.setUp(); + + handler = new AuthenticateUserCommandHandler(new UserAuthenticator(repository)); + } + + @Test + void authenticate_a_valid_user() { + AuthenticateUserCommand command = AuthenticateUserCommandMother.random(); + AuthUser authUser = AuthUserMother.fromCommand(command); + + shouldSearch(authUser.username(), authUser); + + handler.handle(command); + } + + @Test + void throw_an_exception_when_the_user_does_not_exist() { + assertThrows(InvalidAuthUsername.class, () -> { + AuthenticateUserCommand command = AuthenticateUserCommandMother.random(); + AuthUser authUser = AuthUserMother.fromCommand(command); + + shouldSearch(authUser.username()); + + handler.handle(command); + }); + } + + @Test + void throw_an_exception_when_the_password_does_not_math() { + assertThrows(InvalidAuthCredentials.class, () -> { + AuthenticateUserCommand command = AuthenticateUserCommandMother.random(); + AuthUser authUser = AuthUserMother.withUsername(command.username()); + + shouldSearch(authUser.username(), authUser); + + handler.handle(command); + }); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandMother.java new file mode 100644 index 0000000..42746f5 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandMother.java @@ -0,0 +1,16 @@ +package tv.codely.backoffice.auth.application.authenticate; + +import tv.codely.backoffice.auth.domain.AuthPassword; +import tv.codely.backoffice.auth.domain.AuthPasswordMother; +import tv.codely.backoffice.auth.domain.AuthUsername; +import tv.codely.backoffice.auth.domain.AuthUsernameMother; + +public final class AuthenticateUserCommandMother { + public static AuthenticateUserCommand create(AuthUsername username, AuthPassword password) { + return new AuthenticateUserCommand(username.value(), password.value()); + } + + public static AuthenticateUserCommand random() { + return create(AuthUsernameMother.random(), AuthPasswordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthPasswordMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthPasswordMother.java new file mode 100644 index 0000000..8e4f0ab --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthPasswordMother.java @@ -0,0 +1,13 @@ +package tv.codely.backoffice.auth.domain; + +import tv.codely.shared.domain.WordMother; + +public final class AuthPasswordMother { + public static AuthPassword create(String value) { + return new AuthPassword(value); + } + + public static AuthPassword random() { + return create(WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUserMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUserMother.java new file mode 100644 index 0000000..14e26c9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUserMother.java @@ -0,0 +1,21 @@ +package tv.codely.backoffice.auth.domain; + +import tv.codely.backoffice.auth.application.authenticate.AuthenticateUserCommand; + +public final class AuthUserMother { + public static AuthUser create(AuthUsername username, AuthPassword password) { + return new AuthUser(username, password); + } + + public static AuthUser random() { + return create(AuthUsernameMother.random(), AuthPasswordMother.random()); + } + + public static AuthUser fromCommand(AuthenticateUserCommand command) { + return create(AuthUsernameMother.create(command.username()), AuthPasswordMother.create(command.password())); + } + + public static AuthUser withUsername(String username) { + return create(AuthUsernameMother.create(username), AuthPasswordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUsernameMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUsernameMother.java new file mode 100644 index 0000000..e96670c --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUsernameMother.java @@ -0,0 +1,13 @@ +package tv.codely.backoffice.auth.domain; + +import tv.codely.shared.domain.WordMother; + +public final class AuthUsernameMother { + public static AuthUsername create(String value) { + return new AuthUsername(value); + } + + public static AuthUsername random() { + return create(WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/ElasticsearchEnvironmentArranger.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/ElasticsearchEnvironmentArranger.java new file mode 100644 index 0000000..08df2f5 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/ElasticsearchEnvironmentArranger.java @@ -0,0 +1,47 @@ +package tv.codely.backoffice.courses; + +import org.elasticsearch.client.Request; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; +import tv.codely.shared.domain.Service; +import tv.codely.shared.infrastructure.elasticsearch.ElasticsearchClient; + +import java.io.IOException; +import java.util.Objects; +import java.util.Scanner; + +@Service +public final class ElasticsearchEnvironmentArranger { + ResourcePatternResolver resourceResolver; + ElasticsearchClient client; + + public ElasticsearchEnvironmentArranger( + ResourcePatternResolver resourceResolver, + ElasticsearchClient client + ) { + this.resourceResolver = resourceResolver; + this.client = client; + } + + public void arrange(String contextName, String index) throws IOException { + client.delete(index); + + Resource[] jsonsIndexes = resourceResolver.getResources( + String.format("classpath:database/%s/%s.json", contextName, index) + ); + + for (Resource jsonIndex : jsonsIndexes) { + String indexName = Objects.requireNonNull(jsonIndex.getFilename()).replace(".json", ""); + + String indexBody = new Scanner( + jsonIndex.getInputStream(), + "UTF-8" + ).useDelimiter("\\A").next(); + + Request request = new Request("PUT", indexName); + request.setJsonEntity(indexBody); + + client.lowLevelClient().performRequest(request); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseCriteriaMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseCriteriaMother.java new file mode 100644 index 0000000..ee83f69 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseCriteriaMother.java @@ -0,0 +1,17 @@ +package tv.codely.backoffice.courses.domain; + +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.domain.criteria.Filter; +import tv.codely.shared.domain.criteria.Filters; +import tv.codely.shared.domain.criteria.Order; + +import java.util.Arrays; + +public final class BackofficeCourseCriteriaMother { + public static Criteria nameAndDurationContains(String name, String duration) { + Filter nameFilter = Filter.create("name", "contains", name); + Filter durationFilter = Filter.create("duration", "contains", duration); + + return new Criteria(new Filters(Arrays.asList(nameFilter, durationFilter)), Order.asc("name")); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseMother.java new file mode 100644 index 0000000..76a1b4b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseMother.java @@ -0,0 +1,18 @@ +package tv.codely.backoffice.courses.domain; + +import tv.codely.shared.domain.UuidMother; +import tv.codely.shared.domain.WordMother; + +public final class BackofficeCourseMother { + public static BackofficeCourse create(String id, String name, String duration) { + return new BackofficeCourse(id, name, duration); + } + + public static BackofficeCourse create(String name, String duration) { + return new BackofficeCourse(UuidMother.random(), name, duration); + } + + public static BackofficeCourse random() { + return create(UuidMother.random(), WordMother.random(), WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java new file mode 100644 index 0000000..f94748b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java @@ -0,0 +1,114 @@ +package tv.codely.backoffice.courses.infrastructure.persistence; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import tv.codely.backoffice.BackofficeContextInfrastructureTestCase; +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseCriteriaMother; +import tv.codely.backoffice.courses.domain.BackofficeCourseMother; +import tv.codely.shared.domain.UuidMother; +import tv.codely.shared.domain.WordMother; +import tv.codely.shared.domain.criteria.Criteria; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class ElasticsearchBackofficeCourseRepositoryShould extends BackofficeContextInfrastructureTestCase { + @Autowired + private ElasticsearchBackofficeCourseRepository repository; + + @BeforeEach + protected void setUp() throws IOException { + clearElasticsearch(); + } + + @Test + void save_a_course() { + repository.save(BackofficeCourseMother.random()); + } + + @Test + void search_an_existing_course() throws Exception { + BackofficeCourse course = BackofficeCourseMother.random(); + + repository.save(course); + + eventually(() -> assertEquals(Optional.of(course), repository.search(course.id()))); + } + + @Test + void update_an_existing_course() throws Exception { + BackofficeCourse course = BackofficeCourseMother.random(); + + repository.save(course); + course.rename(WordMother.random()); + repository.save(course); + + eventually(() -> assertEquals(Optional.of(course), repository.search(course.id()))); + } + + @Test + void not_find_a_non_existing_course() { + assertEquals(Optional.empty(), repository.search(UuidMother.random())); + } + + @Test + void search_all_existing_courses() throws Exception { + BackofficeCourse course = BackofficeCourseMother.random(); + BackofficeCourse anotherCourse = BackofficeCourseMother.random(); + + List expected = Arrays.asList(course, anotherCourse); + + repository.save(course); + repository.save(anotherCourse); + + eventually(() -> { + List actual = repository.searchAll(); + + List sortedExpected = expected.stream() + .sorted(Comparator.comparing(BackofficeCourse::id)) + .collect(Collectors.toList()); + List sortedActual = actual.stream() + .sorted(Comparator.comparing(BackofficeCourse::id)) + .collect(Collectors.toList()); + + assertEquals(sortedExpected, sortedActual); + }); + } + + @Test + void search_courses_using_a_criteria() throws Exception { + BackofficeCourse matchingCourse = BackofficeCourseMother.create("DDD en Java", "3 days"); + BackofficeCourse anotherMatchingCourse = BackofficeCourseMother.create("DDD en TypeScript", "2.5 days"); + BackofficeCourse intellijCourse = BackofficeCourseMother.create("Exprimiendo Intellij", "48 hours"); + BackofficeCourse cobolCourse = BackofficeCourseMother.create("DDD en Cobol", "5 years"); + + Criteria criteria = BackofficeCourseCriteriaMother.nameAndDurationContains("DDD", "days"); + List expected = Arrays.asList(matchingCourse, anotherMatchingCourse); + + repository.save(matchingCourse); + repository.save(anotherMatchingCourse); + repository.save(intellijCourse); + repository.save(cobolCourse); + + eventually(() -> { + List actual = repository.matching(criteria); + + List sortedExpected = expected.stream() + .sorted(Comparator.comparing(BackofficeCourse::id)) + .collect(Collectors.toList()); + List sortedActual = actual.stream() + .sorted(Comparator.comparing(BackofficeCourse::id)) + .collect(Collectors.toList()); + + assertEquals(sortedExpected, sortedActual); + }); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepositoryShould.java new file mode 100644 index 0000000..e82dffb --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepositoryShould.java @@ -0,0 +1,100 @@ +package tv.codely.backoffice.courses.infrastructure.persistence; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import tv.codely.backoffice.BackofficeContextInfrastructureTestCase; +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseCriteriaMother; +import tv.codely.backoffice.courses.domain.BackofficeCourseMother; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.criteria.Criteria; + +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.mockito.Mockito.*; + +final class InMemoryCacheBackofficeCourseRepositoryShould extends BackofficeContextInfrastructureTestCase { + private InMemoryCacheBackofficeCourseRepository repository; + private BackofficeCourseRepository innerRepository; + + @BeforeEach + protected void setUp() { + innerRepository = mock(BackofficeCourseRepository.class); + repository = new InMemoryCacheBackofficeCourseRepository(innerRepository); + } + + @Test + void save_a_course() { + BackofficeCourse course = BackofficeCourseMother.random(); + + repository.save(course); + + shouldHaveSaved(course); + } + + @Test + void search_all_existing_courses() { + BackofficeCourse course = BackofficeCourseMother.random(); + BackofficeCourse anotherCourse = BackofficeCourseMother.random(); + List courses = Arrays.asList(course, anotherCourse); + + shouldSearchAll(courses); + + assertThat(courses, containsInAnyOrder(repository.searchAll().toArray())); + } + + @Test + void search_all_existing_courses_without_hitting_the_inner_repository_the_second_time() { + BackofficeCourse course = BackofficeCourseMother.random(); + BackofficeCourse anotherCourse = BackofficeCourseMother.random(); + List courses = Arrays.asList(course, anotherCourse); + + shouldSearchAll(courses); + + assertThat(courses, containsInAnyOrder(repository.searchAll().toArray())); + assertThat(courses, containsInAnyOrder(repository.searchAll().toArray())); + } + + @Test + void search_courses_using_a_criteria() { + BackofficeCourse matchingCourse = BackofficeCourseMother.create("DDD en Java", "3 days"); + BackofficeCourse anotherMatchingCourse = BackofficeCourseMother.create("DDD en TypeScript", "2.5 days"); + List matchingCourses = Arrays.asList(matchingCourse, anotherMatchingCourse); + + Criteria criteria = BackofficeCourseCriteriaMother.nameAndDurationContains("DDD", "days"); + + shouldSearchMatching(criteria, matchingCourses); + + assertThat(matchingCourses, containsInAnyOrder(repository.matching(criteria).toArray())); + } + + @Test + void search_courses_using_a_criteria_without_hitting_the_inner_repository_the_second_time() { + BackofficeCourse matchingCourse = BackofficeCourseMother.create("DDD en Java", "3 days"); + BackofficeCourse anotherMatchingCourse = BackofficeCourseMother.create("DDD en TypeScript", "2.5 days"); + List matchingCourses = Arrays.asList(matchingCourse, anotherMatchingCourse); + + Criteria criteria = BackofficeCourseCriteriaMother.nameAndDurationContains("DDD", "days"); + + shouldSearchMatching(criteria, matchingCourses); + + assertThat(matchingCourses, containsInAnyOrder(repository.matching(criteria).toArray())); + assertThat(matchingCourses, containsInAnyOrder(repository.matching(criteria).toArray())); + } + + private void shouldSearchAll(List courses) { + Mockito.when(innerRepository.searchAll()).thenReturn(courses); + } + + private void shouldSearchMatching(Criteria criteria, List courses) { + Mockito.when(innerRepository.matching(criteria)).thenReturn(courses); + } + + private void shouldHaveSaved(BackofficeCourse course) { + verify(innerRepository, atLeastOnce()).save(course); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepositoryShould.java new file mode 100644 index 0000000..8d9d5ce --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepositoryShould.java @@ -0,0 +1,60 @@ +package tv.codely.backoffice.courses.infrastructure.persistence; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import tv.codely.backoffice.BackofficeContextInfrastructureTestCase; +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseCriteriaMother; +import tv.codely.backoffice.courses.domain.BackofficeCourseMother; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.criteria.Criteria; + +import jakarta.transaction.Transactional; +import java.util.Arrays; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; + +@Transactional +class MySqlBackofficeCourseRepositoryShould extends BackofficeContextInfrastructureTestCase { + @Autowired + @Qualifier("mySqlBackofficeCourseRepository") + private BackofficeCourseRepository repository; + + @Test + void save_a_course() { + repository.save(BackofficeCourseMother.random()); + } + + @Test + void search_all_existing_courses() { + BackofficeCourse course = BackofficeCourseMother.random(); + BackofficeCourse anotherCourse = BackofficeCourseMother.random(); + + repository.save(course); + repository.save(anotherCourse); + + assertThat(Arrays.asList(course, anotherCourse), containsInAnyOrder(repository.searchAll().toArray())); + } + + @Test + void search_courses_using_a_criteria() { + BackofficeCourse matchingCourse = BackofficeCourseMother.create("DDD en Java", "3 days"); + BackofficeCourse anotherMatchingCourse = BackofficeCourseMother.create("DDD en TypeScript", "2.5 days"); + BackofficeCourse intellijCourse = BackofficeCourseMother.create("Exprimiendo Intellij", "48 hours"); + BackofficeCourse cobolCourse = BackofficeCourseMother.create("DDD en Cobol", "5 years"); + + Criteria criteria = BackofficeCourseCriteriaMother.nameAndDurationContains("DDD", "days"); + + repository.save(matchingCourse); + repository.save(anotherMatchingCourse); + repository.save(intellijCourse); + repository.save(cobolCourse); + + assertThat( + Arrays.asList(matchingCourse, anotherMatchingCourse), + containsInAnyOrder(repository.matching(criteria).toArray()) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/build.gradle b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/build.gradle new file mode 100644 index 0000000..7d82dc7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/resources/database/mooc.sql b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/resources/database/mooc.sql new file mode 100644 index 0000000..6cc6def --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/resources/database/mooc.sql @@ -0,0 +1,62 @@ +CREATE TABLE IF NOT EXISTS courses ( + id CHAR(36) NOT NULL, + name VARCHAR(255) NOT NULL, + duration VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS courses_counter ( + id CHAR(36) NOT NULL, + total INT NOT NULL, + existing_courses JSON NOT NULL, + PRIMARY KEY (id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; +INSERT IGNORE INTO courses_counter (id, total, existing_courses) VALUES ('efbaff16-8fcd-4689-9fc9-ec545d641c46', 0, '[]'); + +CREATE TABLE IF NOT EXISTS steps ( + id CHAR(36) NOT NULL, + title VARCHAR(155) NOT NULL, + PRIMARY KEY (id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS steps_challenges ( + id CHAR(36) NOT NULL, + statement TEXT NOT NULL, + PRIMARY KEY (id), + CONSTRAINT fk_steps_challenges__step_id FOREIGN KEY (id) REFERENCES steps(id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS steps_videos ( + id CHAR(36) NOT NULL, + url VARCHAR(255) NOT NULL, + text TEXT NOT NULL, + PRIMARY KEY (id), + CONSTRAINT fk_steps_video__step_id FOREIGN KEY (id) REFERENCES steps(id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS domain_events ( + id CHAR(36) NOT NULL, + aggregate_id CHAR(36) NOT NULL, + name VARCHAR(255) NOT NULL, + body JSON NOT NULL, + occurred_on TIMESTAMP NOT NULL, + PRIMARY KEY (id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/CourseResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/CourseResponse.java new file mode 100644 index 0000000..22c8798 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/CourseResponse.java @@ -0,0 +1,32 @@ +package tv.codely.mooc.courses.application; + +import tv.codely.mooc.courses.domain.Course; +import tv.codely.shared.domain.bus.query.Response; + +public final class CourseResponse implements Response { + private final String id; + private final String name; + private final String duration; + + public CourseResponse(String id, String name, String duration) { + this.id = id; + this.name = name; + this.duration = duration; + } + + public static CourseResponse fromAggregate(Course course) { + return new CourseResponse(course.id().value(), course.name().value(), course.duration().value()); + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public String duration() { + return duration; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/CoursesResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/CoursesResponse.java new file mode 100644 index 0000000..cd39f43 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/CoursesResponse.java @@ -0,0 +1,17 @@ +package tv.codely.mooc.courses.application; + +import tv.codely.shared.domain.bus.query.Response; + +import java.util.List; + +public final class CoursesResponse implements Response { + private final List courses; + + public CoursesResponse(List courses) { + this.courses = courses; + } + + public List courses() { + return courses; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/create/CourseCreator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/create/CourseCreator.java new file mode 100644 index 0000000..dbbec32 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/create/CourseCreator.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.courses.application.create; + +import tv.codely.mooc.courses.domain.*; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.EventBus; + +@Service +public final class CourseCreator { + private final CourseRepository repository; + private final EventBus eventBus; + + public CourseCreator(CourseRepository repository, EventBus eventBus) { + this.repository = repository; + this.eventBus = eventBus; + } + + public void create(CourseId id, CourseName name, CourseDuration duration) { + Course course = Course.create(id, name, duration); + + repository.save(course); + eventBus.publish(course.pullDomainEvents()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommand.java new file mode 100644 index 0000000..5f6f783 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommand.java @@ -0,0 +1,27 @@ +package tv.codely.mooc.courses.application.create; + +import tv.codely.shared.domain.bus.command.Command; + +public final class CreateCourseCommand implements Command { + private final String id; + private final String name; + private final String duration; + + public CreateCourseCommand(String id, String name, String duration) { + this.id = id; + this.name = name; + this.duration = duration; + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public String duration() { + return duration; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommandHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommandHandler.java new file mode 100644 index 0000000..85127f4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommandHandler.java @@ -0,0 +1,25 @@ +package tv.codely.mooc.courses.application.create; + +import tv.codely.mooc.courses.domain.CourseDuration; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseName; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.command.CommandHandler; + +@Service +public final class CreateCourseCommandHandler implements CommandHandler { + private final CourseCreator creator; + + public CreateCourseCommandHandler(CourseCreator creator) { + this.creator = creator; + } + + @Override + public void handle(CreateCourseCommand command) { + CourseId id = new CourseId(command.id()); + CourseName name = new CourseName(command.name()); + CourseDuration duration = new CourseDuration(command.duration()); + + creator.create(id, name, duration); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java new file mode 100644 index 0000000..b912a3e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java @@ -0,0 +1,22 @@ +package tv.codely.mooc.courses.application.find; + +import tv.codely.mooc.courses.application.CourseResponse; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseNotExist; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.shared.domain.Service; + +@Service +public final class CourseFinder { + private final CourseRepository repository; + + public CourseFinder(CourseRepository repository) { + this.repository = repository; + } + + public CourseResponse find(CourseId id) throws CourseNotExist { + return repository.search(id) + .map(CourseResponse::fromAggregate) + .orElseThrow(() -> new CourseNotExist(id)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQuery.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQuery.java new file mode 100644 index 0000000..187e5e0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQuery.java @@ -0,0 +1,15 @@ +package tv.codely.mooc.courses.application.find; + +import tv.codely.shared.domain.bus.query.Query; + +public final class FindCourseQuery implements Query { + private final String id; + + public FindCourseQuery(String id) { + this.id = id; + } + + public String id() { + return id; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQueryHandler.java new file mode 100644 index 0000000..bc8d380 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQueryHandler.java @@ -0,0 +1,21 @@ +package tv.codely.mooc.courses.application.find; + +import tv.codely.mooc.courses.application.CourseResponse; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseNotExist; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.QueryHandler; + +@Service +public final class FindCourseQueryHandler implements QueryHandler { + private final CourseFinder finder; + + public FindCourseQueryHandler(CourseFinder finder) { + this.finder = finder; + } + + @Override + public CourseResponse handle(FindCourseQuery query) throws CourseNotExist { + return finder.find(new CourseId(query.id())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/search_last/LastCoursesSearcher.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/search_last/LastCoursesSearcher.java new file mode 100644 index 0000000..1e0f6fa --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/search_last/LastCoursesSearcher.java @@ -0,0 +1,34 @@ +package tv.codely.mooc.courses.application.search_last; + +import tv.codely.mooc.courses.application.CourseResponse; +import tv.codely.mooc.courses.application.CoursesResponse; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.domain.criteria.Filters; +import tv.codely.shared.domain.criteria.Order; + +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +public final class LastCoursesSearcher { + private final CourseRepository repository; + + public LastCoursesSearcher(CourseRepository repository) { + this.repository = repository; + } + + public CoursesResponse search(int courses) { + Criteria criteria = new Criteria( + Filters.none(), + Order.none(), + Optional.of(courses), + Optional.empty() + ); + + return new CoursesResponse( + repository.matching(criteria).stream().map(CourseResponse::fromAggregate).collect(Collectors.toList()) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQuery.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQuery.java new file mode 100644 index 0000000..834a847 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQuery.java @@ -0,0 +1,34 @@ +package tv.codely.mooc.courses.application.search_last; + +import tv.codely.shared.domain.bus.query.Query; + +import java.util.Objects; + +public final class SearchLastCoursesQuery implements Query { + private final Integer total; + + public SearchLastCoursesQuery(Integer total) { + this.total = total; + } + + public Integer total() { + return total; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SearchLastCoursesQuery that = (SearchLastCoursesQuery) o; + return total.equals(that.total); + } + + @Override + public int hashCode() { + return Objects.hash(total); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryHandler.java new file mode 100644 index 0000000..91bea0f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryHandler.java @@ -0,0 +1,19 @@ +package tv.codely.mooc.courses.application.search_last; + +import tv.codely.mooc.courses.application.CoursesResponse; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.QueryHandler; + +@Service +public final class SearchLastCoursesQueryHandler implements QueryHandler { + private final LastCoursesSearcher searcher; + + public SearchLastCoursesQueryHandler(LastCoursesSearcher searcher) { + this.searcher = searcher; + } + + @Override + public CoursesResponse handle(SearchLastCoursesQuery query) { + return searcher.search(query.total()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/Course.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/Course.java new file mode 100644 index 0000000..ef44a89 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/Course.java @@ -0,0 +1,63 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.AggregateRoot; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +import java.util.Objects; + +public final class Course extends AggregateRoot { + private final CourseId id; + private final CourseName name; + private final CourseDuration duration; + + public Course(CourseId id, CourseName name, CourseDuration duration) { + this.id = id; + this.name = name; + this.duration = duration; + } + + private Course() { + id = null; + name = null; + duration = null; + } + + public static Course create(CourseId id, CourseName name, CourseDuration duration) { + Course course = new Course(id, name, duration); + + course.record(new CourseCreatedDomainEvent(id.value(), name.value(), duration.value())); + + return course; + } + + public CourseId id() { + return id; + } + + public CourseName name() { + return name; + } + + public CourseDuration duration() { + return duration; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Course course = (Course) o; + return id.equals(course.id) && + name.equals(course.name) && + duration.equals(course.duration); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, duration); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseDuration.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseDuration.java new file mode 100644 index 0000000..42c4482 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseDuration.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.StringValueObject; + +public final class CourseDuration extends StringValueObject { + public CourseDuration(String value) { + super(value); + } + + private CourseDuration() { + super(""); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseId.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseId.java new file mode 100644 index 0000000..dd6c3c2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseId.java @@ -0,0 +1,12 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.Identifier; + +public final class CourseId extends Identifier { + public CourseId(String value) { + super(value); + } + + public CourseId() { + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseName.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseName.java new file mode 100644 index 0000000..67e0a06 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseName.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.StringValueObject; + +public final class CourseName extends StringValueObject { + public CourseName(String value) { + super(value); + } + + public CourseName() { + super(""); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseNotExist.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseNotExist.java new file mode 100644 index 0000000..d3490be --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseNotExist.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.DomainError; + +public final class CourseNotExist extends DomainError { + public CourseNotExist(CourseId id) { + super("course_not_exist", String.format("The course <%s> doesn't exist", id.value())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseRepository.java new file mode 100644 index 0000000..4d2e696 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/domain/CourseRepository.java @@ -0,0 +1,14 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.criteria.Criteria; + +import java.util.List; +import java.util.Optional; + +public interface CourseRepository { + void save(Course course); + + Optional search(CourseId id); + + List matching(Criteria criteria); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepository.java new file mode 100644 index 0000000..622d60a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepository.java @@ -0,0 +1,28 @@ +package tv.codely.mooc.courses.infrastructure.persistence; + +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.shared.domain.criteria.Criteria; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +public final class InMemoryCourseRepository implements CourseRepository { + private HashMap courses = new HashMap<>(); + + @Override + public void save(Course course) { + courses.put(course.id().value(), course); + } + + public Optional search(CourseId id) { + return Optional.ofNullable(courses.get(id.value())); + } + + @Override + public List matching(Criteria criteria) { + return null; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepository.java new file mode 100644 index 0000000..b90c0cf --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepository.java @@ -0,0 +1,37 @@ +package tv.codely.mooc.courses.infrastructure.persistence; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.transaction.annotation.Transactional; +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.infrastructure.hibernate.HibernateRepository; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional("mooc-transaction_manager") +public class MySqlCourseRepository extends HibernateRepository implements CourseRepository { + public MySqlCourseRepository(@Qualifier("mooc-session_factory") SessionFactory sessionFactory) { + super(sessionFactory, Course.class); + } + + @Override + public void save(Course course) { + persist(course); + } + + @Override + public Optional search(CourseId id) { + return byId(id); + } + + @Override + public List matching(Criteria criteria) { + return byCriteria(criteria); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml new file mode 100644 index 0000000..88809da --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterFinder.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterFinder.java new file mode 100644 index 0000000..e0ccfe7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterFinder.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.courses_counter.application.find; + +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterNotInitialized; +import tv.codely.mooc.courses_counter.domain.CoursesCounterRepository; +import tv.codely.shared.domain.Service; + +@Service +public final class CoursesCounterFinder { + private CoursesCounterRepository repository; + + public CoursesCounterFinder(CoursesCounterRepository repository) { + this.repository = repository; + } + + public CoursesCounterResponse find() { + CoursesCounter coursesCounter = repository.search().orElseGet(() -> { + throw new CoursesCounterNotInitialized(); + }); + + return new CoursesCounterResponse(coursesCounter.total().value()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponse.java new file mode 100644 index 0000000..a4ee91a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponse.java @@ -0,0 +1,34 @@ +package tv.codely.mooc.courses_counter.application.find; + +import tv.codely.shared.domain.bus.query.Response; + +import java.util.Objects; + +public final class CoursesCounterResponse implements Response { + private Integer total; + + public CoursesCounterResponse(Integer total) { + this.total = total; + } + + public Integer total() { + return total; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CoursesCounterResponse that = (CoursesCounterResponse) o; + return total.equals(that.total); + } + + @Override + public int hashCode() { + return Objects.hash(total); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQuery.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQuery.java new file mode 100644 index 0000000..7586b4b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQuery.java @@ -0,0 +1,6 @@ +package tv.codely.mooc.courses_counter.application.find; + +import tv.codely.shared.domain.bus.query.Query; + +public final class FindCoursesCounterQuery implements Query { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandler.java new file mode 100644 index 0000000..1eefb82 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandler.java @@ -0,0 +1,18 @@ +package tv.codely.mooc.courses_counter.application.find; + +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.QueryHandler; + +@Service +public final class FindCoursesCounterQueryHandler implements QueryHandler { + private final CoursesCounterFinder finder; + + public FindCoursesCounterQueryHandler(CoursesCounterFinder finder) { + this.finder = finder; + } + + @Override + public CoursesCounterResponse handle(FindCoursesCounterQuery query) { + return finder.find(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java new file mode 100644 index 0000000..c4433c0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java @@ -0,0 +1,29 @@ +package tv.codely.mooc.courses_counter.application.increment; + +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.UuidGenerator; + +@Service +public final class CoursesCounterIncrementer { + private CoursesCounterRepository repository; + private UuidGenerator uuidGenerator; + + public CoursesCounterIncrementer(CoursesCounterRepository repository, UuidGenerator uuidGenerator) { + this.repository = repository; + this.uuidGenerator = uuidGenerator; + } + + public void increment(CourseId id) { + CoursesCounter counter = repository.search() + .orElseGet(() -> CoursesCounter.initialize(uuidGenerator.generate())); + + if (!counter.hasIncremented(id)) { + counter.increment(id); + + repository.save(counter); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreated.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreated.java new file mode 100644 index 0000000..f8465fb --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreated.java @@ -0,0 +1,24 @@ +package tv.codely.mooc.courses_counter.application.increment; + +import org.springframework.context.event.EventListener; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +@Service +@DomainEventSubscriber({CourseCreatedDomainEvent.class}) +public final class IncrementCoursesCounterOnCourseCreated { + private final CoursesCounterIncrementer incrementer; + + public IncrementCoursesCounterOnCourseCreated(CoursesCounterIncrementer incrementer) { + this.incrementer = incrementer; + } + + @EventListener + public void on(CourseCreatedDomainEvent event) { + CourseId courseId = new CourseId(event.aggregateId()); + + incrementer.increment(courseId); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounter.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounter.java new file mode 100644 index 0000000..2a13583 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounter.java @@ -0,0 +1,69 @@ +package tv.codely.mooc.courses_counter.domain; + +import tv.codely.mooc.courses.domain.CourseId; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class CoursesCounter { + private CoursesCounterId id; + private CoursesCounterTotal total; + private List existingCourses; + + public CoursesCounter(CoursesCounterId id, CoursesCounterTotal total, List existingCourses) { + this.id = id; + this.total = total; + this.existingCourses = existingCourses; + } + + private CoursesCounter() { + this.id = null; + this.total = null; + this.existingCourses = null; + } + + public static CoursesCounter initialize(String id) { + return new CoursesCounter(new CoursesCounterId(id), CoursesCounterTotal.initialize(), new ArrayList<>()); + } + + public CoursesCounterId id() { + return id; + } + + public CoursesCounterTotal total() { + return total; + } + + public List existingCourses() { + return existingCourses; + } + + public boolean hasIncremented(CourseId id) { + return existingCourses.contains(id); + } + + public void increment(CourseId id) { + total = total.increment(); + existingCourses.add(id); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CoursesCounter that = (CoursesCounter) o; + return id.equals(that.id) && + total.equals(that.total) && + existingCourses.equals(that.existingCourses); + } + + @Override + public int hashCode() { + return Objects.hash(id, total, existingCourses); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterId.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterId.java new file mode 100644 index 0000000..3899c15 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterId.java @@ -0,0 +1,12 @@ +package tv.codely.mooc.courses_counter.domain; + +import tv.codely.shared.domain.Identifier; + +public final class CoursesCounterId extends Identifier { + public CoursesCounterId(String value) { + super(value); + } + + private CoursesCounterId() { + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterNotInitialized.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterNotInitialized.java new file mode 100644 index 0000000..b20ca87 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterNotInitialized.java @@ -0,0 +1,4 @@ +package tv.codely.mooc.courses_counter.domain; + +public final class CoursesCounterNotInitialized extends RuntimeException { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterRepository.java new file mode 100644 index 0000000..ccbbb71 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterRepository.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.courses_counter.domain; + +import java.util.Optional; + +public interface CoursesCounterRepository { + void save(CoursesCounter counter); + + Optional search(); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterTotal.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterTotal.java new file mode 100644 index 0000000..65a299d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterTotal.java @@ -0,0 +1,21 @@ +package tv.codely.mooc.courses_counter.domain; + +import tv.codely.shared.domain.IntValueObject; + +public final class CoursesCounterTotal extends IntValueObject { + public CoursesCounterTotal(Integer value) { + super(value); + } + + public CoursesCounterTotal() { + super(null); + } + + public static CoursesCounterTotal initialize() { + return new CoursesCounterTotal(0); + } + + public CoursesCounterTotal increment() { + return new CoursesCounterTotal(value() + 1); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepository.java new file mode 100644 index 0000000..f9cb245 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepository.java @@ -0,0 +1,32 @@ +package tv.codely.mooc.courses_counter.infrastructure.persistence; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.transaction.annotation.Transactional; +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.infrastructure.hibernate.HibernateRepository; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional("mooc-transaction_manager") +public class MySqlCoursesCounterRepository extends HibernateRepository implements CoursesCounterRepository { + public MySqlCoursesCounterRepository(@Qualifier("mooc-session_factory") SessionFactory sessionFactory) { + super(sessionFactory, CoursesCounter.class); + } + + @Override + public void save(CoursesCounter counter) { + persist(counter); + } + + @Override + public Optional search() { + List coursesCounter = all(); + + return 0 == coursesCounter.size() ? Optional.empty() : Optional.ofNullable(coursesCounter.get(0)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/hibernate/CoursesCounter.hbm.xml b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/hibernate/CoursesCounter.hbm.xml new file mode 100644 index 0000000..8ea7482 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/hibernate/CoursesCounter.hbm.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + tv.codely.mooc.courses.domain.CourseId + + + + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/NewCoursesNewsletterSender.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/NewCoursesNewsletterSender.java new file mode 100644 index 0000000..0e2ea20 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/NewCoursesNewsletterSender.java @@ -0,0 +1,52 @@ +package tv.codely.mooc.notifications.application.send_new_courses_newsletter; + +import tv.codely.mooc.courses.application.CoursesResponse; +import tv.codely.mooc.courses.application.search_last.SearchLastCoursesQuery; +import tv.codely.mooc.notifications.domain.EmailSender; +import tv.codely.mooc.notifications.domain.NewCoursesNewsletter; +import tv.codely.mooc.students.application.StudentResponse; +import tv.codely.mooc.students.application.StudentsResponse; +import tv.codely.mooc.students.application.search_all.SearchAllStudentsQuery; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.UuidGenerator; +import tv.codely.shared.domain.bus.event.EventBus; +import tv.codely.shared.domain.bus.query.QueryBus; + +@Service +public final class NewCoursesNewsletterSender { + private final static Integer TOTAL_COURSES = 3; + private final QueryBus queryBus; + private final EmailSender sender; + private final UuidGenerator uuidGenerator; + private final EventBus eventBus; + + public NewCoursesNewsletterSender( + QueryBus queryBus, + UuidGenerator uuidGenerator, + EmailSender sender, + EventBus eventBus + ) { + this.queryBus = queryBus; + this.uuidGenerator = uuidGenerator; + this.sender = sender; + this.eventBus = eventBus; + } + + public void send() { + CoursesResponse courses = queryBus.ask(new SearchLastCoursesQuery(TOTAL_COURSES)); + + if (courses.courses().size() > 0) { + StudentsResponse students = queryBus.ask(new SearchAllStudentsQuery()); + + students.students().forEach(student -> send(student, courses)); + } + } + + public void send(StudentResponse student, CoursesResponse courses) { + NewCoursesNewsletter newsletter = NewCoursesNewsletter.send(uuidGenerator.generate(), student, courses); + + sender.send(newsletter); + + eventBus.publish(newsletter.pullDomainEvents()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommand.java new file mode 100644 index 0000000..51f6c52 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommand.java @@ -0,0 +1,15 @@ +package tv.codely.mooc.notifications.application.send_new_courses_newsletter; + +import tv.codely.shared.domain.bus.command.Command; + +public final class SendNewCoursesNewsletterCommand implements Command { + private final String id; + + public SendNewCoursesNewsletterCommand(String id) { + this.id = id; + } + + public String id() { + return id; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandler.java new file mode 100644 index 0000000..2c6dad7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandler.java @@ -0,0 +1,18 @@ +package tv.codely.mooc.notifications.application.send_new_courses_newsletter; + +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.command.CommandHandler; + +@Service +public final class SendNewCoursesNewsletterCommandHandler implements CommandHandler { + private final NewCoursesNewsletterSender sender; + + public SendNewCoursesNewsletterCommandHandler(NewCoursesNewsletterSender sender) { + this.sender = sender; + } + + @Override + public void handle(SendNewCoursesNewsletterCommand command) { + sender.send(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/Email.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/Email.java new file mode 100644 index 0000000..120feff --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/Email.java @@ -0,0 +1,62 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.shared.domain.AggregateRoot; + +import java.util.Objects; + +public abstract class Email extends AggregateRoot { + private final EmailId id; + private final String from; + private final String to; + private final String subject; + private final String body; + + public Email(EmailId id, String from, String to, String subject, String body) { + this.id = id; + this.from = from; + this.to = to; + this.subject = subject; + this.body = body; + } + + public EmailId id() { + return id; + } + + public String from() { + return from; + } + + public String to() { + return to; + } + + public String subject() { + return subject; + } + + public String body() { + return body; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Email email = (Email) o; + return id.equals(email.id) && + from.equals(email.from) && + to.equals(email.to) && + subject.equals(email.subject) && + body.equals(email.body); + } + + @Override + public int hashCode() { + return Objects.hash(id, from, to, subject, body); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/EmailId.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/EmailId.java new file mode 100644 index 0000000..dd03323 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/EmailId.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.shared.domain.Identifier; + +public final class EmailId extends Identifier { + public EmailId(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/EmailSender.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/EmailSender.java new file mode 100644 index 0000000..b393b2d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/EmailSender.java @@ -0,0 +1,5 @@ +package tv.codely.mooc.notifications.domain; + +public interface EmailSender { + void send(Email email); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletter.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletter.java new file mode 100644 index 0000000..0fc60cd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletter.java @@ -0,0 +1,55 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.mooc.courses.application.CoursesResponse; +import tv.codely.mooc.students.application.StudentResponse; + +import java.util.Objects; + +public final class NewCoursesNewsletter extends Email { + private final StudentResponse student; + private final CoursesResponse courses; + + public NewCoursesNewsletter(EmailId id, StudentResponse student, CoursesResponse courses) { + super(id, "news@codely.tv", student.email(), "Último cursos en CodelyTV", formatBody(student, courses)); + + this.student = student; + this.courses = courses; + } + + private static String formatBody(StudentResponse student, CoursesResponse courses) { + return String.format( + "Hoy es tu día de suerte... %s vas a ver %s nuevos cursos", + student.name(), + courses.courses().size() + ); + } + + public static NewCoursesNewsletter send(String id, StudentResponse student, CoursesResponse courses) { + NewCoursesNewsletter newsletter = new NewCoursesNewsletter(new EmailId(id), student, courses); + + newsletter.record(new NewCoursesNewsletterEmailSent(id, student.id())); + + return newsletter; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + NewCoursesNewsletter that = (NewCoursesNewsletter) o; + return student.equals(that.student) && + courses.equals(that.courses); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), student, courses); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSent.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSent.java new file mode 100644 index 0000000..b03f209 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSent.java @@ -0,0 +1,78 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Objects; + +public final class NewCoursesNewsletterEmailSent extends DomainEvent { + private final String studentId; + + public NewCoursesNewsletterEmailSent() { + super(null); + + this.studentId = null; + } + + public NewCoursesNewsletterEmailSent(String aggregateId, String studentId) { + super(aggregateId); + + this.studentId = studentId; + } + + public NewCoursesNewsletterEmailSent( + String aggregateId, + String eventId, + String occurredOn, + String studentId + ) { + super(aggregateId, eventId, occurredOn); + + this.studentId = studentId; + } + + @Override + public String eventName() { + return "new_courses_newsletter_email.sent"; + } + + @Override + public HashMap toPrimitives() { + return new HashMap() {{ + put("student_id", studentId); + }}; + } + + @Override + public NewCoursesNewsletterEmailSent fromPrimitives( + String aggregateId, + HashMap body, + String eventId, + String occurredOn + ) { + return new NewCoursesNewsletterEmailSent( + aggregateId, + eventId, + occurredOn, + (String) body.get("student_id") + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NewCoursesNewsletterEmailSent that = (NewCoursesNewsletterEmailSent) o; + return studentId.equals(that.studentId); + } + + @Override + public int hashCode() { + return Objects.hash(studentId); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/infrastructure/FakeEmailSender.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/infrastructure/FakeEmailSender.java new file mode 100644 index 0000000..17501f3 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/notifications/infrastructure/FakeEmailSender.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.notifications.infrastructure; + +import tv.codely.mooc.notifications.domain.Email; +import tv.codely.mooc.notifications.domain.EmailSender; +import tv.codely.shared.domain.Service; + +@Service +public final class FakeEmailSender implements EmailSender { + @Override + public void send(Email email) { + // In the future... + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocHibernateConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocHibernateConfiguration.java new file mode 100644 index 0000000..88f915d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocHibernateConfiguration.java @@ -0,0 +1,47 @@ +package tv.codely.mooc.shared.infrastructure.persistence; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; +import tv.codely.shared.infrastructure.hibernate.HibernateConfigurationFactory; + +import javax.sql.DataSource; +import java.io.IOException; + +@Configuration +@EnableTransactionManagement +public class MoocHibernateConfiguration { + private final HibernateConfigurationFactory factory; + private final Parameter config; + private final String CONTEXT_NAME = "mooc"; + + public MoocHibernateConfiguration(HibernateConfigurationFactory factory, Parameter config) { + this.factory = factory; + this.config = config; + } + + @Bean("mooc-transaction_manager") + public PlatformTransactionManager hibernateTransactionManager() throws IOException, ParameterNotExist { + return factory.hibernateTransactionManager(sessionFactory()); + } + + @Bean("mooc-session_factory") + public LocalSessionFactoryBean sessionFactory() throws IOException, ParameterNotExist { + return factory.sessionFactory(CONTEXT_NAME, dataSource()); + } + + @Bean("mooc-data_source") + public DataSource dataSource() throws IOException, ParameterNotExist { + return factory.dataSource( + config.get("MOOC_DATABASE_HOST"), + config.getInt("MOOC_DATABASE_PORT"), + config.get("MOOC_DATABASE_NAME"), + config.get("MOOC_DATABASE_USER"), + config.get("MOOC_DATABASE_PASSWORD") + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocMySqlEventBusConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocMySqlEventBusConfiguration.java new file mode 100644 index 0000000..dc8f346 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocMySqlEventBusConfiguration.java @@ -0,0 +1,37 @@ +package tv.codely.mooc.shared.infrastructure.persistence; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tv.codely.shared.infrastructure.bus.event.DomainEventsInformation; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlDomainEventsConsumer; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; +import tv.codely.shared.infrastructure.bus.event.spring.SpringApplicationEventBus; + +@Configuration +public class MoocMySqlEventBusConfiguration { + private final SessionFactory sessionFactory; + private final DomainEventsInformation domainEventsInformation; + private final SpringApplicationEventBus bus; + + public MoocMySqlEventBusConfiguration( + @Qualifier("mooc-session_factory") SessionFactory sessionFactory, + DomainEventsInformation domainEventsInformation, + SpringApplicationEventBus bus + ) { + this.sessionFactory = sessionFactory; + this.domainEventsInformation = domainEventsInformation; + this.bus = bus; + } + + @Bean + public MySqlEventBus moocMysqlEventBus() { + return new MySqlEventBus(sessionFactory); + } + + @Bean + public MySqlDomainEventsConsumer moocMySqlDomainEventsConsumer() { + return new MySqlDomainEventsConsumer(sessionFactory, domainEventsInformation, bus); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocRabbitMqEventBusConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocRabbitMqEventBusConfiguration.java new file mode 100644 index 0000000..df81787 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocRabbitMqEventBusConfiguration.java @@ -0,0 +1,27 @@ +package tv.codely.mooc.shared.infrastructure.persistence; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqPublisher; + +@Configuration +public class MoocRabbitMqEventBusConfiguration { + private final RabbitMqPublisher publisher; + private final MySqlEventBus failoverPublisher; + + public MoocRabbitMqEventBusConfiguration( + RabbitMqPublisher publisher, + @Qualifier("moocMysqlEventBus") MySqlEventBus failoverPublisher + ) { + this.publisher = publisher; + this.failoverPublisher = failoverPublisher; + } + + @Bean + public RabbitMqEventBus moocRabbitMqEventBus() { + return new RabbitMqEventBus(publisher, failoverPublisher); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/Step.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/Step.java new file mode 100644 index 0000000..838d110 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/Step.java @@ -0,0 +1,35 @@ +package tv.codely.mooc.steps.domain; + +import java.util.Objects; + +public abstract class Step { + private final StepId id; + private final StepTitle title; + + public Step(StepId id, StepTitle title) { + this.id = id; + this.title = title; + } + + public StepId id() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Step step = (Step) o; + return id.equals(step.id) && + title.equals(step.title); + } + + @Override + public int hashCode() { + return Objects.hash(id, title); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/StepId.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/StepId.java new file mode 100644 index 0000000..7f5d07b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/StepId.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.steps.domain; + +import tv.codely.shared.domain.Identifier; + +public final class StepId extends Identifier { + public StepId(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/StepRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/StepRepository.java new file mode 100644 index 0000000..419f53d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/StepRepository.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.steps.domain; + +import java.util.Optional; + +public interface StepRepository { + void save(Step step); + + Optional search(StepId id); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/StepTitle.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/StepTitle.java new file mode 100644 index 0000000..b1a14a5 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/StepTitle.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain; + +import tv.codely.shared.domain.StringValueObject; + +public final class StepTitle extends StringValueObject { + public StepTitle(String value) { + super(value); + } + + private StepTitle() { + super(null); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStep.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStep.java new file mode 100644 index 0000000..9d2be39 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStep.java @@ -0,0 +1,43 @@ +package tv.codely.mooc.steps.domain.challenge; + +import tv.codely.mooc.steps.domain.Step; +import tv.codely.mooc.steps.domain.StepId; +import tv.codely.mooc.steps.domain.StepTitle; + +import java.util.Objects; + +public final class ChallengeStep extends Step { + private final ChallengeStepStatement statement; + + public ChallengeStep(StepId id, StepTitle title, ChallengeStepStatement statement) { + super(id, title); + + this.statement = statement; + } + + private ChallengeStep() { + super(null, null); + + this.statement = null; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + ChallengeStep that = (ChallengeStep) o; + return statement.equals(that.statement); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), statement); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatement.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatement.java new file mode 100644 index 0000000..bfc8a40 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatement.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain.challenge; + +import tv.codely.shared.domain.StringValueObject; + +public final class ChallengeStepStatement extends StringValueObject { + public ChallengeStepStatement(String value) { + super(value); + } + + public ChallengeStepStatement() { + super(null); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStep.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStep.java new file mode 100644 index 0000000..81f6c2d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStep.java @@ -0,0 +1,25 @@ +package tv.codely.mooc.steps.domain.video; + +import tv.codely.mooc.steps.domain.Step; +import tv.codely.mooc.steps.domain.StepId; +import tv.codely.mooc.steps.domain.StepTitle; +import tv.codely.shared.domain.VideoUrl; + +public final class VideoStep extends Step { + private final VideoUrl videoUrl; + private final VideoStepText text; + + public VideoStep(StepId id, StepTitle title, VideoUrl videoUrl, VideoStepText text) { + super(id, title); + + this.videoUrl = videoUrl; + this.text = text; + } + + private VideoStep() { + super(null, null); + + this.videoUrl = null; + this.text = null; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStepText.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStepText.java new file mode 100644 index 0000000..99e36c9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStepText.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain.video; + +import tv.codely.shared.domain.StringValueObject; + +public final class VideoStepText extends StringValueObject { + public VideoStepText(String value) { + super(value); + } + + private VideoStepText() { + super(null); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepository.java new file mode 100644 index 0000000..6b4b103 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepository.java @@ -0,0 +1,30 @@ +package tv.codely.mooc.steps.infrastructure.persistence; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.transaction.annotation.Transactional; +import tv.codely.mooc.steps.domain.Step; +import tv.codely.mooc.steps.domain.StepId; +import tv.codely.mooc.steps.domain.StepRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.infrastructure.hibernate.HibernateRepository; + +import java.util.Optional; + +@Service +@Transactional("mooc-transaction_manager") +public class MySqlStepRepository extends HibernateRepository implements StepRepository { + public MySqlStepRepository(@Qualifier("mooc-session_factory") SessionFactory sessionFactory) { + super(sessionFactory, Step.class); + } + + @Override + public void save(Step step) { + persist(step); + } + + @Override + public Optional search(StepId id) { + return byId(id); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml new file mode 100644 index 0000000..31cd919 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/StudentResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/StudentResponse.java new file mode 100644 index 0000000..9b8bc05 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/StudentResponse.java @@ -0,0 +1,38 @@ +package tv.codely.mooc.students.application; + +import tv.codely.mooc.students.domain.Student; +import tv.codely.shared.domain.bus.query.Response; + +public final class StudentResponse implements Response { + private final String id; + private final String name; + private final String surname; + private final String email; + + public StudentResponse(String id, String name, String surname, String email) { + this.id = id; + this.name = name; + this.surname = surname; + this.email = email; + } + + public static StudentResponse fromAggregate(Student student) { + return new StudentResponse(student.id().value(), student.name(), student.surname(), student.email()); + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public String surname() { + return surname; + } + + public String email() { + return email; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/StudentsResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/StudentsResponse.java new file mode 100644 index 0000000..9a7e035 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/StudentsResponse.java @@ -0,0 +1,17 @@ +package tv.codely.mooc.students.application; + +import tv.codely.shared.domain.bus.query.Response; + +import java.util.List; + +public final class StudentsResponse implements Response { + private final List students; + + public StudentsResponse(List students) { + this.students = students; + } + + public List students() { + return students; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/search_all/AllStudentsSearcher.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/search_all/AllStudentsSearcher.java new file mode 100644 index 0000000..a079520 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/search_all/AllStudentsSearcher.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.students.application.search_all; + +import tv.codely.mooc.students.application.StudentResponse; +import tv.codely.mooc.students.application.StudentsResponse; +import tv.codely.mooc.students.domain.StudentRepository; +import tv.codely.shared.domain.Service; + +import java.util.stream.Collectors; + +@Service +public final class AllStudentsSearcher { + private final StudentRepository repository; + + public AllStudentsSearcher(StudentRepository repository) { + this.repository = repository; + } + + public StudentsResponse search() { + return new StudentsResponse( + repository.searchAll().stream().map(StudentResponse::fromAggregate).collect(Collectors.toList()) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQuery.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQuery.java new file mode 100644 index 0000000..618a9fa --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQuery.java @@ -0,0 +1,20 @@ +package tv.codely.mooc.students.application.search_all; + +import tv.codely.shared.domain.bus.query.Query; + +import java.util.Objects; + +public final class SearchAllStudentsQuery implements Query { + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return o != null && getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return Objects.hash("SearchAllStudentsQuery"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryHandler.java new file mode 100644 index 0000000..c9e3fa4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryHandler.java @@ -0,0 +1,19 @@ +package tv.codely.mooc.students.application.search_all; + +import tv.codely.mooc.students.application.StudentsResponse; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.QueryHandler; + +@Service +public final class SearchAllStudentsQueryHandler implements QueryHandler { + private final AllStudentsSearcher searcher; + + public SearchAllStudentsQueryHandler(AllStudentsSearcher searcher) { + this.searcher = searcher; + } + + @Override + public StudentsResponse handle(SearchAllStudentsQuery query) { + return searcher.search(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/domain/Student.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/domain/Student.java new file mode 100644 index 0000000..eb0353d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/domain/Student.java @@ -0,0 +1,31 @@ +package tv.codely.mooc.students.domain; + +public final class Student { + private final StudentId id; + private final String name; + private final String surname; + private final String email; + + public Student(StudentId id, String name, String surname, String email) { + this.id = id; + this.name = name; + this.surname = surname; + this.email = email; + } + + public StudentId id() { + return id; + } + + public String name() { + return name; + } + + public String surname() { + return surname; + } + + public String email() { + return email; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/domain/StudentId.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/domain/StudentId.java new file mode 100644 index 0000000..3e6735e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/domain/StudentId.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.students.domain; + +import tv.codely.shared.domain.Identifier; + +public final class StudentId extends Identifier { + public StudentId(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/domain/StudentRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/domain/StudentRepository.java new file mode 100644 index 0000000..0fbd2e6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/domain/StudentRepository.java @@ -0,0 +1,7 @@ +package tv.codely.mooc.students.domain; + +import java.util.List; + +public interface StudentRepository { + List searchAll(); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/infrastructure/InMemoryStudentRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/infrastructure/InMemoryStudentRepository.java new file mode 100644 index 0000000..aabd3ab --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/main/tv/codely/mooc/students/infrastructure/InMemoryStudentRepository.java @@ -0,0 +1,27 @@ +package tv.codely.mooc.students.infrastructure; + +import tv.codely.mooc.students.domain.Student; +import tv.codely.mooc.students.domain.StudentId; +import tv.codely.mooc.students.domain.StudentRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.UuidGenerator; + +import java.util.Arrays; +import java.util.List; + +@Service +public final class InMemoryStudentRepository implements StudentRepository { + private UuidGenerator generator; + + public InMemoryStudentRepository(UuidGenerator generator) { + this.generator = generator; + } + + @Override + public List searchAll() { + return Arrays.asList( + new Student(new StudentId(generator.generate()), "name", "surname", "email@mail.com"), + new Student(new StudentId(generator.generate()), "Other name", "Other surname", "another@mail.com") + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/MoocContextInfrastructureTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/MoocContextInfrastructureTestCase.java new file mode 100644 index 0000000..eb574c4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/MoocContextInfrastructureTestCase.java @@ -0,0 +1,11 @@ +package tv.codely.mooc; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import tv.codely.apps.mooc.backend.MoocBackendApplication; +import tv.codely.shared.infrastructure.InfrastructureTestCase; + +@ContextConfiguration(classes = MoocBackendApplication.class) +@SpringBootTest +public abstract class MoocContextInfrastructureTestCase extends InfrastructureTestCase { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/CoursesModuleInfrastructureTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/CoursesModuleInfrastructureTestCase.java new file mode 100644 index 0000000..a52cf56 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/CoursesModuleInfrastructureTestCase.java @@ -0,0 +1,12 @@ +package tv.codely.mooc.courses; + +import org.springframework.beans.factory.annotation.Autowired; +import tv.codely.mooc.MoocContextInfrastructureTestCase; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.mooc.courses.infrastructure.persistence.InMemoryCourseRepository; + +public abstract class CoursesModuleInfrastructureTestCase extends MoocContextInfrastructureTestCase { + protected InMemoryCourseRepository inMemoryCourseRepository = new InMemoryCourseRepository(); + @Autowired + protected CourseRepository mySqlCourseRepository; +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java new file mode 100644 index 0000000..eb7454d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.courses; + +import org.junit.jupiter.api.BeforeEach; +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.shared.infrastructure.UnitTestCase; + +import static org.mockito.Mockito.*; + +public abstract class CoursesModuleUnitTestCase extends UnitTestCase { + protected CourseRepository repository; + + @BeforeEach + protected void setUp() { + super.setUp(); + + repository = mock(CourseRepository.class); + } + + public void shouldHaveSaved(Course course) { + verify(repository, atLeastOnce()).save(course); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/CourseResponseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/CourseResponseMother.java new file mode 100644 index 0000000..564e527 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/CourseResponseMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.application; + +import tv.codely.mooc.courses.domain.*; + +public final class CourseResponseMother { + public static CourseResponse create(CourseId id, CourseName name, CourseDuration duration) { + return new CourseResponse(id.value(), name.value(), duration.value()); + } + + public static CourseResponse random() { + return create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/CoursesResponseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/CoursesResponseMother.java new file mode 100644 index 0000000..1067e22 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/CoursesResponseMother.java @@ -0,0 +1,24 @@ +package tv.codely.mooc.courses.application; + +import tv.codely.shared.domain.ListMother; + +import java.util.Collections; +import java.util.List; + +public final class CoursesResponseMother { + public static CoursesResponse create(List courses) { + return new CoursesResponse(courses); + } + + public static CoursesResponse random() { + return create(ListMother.random(CourseResponseMother::random)); + } + + public static CoursesResponse times(int times) { + return create(ListMother.create(times, CourseResponseMother::random)); + } + + public static CoursesResponse empty() { + return create(Collections.emptyList()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandHandlerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandHandlerShould.java new file mode 100644 index 0000000..efdd3f7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandHandlerShould.java @@ -0,0 +1,33 @@ +package tv.codely.mooc.courses.application.create; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses.CoursesModuleUnitTestCase; +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseCreatedDomainEventMother; +import tv.codely.mooc.courses.domain.CourseMother; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +final class CreateCourseCommandHandlerShould extends CoursesModuleUnitTestCase { + private CreateCourseCommandHandler handler; + + @BeforeEach + protected void setUp() { + super.setUp(); + + handler = new CreateCourseCommandHandler(new CourseCreator(repository, eventBus)); + } + + @Test + void create_a_valid_course() { + CreateCourseCommand command = CreateCourseCommandMother.random(); + + Course course = CourseMother.fromRequest(command); + CourseCreatedDomainEvent domainEvent = CourseCreatedDomainEventMother.fromCourse(course); + + handler.handle(command); + + shouldHaveSaved(course); + shouldHavePublished(domainEvent); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandMother.java new file mode 100644 index 0000000..f4dad05 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.application.create; + +import tv.codely.mooc.courses.domain.*; + +public final class CreateCourseCommandMother { + public static CreateCourseCommand create(CourseId id, CourseName name, CourseDuration duration) { + return new CreateCourseCommand(id.value(), name.value(), duration.value()); + } + + public static CreateCourseCommand random() { + return create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryMother.java new file mode 100644 index 0000000..c589326 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.application.search_last; + +import tv.codely.shared.domain.IntegerMother; + +public final class SearchLastCoursesQueryMother { + public static SearchLastCoursesQuery create(Integer total) { + return new SearchLastCoursesQuery(total); + } + + public static SearchLastCoursesQuery random() { + return create(IntegerMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseCreatedDomainEventMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseCreatedDomainEventMother.java new file mode 100644 index 0000000..e73f2d4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseCreatedDomainEventMother.java @@ -0,0 +1,17 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +public final class CourseCreatedDomainEventMother { + public static CourseCreatedDomainEvent create(CourseId id, CourseName name, CourseDuration duration) { + return new CourseCreatedDomainEvent(id.value(), name.value(), duration.value()); + } + + public static CourseCreatedDomainEvent fromCourse(Course course) { + return create(course.id(), course.name(), course.duration()); + } + + public static CourseCreatedDomainEvent random() { + return create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseDurationMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseDurationMother.java new file mode 100644 index 0000000..d9ed986 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseDurationMother.java @@ -0,0 +1,20 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.IntegerMother; +import tv.codely.shared.domain.RandomElementPicker; + +public final class CourseDurationMother { + public static CourseDuration create(String value) { + return new CourseDuration(value); + } + + public static CourseDuration random() { + return create( + String.format( + "%s %s", + IntegerMother.random(), + RandomElementPicker.from("months", "years", "days", "hours", "minutes", "seconds") + ) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseIdMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseIdMother.java new file mode 100644 index 0000000..76bf0d9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseIdMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.UuidMother; + +public final class CourseIdMother { + public static CourseId create(String value) { + return new CourseId(value); + } + + public static CourseId random() { + return create(UuidMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseMother.java new file mode 100644 index 0000000..20f225e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseMother.java @@ -0,0 +1,21 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.mooc.courses.application.create.CreateCourseCommand; + +public final class CourseMother { + public static Course create(CourseId id, CourseName name, CourseDuration duration) { + return new Course(id, name, duration); + } + + public static Course fromRequest(CreateCourseCommand request) { + return create( + CourseIdMother.create(request.id()), + CourseNameMother.create(request.name()), + CourseDurationMother.create(request.duration()) + ); + } + + public static Course random() { + return create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseNameMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseNameMother.java new file mode 100644 index 0000000..1ca25f8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/domain/CourseNameMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.WordMother; + +public final class CourseNameMother { + public static CourseName create(String value) { + return new CourseName(value); + } + + public static CourseName random() { + return create(WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepositoryShould.java new file mode 100644 index 0000000..04e3d6a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepositoryShould.java @@ -0,0 +1,35 @@ +package tv.codely.mooc.courses.infrastructure.persistence; + +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses.CoursesModuleInfrastructureTestCase; +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseIdMother; +import tv.codely.mooc.courses.domain.CourseMother; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +final class InMemoryCourseRepositoryShould extends CoursesModuleInfrastructureTestCase { + @Test + void save_a_course() { + Course course = CourseMother.random(); + + inMemoryCourseRepository.save(course); + } + + @Test + void return_an_existing_course() { + Course course = CourseMother.random(); + + inMemoryCourseRepository.save(course); + + assertEquals(Optional.of(course), inMemoryCourseRepository.search(course.id())); + } + + @Test + void not_return_a_non_existing_course() { + assertFalse(inMemoryCourseRepository.search(CourseIdMother.random()).isPresent()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepositoryShould.java new file mode 100644 index 0000000..45b8a1b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepositoryShould.java @@ -0,0 +1,37 @@ +package tv.codely.mooc.courses.infrastructure.persistence; + +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses.CoursesModuleInfrastructureTestCase; +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseIdMother; +import tv.codely.mooc.courses.domain.CourseMother; + +import jakarta.transaction.Transactional; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@Transactional +class MySqlCourseRepositoryShould extends CoursesModuleInfrastructureTestCase { + @Test + void save_a_course() { + Course course = CourseMother.random(); + + mySqlCourseRepository.save(course); + } + + @Test + void return_an_existing_course() { + Course course = CourseMother.random(); + + mySqlCourseRepository.save(course); + + assertEquals(Optional.of(course), mySqlCourseRepository.search(course.id())); + } + + @Test + void not_return_a_non_existing_course() { + assertFalse(mySqlCourseRepository.search(CourseIdMother.random()).isPresent()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleInfrastructureTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleInfrastructureTestCase.java new file mode 100644 index 0000000..18978bb --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleInfrastructureTestCase.java @@ -0,0 +1,10 @@ +package tv.codely.mooc.courses_counter; + +import org.springframework.beans.factory.annotation.Autowired; +import tv.codely.mooc.MoocContextInfrastructureTestCase; +import tv.codely.mooc.courses_counter.domain.CoursesCounterRepository; + +public abstract class CoursesCounterModuleInfrastructureTestCase extends MoocContextInfrastructureTestCase { + @Autowired + protected CoursesCounterRepository repository; +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleUnitTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleUnitTestCase.java new file mode 100644 index 0000000..8c06e7f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleUnitTestCase.java @@ -0,0 +1,34 @@ +package tv.codely.mooc.courses_counter; + +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mockito; +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterRepository; +import tv.codely.shared.infrastructure.UnitTestCase; + +import java.util.Optional; + +import static org.mockito.Mockito.*; + +public abstract class CoursesCounterModuleUnitTestCase extends UnitTestCase { + protected CoursesCounterRepository repository; + + @BeforeEach + protected void setUp() { + super.setUp(); + + repository = mock(CoursesCounterRepository.class); + } + + public void shouldSearch(CoursesCounter course) { + Mockito.when(repository.search()).thenReturn(Optional.of(course)); + } + + public void shouldSearch() { + Mockito.when(repository.search()).thenReturn(Optional.empty()); + } + + public void shouldHaveSaved(CoursesCounter course) { + verify(repository, atLeastOnce()).save(course); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponseMother.java new file mode 100644 index 0000000..4b38949 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponseMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses_counter.application.find; + +import tv.codely.shared.domain.IntegerMother; + +final class CoursesCounterResponseMother { + public static CoursesCounterResponse create(Integer value) { + return new CoursesCounterResponse(value); + } + + public static CoursesCounterResponse random() { + return create(IntegerMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandlerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandlerShould.java new file mode 100644 index 0000000..1065514 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandlerShould.java @@ -0,0 +1,42 @@ +package tv.codely.mooc.courses_counter.application.find; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses_counter.CoursesCounterModuleUnitTestCase; +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterMother; +import tv.codely.mooc.courses_counter.domain.CoursesCounterNotInitialized; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class FindCoursesCounterQueryHandlerShould extends CoursesCounterModuleUnitTestCase { + FindCoursesCounterQueryHandler handler; + + @BeforeEach + protected void setUp() { + super.setUp(); + + handler = new FindCoursesCounterQueryHandler(new CoursesCounterFinder(repository)); + } + + @Test + void it_should_find_an_existing_courses_counter() { + CoursesCounter counter = CoursesCounterMother.random(); + FindCoursesCounterQuery query = new FindCoursesCounterQuery(); + CoursesCounterResponse response = CoursesCounterResponseMother.create(counter.total().value()); + + shouldSearch(counter); + + assertEquals(response, handler.handle(query)); + } + + @Test + void it_should_throw_an_exception_when_courses_counter_does_not_exists() { + FindCoursesCounterQuery query = new FindCoursesCounterQuery(); + + shouldSearch(); + + assertThrows(CoursesCounterNotInitialized.class, () -> handler.handle(query)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreatedShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreatedShould.java new file mode 100644 index 0000000..13d6218 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreatedShould.java @@ -0,0 +1,66 @@ +package tv.codely.mooc.courses_counter.application.increment; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses.domain.CourseCreatedDomainEventMother; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseIdMother; +import tv.codely.mooc.courses_counter.CoursesCounterModuleUnitTestCase; +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterMother; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +final class IncrementCoursesCounterOnCourseCreatedShould extends CoursesCounterModuleUnitTestCase { + IncrementCoursesCounterOnCourseCreated subscriber; + + @BeforeEach + protected void setUp() { + super.setUp(); + + subscriber = new IncrementCoursesCounterOnCourseCreated( + new CoursesCounterIncrementer(repository, uuidGenerator) + ); + } + + @Test + void it_should_initialize_a_new_counter() { + CourseCreatedDomainEvent event = CourseCreatedDomainEventMother.random(); + + CourseId courseId = CourseIdMother.create(event.aggregateId()); + CoursesCounter newCounter = CoursesCounterMother.withOne(courseId); + + shouldSearch(); + shouldGenerateUuid(newCounter.id().value()); + + subscriber.on(event); + + shouldHaveSaved(newCounter); + } + + @Test + void it_should_increment_an_existing_counter() { + CourseCreatedDomainEvent event = CourseCreatedDomainEventMother.random(); + + CourseId courseId = CourseIdMother.create(event.aggregateId()); + CoursesCounter existingCounter = CoursesCounterMother.random(); + CoursesCounter incrementedCounter = CoursesCounterMother.incrementing(existingCounter, courseId); + + shouldSearch(existingCounter); + + subscriber.on(event); + + shouldHaveSaved(incrementedCounter); + } + + @Test + void it_should_not_increment_an_already_incremented_course() { + CourseCreatedDomainEvent event = CourseCreatedDomainEventMother.random(); + + CourseId courseId = CourseIdMother.create(event.aggregateId()); + CoursesCounter existingCounter = CoursesCounterMother.withOne(courseId); + + shouldSearch(existingCounter); + + subscriber.on(event); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterIdMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterIdMother.java new file mode 100644 index 0000000..596a118 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterIdMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses_counter.domain; + +import tv.codely.shared.domain.UuidMother; + +public final class CoursesCounterIdMother { + public static CoursesCounterId create(String value) { + return new CoursesCounterId(value); + } + + public static CoursesCounterId random() { + return create(UuidMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterMother.java new file mode 100644 index 0000000..9e5a27f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterMother.java @@ -0,0 +1,43 @@ +package tv.codely.mooc.courses_counter.domain; + +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseIdMother; +import tv.codely.shared.domain.ListMother; + +import java.util.ArrayList; +import java.util.List; + +public final class CoursesCounterMother { + public static CoursesCounter create( + CoursesCounterId id, + CoursesCounterTotal total, + List existingCourses + ) { + return new CoursesCounter(id, total, existingCourses); + } + + public static CoursesCounter withOne(CourseId courseId) { + return create(CoursesCounterIdMother.random(), CoursesCounterTotalMother.one(), ListMother.one(courseId)); + } + + public static CoursesCounter random() { + List existingCourses = ListMother.random(CourseIdMother::random); + + return create( + CoursesCounterIdMother.random(), + CoursesCounterTotalMother.create(existingCourses.size()), + existingCourses + ); + } + + public static CoursesCounter incrementing(CoursesCounter existingCounter, CourseId courseId) { + List existingCourses = new ArrayList<>(existingCounter.existingCourses()); + existingCourses.add(courseId); + + return create( + existingCounter.id(), + CoursesCounterTotalMother.create(existingCounter.total().value() + 1), + existingCourses + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterTotalMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterTotalMother.java new file mode 100644 index 0000000..b8c22cb --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterTotalMother.java @@ -0,0 +1,17 @@ +package tv.codely.mooc.courses_counter.domain; + +import tv.codely.shared.domain.IntegerMother; + +public final class CoursesCounterTotalMother { + public static CoursesCounterTotal create(Integer value) { + return new CoursesCounterTotal(value); + } + + public static CoursesCounterTotal random() { + return create(IntegerMother.random()); + } + + public static CoursesCounterTotal one() { + return create(1); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepositoryShould.java new file mode 100644 index 0000000..fabdfe8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepositoryShould.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.courses_counter.infrastructure.persistence; + +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses_counter.CoursesCounterModuleInfrastructureTestCase; +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterMother; + +import jakarta.transaction.Transactional; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Transactional +class MySqlCoursesCounterRepositoryShould extends CoursesCounterModuleInfrastructureTestCase { + @Test + void return_an_existing_courses_counter() { + CoursesCounter counter = CoursesCounterMother.random(); + + repository.save(counter); + + assertEquals(Optional.of(counter), repository.search()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/application/NotificationsModuleUnitTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/application/NotificationsModuleUnitTestCase.java new file mode 100644 index 0000000..6b12480 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/application/NotificationsModuleUnitTestCase.java @@ -0,0 +1,33 @@ +package tv.codely.mooc.notifications.application; + +import org.junit.jupiter.api.BeforeEach; +import org.mockito.ArgumentCaptor; +import tv.codely.mooc.notifications.domain.Email; +import tv.codely.mooc.notifications.domain.EmailSender; +import tv.codely.shared.infrastructure.UnitTestCase; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +public abstract class NotificationsModuleUnitTestCase extends UnitTestCase { + protected EmailSender sender; + + @BeforeEach + protected void setUp() { + super.setUp(); + + sender = mock(EmailSender.class); + } + + public void shouldHaveSentEmail(Email email) { + ArgumentCaptor argument = ArgumentCaptor.forClass(Email.class); + + verify(sender, atLeastOnce()).send(argument.capture()); + + List emails = argument.getAllValues(); + + assertTrue(emails.contains(email)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandlerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandlerShould.java new file mode 100644 index 0000000..8134c05 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandlerShould.java @@ -0,0 +1,102 @@ +package tv.codely.mooc.notifications.application.send_new_courses_newsletter; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses.application.CoursesResponse; +import tv.codely.mooc.courses.application.CoursesResponseMother; +import tv.codely.mooc.courses.application.search_last.SearchLastCoursesQuery; +import tv.codely.mooc.courses.application.search_last.SearchLastCoursesQueryMother; +import tv.codely.mooc.notifications.application.NotificationsModuleUnitTestCase; +import tv.codely.mooc.notifications.domain.NewCoursesNewsletter; +import tv.codely.mooc.notifications.domain.NewCoursesNewsletterEmailSent; +import tv.codely.mooc.notifications.domain.NewCoursesNewsletterEmailSentMother; +import tv.codely.mooc.notifications.domain.NewCoursesNewsletterMother; +import tv.codely.mooc.students.application.StudentResponse; +import tv.codely.mooc.students.application.StudentResponseMother; +import tv.codely.mooc.students.application.StudentsResponse; +import tv.codely.mooc.students.application.StudentsResponseMother; +import tv.codely.mooc.students.application.search_all.SearchAllStudentsQuery; +import tv.codely.mooc.students.application.search_all.SearchAllStudentsQueryMother; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; + +import java.util.Arrays; + +final class SendNewCoursesNewsletterCommandHandlerShould extends NotificationsModuleUnitTestCase { + SendNewCoursesNewsletterCommandHandler handler; + + @BeforeEach + protected void setUp() { + super.setUp(); + + handler = new SendNewCoursesNewsletterCommandHandler( + new NewCoursesNewsletterSender(queryBus, uuidGenerator, sender, eventBus) + ); + } + + @Test + void not_send_the_newsletter_when_there_are_no_courses() throws QueryHandlerExecutionError, CommandHandlerExecutionError { + SendNewCoursesNewsletterCommand command = SendNewCoursesNewsletterCommandMother.random(); + + SearchLastCoursesQuery coursesQuery = SearchLastCoursesQueryMother.create(3); + CoursesResponse coursesResponse = CoursesResponseMother.empty(); + + shouldAsk(coursesQuery, coursesResponse); + + handler.handle(command); + } + + @Test + void not_send_the_newsletter_when_there_are_no_students() throws QueryHandlerExecutionError, CommandHandlerExecutionError { + SendNewCoursesNewsletterCommand command = SendNewCoursesNewsletterCommandMother.random(); + + SearchLastCoursesQuery coursesQuery = SearchLastCoursesQueryMother.create(3); + CoursesResponse coursesResponse = CoursesResponseMother.random(); + + SearchAllStudentsQuery studentsQuery = SearchAllStudentsQueryMother.random(); + StudentsResponse studentsResponse = StudentsResponseMother.empty(); + + shouldAsk(coursesQuery, coursesResponse); + shouldAsk(studentsQuery, studentsResponse); + + handler.handle(command); + } + + @Test + void send_the_new_courses_newsletter() throws QueryHandlerExecutionError, CommandHandlerExecutionError { + SendNewCoursesNewsletterCommand command = SendNewCoursesNewsletterCommandMother.random(); + + SearchLastCoursesQuery coursesQuery = SearchLastCoursesQueryMother.create(3); + CoursesResponse coursesResponse = CoursesResponseMother.times(3); + + SearchAllStudentsQuery studentsQuery = SearchAllStudentsQueryMother.random(); + StudentResponse student = StudentResponseMother.random(); + StudentResponse otherStudent = StudentResponseMother.random(); + StudentsResponse studentsResponse = StudentsResponseMother.create(Arrays.asList(student, otherStudent)); + + NewCoursesNewsletter newsletter = NewCoursesNewsletterMother.create(student, coursesResponse); + NewCoursesNewsletter otherNewsletter = NewCoursesNewsletterMother.create(otherStudent, coursesResponse); + + NewCoursesNewsletterEmailSent domainEvent = NewCoursesNewsletterEmailSentMother.create( + newsletter.id(), + student.id() + ); + NewCoursesNewsletterEmailSent otherDomainEvent = NewCoursesNewsletterEmailSentMother.create( + otherNewsletter.id(), + otherStudent.id() + ); + + shouldAsk(coursesQuery, coursesResponse); + shouldAsk(studentsQuery, studentsResponse); + + shouldGenerateUuids(newsletter.id().value(), otherNewsletter.id().value()); + + handler.handle(command); + + shouldHaveSentEmail(newsletter); + shouldHavePublished(domainEvent); + + shouldHaveSentEmail(otherNewsletter); + shouldHavePublished(otherDomainEvent); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandMother.java new file mode 100644 index 0000000..f91caff --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandMother.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.notifications.application.send_new_courses_newsletter; + +import tv.codely.shared.domain.UuidMother; + +public final class SendNewCoursesNewsletterCommandMother { + public static SendNewCoursesNewsletterCommand random() { + return new SendNewCoursesNewsletterCommand(UuidMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/domain/EmailIdMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/domain/EmailIdMother.java new file mode 100644 index 0000000..0211444 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/domain/EmailIdMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.shared.domain.UuidMother; + +public final class EmailIdMother { + public static EmailId create(String value) { + return new EmailId(value); + } + + public static EmailId random() { + return create(UuidMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSentMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSentMother.java new file mode 100644 index 0000000..97afd0f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSentMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.mooc.students.domain.StudentIdMother; + +public final class NewCoursesNewsletterEmailSentMother { + public static NewCoursesNewsletterEmailSent create(EmailId id, String studentId) { + return new NewCoursesNewsletterEmailSent(id.value(), studentId); + } + + public static NewCoursesNewsletterEmailSent random() { + return create(EmailIdMother.random(), StudentIdMother.random().value()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterMother.java new file mode 100644 index 0000000..a5ff7f5 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterMother.java @@ -0,0 +1,20 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.mooc.courses.application.CoursesResponse; +import tv.codely.mooc.courses.application.CoursesResponseMother; +import tv.codely.mooc.students.application.StudentResponse; +import tv.codely.mooc.students.application.StudentResponseMother; + +public final class NewCoursesNewsletterMother { + public static NewCoursesNewsletter create(EmailId id, StudentResponse student, CoursesResponse courses) { + return new NewCoursesNewsletter(id, student, courses); + } + + public static NewCoursesNewsletter create(StudentResponse student, CoursesResponse courses) { + return new NewCoursesNewsletter(EmailIdMother.random(), student, courses); + } + + public static NewCoursesNewsletter random() { + return create(EmailIdMother.random(), StudentResponseMother.random(), CoursesResponseMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/mysql/MySqlEventBusShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/mysql/MySqlEventBusShould.java new file mode 100644 index 0000000..17dc703 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/mysql/MySqlEventBusShould.java @@ -0,0 +1,34 @@ +package tv.codely.mooc.shared.infrastructure.bus.event.mysql; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import tv.codely.mooc.MoocContextInfrastructureTestCase; +import tv.codely.mooc.courses.domain.CourseCreatedDomainEventMother; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlDomainEventsConsumer; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; + +import jakarta.transaction.Transactional; +import java.util.Collections; + +@Transactional +class MySqlEventBusShould extends MoocContextInfrastructureTestCase { + @Autowired + private MySqlEventBus eventBus; + @Autowired + private MySqlDomainEventsConsumer consumer; + + @Test + void publish_and_consume_domain_events_from_msql() throws InterruptedException { + CourseCreatedDomainEvent domainEvent = CourseCreatedDomainEventMother.random(); + + eventBus.publish(Collections.singletonList(domainEvent)); + + Thread consumerProcess = new Thread(() -> consumer.consume()); + consumerProcess.start(); + + Thread.sleep(100); + + consumer.stop(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java new file mode 100644 index 0000000..70c8de2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java @@ -0,0 +1,53 @@ +package tv.codely.mooc.shared.infrastructure.bus.event.rabbitmq; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import tv.codely.mooc.MoocContextInfrastructureTestCase; +import tv.codely.mooc.courses.domain.CourseCreatedDomainEventMother; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; +import tv.codely.shared.infrastructure.bus.event.DomainEventSubscriberInformation; +import tv.codely.shared.infrastructure.bus.event.DomainEventSubscribersInformation; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqDomainEventsConsumer; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqEventBus; + +import java.util.Collections; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class RabbitMqEventBusShould extends MoocContextInfrastructureTestCase { + @Autowired + private RabbitMqEventBus eventBus; + @Autowired + private RabbitMqDomainEventsConsumer consumer; + @Autowired + private TestAllWorksOnRabbitMqEventsPublished subscriber; + + @BeforeEach + protected void setUp() { + subscriber.hasBeenExecuted = false; + + consumer.withSubscribersInformation( + new DomainEventSubscribersInformation( + new HashMap, DomainEventSubscriberInformation>() {{ + put(TestAllWorksOnRabbitMqEventsPublished.class, new DomainEventSubscriberInformation( + TestAllWorksOnRabbitMqEventsPublished.class, + Collections.singletonList(CourseCreatedDomainEvent.class) + )); + }} + ) + ); + } + + @Test + void publish_and_consume_domain_events_from_rabbitmq() throws Exception { + CourseCreatedDomainEvent domainEvent = CourseCreatedDomainEventMother.random(); + + eventBus.publish(Collections.singletonList(domainEvent)); + + consumer.consume("mooc"); + + eventually(() -> assertTrue(subscriber.hasBeenExecuted)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/TestAllWorksOnRabbitMqEventsPublished.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/TestAllWorksOnRabbitMqEventsPublished.java new file mode 100644 index 0000000..8b1981d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/TestAllWorksOnRabbitMqEventsPublished.java @@ -0,0 +1,15 @@ +package tv.codely.mooc.shared.infrastructure.bus.event.rabbitmq; + +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +@Service +@DomainEventSubscriber({CourseCreatedDomainEvent.class}) +public final class TestAllWorksOnRabbitMqEventsPublished { + public Boolean hasBeenExecuted = false; + + public void on(CourseCreatedDomainEvent event) { + hasBeenExecuted = true; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/StepsModuleInfrastructureTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/StepsModuleInfrastructureTestCase.java new file mode 100644 index 0000000..cc0c90d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/StepsModuleInfrastructureTestCase.java @@ -0,0 +1,10 @@ +package tv.codely.mooc.steps; + +import org.springframework.beans.factory.annotation.Autowired; +import tv.codely.mooc.MoocContextInfrastructureTestCase; +import tv.codely.mooc.steps.domain.StepRepository; + +public abstract class StepsModuleInfrastructureTestCase extends MoocContextInfrastructureTestCase { + @Autowired + protected StepRepository repository; +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/StepIdMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/StepIdMother.java new file mode 100644 index 0000000..7747f37 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/StepIdMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain; + +import tv.codely.shared.domain.UuidMother; + +public final class StepIdMother { + public static StepId create(String value) { + return new StepId(value); + } + + public static StepId random() { + return create(UuidMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/StepTitleMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/StepTitleMother.java new file mode 100644 index 0000000..571bc13 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/StepTitleMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain; + +import tv.codely.shared.domain.WordMother; + +public final class StepTitleMother { + public static StepTitle create(String value) { + return new StepTitle(value); + } + + public static StepTitle random() { + return create(WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepMother.java new file mode 100644 index 0000000..46fcb75 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepMother.java @@ -0,0 +1,16 @@ +package tv.codely.mooc.steps.domain.challenge; + +import tv.codely.mooc.steps.domain.StepId; +import tv.codely.mooc.steps.domain.StepIdMother; +import tv.codely.mooc.steps.domain.StepTitle; +import tv.codely.mooc.steps.domain.StepTitleMother; + +public final class ChallengeStepMother { + public static ChallengeStep create(StepId id, StepTitle title, ChallengeStepStatement statement) { + return new ChallengeStep(id, title, statement); + } + + public static ChallengeStep random() { + return create(StepIdMother.random(), StepTitleMother.random(), ChallengeStepStatementMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatementMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatementMother.java new file mode 100644 index 0000000..8e85970 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatementMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain.challenge; + +import tv.codely.shared.domain.WordMother; + +public final class ChallengeStepStatementMother { + public static ChallengeStepStatement create(String value) { + return new ChallengeStepStatement(value); + } + + public static ChallengeStepStatement random() { + return create(WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepMother.java new file mode 100644 index 0000000..3cb9432 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepMother.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.steps.domain.video; + +import tv.codely.mooc.steps.domain.StepId; +import tv.codely.mooc.steps.domain.StepIdMother; +import tv.codely.mooc.steps.domain.StepTitle; +import tv.codely.mooc.steps.domain.StepTitleMother; +import tv.codely.shared.domain.VideoUrl; +import tv.codely.shared.domain.VideoUrlMother; + +public final class VideoStepMother { + public static VideoStep create(StepId id, StepTitle title, VideoUrl videoUrl, VideoStepText text) { + return new VideoStep(id, title, videoUrl, text); + } + + public static VideoStep random() { + return create( + StepIdMother.random(), + StepTitleMother.random(), + VideoUrlMother.random(), + VideoStepTextMother.random() + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepTextMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepTextMother.java new file mode 100644 index 0000000..7dca6fa --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepTextMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain.video; + +import tv.codely.shared.domain.WordMother; + +public final class VideoStepTextMother { + public static VideoStepText create(String value) { + return new VideoStepText(value); + } + + public static VideoStepText random() { + return create(WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepositoryShould.java new file mode 100644 index 0000000..88c684e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepositoryShould.java @@ -0,0 +1,44 @@ +package tv.codely.mooc.steps.infrastructure.persistence; + +import org.junit.jupiter.api.Test; +import tv.codely.mooc.steps.StepsModuleInfrastructureTestCase; +import tv.codely.mooc.steps.domain.Step; +import tv.codely.mooc.steps.domain.StepIdMother; +import tv.codely.mooc.steps.domain.challenge.ChallengeStepMother; +import tv.codely.mooc.steps.domain.video.VideoStepMother; + +import jakarta.transaction.Transactional; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@Transactional +class MySqlStepRepositoryShould extends StepsModuleInfrastructureTestCase { + @Test + void save_a_step() { + for (Step step : steps()) { + repository.save(step); + } + } + + @Test + void return_an_existing_step() { + for (Step step : steps()) { + repository.save(step); + + assertEquals(Optional.of(step), repository.search(step.id())); + } + } + + @Test + void not_return_a_non_existing_course() { + assertFalse(repository.search(StepIdMother.random()).isPresent()); + } + + private List steps() { + return Arrays.asList(ChallengeStepMother.random(), VideoStepMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/students/application/StudentResponseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/students/application/StudentResponseMother.java new file mode 100644 index 0000000..b29fb25 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/students/application/StudentResponseMother.java @@ -0,0 +1,16 @@ +package tv.codely.mooc.students.application; + +import tv.codely.mooc.students.domain.StudentId; +import tv.codely.mooc.students.domain.StudentIdMother; +import tv.codely.shared.domain.EmailMother; +import tv.codely.shared.domain.WordMother; + +public final class StudentResponseMother { + public static StudentResponse create(StudentId id, String name, String surname, String email) { + return new StudentResponse(id.value(), name, surname, email); + } + + public static StudentResponse random() { + return create(StudentIdMother.random(), WordMother.random(), WordMother.random(), EmailMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/students/application/StudentsResponseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/students/application/StudentsResponseMother.java new file mode 100644 index 0000000..89e879a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/students/application/StudentsResponseMother.java @@ -0,0 +1,20 @@ +package tv.codely.mooc.students.application; + +import tv.codely.shared.domain.ListMother; + +import java.util.Collections; +import java.util.List; + +public final class StudentsResponseMother { + public static StudentsResponse create(List courses) { + return new StudentsResponse(courses); + } + + public static StudentsResponse random() { + return create(ListMother.random(StudentResponseMother::random)); + } + + public static StudentsResponse empty() { + return create(Collections.emptyList()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryMother.java new file mode 100644 index 0000000..93a0e28 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryMother.java @@ -0,0 +1,7 @@ +package tv.codely.mooc.students.application.search_all; + +public final class SearchAllStudentsQueryMother { + public static SearchAllStudentsQuery random() { + return new SearchAllStudentsQuery(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/students/domain/StudentIdMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/students/domain/StudentIdMother.java new file mode 100644 index 0000000..b98ca0e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/mooc/test/tv/codely/mooc/students/domain/StudentIdMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.students.domain; + +import tv.codely.shared.domain.UuidMother; + +public final class StudentIdMother { + public static StudentId create(String value) { + return new StudentId(value); + } + + public static StudentId random() { + return create(UuidMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/build.gradle b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/build.gradle new file mode 100644 index 0000000..7d82dc7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/AggregateRoot.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/AggregateRoot.java new file mode 100644 index 0000000..796e327 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/AggregateRoot.java @@ -0,0 +1,23 @@ +package tv.codely.shared.domain; + +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class AggregateRoot { + private List domainEvents = new ArrayList<>(); + + final public List pullDomainEvents() { + List events = domainEvents; + + domainEvents = Collections.emptyList(); + + return events; + } + + final protected void record(DomainEvent event) { + domainEvents.add(event); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/DomainError.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/DomainError.java new file mode 100644 index 0000000..1d65456 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/DomainError.java @@ -0,0 +1,21 @@ +package tv.codely.shared.domain; + +public abstract class DomainError extends RuntimeException { + private final String errorCode; + private final String errorMessage; + + public DomainError(String errorCode, String errorMessage) { + super(errorMessage); + + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } + + public String errorCode() { + return errorCode; + } + + public String errorMessage() { + return errorMessage; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Identifier.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Identifier.java new file mode 100644 index 0000000..b25970b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Identifier.java @@ -0,0 +1,44 @@ +package tv.codely.shared.domain; + +import java.io.Serializable; +import java.util.Objects; +import java.util.UUID; + +public abstract class Identifier implements Serializable { + final protected String value; + + public Identifier(String value) { + ensureValidUuid(value); + + this.value = value; + } + + protected Identifier() { + this.value = null; + } + + public String value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Identifier that = (Identifier) o; + return value.equals(that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + private void ensureValidUuid(String value) throws IllegalArgumentException { + UUID.fromString(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/IntValueObject.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/IntValueObject.java new file mode 100644 index 0000000..4e943e5 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/IntValueObject.java @@ -0,0 +1,32 @@ +package tv.codely.shared.domain; + +import java.util.Objects; + +public abstract class IntValueObject { + private Integer value; + + public IntValueObject(Integer value) { + this.value = value; + } + + public Integer value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + IntValueObject that = (IntValueObject) o; + return value.equals(that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Logger.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Logger.java new file mode 100644 index 0000000..efeab9f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Logger.java @@ -0,0 +1,15 @@ +package tv.codely.shared.domain; + +import java.io.Serializable; +import java.util.HashMap; + +public interface Logger { + void info(String $message); + void info(String $message, HashMap $context); + + void warning(String $message); + void warning(String $message, HashMap $context); + + void critical(String $message); + void critical(String $message, HashMap $context); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Monitoring.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Monitoring.java new file mode 100644 index 0000000..0958579 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Monitoring.java @@ -0,0 +1,13 @@ +package tv.codely.shared.domain; + +import java.util.HashMap; + +public interface Monitoring { + void incrementCounter(int times); + + void incrementGauge(int times); + void decrementGauge(int times); + void setGauge(int value); + + void observeHistogram(int value, HashMap labels); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Service.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Service.java new file mode 100644 index 0000000..d3f9566 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Service.java @@ -0,0 +1,9 @@ +package tv.codely.shared.domain; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface Service { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/StringValueObject.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/StringValueObject.java new file mode 100644 index 0000000..45525e5 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/StringValueObject.java @@ -0,0 +1,37 @@ +package tv.codely.shared.domain; + +import java.util.Objects; + +public abstract class StringValueObject { + private String value; + + public StringValueObject(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public String toString() { + return this.value(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof StringValueObject)) { + return false; + } + StringValueObject that = (StringValueObject) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Utils.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Utils.java new file mode 100644 index 0000000..a3a56a6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/Utils.java @@ -0,0 +1,81 @@ +package tv.codely.shared.domain; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.CaseFormat; + +import java.io.IOException; +import java.io.Serializable; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; + +public final class Utils { + public static String dateToString(LocalDateTime dateTime) { + return dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE); + } + + public static String dateToString(Timestamp timestamp) { + return dateToString(timestamp.toLocalDateTime()); + } + + public static String jsonEncode(HashMap map) { + try { + return new ObjectMapper().writeValueAsString(map); + } catch (JsonProcessingException e) { + return ""; + } + } + + public static String jsonEncode(Object map) { + try { + return new ObjectMapper().writeValueAsString(map); + } catch (JsonProcessingException e) { + return ""; + } + } + + public static HashMap jsonDecode(String body) { + try { + return new ObjectMapper().readValue(body, HashMap.class); + } catch (IOException e) { + return null; + } + } + + public static String toSnake(String text) { + return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, text); + } + + public static String toCamel(String text) { + return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, text); + } + + public static String toCamelFirstLower(String text) { + return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, text); + } + + public static void retry(int numberOfRetries, long waitTimeInMillis, Runnable operation) throws Exception { + for (int i = 0; i < numberOfRetries; i++) { + try { + operation.run(); + return; // Success, exit the method + } catch (Exception ex) { + System.out.println("Retry " + (i + 1) + "/" + numberOfRetries + " fail. Retrying…"); + if (i >= numberOfRetries - 1) { + throw ex; + } + + try { + Thread.sleep(waitTimeInMillis); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + + throw new Exception("Operation interrupted while retrying", ie); + } + } + } + } + +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/UuidGenerator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/UuidGenerator.java new file mode 100644 index 0000000..8348a24 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/UuidGenerator.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain; + +public interface UuidGenerator { + String generate(); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/VideoUrl.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/VideoUrl.java new file mode 100644 index 0000000..47aaccc --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/VideoUrl.java @@ -0,0 +1,11 @@ +package tv.codely.shared.domain; + +public final class VideoUrl extends StringValueObject { + public VideoUrl(String value) { + super(value); + } + + public VideoUrl() { + super(null); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/Command.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/Command.java new file mode 100644 index 0000000..da5a342 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/Command.java @@ -0,0 +1,4 @@ +package tv.codely.shared.domain.bus.command; + +public interface Command { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/CommandBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/CommandBus.java new file mode 100644 index 0000000..dabddf2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/CommandBus.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain.bus.command; + +public interface CommandBus { + void dispatch(Command command) throws CommandHandlerExecutionError; +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandler.java new file mode 100644 index 0000000..177e09b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandler.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain.bus.command; + +public interface CommandHandler { + void handle(T command); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandlerExecutionError.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandlerExecutionError.java new file mode 100644 index 0000000..60d0d74 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandlerExecutionError.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain.bus.command; + +public final class CommandHandlerExecutionError extends RuntimeException { + public CommandHandlerExecutionError(Throwable cause) { + super(cause); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/CommandNotRegisteredError.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/CommandNotRegisteredError.java new file mode 100644 index 0000000..2d3af85 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/command/CommandNotRegisteredError.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain.bus.command; + +public final class CommandNotRegisteredError extends Exception { + public CommandNotRegisteredError(Class command) { + super(String.format("The command <%s> hasn't a command handler associated", command.toString())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/event/DomainEvent.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/event/DomainEvent.java new file mode 100644 index 0000000..901955c --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/event/DomainEvent.java @@ -0,0 +1,52 @@ +package tv.codely.shared.domain.bus.event; + +import tv.codely.shared.domain.Utils; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.UUID; + +public abstract class DomainEvent { + private String aggregateId; + private String eventId; + private String occurredOn; + + public DomainEvent(String aggregateId) { + this.aggregateId = aggregateId; + this.eventId = UUID.randomUUID().toString(); + this.occurredOn = Utils.dateToString(LocalDateTime.now()); + } + + public DomainEvent(String aggregateId, String eventId, String occurredOn) { + this.aggregateId = aggregateId; + this.eventId = eventId; + this.occurredOn = occurredOn; + } + + protected DomainEvent() { + } + + public abstract String eventName(); + + public abstract HashMap toPrimitives(); + + public abstract DomainEvent fromPrimitives( + String aggregateId, + HashMap body, + String eventId, + String occurredOn + ); + + public String aggregateId() { + return aggregateId; + } + + public String eventId() { + return eventId; + } + + public String occurredOn() { + return occurredOn; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/event/DomainEventSubscriber.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/event/DomainEventSubscriber.java new file mode 100644 index 0000000..9895464 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/event/DomainEventSubscriber.java @@ -0,0 +1,10 @@ +package tv.codely.shared.domain.bus.event; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface DomainEventSubscriber { + Class[] value(); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/event/EventBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/event/EventBus.java new file mode 100644 index 0000000..cd13e0b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/event/EventBus.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain.bus.event; + +import java.util.List; + +public interface EventBus { + void publish(final List events); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/Query.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/Query.java new file mode 100644 index 0000000..cdc5477 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/Query.java @@ -0,0 +1,4 @@ +package tv.codely.shared.domain.bus.query; + +public interface Query { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/QueryBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/QueryBus.java new file mode 100644 index 0000000..197945f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/QueryBus.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain.bus.query; + +public interface QueryBus { + R ask(Query query) throws QueryHandlerExecutionError; +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandler.java new file mode 100644 index 0000000..4b56bd8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandler.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain.bus.query; + +public interface QueryHandler { + R handle(Q query); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandlerExecutionError.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandlerExecutionError.java new file mode 100644 index 0000000..2e5135b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandlerExecutionError.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain.bus.query; + +public final class QueryHandlerExecutionError extends RuntimeException { + public QueryHandlerExecutionError(Throwable cause) { + super(cause); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/QueryNotRegisteredError.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/QueryNotRegisteredError.java new file mode 100644 index 0000000..d2d380f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/QueryNotRegisteredError.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain.bus.query; + +public final class QueryNotRegisteredError extends Exception { + public QueryNotRegisteredError(Class query) { + super(String.format("The query <%s> hasn't a query handler associated", query.toString())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/Response.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/Response.java new file mode 100644 index 0000000..ac3eefc --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/bus/query/Response.java @@ -0,0 +1,4 @@ +package tv.codely.shared.domain.bus.query; + +public interface Response { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/course/CourseCreatedDomainEvent.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/course/CourseCreatedDomainEvent.java new file mode 100644 index 0000000..23181f0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/course/CourseCreatedDomainEvent.java @@ -0,0 +1,94 @@ +package tv.codely.shared.domain.course; + +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Objects; + +public final class CourseCreatedDomainEvent extends DomainEvent { + private final String name; + private final String duration; + + public CourseCreatedDomainEvent() { + super(null); + + this.name = null; + this.duration = null; + } + + public CourseCreatedDomainEvent(String aggregateId, String name, String duration) { + super(aggregateId); + + this.name = name; + this.duration = duration; + } + + public CourseCreatedDomainEvent( + String aggregateId, + String eventId, + String occurredOn, + String name, + String duration + ) { + super(aggregateId, eventId, occurredOn); + + this.name = name; + this.duration = duration; + } + + @Override + public String eventName() { + return "course.created"; + } + + @Override + public HashMap toPrimitives() { + return new HashMap() {{ + put("name", name); + put("duration", duration); + }}; + } + + @Override + public CourseCreatedDomainEvent fromPrimitives( + String aggregateId, + HashMap body, + String eventId, + String occurredOn + ) { + return new CourseCreatedDomainEvent( + aggregateId, + eventId, + occurredOn, + (String) body.get("name"), + (String) body.get("duration") + ); + } + + public String name() { + return name; + } + + public String duration() { + return duration; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CourseCreatedDomainEvent that = (CourseCreatedDomainEvent) o; + return name.equals(that.name) && + duration.equals(that.duration); + } + + @Override + public int hashCode() { + return Objects.hash(name, duration); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java new file mode 100644 index 0000000..2fb4097 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java @@ -0,0 +1,78 @@ +package tv.codely.shared.domain.course; + +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Objects; + +public final class CourseRenamedDomainEvent extends DomainEvent { + private final String name; + + public CourseRenamedDomainEvent() { + super(null); + + this.name = null; + } + + public CourseRenamedDomainEvent(String aggregateId, String name) { + super(aggregateId); + + this.name = name; + } + + public CourseRenamedDomainEvent( + String aggregateId, + String eventId, + String occurredOn, + String name + ) { + super(aggregateId, eventId, occurredOn); + + this.name = name; + } + + @Override + public String eventName() { + return "course.renamed"; + } + + @Override + public HashMap toPrimitives() { + return new HashMap<>() {{ + put("name", name); + }}; + } + + @Override + public CourseRenamedDomainEvent fromPrimitives( + String aggregateId, + HashMap body, + String eventId, + String occurredOn + ) { + return new CourseRenamedDomainEvent( + aggregateId, + eventId, + occurredOn, + (String) body.get("name") + ); + } + + public String name() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CourseRenamedDomainEvent that = (CourseRenamedDomainEvent) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/Criteria.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/Criteria.java new file mode 100644 index 0000000..aea18af --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/Criteria.java @@ -0,0 +1,54 @@ +package tv.codely.shared.domain.criteria; + +import java.util.Optional; + +public final class Criteria { + private final Filters filters; + private final Order order; + private final Optional limit; + private final Optional offset; + + public Criteria(Filters filters, Order order, Optional limit, Optional offset) { + this.filters = filters; + this.order = order; + this.limit = limit; + this.offset = offset; + } + + public Criteria(Filters filters, Order order) { + this.filters = filters; + this.order = order; + this.limit = Optional.empty(); + this.offset = Optional.empty(); + } + + public Filters filters() { + return filters; + } + + public Order order() { + return order; + } + + public Optional limit() { + return limit; + } + + public Optional offset() { + return offset; + } + + public boolean hasFilters() { + return filters.filters().size() > 0; + } + + public String serialize() { + return String.format( + "%s~~%s~~%s~~%s", + filters.serialize(), + order.serialize(), + offset.orElse(0), + limit.orElse(0) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/Filter.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/Filter.java new file mode 100644 index 0000000..b3244a6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/Filter.java @@ -0,0 +1,47 @@ +package tv.codely.shared.domain.criteria; + +import java.util.HashMap; + +public final class Filter { + private final FilterField field; + private final FilterOperator operator; + private final FilterValue value; + + public Filter(FilterField field, FilterOperator operator, FilterValue value) { + this.field = field; + this.operator = operator; + this.value = value; + } + + public static Filter create(String field, String operator, String value) { + return new Filter( + new FilterField(field), + FilterOperator.fromValue(operator.toUpperCase()), + new FilterValue(value) + ); + } + + public static Filter fromValues(HashMap values) { + return new Filter( + new FilterField(values.get("field")), + FilterOperator.fromValue(values.get("operator")), + new FilterValue(values.get("value")) + ); + } + + public FilterField field() { + return field; + } + + public FilterOperator operator() { + return operator; + } + + public FilterValue value() { + return value; + } + + public String serialize() { + return String.format("%s.%s.%s", field.value(), operator.value(), value.value()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/FilterField.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/FilterField.java new file mode 100644 index 0000000..c5f230d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/FilterField.java @@ -0,0 +1,9 @@ +package tv.codely.shared.domain.criteria; + +import tv.codely.shared.domain.StringValueObject; + +public final class FilterField extends StringValueObject { + public FilterField(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/FilterOperator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/FilterOperator.java new file mode 100644 index 0000000..8614f12 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/FilterOperator.java @@ -0,0 +1,36 @@ +package tv.codely.shared.domain.criteria; + +public enum FilterOperator { + EQUAL("="), + NOT_EQUAL("!="), + GT(">"), + LT("<"), + CONTAINS("CONTAINS"), + NOT_CONTAINS("NOT_CONTAINS"); + + private final String operator; + + FilterOperator(String operator) { + this.operator = operator; + } + + public static FilterOperator fromValue(String value) { + switch (value) { + case "=": return FilterOperator.EQUAL; + case "!=": return FilterOperator.NOT_EQUAL; + case ">": return FilterOperator.GT; + case "<": return FilterOperator.LT; + case "CONTAINS": return FilterOperator.CONTAINS; + case "NOT_CONTAINS": return FilterOperator.NOT_CONTAINS; + default: return null; + } + } + + public boolean isPositive() { + return this != NOT_EQUAL && this != NOT_CONTAINS; + } + + public String value() { + return operator; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/FilterValue.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/FilterValue.java new file mode 100644 index 0000000..28a4f48 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/FilterValue.java @@ -0,0 +1,9 @@ +package tv.codely.shared.domain.criteria; + +import tv.codely.shared.domain.StringValueObject; + +public final class FilterValue extends StringValueObject { + public FilterValue(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/Filters.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/Filters.java new file mode 100644 index 0000000..83e36ae --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/Filters.java @@ -0,0 +1,30 @@ +package tv.codely.shared.domain.criteria; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +public final class Filters { + private final List filters; + + public Filters(List filters) { + this.filters = filters; + } + + public static Filters fromValues(List> filters) { + return new Filters(filters.stream().map(Filter::fromValues).collect(Collectors.toList())); + } + + public static Filters none() { + return new Filters(Collections.emptyList()); + } + + public List filters() { + return filters; + } + + public String serialize() { + return filters.stream().map(Filter::serialize).collect(Collectors.joining("^")); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/Order.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/Order.java new file mode 100644 index 0000000..f95eaf8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/Order.java @@ -0,0 +1,46 @@ +package tv.codely.shared.domain.criteria; + +import java.util.Optional; + +public final class Order { + private final OrderBy orderBy; + private final OrderType orderType; + + public Order(OrderBy orderBy, OrderType orderType) { + this.orderBy = orderBy; + this.orderType = orderType; + } + + public static Order fromValues(Optional orderBy, Optional orderType) { + return orderBy.map(order -> new Order(new OrderBy(order), OrderType.valueOf(orderType.orElse("ASC")))) + .orElseGet(Order::none); + } + + public static Order none() { + return new Order(new OrderBy(""), OrderType.NONE); + } + + public static Order desc(String orderBy) { + return new Order(new OrderBy(orderBy), OrderType.DESC); + } + + public static Order asc(String orderBy) { + return new Order(new OrderBy(orderBy), OrderType.ASC); + } + + public OrderBy orderBy() { + return orderBy; + } + + public OrderType orderType() { + return orderType; + } + + public boolean hasOrder() { + return !orderType.isNone(); + } + + public String serialize() { + return String.format("%s.%s", orderBy.value(), orderType.value()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/OrderBy.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/OrderBy.java new file mode 100644 index 0000000..2c1450d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/OrderBy.java @@ -0,0 +1,9 @@ +package tv.codely.shared.domain.criteria; + +import tv.codely.shared.domain.StringValueObject; + +public final class OrderBy extends StringValueObject { + public OrderBy(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/OrderType.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/OrderType.java new file mode 100644 index 0000000..52deae0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/domain/criteria/OrderType.java @@ -0,0 +1,25 @@ +package tv.codely.shared.domain.criteria; + +public enum OrderType { + ASC("asc"), + DESC("desc"), + NONE("none"); + private final String type; + + OrderType(String type) { + this.type = type; + } + + public boolean isNone() { + return this == NONE; + } + + public boolean isAsc() { + return this == ASC; + } + + public String value() { + return type; + } +} + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/JavaUuidGenerator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/JavaUuidGenerator.java new file mode 100644 index 0000000..21667a8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/JavaUuidGenerator.java @@ -0,0 +1,14 @@ +package tv.codely.shared.infrastructure; + +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.UuidGenerator; + +import java.util.UUID; + +@Service +public final class JavaUuidGenerator implements UuidGenerator { + @Override + public String generate() { + return UUID.randomUUID().toString(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/command/CommandHandlersInformation.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/command/CommandHandlersInformation.java new file mode 100644 index 0000000..5bdd8fc --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/command/CommandHandlersInformation.java @@ -0,0 +1,48 @@ +package tv.codely.shared.infrastructure.bus.command; + +import org.reflections.Reflections; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.command.Command; +import tv.codely.shared.domain.bus.command.CommandHandler; +import tv.codely.shared.domain.bus.command.CommandNotRegisteredError; + +import java.lang.reflect.ParameterizedType; +import java.util.HashMap; +import java.util.Set; + +@Service +public final class CommandHandlersInformation { + HashMap, Class> indexedCommandHandlers; + + public CommandHandlersInformation() { + Reflections reflections = new Reflections("tv.codely"); + Set> classes = reflections.getSubTypesOf(CommandHandler.class); + + indexedCommandHandlers = formatHandlers(classes); + } + + public Class search(Class commandClass) throws CommandNotRegisteredError { + Class commandHandlerClass = indexedCommandHandlers.get(commandClass); + + if (null == commandHandlerClass) { + throw new CommandNotRegisteredError(commandClass); + } + + return commandHandlerClass; + } + + private HashMap, Class> formatHandlers( + Set> commandHandlers + ) { + HashMap, Class> handlers = new HashMap<>(); + + for (Class handler : commandHandlers) { + ParameterizedType paramType = (ParameterizedType) handler.getGenericInterfaces()[0]; + Class commandClass = (Class) paramType.getActualTypeArguments()[0]; + + handlers.put(commandClass, handler); + } + + return handlers; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/command/InMemoryCommandBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/command/InMemoryCommandBus.java new file mode 100644 index 0000000..d8b23c7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/command/InMemoryCommandBus.java @@ -0,0 +1,32 @@ +package tv.codely.shared.infrastructure.bus.command; + +import org.springframework.context.ApplicationContext; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.command.Command; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.command.CommandHandler; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; + +@Service +public final class InMemoryCommandBus implements CommandBus { + private final CommandHandlersInformation information; + private final ApplicationContext context; + + public InMemoryCommandBus(CommandHandlersInformation information, ApplicationContext context) { + this.information = information; + this.context = context; + } + + @Override + public void dispatch(Command command) throws CommandHandlerExecutionError { + try { + Class commandHandlerClass = information.search(command.getClass()); + + CommandHandler handler = context.getBean(commandHandlerClass); + + handler.handle(command); + } catch (Throwable error) { + throw new CommandHandlerExecutionError(error); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonDeserializer.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonDeserializer.java new file mode 100644 index 0000000..ccfa660 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonDeserializer.java @@ -0,0 +1,46 @@ +package tv.codely.shared.infrastructure.bus.event; + +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; + +@Service +public final class DomainEventJsonDeserializer { + private final DomainEventsInformation information; + + public DomainEventJsonDeserializer(DomainEventsInformation information) { + this.information = information; + } + + public DomainEvent deserialize(String body) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException { + HashMap eventData = Utils.jsonDecode(body); + HashMap data = (HashMap) eventData.get("data"); + HashMap attributes = (HashMap) data.get("attributes"); + Class domainEventClass = information.forName((String) data.get("type")); + + DomainEvent nullInstance = domainEventClass.getConstructor().newInstance(); + + Method fromPrimitivesMethod = domainEventClass.getMethod( + "fromPrimitives", + String.class, + HashMap.class, + String.class, + String.class + ); + + Object domainEvent = fromPrimitivesMethod.invoke( + nullInstance, + (String) attributes.get("id"), + attributes, + (String) data.get("id"), + (String) data.get("occurred_on") + ); + + return (DomainEvent) domainEvent; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonSerializer.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonSerializer.java new file mode 100644 index 0000000..adbedb9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonSerializer.java @@ -0,0 +1,24 @@ +package tv.codely.shared.infrastructure.bus.event; + +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.io.Serializable; +import java.util.HashMap; + +public final class DomainEventJsonSerializer { + public static String serialize(DomainEvent domainEvent) { + HashMap attributes = domainEvent.toPrimitives(); + attributes.put("id", domainEvent.aggregateId()); + + return Utils.jsonEncode(new HashMap() {{ + put("data", new HashMap() {{ + put("id", domainEvent.eventId()); + put("type", domainEvent.eventName()); + put("occurred_on", domainEvent.occurredOn()); + put("attributes", attributes); + }}); + put("meta", new HashMap()); + }}); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscriberInformation.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscriberInformation.java new file mode 100644 index 0000000..e14d3d2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscriberInformation.java @@ -0,0 +1,49 @@ +package tv.codely.shared.infrastructure.bus.event; + +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.util.List; + +public final class DomainEventSubscriberInformation { + private final Class subscriberClass; + private final List> subscribedEvents; + + public DomainEventSubscriberInformation( + Class subscriberClass, + List> subscribedEvents + ) { + this.subscriberClass = subscriberClass; + this.subscribedEvents = subscribedEvents; + } + + public Class subscriberClass() { + return subscriberClass; + } + + public String contextName() { + String[] nameParts = subscriberClass.getName().split("\\."); + + return nameParts[2]; + } + + public String moduleName() { + String[] nameParts = subscriberClass.getName().split("\\."); + + return nameParts[3]; + } + + public String className() { + String[] nameParts = subscriberClass.getName().split("\\."); + + return nameParts[nameParts.length - 1]; + } + + public List> subscribedEvents() { + return subscribedEvents; + } + + public String formatRabbitMqQueueName() { + return String.format("codely.%s.%s.%s", contextName(), moduleName(), Utils.toSnake(className())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java new file mode 100644 index 0000000..88556f1 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java @@ -0,0 +1,54 @@ +package tv.codely.shared.infrastructure.bus.event; + +import org.reflections.Reflections; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Set; + +@Service +public final class DomainEventSubscribersInformation { + HashMap, DomainEventSubscriberInformation> information; + + public DomainEventSubscribersInformation(HashMap, DomainEventSubscriberInformation> information) { + this.information = information; + } + + public DomainEventSubscribersInformation() { + this(scanDomainEventSubscribers()); + } + + private static HashMap, DomainEventSubscriberInformation> scanDomainEventSubscribers() { + Reflections reflections = new Reflections("tv.codely"); + Set> subscribers = reflections.getTypesAnnotatedWith(DomainEventSubscriber.class); + + HashMap, DomainEventSubscriberInformation> subscribersInformation = new HashMap<>(); + + for (Class subscriberClass : subscribers) { + DomainEventSubscriber annotation = subscriberClass.getAnnotation(DomainEventSubscriber.class); + + subscribersInformation.put( + subscriberClass, + new DomainEventSubscriberInformation(subscriberClass, Arrays.asList(annotation.value())) + ); + } + + return subscribersInformation; + } + + public Collection all() { + return information.values(); + } + + public String[] rabbitMqFormattedNamesFor(String contextName) { + return information.values() + .stream() + .map(DomainEventSubscriberInformation::formatRabbitMqQueueName) + .distinct() + .filter(queueName -> queueName.contains("." + contextName + ".")) + .toArray(String[]::new); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventsInformation.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventsInformation.java new file mode 100644 index 0000000..9498913 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventsInformation.java @@ -0,0 +1,53 @@ +package tv.codely.shared.infrastructure.bus.event; + +import org.reflections.Reflections; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +@Service +public final class DomainEventsInformation { + HashMap> indexedDomainEvents; + + public DomainEventsInformation() { + Reflections reflections = new Reflections("tv.codely"); + Set> classes = reflections.getSubTypesOf(DomainEvent.class); + + try { + indexedDomainEvents = formatEvents(classes); + } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) { + e.printStackTrace(); + } + } + + public Class forName(String name) { + return indexedDomainEvents.get(name); + } + + public String forClass(Class domainEventClass) { + return indexedDomainEvents.entrySet() + .stream() + .filter(entry -> Objects.equals(entry.getValue(), domainEventClass)) + .map(Map.Entry::getKey) + .findFirst().orElse(""); + } + + private HashMap> formatEvents( + Set> domainEvents + ) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + HashMap> events = new HashMap<>(); + + for (Class domainEvent : domainEvents) { + DomainEvent nullInstance = domainEvent.getConstructor().newInstance(); + + events.put((String) domainEvent.getMethod("eventName").invoke(nullInstance), domainEvent); + } + + return events; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlDomainEventsConsumer.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlDomainEventsConsumer.java new file mode 100644 index 0000000..263da44 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlDomainEventsConsumer.java @@ -0,0 +1,95 @@ +package tv.codely.shared.infrastructure.bus.event.mysql; + +import jakarta.transaction.Transactional; +import org.hibernate.SessionFactory; +import org.hibernate.query.NativeQuery; +import org.springframework.beans.factory.annotation.Qualifier; +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.infrastructure.bus.event.DomainEventsInformation; +import tv.codely.shared.infrastructure.bus.event.spring.SpringApplicationEventBus; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Timestamp; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class MySqlDomainEventsConsumer { + private final SessionFactory sessionFactory; + private final DomainEventsInformation domainEventsInformation; + private final SpringApplicationEventBus bus; + private final Integer CHUNKS = 200; + private Boolean shouldStop = false; + + public MySqlDomainEventsConsumer( + @Qualifier("mooc-session_factory") SessionFactory sessionFactory, + DomainEventsInformation domainEventsInformation, + SpringApplicationEventBus bus + ) { + this.sessionFactory = sessionFactory; + this.domainEventsInformation = domainEventsInformation; + this.bus = bus; + } + + @Transactional + public void consume() { + while (!shouldStop) { + NativeQuery query = sessionFactory.getCurrentSession().createNativeQuery( + "SELECT * FROM domain_events ORDER BY occurred_on ASC LIMIT :chunk" + ); + + query.setParameter("chunk", CHUNKS); + + List events = query.list(); + + try { + for (Object[] event : events) { + executeSubscribers( + (String) event[0], + (String) event[1], + (String) event[2], + (String) event[3], + (Timestamp) event[4] + ); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) { + e.printStackTrace(); + } + + sessionFactory.getCurrentSession().clear(); + } + } + + public void stop() { + shouldStop = true; + } + + private void executeSubscribers( + String id, String aggregateId, String eventName, String body, Timestamp occurredOn + ) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + + Class domainEventClass = domainEventsInformation.forName(eventName); + + DomainEvent nullInstance = domainEventClass.getConstructor().newInstance(); + + Method fromPrimitivesMethod = domainEventClass.getMethod( + "fromPrimitives", + String.class, + HashMap.class, + String.class, + String.class + ); + + Object domainEvent = fromPrimitivesMethod.invoke( + nullInstance, + aggregateId, + Utils.jsonDecode(body), + id, + Utils.dateToString(occurredOn) + ); + + bus.publish(Collections.singletonList((DomainEvent) domainEvent)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlEventBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlEventBus.java new file mode 100644 index 0000000..231ab21 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlEventBus.java @@ -0,0 +1,45 @@ +package tv.codely.shared.infrastructure.bus.event.mysql; + +import org.hibernate.SessionFactory; +import org.hibernate.query.NativeQuery; +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.domain.bus.event.EventBus; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; + +public final class MySqlEventBus implements EventBus { + private final SessionFactory sessionFactory; + + public MySqlEventBus(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + @Override + public void publish(List events) { + events.forEach(this::publish); + } + + private void publish(DomainEvent domainEvent) { + String id = domainEvent.eventId(); + String aggregateId = domainEvent.aggregateId(); + String name = domainEvent.eventName(); + HashMap body = domainEvent.toPrimitives(); + String occurredOn = domainEvent.occurredOn(); + + NativeQuery query = sessionFactory.getCurrentSession().createNativeQuery( + "INSERT INTO domain_events (id, aggregate_id, name, body, occurred_on) " + + "VALUES (:id, :aggregateId, :name, :body, :occurredOn);" + ); + + query.setParameter("id", id) + .setParameter("aggregateId", aggregateId) + .setParameter("name", name) + .setParameter("body", Utils.jsonEncode(body)) + .setParameter("occurredOn", occurredOn); + + query.executeUpdate(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java new file mode 100644 index 0000000..1311278 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java @@ -0,0 +1,134 @@ +package tv.codely.shared.infrastructure.bus.event.rabbitmq; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; +import org.springframework.context.ApplicationContext; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.infrastructure.bus.event.DomainEventJsonDeserializer; +import tv.codely.shared.infrastructure.bus.event.DomainEventSubscribersInformation; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +@Service +public final class RabbitMqDomainEventsConsumer { + private final String CONSUMER_NAME = "domain_events_consumer"; + private final int MAX_RETRIES = 10; + private final DomainEventJsonDeserializer deserializer; + private final ApplicationContext context; + private final RabbitMqPublisher publisher; + private final HashMap domainEventSubscribers = new HashMap<>(); + RabbitListenerEndpointRegistry registry; + private DomainEventSubscribersInformation information; + private String contextName; + + public RabbitMqDomainEventsConsumer( + RabbitListenerEndpointRegistry registry, + DomainEventSubscribersInformation information, + DomainEventJsonDeserializer deserializer, + ApplicationContext context, + RabbitMqPublisher publisher + ) { + this.registry = registry; + this.information = information; + this.deserializer = deserializer; + this.context = context; + this.publisher = publisher; + } + + public void consume(String contextName) { + this.contextName = contextName; + + AbstractMessageListenerContainer container = (AbstractMessageListenerContainer) registry.getListenerContainer( + CONSUMER_NAME + ); + + container.addQueueNames(information.rabbitMqFormattedNamesFor(contextName)); + + container.start(); + } + + @RabbitListener(id = CONSUMER_NAME, autoStartup = "false") + public void consumer(Message message) throws Exception { + String serializedMessage = new String(message.getBody()); + DomainEvent domainEvent = deserializer.deserialize(serializedMessage); + + String queue = message.getMessageProperties().getConsumerQueue(); + + Object subscriber = domainEventSubscribers.containsKey(queue) + ? domainEventSubscribers.get(queue) + : subscriberFor(queue); + + Method subscriberOnMethod = subscriber.getClass().getMethod("on", domainEvent.getClass()); + + try { + subscriberOnMethod.invoke(subscriber, domainEvent); + } catch (Exception error) { + handleConsumptionError(message, queue); + } + } + + public void withSubscribersInformation(DomainEventSubscribersInformation information) { + this.information = information; + } + + private void handleConsumptionError(Message message, String queue) { + if (hasBeenRedeliveredTooMuch(message)) { + sendToDeadLetter(message, queue); + } else { + sendToRetry(message, queue); + } + } + + private void sendToRetry(Message message, String queue) { + System.out.println("SENDING TO RETRY: " + contextName + " - " + queue); + + sendMessageTo(RabbitMqExchangeNameFormatter.retry("domain_events"), message, queue); + } + + private void sendToDeadLetter(Message message, String queue) { + System.out.println("SENDING TO DEAD LETTER: " + contextName + " - " + queue); + + sendMessageTo(RabbitMqExchangeNameFormatter.deadLetter("domain_events"), message, queue); + } + + private void sendMessageTo(String exchange, Message message, String queue) { + Map headers = message.getMessageProperties().getHeaders(); + + headers.put("redelivery_count", (int) headers.getOrDefault("redelivery_count", 0) + 1); + + MessageBuilder.fromMessage(message).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentEncoding("utf-8") + .setContentType("application/json") + .copyHeaders(headers) + .build()); + + publisher.publish(message, exchange, queue); + } + + private boolean hasBeenRedeliveredTooMuch(Message message) { + return (int) message.getMessageProperties().getHeaders().getOrDefault("redelivery_count", 0) >= MAX_RETRIES; + } + + private Object subscriberFor(String queue) throws Exception { + String[] queueParts = queue.split("\\."); + String subscriberName = Utils.toCamelFirstLower(queueParts[queueParts.length - 1]); + + try { + Object subscriber = context.getBean(subscriberName); + domainEventSubscribers.put(queue, subscriber); + + return subscriber; + } catch (Exception e) { + throw new Exception(String.format("There are not registered subscribers for <%s> queue", queue)); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java new file mode 100644 index 0000000..2bad6fd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java @@ -0,0 +1,38 @@ +package tv.codely.shared.infrastructure.bus.event.rabbitmq; + +import org.springframework.amqp.AmqpException; +import org.springframework.context.annotation.Primary; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.domain.bus.event.EventBus; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; + +import java.util.Collections; +import java.util.List; + +@Primary +@Service +public class RabbitMqEventBus implements EventBus { + private final RabbitMqPublisher publisher; + private final MySqlEventBus failoverPublisher; + private final String exchangeName; + + public RabbitMqEventBus(RabbitMqPublisher publisher, MySqlEventBus failoverPublisher) { + this.publisher = publisher; + this.failoverPublisher = failoverPublisher; + this.exchangeName = "domain_events"; + } + + @Override + public void publish(List events) { + events.forEach(this::publish); + } + + private void publish(DomainEvent domainEvent) { + try { + this.publisher.publish(domainEvent, exchangeName); + } catch (AmqpException error) { + failoverPublisher.publish(Collections.singletonList(domainEvent)); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java new file mode 100644 index 0000000..f95e188 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java @@ -0,0 +1,134 @@ +package tv.codely.shared.infrastructure.bus.event.rabbitmq; + +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tv.codely.shared.infrastructure.bus.event.DomainEventSubscribersInformation; +import tv.codely.shared.infrastructure.bus.event.DomainEventsInformation; +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +@Configuration +public class RabbitMqEventBusConfiguration { + private final DomainEventSubscribersInformation domainEventSubscribersInformation; + private final DomainEventsInformation domainEventsInformation; + private final Parameter config; + private final String exchangeName; + + public RabbitMqEventBusConfiguration( + DomainEventSubscribersInformation domainEventSubscribersInformation, + DomainEventsInformation domainEventsInformation, + Parameter config + ) throws ParameterNotExist { + this.domainEventSubscribersInformation = domainEventSubscribersInformation; + this.domainEventsInformation = domainEventsInformation; + this.config = config; + this.exchangeName = config.get("RABBITMQ_EXCHANGE"); + } + + @Bean + public CachingConnectionFactory connection() throws ParameterNotExist { + CachingConnectionFactory factory = new CachingConnectionFactory(); + + factory.setHost(config.get("RABBITMQ_HOST")); + factory.setPort(config.getInt("RABBITMQ_PORT")); + factory.setUsername(config.get("RABBITMQ_LOGIN")); + factory.setPassword(config.get("RABBITMQ_PASSWORD")); + + return factory; + } + + @Bean + public Declarables declaration() { + String retryExchangeName = RabbitMqExchangeNameFormatter.retry(exchangeName); + String deadLetterExchangeName = RabbitMqExchangeNameFormatter.deadLetter(exchangeName); + + TopicExchange domainEventsExchange = new TopicExchange(exchangeName, true, false); + TopicExchange retryDomainEventsExchange = new TopicExchange(retryExchangeName, true, false); + TopicExchange deadLetterDomainEventsExchange = new TopicExchange(deadLetterExchangeName, true, false); + List declarables = new ArrayList<>(); + declarables.add(domainEventsExchange); + declarables.add(retryDomainEventsExchange); + declarables.add(deadLetterDomainEventsExchange); + + Collection queuesAndBindings = declareQueuesAndBindings( + domainEventsExchange, + retryDomainEventsExchange, + deadLetterDomainEventsExchange + ).stream().flatMap(Collection::stream).collect(Collectors.toList()); + + declarables.addAll(queuesAndBindings); + + return new Declarables(declarables); + } + + private Collection> declareQueuesAndBindings( + TopicExchange domainEventsExchange, + TopicExchange retryDomainEventsExchange, + TopicExchange deadLetterDomainEventsExchange + ) { + return domainEventSubscribersInformation.all().stream().map(information -> { + String queueName = RabbitMqQueueNameFormatter.format(information); + String retryQueueName = RabbitMqQueueNameFormatter.formatRetry(information); + String deadLetterQueueName = RabbitMqQueueNameFormatter.formatDeadLetter(information); + + Queue queue = QueueBuilder.durable(queueName).build(); + Queue retryQueue = QueueBuilder.durable(retryQueueName).withArguments( + retryQueueArguments(domainEventsExchange, queueName) + ).build(); + Queue deadLetterQueue = QueueBuilder.durable(deadLetterQueueName).build(); + + Binding fromExchangeSameQueueBinding = BindingBuilder + .bind(queue) + .to(domainEventsExchange) + .with(queueName); + + Binding fromRetryExchangeSameQueueBinding = BindingBuilder + .bind(retryQueue) + .to(retryDomainEventsExchange) + .with(queueName); + + Binding fromDeadLetterExchangeSameQueueBinding = BindingBuilder + .bind(deadLetterQueue) + .to(deadLetterDomainEventsExchange) + .with(queueName); + + List fromExchangeDomainEventsBindings = information.subscribedEvents().stream().map( + domainEventClass -> { + String eventName = domainEventsInformation.forClass(domainEventClass); + return BindingBuilder + .bind(queue) + .to(domainEventsExchange) + .with(eventName); + }).collect(Collectors.toList()); + + List queuesAndBindings = new ArrayList<>(); + queuesAndBindings.add(queue); + queuesAndBindings.add(fromExchangeSameQueueBinding); + queuesAndBindings.addAll(fromExchangeDomainEventsBindings); + + queuesAndBindings.add(retryQueue); + queuesAndBindings.add(fromRetryExchangeSameQueueBinding); + + queuesAndBindings.add(deadLetterQueue); + queuesAndBindings.add(fromDeadLetterExchangeSameQueueBinding); + + return queuesAndBindings; + }).collect(Collectors.toList()); + } + + private HashMap retryQueueArguments(TopicExchange exchange, String routingKey) { + return new HashMap() {{ + put("x-dead-letter-exchange", exchange.getName()); + put("x-dead-letter-routing-key", routingKey); + put("x-message-ttl", 3000); + }}; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqExchangeNameFormatter.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqExchangeNameFormatter.java new file mode 100644 index 0000000..f399cac --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqExchangeNameFormatter.java @@ -0,0 +1,11 @@ +package tv.codely.shared.infrastructure.bus.event.rabbitmq; + +public final class RabbitMqExchangeNameFormatter { + public static String retry(String exchangeName) { + return String.format("retry-%s", exchangeName); + } + + public static String deadLetter(String exchangeName) { + return String.format("dead_letter-%s", exchangeName); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqPublisher.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqPublisher.java new file mode 100644 index 0000000..61f7fe4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqPublisher.java @@ -0,0 +1,36 @@ +package tv.codely.shared.infrastructure.bus.event.rabbitmq; + +import org.springframework.amqp.AmqpException; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.infrastructure.bus.event.DomainEventJsonSerializer; + +@Service +public final class RabbitMqPublisher { + private final RabbitTemplate rabbitTemplate; + + public RabbitMqPublisher(RabbitTemplate rabbitTemplate) { + this.rabbitTemplate = rabbitTemplate; + } + + public void publish(DomainEvent domainEvent, String exchangeName) throws AmqpException { + String serializedDomainEvent = DomainEventJsonSerializer.serialize(domainEvent); + + Message message = new Message( + serializedDomainEvent.getBytes(), + MessagePropertiesBuilder.newInstance() + .setContentEncoding("utf-8") + .setContentType("application/json") + .build() + ); + + rabbitTemplate.send(exchangeName, domainEvent.eventName(), message); + } + + public void publish(Message domainEvent, String exchangeName, String routingKey) throws AmqpException { + rabbitTemplate.send(exchangeName, routingKey, domainEvent); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqQueueNameFormatter.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqQueueNameFormatter.java new file mode 100644 index 0000000..13c091f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqQueueNameFormatter.java @@ -0,0 +1,17 @@ +package tv.codely.shared.infrastructure.bus.event.rabbitmq; + +import tv.codely.shared.infrastructure.bus.event.DomainEventSubscriberInformation; + +public final class RabbitMqQueueNameFormatter { + public static String format(DomainEventSubscriberInformation information) { + return information.formatRabbitMqQueueName(); + } + + public static String formatRetry(DomainEventSubscriberInformation information) { + return String.format("retry.%s", format(information)); + } + + public static String formatDeadLetter(DomainEventSubscriberInformation information) { + return String.format("dead_letter.%s", format(information)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java new file mode 100644 index 0000000..56a6205 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java @@ -0,0 +1,26 @@ +package tv.codely.shared.infrastructure.bus.event.spring; + +import org.springframework.context.ApplicationEventPublisher; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.domain.bus.event.EventBus; + +import java.util.List; + +@Service +public class SpringApplicationEventBus implements EventBus { + private final ApplicationEventPublisher publisher; + + public SpringApplicationEventBus(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @Override + public void publish(final List events) { + events.forEach(this::publish); + } + + private void publish(final DomainEvent event) { + this.publisher.publishEvent(event); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/query/InMemoryQueryBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/query/InMemoryQueryBus.java new file mode 100644 index 0000000..8d448c6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/query/InMemoryQueryBus.java @@ -0,0 +1,29 @@ +package tv.codely.shared.infrastructure.bus.query; + +import org.springframework.context.ApplicationContext; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.*; + +@Service +public final class InMemoryQueryBus implements QueryBus { + private final QueryHandlersInformation information; + private final ApplicationContext context; + + public InMemoryQueryBus(QueryHandlersInformation information, ApplicationContext context) { + this.information = information; + this.context = context; + } + + @Override + public Response ask(Query query) throws QueryHandlerExecutionError { + try { + Class queryHandlerClass = information.search(query.getClass()); + + QueryHandler handler = context.getBean(queryHandlerClass); + + return handler.handle(query); + } catch (Throwable error) { + throw new QueryHandlerExecutionError(error); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/query/QueryHandlersInformation.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/query/QueryHandlersInformation.java new file mode 100644 index 0000000..e9d7606 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/bus/query/QueryHandlersInformation.java @@ -0,0 +1,48 @@ +package tv.codely.shared.infrastructure.bus.query; + +import org.reflections.Reflections; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.Query; +import tv.codely.shared.domain.bus.query.QueryHandler; +import tv.codely.shared.domain.bus.query.QueryNotRegisteredError; + +import java.lang.reflect.ParameterizedType; +import java.util.HashMap; +import java.util.Set; + +@Service +public final class QueryHandlersInformation { + HashMap, Class> indexedQueryHandlers; + + public QueryHandlersInformation() { + Reflections reflections = new Reflections("tv.codely"); + Set> classes = reflections.getSubTypesOf(QueryHandler.class); + + indexedQueryHandlers = formatHandlers(classes); + } + + public Class search(Class queryClass) throws QueryNotRegisteredError { + Class queryHandlerClass = indexedQueryHandlers.get(queryClass); + + if (null == queryHandlerClass) { + throw new QueryNotRegisteredError(queryClass); + } + + return queryHandlerClass; + } + + private HashMap, Class> formatHandlers( + Set> queryHandlers + ) { + HashMap, Class> handlers = new HashMap<>(); + + for (Class handler : queryHandlers) { + ParameterizedType paramType = (ParameterizedType) handler.getGenericInterfaces()[0]; + Class queryClass = (Class) paramType.getActualTypeArguments()[0]; + + handlers.put(queryClass, handler); + } + + return handlers; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java new file mode 100644 index 0000000..dd90a88 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java @@ -0,0 +1,25 @@ +package tv.codely.shared.infrastructure.cli; + +import tv.codely.shared.domain.Service; + +@Service +public abstract class ConsoleCommand { + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_RED = "\u001B[31m"; + private static final String ANSI_CYAN = "\u001B[36m"; + private static final String ANSI_GREEN = "\u001B[32m"; + + abstract public void execute(String[] args); + + protected void log(String text) { + System.out.println(String.format("%s%s%s", ANSI_GREEN, text, ANSI_RESET)); + } + + protected void info(String text) { + System.out.println(String.format("%s%s%s", ANSI_CYAN, text, ANSI_RESET)); + } + + protected void error(String text) { + System.out.println(String.format("%s%s%s", ANSI_RED, text, ANSI_RESET)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/config/EnvironmentConfig.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/config/EnvironmentConfig.java new file mode 100644 index 0000000..c9d898f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/config/EnvironmentConfig.java @@ -0,0 +1,27 @@ +package tv.codely.shared.infrastructure.config; + +import io.github.cdimascio.dotenv.Dotenv; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +@Configuration +public class EnvironmentConfig { + ResourceLoader resourceLoader; + + public EnvironmentConfig(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Bean + public Dotenv dotenv() { + Resource resource = resourceLoader.getResource("classpath:/.env.local"); + + return Dotenv + .configure() + .directory("/") + .filename(resource.exists() ? ".env.local" : ".env") + .load(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/config/Parameter.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/config/Parameter.java new file mode 100644 index 0000000..ac521bb --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/config/Parameter.java @@ -0,0 +1,29 @@ +package tv.codely.shared.infrastructure.config; + +import io.github.cdimascio.dotenv.Dotenv; +import tv.codely.shared.domain.Service; + +@Service +public final class Parameter { + private final Dotenv dotenv; + + public Parameter(Dotenv dotenv) { + this.dotenv = dotenv; + } + + public String get(String key) throws ParameterNotExist { + String value = dotenv.get(key); + + if (null == value) { + throw new ParameterNotExist(key); + } + + return value; + } + + public Integer getInt(String key) throws ParameterNotExist { + String value = get(key); + + return Integer.parseInt(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/config/ParameterNotExist.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/config/ParameterNotExist.java new file mode 100644 index 0000000..aa329d1 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/config/ParameterNotExist.java @@ -0,0 +1,7 @@ +package tv.codely.shared.infrastructure.config; + +public final class ParameterNotExist extends Throwable { + public ParameterNotExist(String key) { + super(String.format("The parameter <%s> does not exist in the environment file", key)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchClient.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchClient.java new file mode 100644 index 0000000..78a458f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchClient.java @@ -0,0 +1,49 @@ +package tv.codely.shared.infrastructure.elasticsearch; + +import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; + +import java.io.IOException; +import java.io.Serializable; +import java.util.HashMap; + +public final class ElasticsearchClient { + private final RestHighLevelClient highLevelClient; + private final RestClient lowLevelClient; + private final String indexPrefix; + + public ElasticsearchClient(RestHighLevelClient highLevelClient, RestClient lowLevelClient, String indexPrefix) { + this.highLevelClient = highLevelClient; + this.lowLevelClient = lowLevelClient; + this.indexPrefix = indexPrefix; + } + + public RestHighLevelClient highLevelClient() { + return highLevelClient; + } + + public RestClient lowLevelClient() { + return lowLevelClient; + } + + public String indexPrefix() { + return indexPrefix; + } + + public void persist(String moduleName, String id, HashMap plainBody) throws IOException { + IndexRequest request = new IndexRequest(indexFor(moduleName), moduleName, id).source(plainBody); + + highLevelClient().index(request, RequestOptions.DEFAULT); + } + + public String indexFor(String moduleName) { + return String.format("%s_%s", indexPrefix(), moduleName); + } + + public void delete(String index) throws IOException { + highLevelClient.indices().delete(new DeleteIndexRequest(index), RequestOptions.DEFAULT); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchCriteriaConverter.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchCriteriaConverter.java new file mode 100644 index 0000000..99e2585 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchCriteriaConverter.java @@ -0,0 +1,92 @@ +package tv.codely.shared.infrastructure.elasticsearch; + +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.sort.SortOrder; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.domain.criteria.Filter; +import tv.codely.shared.domain.criteria.FilterOperator; +import tv.codely.shared.domain.criteria.Filters; + +import java.util.HashMap; +import java.util.function.Function; + +public final class ElasticsearchCriteriaConverter { + private final HashMap> queryTransformers = new HashMap>() {{ + put(FilterOperator.EQUAL, ElasticsearchCriteriaConverter.this::termQuery); + put(FilterOperator.NOT_EQUAL, ElasticsearchCriteriaConverter.this::termQuery); + put(FilterOperator.GT, ElasticsearchCriteriaConverter.this::greaterThanQueryTransformer); + put(FilterOperator.LT, ElasticsearchCriteriaConverter.this::lowerThanQueryTransformer); + put(FilterOperator.CONTAINS, ElasticsearchCriteriaConverter.this::wildcardTransformer); + put(FilterOperator.NOT_CONTAINS, ElasticsearchCriteriaConverter.this::wildcardTransformer); + }}; + + public SearchSourceBuilder convert(Criteria criteria) { + SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); + + sourceBuilder.from(criteria.offset().orElse(0)); + sourceBuilder.size(criteria.limit().orElse(1000)); + + if (criteria.order().hasOrder()) { + sourceBuilder.sort( + criteria.order().orderBy().value(), + SortOrder.fromString(criteria.order().orderType().value()) + ); + } + + if (criteria.hasFilters()) { + QueryBuilder queryBuilder = generateQueryBuilder(criteria.filters()); + + sourceBuilder.query(queryBuilder); + } + + return sourceBuilder; + } + + private QueryBuilder generateQueryBuilder(Filters filters) { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + + for (Filter filter : filters.filters()) { + QueryBuilder query = queryForFilter(filter); + + if (isPositiveOperator(filter.operator())) { + boolQueryBuilder.must(query); + } else { + boolQueryBuilder.mustNot(query); + } + } + + return boolQueryBuilder; + } + + private boolean isPositiveOperator(FilterOperator operator) { + return operator.isPositive(); + } + + private QueryBuilder queryForFilter(Filter filter) { + Function transformer = queryTransformers.get(filter.operator()); + + return transformer.apply(filter); + } + + private QueryBuilder termQuery(Filter filter) { + return QueryBuilders.termQuery(filter.field().value(), filter.value().value().toLowerCase()); + } + + private QueryBuilder greaterThanQueryTransformer(Filter filter) { + return QueryBuilders.rangeQuery(filter.field().value()).gt(filter.value().value().toLowerCase()); + } + + private QueryBuilder lowerThanQueryTransformer(Filter filter) { + return QueryBuilders.rangeQuery(filter.field().value()).lt(filter.value().value().toLowerCase()); + } + + private QueryBuilder wildcardTransformer(Filter filter) { + return QueryBuilders.wildcardQuery( + filter.field().value(), + String.format("*%s*", filter.value().value().toLowerCase()) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java new file mode 100644 index 0000000..c87903b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java @@ -0,0 +1,79 @@ +package tv.codely.shared.infrastructure.elasticsearch; + +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import tv.codely.shared.domain.criteria.Criteria; + +import java.io.IOException; +import java.io.Serializable; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +public abstract class ElasticsearchRepository { + private final ElasticsearchClient client; + private final ElasticsearchCriteriaConverter criteriaConverter; + + public ElasticsearchRepository(ElasticsearchClient client) { + this.client = client; + this.criteriaConverter = new ElasticsearchCriteriaConverter(); + } + + abstract protected String moduleName(); + + protected List searchAllInElastic(Function, T> unserializer) { + return searchAllInElastic(unserializer, new SearchSourceBuilder()); + } + + protected Optional searchById(String id, Function, T> unserializer) { + GetRequest request = new GetRequest(client.indexFor(moduleName()), "_doc", id); + + try { + GetResponse getResponse = client.highLevelClient().get(request, RequestOptions.DEFAULT); + + if (!getResponse.isExists()) { + return Optional.empty(); + } + + return Optional.of(unserializer.apply(getResponse.getSourceAsMap())); + } catch (IOException e) { + e.printStackTrace(); + return Optional.empty(); + } + } + + + protected List searchAllInElastic( + Function, T> unserializer, + SearchSourceBuilder sourceBuilder + ) { + SearchRequest request = new SearchRequest(client.indexFor(moduleName())).source(sourceBuilder); + try { + SearchResponse response = client.highLevelClient().search(request, RequestOptions.DEFAULT); + + return Arrays.stream(response.getHits().getHits()) + .map(hit -> unserializer.apply(hit.getSourceAsMap())) + .collect(Collectors.toList()); + } catch (IOException e) { + e.printStackTrace(); + } + + return Collections.emptyList(); + } + + protected List searchByCriteria(Criteria criteria, Function, T> unserializer) { + return searchAllInElastic(unserializer, criteriaConverter.convert(criteria)); + } + + protected void persist(String id, HashMap plainBody) { + try { + client.persist(moduleName(), id, plainBody); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateConfigurationFactory.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateConfigurationFactory.java new file mode 100644 index 0000000..2dbd1ba --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateConfigurationFactory.java @@ -0,0 +1,137 @@ +package tv.codely.shared.infrastructure.hibernate; + +import org.apache.tomcat.dbcp.dbcp2.BasicDataSource; +import org.hibernate.cfg.AvailableSettings; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.orm.hibernate5.HibernateTransactionManager; +import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; +import tv.codely.shared.domain.Service; + +import javax.sql.DataSource; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public final class HibernateConfigurationFactory { + private final ResourcePatternResolver resourceResolver; + + public HibernateConfigurationFactory(ResourcePatternResolver resourceResolver) { + this.resourceResolver = resourceResolver; + } + + public PlatformTransactionManager hibernateTransactionManager(LocalSessionFactoryBean sessionFactory) { + HibernateTransactionManager transactionManager = new HibernateTransactionManager(); + transactionManager.setSessionFactory(sessionFactory.getObject()); + + return transactionManager; + } + + public LocalSessionFactoryBean sessionFactory(String contextName, DataSource dataSource) { + LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); + sessionFactory.setDataSource(dataSource); + sessionFactory.setHibernateProperties(hibernateProperties()); + + List mappingFiles = searchMappingFiles(contextName); + + sessionFactory.setMappingLocations(mappingFiles.toArray(new Resource[mappingFiles.size()])); + + return sessionFactory; + } + + public DataSource dataSource( + String host, + Integer port, + String databaseName, + String username, + String password + ) throws IOException { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); + dataSource.setUrl( + String.format( + "jdbc:mysql://%s:%s/%s?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC", + host, + port, + databaseName + ) + ); + dataSource.setUsername(username); + dataSource.setPassword(password); + + Resource mysqlResource = resourceResolver.getResource(String.format( + "classpath:database/%s.sql", + databaseName + )); + String mysqlSentences = new Scanner(mysqlResource.getInputStream(), StandardCharsets.UTF_8).useDelimiter("\\A").next(); + + dataSource.setConnectionInitSqls(new ArrayList<>(Arrays.asList(mysqlSentences.split(";")))); + + return dataSource; + } + + private List searchMappingFiles(String contextName) { + List modules = subdirectoriesFor(contextName); + List goodPaths = new ArrayList<>(); + + for (String module : modules) { + String[] files = mappingFilesIn(module + "/infrastructure/persistence/hibernate/"); + + for (String file : files) { + goodPaths.add(module + "/infrastructure/persistence/hibernate/" + file); + } + } + + return goodPaths.stream().map(FileSystemResource::new).collect(Collectors.toList()); + } + + private List subdirectoriesFor(String contextName) { + String path = "./src/" + contextName + "/main/tv/codely/" + contextName + "/"; + + String[] files = new File(path).list((current, name) -> new File(current, name).isDirectory()); + + if (null == files) { + path = "./main/tv/codely/" + contextName + "/"; + files = new File(path).list((current, name) -> new File(current, name).isDirectory()); + } + + if (null == files) { + return Collections.emptyList(); + } + + String finalPath = path; + + return Arrays.stream(files).map(file -> finalPath + file).collect(Collectors.toList()); + } + + private String[] mappingFilesIn(String path) { + List fileList = new ArrayList<>(); + + String[] hbmFiles = new File(path).list((current, name) -> new File(current, name).getName().contains(".hbm.xml")); + String[] ormFiles = new File(path).list((current, name) -> new File(current, name).getName().contains(".orm.xml")); + + if (hbmFiles != null) { + fileList.addAll(Arrays.asList(hbmFiles)); + } + if (ormFiles != null) { + fileList.addAll(Arrays.asList(ormFiles)); + } + + return fileList.toArray(new String[0]); + } + + private Properties hibernateProperties() { + Properties hibernateProperties = new Properties(); + hibernateProperties.put(AvailableSettings.HBM2DDL_AUTO, "none"); + hibernateProperties.put(AvailableSettings.SHOW_SQL, "false"); + hibernateProperties.put(AvailableSettings.DIALECT, "org.hibernate.dialect.MySQLDialect"); + hibernateProperties.put(AvailableSettings.TRANSFORM_HBM_XML, true); + + return hibernateProperties; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateCriteriaConverter.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateCriteriaConverter.java new file mode 100644 index 0000000..5679bbd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateCriteriaConverter.java @@ -0,0 +1,85 @@ +package tv.codely.shared.infrastructure.hibernate; + +import jakarta.persistence.criteria.*; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.domain.criteria.Filter; +import tv.codely.shared.domain.criteria.FilterOperator; + +import java.util.HashMap; +import java.util.List; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +public final class HibernateCriteriaConverter { + private final CriteriaBuilder builder; + private final HashMap, Predicate>> predicateTransformers = new HashMap, Predicate>>() {{ + put(FilterOperator.EQUAL, HibernateCriteriaConverter.this::equalsPredicateTransformer); + put(FilterOperator.NOT_EQUAL, HibernateCriteriaConverter.this::notEqualsPredicateTransformer); + put(FilterOperator.GT, HibernateCriteriaConverter.this::greaterThanPredicateTransformer); + put(FilterOperator.LT, HibernateCriteriaConverter.this::lowerThanPredicateTransformer); + put(FilterOperator.CONTAINS, HibernateCriteriaConverter.this::containsPredicateTransformer); + put(FilterOperator.NOT_CONTAINS, HibernateCriteriaConverter.this::notContainsPredicateTransformer); + }}; + + public HibernateCriteriaConverter(CriteriaBuilder builder) { + this.builder = builder; + } + + public CriteriaQuery convert(Criteria criteria, Class aggregateClass) { + CriteriaQuery hibernateCriteria = builder.createQuery(aggregateClass); + Root root = hibernateCriteria.from(aggregateClass); + + hibernateCriteria.where(formatPredicates(criteria.filters().filters(), root)); + + if (criteria.order().hasOrder()) { + Path orderBy = root.get(criteria.order().orderBy().value()); + Order order = criteria.order().orderType().isAsc() ? builder.asc(orderBy) : builder.desc(orderBy); + + hibernateCriteria.orderBy(order); + } + + return hibernateCriteria; + } + + private Predicate[] formatPredicates(List filters, Root root) { + List predicates = filters.stream().map(filter -> formatPredicate( + filter, + root + )).collect(Collectors.toList()); + + Predicate[] predicatesArray = new Predicate[predicates.size()]; + predicatesArray = predicates.toArray(predicatesArray); + + return predicatesArray; + } + + private Predicate formatPredicate(Filter filter, Root root) { + BiFunction, Predicate> transformer = predicateTransformers.get(filter.operator()); + + return transformer.apply(filter, root); + } + + private Predicate equalsPredicateTransformer(Filter filter, Root root) { + return builder.equal(root.get(filter.field().value()), filter.value().value()); + } + + private Predicate notEqualsPredicateTransformer(Filter filter, Root root) { + return builder.notEqual(root.get(filter.field().value()), filter.value().value()); + } + + private Predicate greaterThanPredicateTransformer(Filter filter, Root root) { + return builder.greaterThan(root.get(filter.field().value()), filter.value().value()); + } + + private Predicate lowerThanPredicateTransformer(Filter filter, Root root) { + return builder.lessThan(root.get(filter.field().value()), filter.value().value()); + } + + private Predicate containsPredicateTransformer(Filter filter, Root root) { + return builder.like(root.get(filter.field().value()), String.format("%%%s%%", filter.value().value())); + } + + private Predicate notContainsPredicateTransformer(Filter filter, Root root) { + return builder.notLike(root.get(filter.field().value()), String.format("%%%s%%", filter.value().value())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java new file mode 100644 index 0000000..b6ae61e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java @@ -0,0 +1,49 @@ +package tv.codely.shared.infrastructure.hibernate; + +import jakarta.persistence.criteria.CriteriaQuery; +import org.hibernate.SessionFactory; +import tv.codely.shared.domain.Identifier; +import tv.codely.shared.domain.criteria.Criteria; + +import java.util.List; +import java.util.Optional; + +public abstract class HibernateRepository { + protected final SessionFactory sessionFactory; + protected final Class aggregateClass; + protected final HibernateCriteriaConverter criteriaConverter; + + public HibernateRepository(SessionFactory sessionFactory, Class aggregateClass) { + this.sessionFactory = sessionFactory; + this.aggregateClass = aggregateClass; + this.criteriaConverter = new HibernateCriteriaConverter<>(sessionFactory.getCriteriaBuilder()); + } + + protected void persist(T entity) { + sessionFactory.getCurrentSession().saveOrUpdate(entity); + sessionFactory.getCurrentSession().flush(); + sessionFactory.getCurrentSession().clear(); + } + + protected Optional byId(Identifier id) { + return Optional.ofNullable(sessionFactory.getCurrentSession().byId(aggregateClass).load(id)); + } + + protected Optional byId(String id) { + return Optional.ofNullable(sessionFactory.getCurrentSession().byId(aggregateClass).load(id)); + } + + protected List byCriteria(Criteria criteria) { + CriteriaQuery hibernateCriteria = criteriaConverter.convert(criteria, aggregateClass); + + return sessionFactory.getCurrentSession().createQuery(hibernateCriteria).getResultList(); + } + + protected List all() { + CriteriaQuery criteria = sessionFactory.getCriteriaBuilder().createQuery(aggregateClass); + + criteria.from(aggregateClass); + + return sessionFactory.getCurrentSession().createQuery(criteria).getResultList(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/hibernate/JsonListType.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/hibernate/JsonListType.java new file mode 100644 index 0000000..188dd98 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/hibernate/JsonListType.java @@ -0,0 +1,140 @@ +package tv.codely.shared.infrastructure.hibernate; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.usertype.DynamicParameterizedType; +import org.hibernate.usertype.UserType; + +import java.io.IOException; +import java.io.Serializable; +import java.io.StringWriter; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.*; + +public class JsonListType implements UserType, DynamicParameterizedType { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private JavaType valueType = null; + private Class classType = null; + + @Override + public int getSqlType() { + return Types.LONGVARCHAR; + } + + @Override + public Class returnedClass() { + return classType; + } + + @Override + public boolean equals(Object x, Object y) throws HibernateException { + return Objects.equals(x, y); + } + + @Override + public int hashCode(Object x) throws HibernateException { + return Objects.hashCode(x); + } + + @Override + public void nullSafeSet( + PreparedStatement st, + Object value, + int index, + SharedSessionContractImplementor session + ) throws HibernateException, SQLException { + nullSafeSet(st, value, index); + } + + @Override + public Object nullSafeGet( + ResultSet resultSet, + int index, + SharedSessionContractImplementor session, + Object owner + ) throws HibernateException, SQLException { + + String value = resultSet.getString(index).replace("\"value\"", "").replace("{:", "").replace("}", ""); + Object result = null; + if (valueType == null) { + throw new HibernateException("Value type not set."); + } + if (value != null && !value.equals("")) { + try { + result = OBJECT_MAPPER.readValue(value, valueType); + } catch (IOException e) { + throw new HibernateException("Exception deserializing value " + value, e); + } + } + return result; + } + + public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException { + StringWriter sw = new StringWriter(); + OBJECT_MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + + if (value == null) { + st.setNull(index, Types.VARCHAR); + } else { + try { + OBJECT_MAPPER.writeValue(sw, value); + st.setString(index, sw.toString()); + } catch (IOException e) { + throw new HibernateException("Exception serializing value " + value, e); + } + } + } + + @Override + public Object deepCopy(Object value) throws HibernateException { + if (value == null) { + return null; + } else if (valueType.isCollectionLikeType()) { + Object newValue = new ArrayList<>(); + Collection newValueCollection = (Collection) newValue; + newValueCollection.addAll((Collection) value); + return newValueCollection; + } + + return null; + } + + @Override + public boolean isMutable() { + return true; + } + + @Override + public Serializable disassemble(Object value) throws HibernateException { + return (Serializable) deepCopy(value); + } + + @Override + public Object assemble(Serializable cached, Object owner) throws HibernateException { + return deepCopy(cached); + } + + @Override + public Object replace(Object original, Object target, Object owner) throws HibernateException { + return deepCopy(original); + } + + @Override + public void setParameterValues(Properties parameters) { + try { + Class entityClass = Class.forName(parameters.getProperty("list_of")); + + valueType = OBJECT_MAPPER.getTypeFactory().constructCollectionType(ArrayList.class, entityClass); + classType = List.class; + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/spring/ApiController.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/spring/ApiController.java new file mode 100644 index 0000000..fafc2a8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/spring/ApiController.java @@ -0,0 +1,32 @@ +package tv.codely.shared.infrastructure.spring; + +import org.springframework.http.HttpStatus; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.Command; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; +import tv.codely.shared.domain.bus.query.Query; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; + +import java.util.HashMap; + +public abstract class ApiController { + private final QueryBus queryBus; + private final CommandBus commandBus; + + public ApiController(QueryBus queryBus, CommandBus commandBus) { + this.queryBus = queryBus; + this.commandBus = commandBus; + } + + protected void dispatch(Command command) throws CommandHandlerExecutionError { + commandBus.dispatch(command); + } + + protected R ask(Query query) throws QueryHandlerExecutionError { + return queryBus.ask(query); + } + + abstract public HashMap, HttpStatus> errorMapping(); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/spring/ApiExceptionMiddleware.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/spring/ApiExceptionMiddleware.java new file mode 100644 index 0000000..ac1db97 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/spring/ApiExceptionMiddleware.java @@ -0,0 +1,95 @@ +package tv.codely.shared.infrastructure.spring; + +import org.springframework.http.HttpStatus; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.NestedServletException; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Objects; + +public final class ApiExceptionMiddleware implements Filter { + private RequestMappingHandlerMapping mapping; + + public ApiExceptionMiddleware(RequestMappingHandlerMapping mapping) { + this.mapping = mapping; + } + + @Override + public void doFilter( + ServletRequest request, + ServletResponse response, + FilterChain chain + ) throws ServletException { + HttpServletRequest httpRequest = ((HttpServletRequest) request); + HttpServletResponse httpResponse = ((HttpServletResponse) response); + + try { + Object possibleController = ( + (HandlerMethod) Objects.requireNonNull( + mapping.getHandler(httpRequest)).getHandler() + ).getBean(); + + try { + chain.doFilter(request, response); + } catch (Exception exception) { + if (possibleController instanceof ApiController) { + handleCustomError(response, httpResponse, (ApiController) possibleController, exception); + } + } + } catch (Exception e) { + throw new ServletException(e); + } + } + + private void handleCustomError( + ServletResponse response, + HttpServletResponse httpResponse, + ApiController possibleController, + Exception exception + ) throws IOException { + HashMap, HttpStatus> errorMapping = possibleController + .errorMapping(); + Throwable error = ( + exception.getCause() instanceof CommandHandlerExecutionError || + exception.getCause() instanceof QueryHandlerExecutionError + ) + ? exception.getCause().getCause() : exception.getCause(); + + int statusCode = statusFor(errorMapping, error); + String errorCode = errorCodeFor(error); + String errorMessage = error.getMessage(); + + httpResponse.reset(); + httpResponse.setHeader("Content-Type", "application/json"); + httpResponse.setStatus(statusCode); + PrintWriter writer = response.getWriter(); + writer.write(String.format( + "{\"error_code\": \"%s\", \"message\": \"%s\"}", + errorCode, + errorMessage + )); + writer.close(); + } + + private String errorCodeFor(Throwable error) { + if (error instanceof DomainError) { + return ((DomainError) error).errorCode(); + } + + return Utils.toSnake(error.getClass().toString()); + } + + private int statusFor(HashMap, HttpStatus> errorMapping, Throwable error) { + return errorMapping.getOrDefault(error.getClass(), HttpStatus.INTERNAL_SERVER_ERROR).value(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/ValidationResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/ValidationResponse.java new file mode 100644 index 0000000..6137031 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/ValidationResponse.java @@ -0,0 +1,20 @@ +package tv.codely.shared.infrastructure.validation; + +import java.util.HashMap; +import java.util.List; + +public final class ValidationResponse { + private HashMap> validationErrors; + + public ValidationResponse(HashMap> validationErrors) { + this.validationErrors = validationErrors; + } + + public Boolean hasErrors() { + return !validationErrors.isEmpty(); + } + + public HashMap> errors() { + return validationErrors; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/Validator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/Validator.java new file mode 100644 index 0000000..d08cc93 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/Validator.java @@ -0,0 +1,46 @@ +package tv.codely.shared.infrastructure.validation; + +import tv.codely.shared.infrastructure.validation.validators.*; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class Validator { + private static final HashMap validators = new HashMap() {{ + put("required", new RequiredValidator()); + put("string", new StringValidator()); + put("not_empty", new NotEmptyValidator()); + put("uuid", new UuidValidator()); + }}; + + public static ValidationResponse validate( + HashMap input, + HashMap combinedRules + ) throws ValidatorNotExist { + HashMap> validationErrors = new HashMap<>(); + + for (Map.Entry entry : combinedRules.entrySet()) { + String[] rules = entry.getValue().split("\\|"); + + for (String rule : rules) { + FieldValidator validator = validators.get(rule); + + if (null == validator) { + throw new ValidatorNotExist(rule); + } + + if (!validator.isValid(entry.getKey(), input)) { + List existingErrors = validationErrors.getOrDefault(entry.getKey(), new ArrayList<>()); + existingErrors.add(validator.errorMessage(entry.getKey())); + + validationErrors.put(entry.getKey(), existingErrors); + } + } + } + + return new ValidationResponse(validationErrors); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/ValidatorNotExist.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/ValidatorNotExist.java new file mode 100644 index 0000000..c75d631 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/ValidatorNotExist.java @@ -0,0 +1,7 @@ +package tv.codely.shared.infrastructure.validation; + +public final class ValidatorNotExist extends Exception { + public ValidatorNotExist(String name) { + super(String.format("The validator <%s> does not exist", name)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/FieldValidator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/FieldValidator.java new file mode 100644 index 0000000..65ace91 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/FieldValidator.java @@ -0,0 +1,10 @@ +package tv.codely.shared.infrastructure.validation.validators; + +import java.io.Serializable; +import java.util.HashMap; + +public interface FieldValidator { + Boolean isValid(String fieldName, HashMap fields); + + String errorMessage(String fieldName); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/NotEmptyValidator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/NotEmptyValidator.java new file mode 100644 index 0000000..106e7ff --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/NotEmptyValidator.java @@ -0,0 +1,16 @@ +package tv.codely.shared.infrastructure.validation.validators; + +import java.io.Serializable; +import java.util.HashMap; + +public final class NotEmptyValidator implements FieldValidator { + @Override + public Boolean isValid(String fieldName, HashMap fields) { + return !fields.get(fieldName).toString().isEmpty(); + } + + @Override + public String errorMessage(String fieldName) { + return String.format("The field <%s> should not be empty", fieldName); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/RequiredValidator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/RequiredValidator.java new file mode 100644 index 0000000..862b5a2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/RequiredValidator.java @@ -0,0 +1,16 @@ +package tv.codely.shared.infrastructure.validation.validators; + +import java.io.Serializable; +import java.util.HashMap; + +public final class RequiredValidator implements FieldValidator { + @Override + public Boolean isValid(String fieldName, HashMap fields) { + return fields.containsKey(fieldName); + } + + @Override + public String errorMessage(String fieldName) { + return String.format("The field <%s> is required", fieldName); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/StringValidator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/StringValidator.java new file mode 100644 index 0000000..f653ac7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/StringValidator.java @@ -0,0 +1,16 @@ +package tv.codely.shared.infrastructure.validation.validators; + +import java.io.Serializable; +import java.util.HashMap; + +public final class StringValidator implements FieldValidator { + @Override + public Boolean isValid(String fieldName, HashMap fields) { + return true; + } + + @Override + public String errorMessage(String fieldName) { + return String.format("The field <%s> should be of type string", fieldName); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/UuidValidator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/UuidValidator.java new file mode 100644 index 0000000..c39d679 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/main/tv/codely/shared/infrastructure/validation/validators/UuidValidator.java @@ -0,0 +1,19 @@ +package tv.codely.shared.infrastructure.validation.validators; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.regex.Pattern; + +public final class UuidValidator implements FieldValidator { + @Override + public Boolean isValid(String fieldName, HashMap fields) { + Pattern uuidPattern = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"); + + return uuidPattern.matcher((String) fields.get(fieldName)).matches(); + } + + @Override + public String errorMessage(String fieldName) { + return String.format("The field <%s> is not a valid uuid", fieldName); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/EmailMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/EmailMother.java new file mode 100644 index 0000000..b8d4077 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/EmailMother.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain; + +public final class EmailMother { + public static String random() { + return MotherCreator.random().internet().emailAddress(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/IntegerMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/IntegerMother.java new file mode 100644 index 0000000..fe197ce --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/IntegerMother.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain; + +public final class IntegerMother { + public static Integer random() { + return MotherCreator.random().number().randomDigit(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/ListMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/ListMother.java new file mode 100644 index 0000000..5dc7622 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/ListMother.java @@ -0,0 +1,26 @@ +package tv.codely.shared.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +public final class ListMother { + public static List create(Integer size, Supplier creator) { + ArrayList list = new ArrayList<>(); + + for (int i = 0; i < size; i++) { + list.add(creator.get()); + } + + return list; + } + + public static List random(Supplier creator) { + return create(IntegerMother.random(), creator); + } + + public static List one(T element) { + return Collections.singletonList(element); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/MotherCreator.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/MotherCreator.java new file mode 100644 index 0000000..eef2ac4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/MotherCreator.java @@ -0,0 +1,11 @@ +package tv.codely.shared.domain; + +import com.github.javafaker.Faker; + +public final class MotherCreator { + private final static Faker faker = new Faker(); + + public static Faker random() { + return faker; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/RandomElementPicker.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/RandomElementPicker.java new file mode 100644 index 0000000..a69497e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/RandomElementPicker.java @@ -0,0 +1,12 @@ +package tv.codely.shared.domain; + +import java.util.Random; + +public final class RandomElementPicker { + @SafeVarargs + public static T from(T... elements) { + Random rand = new Random(); + + return elements[rand.nextInt(elements.length)]; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/UuidMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/UuidMother.java new file mode 100644 index 0000000..1075dd7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/UuidMother.java @@ -0,0 +1,9 @@ +package tv.codely.shared.domain; + +import java.util.UUID; + +public final class UuidMother { + public static String random() { + return UUID.randomUUID().toString(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/VideoUrlMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/VideoUrlMother.java new file mode 100644 index 0000000..1588ec6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/VideoUrlMother.java @@ -0,0 +1,11 @@ +package tv.codely.shared.domain; + +public final class VideoUrlMother { + public static VideoUrl create(String value) { + return new VideoUrl(value); + } + + public static VideoUrl random() { + return create(MotherCreator.random().internet().url()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/WordMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/WordMother.java new file mode 100644 index 0000000..4bad17a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/domain/WordMother.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain; + +public final class WordMother { + public static String random() { + return MotherCreator.random().lorem().word(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java new file mode 100644 index 0000000..af93e67 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java @@ -0,0 +1,28 @@ +package tv.codely.shared.infrastructure; + +public abstract class InfrastructureTestCase { + private final int MAX_ATTEMPTS = 3; + private final int MILLIS_TO_WAIT_BETWEEN_RETRIES = 700; + + protected void eventually(Runnable assertion) throws Exception { + int attempts = 0; + + while (true) { + try { + assertion.run(); + return; + } catch (Throwable error) { + attempts++; + + if (attempts >= MAX_ATTEMPTS) { + throw new Exception( + String.format("Could not assert after %d retries. Last error: %s", MAX_ATTEMPTS, error.getMessage()), + error + ); + } + + Thread.sleep(MILLIS_TO_WAIT_BETWEEN_RETRIES); + } + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/infrastructure/UnitTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/infrastructure/UnitTestCase.java new file mode 100644 index 0000000..96e007e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/src/shared/test/tv/codely/shared/infrastructure/UnitTestCase.java @@ -0,0 +1,45 @@ +package tv.codely.shared.infrastructure; + +import org.junit.jupiter.api.BeforeEach; +import tv.codely.shared.domain.UuidGenerator; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.domain.bus.event.EventBus; +import tv.codely.shared.domain.bus.query.*; + +import java.util.Collections; +import java.util.List; + +import static org.mockito.Mockito.*; + +public abstract class UnitTestCase { + protected EventBus eventBus; + protected QueryBus queryBus; + protected UuidGenerator uuidGenerator; + + @BeforeEach + protected void setUp() { + eventBus = mock(EventBus.class); + queryBus = mock(QueryBus.class); + uuidGenerator = mock(UuidGenerator.class); + } + + public void shouldHavePublished(List domainEvents) { + verify(eventBus, atLeastOnce()).publish(domainEvents); + } + + public void shouldHavePublished(DomainEvent domainEvent) { + shouldHavePublished(Collections.singletonList(domainEvent)); + } + + public void shouldGenerateUuid(String uuid) { + when(uuidGenerator.generate()).thenReturn(uuid); + } + + public void shouldGenerateUuids(String uuid, String... others) { + when(uuidGenerator.generate()).thenReturn(uuid, others); + } + + public void shouldAsk(Query query, Response response) { + when(queryBus.ask(query)).thenReturn(response); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/var/log/.gitkeep b/01-unordered_events/3-consume_unordered_courses_renamed/1-last_event_has_preference/var/log/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/.editorconfig b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/.editorconfig new file mode 100644 index 0000000..e6e6a12 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.java] +indent_size = 4 +indent_style = tab diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/.env b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/.env new file mode 100644 index 0000000..b934e03 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/.env @@ -0,0 +1 @@ +# See apps/main/resources/.env diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/.github/workflows/ci.yml b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/.github/workflows/ci.yml new file mode 100644 index 0000000..8dcdf5b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: 🐳 Start all the environment + run: make start + + - name: 🔦 Lint + run: make lint + + - name: 🦭 Wait for the environment to get up + run: | + while ! make ping-mysql &>/dev/null; do + echo "Waiting for database connection..." + sleep 2 + done + + - name: ✅ Run the tests + run: make test diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/.gitignore b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/.gitignore new file mode 100644 index 0000000..8d89f10 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/.gitignore @@ -0,0 +1,12 @@ +# Gradle +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +/var/log/* +!/var/log/.gitkeep + +.env.local +.env.*.local diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/Dockerfile b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/Dockerfile new file mode 100644 index 0000000..98f0b27 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/Dockerfile @@ -0,0 +1,15 @@ +FROM openjdk:21-slim-buster +WORKDIR /app + +RUN apt update && apt install -y curl git + +ENV NVM_DIR /usr/local/nvm +ENV NODE_VERSION 18.0.0 + +RUN mkdir -p $NVM_DIR +RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash + +RUN . $NVM_DIR/nvm.sh && nvm install $NODE_VERSION && nvm alias default $NODE_VERSION && nvm use default + +ENV NODE_PATH $NVM_DIR/versions/node/v$NODE_VERSION/lib/node_modules +ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/Makefile b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/Makefile new file mode 100644 index 0000000..4855cd1 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/Makefile @@ -0,0 +1,29 @@ +all: build + +start: + @docker compose -f docker-compose.ci.yml up -d + +build: + @./gradlew build --warning-mode all + +lint: + @docker exec codely-java_ddd_example-test_server ./gradlew spotlessCheck + +run-tests: + @./gradlew test --warning-mode all + +test: + @docker exec codely-java_ddd_example-test_server ./gradlew test --warning-mode all + +run: + @./gradlew :run + +ping-mysql: + @docker exec codely-java_ddd_example-mysql mysqladmin --user=root --password= --host "127.0.0.1" ping --silent + +# Start the app +start-mooc_backend: + @./gradlew bootRun --args='mooc_backend server' + +start-backoffice_frontend: + @./gradlew bootRun --args='backoffice_frontend server' diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/README.md b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/README.md new file mode 100644 index 0000000..6c6bb26 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/README.md @@ -0,0 +1,52 @@ +# ☕🚀 Java DDD example: Save the boilerplate in your new projects + + + + +> ⚡ Start your Java projects as fast as possible + +[![CodelyTV](https://img.shields.io/badge/codely-tv-green.svg?style=flat-square)](https://codely.tv) +[![CI pipeline status](https://github.com/CodelyTV/java-ddd-example/workflows/CI/badge.svg)](https://github.com/CodelyTV/java-ddd-example/actions) + +## ℹ️ Introduction + +This is a repository intended to serve as a starting point if you want to bootstrap a Java project with JUnit and Gradle. + +Here you have the [course on CodelyTV Pro where we explain step by step all this](https://pro.codely.tv/library/ddd-en-java/about/?utm_source=github&utm_medium=social&utm_campaign=readme) (Spanish) + +## 🏁 How To Start + +1. Install Java 11: `brew cask install corretto` +2. Set it as your default JVM: `export JAVA_HOME='/Library/Java/JavaVirtualMachines/amazon-corretto-11.jdk/Contents/Home'` +3. Clone this repository: `git clone https://github.com/CodelyTV/java-ddd-example`. +4. Bring up the Docker environment: `make up`. +5. Execute some [Gradle lifecycle tasks](https://docs.gradle.org/current/userguide/java_plugin.html#lifecycle_tasks) in order to check everything is OK: + 1. Create [the project JAR](https://docs.gradle.org/current/userguide/java_plugin.html#sec:jar): `make build` + 2. Run the tests and plugins verification tasks: `make test` +6. Start developing! + +## ☝️ How to update dependencies + +* Gradle ([releases](https://gradle.org/releases/)): `./gradlew wrapper --gradle-version=WANTED_VERSION --distribution-type=bin` + +## 💡 Related repositories + +### ☕ Java + +* 📂 [Java Basic example](https://github.com/CodelyTV/java-basic-example) +* ⚛ [Java OOP Examples](https://github.com/CodelyTV/java-oop-examples) +* 🧱 [Java SOLID Examples](https://github.com/CodelyTV/java-solid-examples) +* 🥦 [Java DDD Example](https://github.com/CodelyTV/java-ddd-example) + +### 🐘 PHP + +* 📂 [PHP Basic example](https://github.com/CodelyTV/php-basic-example) +* 🎩 [PHP DDD example](https://github.com/CodelyTV/php-ddd-example) +* 🥦 [PHP DDD Example](https://github.com/CodelyTV/php-ddd-example) + +### 🧬 Scala + +* 📂 [Scala Basic example](https://github.com/CodelyTV/scala-basic-example) +* ⚡ [Scala Basic example (g8 template)](https://github.com/CodelyTV/scala-basic-example.g8) +* ⚛ [Scala Examples](https://github.com/CodelyTV/scala-examples) +* 🥦 [Scala DDD Example](https://github.com/CodelyTV/scala-ddd-example) diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/.env b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/.env new file mode 100644 index 0000000..cd58c25 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/.env @@ -0,0 +1,34 @@ +# MOOC # +#--------------------------------# +MOOC_BACKEND_SERVER_PORT=8030 +# MySql +MOOC_DATABASE_HOST=codely-java_ddd_example-mysql +MOOC_DATABASE_PORT=3306 +MOOC_DATABASE_NAME=mooc +MOOC_DATABASE_USER=root +MOOC_DATABASE_PASSWORD= + +# BACKOFFICE # +#--------------------------------# +BACKOFFICE_BACKEND_SERVER_PORT=8040 +BACKOFFICE_FRONTEND_SERVER_PORT=8041 +# MySql +BACKOFFICE_DATABASE_HOST=codely-java_ddd_example-mysql +BACKOFFICE_DATABASE_PORT=3306 +BACKOFFICE_DATABASE_NAME=backoffice +BACKOFFICE_DATABASE_USER=root +BACKOFFICE_DATABASE_PASSWORD= +# Elasticsearch +BACKOFFICE_ELASTICSEARCH_HOST=codely-java_ddd_example-elasticsearch +BACKOFFICE_ELASTICSEARCH_PORT=9200 +BACKOFFICE_ELASTICSEARCH_INDEX_PREFIX=backoffice + +# COMMON # +#--------------------------------# +# RabbitMQ +RABBITMQ_HOST=codely-java_ddd_example-rabbitmq +RABBITMQ_PORT=5672 +RABBITMQ_LOGIN=codely +RABBITMQ_PASSWORD=c0d3ly +RABBITMQ_EXCHANGE=domain_events +RABBITMQ_MAX_RETRIES=5 diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/.no.env.local b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/.no.env.local new file mode 100644 index 0000000..8f100bc --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/.no.env.local @@ -0,0 +1,34 @@ +# MOOC # +#--------------------------------# +MOOC_BACKEND_SERVER_PORT=8030 +# MySql +MOOC_DATABASE_HOST=localhost +MOOC_DATABASE_PORT=3306 +MOOC_DATABASE_NAME=mooc +MOOC_DATABASE_USER=root +MOOC_DATABASE_PASSWORD= + +# BACKOFFICE # +#--------------------------------# +BACKOFFICE_BACKEND_SERVER_PORT=8040 +BACKOFFICE_FRONTEND_SERVER_PORT=8041 +# MySql +BACKOFFICE_DATABASE_HOST=localhost +BACKOFFICE_DATABASE_PORT=3306 +BACKOFFICE_DATABASE_NAME=backoffice +BACKOFFICE_DATABASE_USER=root +BACKOFFICE_DATABASE_PASSWORD= +# Elasticsearch +BACKOFFICE_ELASTICSEARCH_HOST=localhost +BACKOFFICE_ELASTICSEARCH_PORT=9200 +BACKOFFICE_ELASTICSEARCH_INDEX_PREFIX=backoffice + +# COMMON # +#--------------------------------# +# RabbitMQ +RABBITMQ_HOST=localhost +RABBITMQ_PORT=5672 +RABBITMQ_LOGIN=codely +RABBITMQ_PASSWORD=c0d3ly +RABBITMQ_EXCHANGE=domain_events +RABBITMQ_MAX_RETRIES=5 diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/application.properties b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/application.properties new file mode 100644 index 0000000..e439ebd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/application.properties @@ -0,0 +1 @@ +spring.main.allow-bean-definition-overriding=true diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/public/images/logo.png b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/public/images/logo.png new file mode 100644 index 0000000..7593959 Binary files /dev/null and b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/public/images/logo.png differ diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/master.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/master.ftl new file mode 100644 index 0000000..0a8bc23 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/master.ftl @@ -0,0 +1,27 @@ +<#import "spring.ftl" as spring /> + + + + + + + + + + ${title} + ${description} + + +<#include "partials/header.ftl"> + +
+

<@page_title/>

+ <@main/> +
+ +
+ +<#include "partials/footer.ftl"> + + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/pages/courses/courses.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/pages/courses/courses.ftl new file mode 100644 index 0000000..8b4c99c --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/pages/courses/courses.ftl @@ -0,0 +1,20 @@ +<#include "../../master.ftl"> + +<#macro page_title>Cursos + +<#macro main> +
+ Sunset in the mountains +
+
Cursos
+

+ Actualmente CodelyTV Pro cuenta con ${courses_counter} cursos. +

+
+
+ + <#include "partials/new_course_form.ftl"> +
+
+ <#include "partials/list_courses.ftl"> + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl new file mode 100644 index 0000000..aa64bee --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl @@ -0,0 +1,153 @@ +

Cursos existentes

+ + +
+
+ +
+
+
+ + + +
+
+ + + + + + + + + + + +
+ Id + + Nombre + + Duración +
+ + + + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/new_course_form.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/new_course_form.ftl new file mode 100644 index 0000000..b7ecd5f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/new_course_form.ftl @@ -0,0 +1,54 @@ +
+

Crear curso

+
+
+ + + + <#if errors['id']?? > + <#list errors['id'] as errorMessage> +

${errorMessage}

+ + +
+
+
+
+ + + + <#if errors['name']?? > + <#list errors['name'] as errorMessage> +

${errorMessage}

+ + +
+
+ + + <#if errors['duration']?? > + <#list errors['duration'] as errorMessage> +

${errorMessage}

+ + +
+
+
+ +
+
diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/pages/home.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/pages/home.ftl new file mode 100644 index 0000000..8ee5f2b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/pages/home.ftl @@ -0,0 +1,7 @@ +<#include "../master.ftl"> + +<#macro page_title>HOME + +<#macro main> + Estamos en la home! + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl new file mode 100644 index 0000000..27a640f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl @@ -0,0 +1,7 @@ +
+
+

+ 🤙 CodelyTV - El mejor backoffice de la historia +

+
+
diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/partials/header.ftl b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/partials/header.ftl new file mode 100644 index 0000000..3f2f738 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/backoffice_frontend/templates/partials/header.ftl @@ -0,0 +1,28 @@ +
+ +
diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/log4j2.properties b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/log4j2.properties new file mode 100644 index 0000000..b577541 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/resources/log4j2.properties @@ -0,0 +1,34 @@ +name = CodelyTvJavaDddExample +property.filename = logs +appenders = console, file + +status = warn + +appender.console.name = CONSOLE +appender.console.type = CONSOLE +appender.console.target = SYSTEM_OUT + +appender.console.logstash.type = LogstashLayout +appender.console.logstash.dateTimeFormatPattern = yyyy-MM-dd'T'HH:mm:ss.SSSZZZ +appender.console.logstash.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.json +appender.console.logstash.prettyPrintEnabled = true +appender.console.logstash.stackTraceEnabled = true + +appender.file.type = File +appender.file.name = LOGFILE +appender.file.fileName = var/log/java-ddd-example.log +appender.file.logstash.type = LogstashLayout +appender.file.logstash.dateTimeFormatPattern = yyyy-MM-dd'T'HH:mm:ss.SSSZZZ +appender.file.logstash.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.json +appender.file.logstash.prettyPrintEnabled = false +appender.file.logstash.stackTraceEnabled = true + +loggers = file +logger.file.name = tv.codely.java_ddd_example +logger.file.level = info +logger.file.appenderRefs = file +logger.file.appenderRef.file.ref = LOGFILE + +rootLogger.level = info +rootLogger.appenderRefs = stdout +rootLogger.appenderRef.stdout.ref = CONSOLE diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/Starter.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/Starter.java new file mode 100644 index 0000000..64ad963 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/Starter.java @@ -0,0 +1,96 @@ +package tv.codely.apps; + +import java.util.Arrays; +import java.util.HashMap; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.context.ConfigurableApplicationContext; + +import tv.codely.apps.backoffice.backend.BackofficeBackendApplication; +import tv.codely.apps.backoffice.frontend.BackofficeFrontendApplication; +import tv.codely.apps.mooc.backend.MoocBackendApplication; +import tv.codely.shared.infrastructure.cli.ConsoleCommand; + +public class Starter { + + public static void main(String[] args) { + if (args.length < 2) { + throw new RuntimeException("There are not enough arguments"); + } + + String applicationName = args[0]; + String commandName = args[1]; + boolean isServerCommand = commandName.equals("server"); + + ensureApplicationExist(applicationName); + ensureCommandExist(applicationName, commandName); + + Class applicationClass = applications().get(applicationName); + + SpringApplication app = new SpringApplication(applicationClass); + + if (!isServerCommand) { + app.setWebApplicationType(WebApplicationType.NONE); + } + + ConfigurableApplicationContext context = app.run(args); + + if (!isServerCommand) { + ConsoleCommand command = (ConsoleCommand) context.getBean(commands().get(applicationName).get(commandName)); + + command.execute(Arrays.copyOfRange(args, 2, args.length)); + } + } + + private static void ensureApplicationExist(String applicationName) { + if (!applications().containsKey(applicationName)) { + throw new RuntimeException( + String.format( + "The application <%s> doesn't exist. Valids:\n- %s", + applicationName, + String.join("\n- ", applications().keySet()) + ) + ); + } + } + + private static void ensureCommandExist(String applicationName, String commandName) { + if (!"server".equals(commandName) && !existCommand(applicationName, commandName)) { + throw new RuntimeException( + String.format( + "The command <%s> for application <%s> doesn't exist. Valids (application.command):\n- api\n- %s", + commandName, + applicationName, + String.join("\n- ", commands().get(applicationName).keySet()) + ) + ); + } + } + + private static HashMap> applications() { + HashMap> applications = new HashMap<>(); + + applications.put("mooc_backend", MoocBackendApplication.class); + applications.put("backoffice_backend", BackofficeBackendApplication.class); + applications.put("backoffice_frontend", BackofficeFrontendApplication.class); + + return applications; + } + + private static HashMap>> commands() { + HashMap>> commands = new HashMap<>(); + + commands.put("mooc_backend", MoocBackendApplication.commands()); + commands.put("backoffice_backend", BackofficeBackendApplication.commands()); + commands.put("backoffice_frontend", BackofficeFrontendApplication.commands()); + + return commands; + } + + private static Boolean existCommand(String applicationName, String commandName) { + HashMap>> commands = commands(); + + return commands.containsKey(applicationName) && commands.get(applicationName).containsKey(commandName); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java new file mode 100644 index 0000000..0a9374e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java @@ -0,0 +1,27 @@ +package tv.codely.apps.backoffice.backend; + +import java.util.HashMap; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +import tv.codely.apps.backoffice.backend.command.ConsumeRabbitMqDomainEventsCommand; +import tv.codely.shared.domain.Service; + +@SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class) +@ComponentScan( + includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Service.class), + value = { "tv.codely.shared", "tv.codely.backoffice", "tv.codely.apps.backoffice.backend" } +) +public class BackofficeBackendApplication { + + public static HashMap> commands() { + return new HashMap<>() { + { + put("domain-events:rabbitmq:consume", ConsumeRabbitMqDomainEventsCommand.class); + } + }; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java new file mode 100644 index 0000000..018972f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java @@ -0,0 +1,18 @@ +package tv.codely.apps.backoffice.backend.command; + +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqDomainEventsConsumer; +import tv.codely.shared.infrastructure.cli.ConsoleCommand; + +public final class ConsumeRabbitMqDomainEventsCommand extends ConsoleCommand { + + private final RabbitMqDomainEventsConsumer consumer; + + public ConsumeRabbitMqDomainEventsCommand(RabbitMqDomainEventsConsumer consumer) { + this.consumer = consumer; + } + + @Override + public void execute(String[] args) { + consumer.consume("backoffice"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerConfiguration.java new file mode 100644 index 0000000..80314c2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerConfiguration.java @@ -0,0 +1,28 @@ +package tv.codely.apps.backoffice.backend.config; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import tv.codely.apps.backoffice.backend.middleware.BasicHttpAuthMiddleware; +import tv.codely.shared.domain.bus.command.CommandBus; + +@Configuration +public class BackofficeBackendServerConfiguration { + + private final CommandBus bus; + + public BackofficeBackendServerConfiguration(CommandBus bus) { + this.bus = bus; + } + + @Bean + public FilterRegistrationBean basicHttpAuthMiddleware() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + + registrationBean.setFilter(new BasicHttpAuthMiddleware(bus)); + registrationBean.addUrlPatterns("/health-check"); + + return registrationBean; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerPortCustomizer.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerPortCustomizer.java new file mode 100644 index 0000000..8c46d55 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerPortCustomizer.java @@ -0,0 +1,28 @@ +package tv.codely.apps.backoffice.backend.config; + +import org.springframework.boot.web.server.ConfigurableWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; + +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; + +@Component +public final class BackofficeBackendServerPortCustomizer + implements WebServerFactoryCustomizer { + + private final Parameter param; + + public BackofficeBackendServerPortCustomizer(Parameter param) { + this.param = param; + } + + @Override + public void customize(ConfigurableWebServerFactory factory) { + try { + factory.setPort(param.getInt("BACKOFFICE_BACKEND_SERVER_PORT")); + } catch (ParameterNotExist parameterNotExist) { + parameterNotExist.printStackTrace(); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/controller/courses/CoursesGetController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/controller/courses/CoursesGetController.java new file mode 100644 index 0000000..48bb8b4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/controller/courses/CoursesGetController.java @@ -0,0 +1,85 @@ +package tv.codely.apps.backoffice.backend.controller.courses; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.*; + +import tv.codely.backoffice.courses.application.BackofficeCoursesResponse; +import tv.codely.backoffice.courses.application.search_by_criteria.SearchBackofficeCoursesByCriteriaQuery; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +@CrossOrigin(origins = "*", methods = { RequestMethod.GET }) +public final class CoursesGetController extends ApiController { + + public CoursesGetController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @GetMapping("/courses") + public List> index(@RequestParam HashMap params) + throws QueryHandlerExecutionError { + BackofficeCoursesResponse courses = ask( + new SearchBackofficeCoursesByCriteriaQuery( + parseFilters(params), + Optional.ofNullable((String) params.get("order_by")), + Optional.ofNullable((String) params.get("order")), + Optional.ofNullable((Integer) params.get("limit")), + Optional.ofNullable((Integer) params.get("offset")) + ) + ); + + return courses + .courses() + .stream() + .map(response -> + new HashMap() { + { + put("id", response.id()); + put("name", response.name()); + put("duration", response.duration()); + } + } + ) + .collect(Collectors.toList()); + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return null; + } + + private List> parseFilters(HashMap params) { + int maxParams = params.size(); + + List> filters = new ArrayList<>(); + + for (int possibleFilterKey = 0; possibleFilterKey < maxParams; possibleFilterKey++) { + if (params.containsKey(String.format("filters[%s][field]", possibleFilterKey))) { + int key = possibleFilterKey; + + filters.add( + new HashMap() { + { + put("field", (String) params.get(String.format("filters[%s][field]", key))); + put("operator", (String) params.get(String.format("filters[%s][operator]", key))); + put("value", (String) params.get(String.format("filters[%s][value]", key))); + } + } + ); + } + } + + return filters; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetController.java new file mode 100644 index 0000000..bef9ec0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetController.java @@ -0,0 +1,34 @@ +package tv.codely.apps.backoffice.backend.controller.health_check; + +import java.util.HashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +public final class HealthCheckGetController extends ApiController { + + public HealthCheckGetController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @GetMapping("/health-check") + public HashMap index() { + HashMap status = new HashMap<>(); + status.put("application", "backoffice_backend"); + status.put("status", "ok"); + + return status; + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return null; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/middleware/BasicHttpAuthMiddleware.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/middleware/BasicHttpAuthMiddleware.java new file mode 100644 index 0000000..2ce6a90 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/backend/middleware/BasicHttpAuthMiddleware.java @@ -0,0 +1,77 @@ +package tv.codely.apps.backoffice.backend.middleware; + +import java.io.IOException; +import java.util.Base64; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import tv.codely.backoffice.auth.application.authenticate.AuthenticateUserCommand; +import tv.codely.backoffice.auth.domain.InvalidAuthCredentials; +import tv.codely.backoffice.auth.domain.InvalidAuthUsername; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; + +public final class BasicHttpAuthMiddleware implements Filter { + + private final CommandBus bus; + + public BasicHttpAuthMiddleware(CommandBus bus) { + this.bus = bus; + } + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + String authorizationHeader = ((HttpServletRequest) request).getHeader("authorization"); + + if (hasIntroducedCredentials(authorizationHeader)) { + authenticate(authorizationHeader, chain, request, response); + } else { + askForCredentials(response); + } + } + + private boolean hasIntroducedCredentials(String authorizationHeader) { + return null != authorizationHeader; + } + + private void authenticate( + String authorizationHeader, + FilterChain chain, + ServletRequest request, + ServletResponse response + ) throws IOException, ServletException { + String[] auth = decodeAuth(authorizationHeader); + String user = auth[0]; + String pass = auth[1]; + + try { + bus.dispatch(new AuthenticateUserCommand(user, pass)); + + request.setAttribute("authentication_username", user); + + chain.doFilter(request, response); + } catch (InvalidAuthUsername | InvalidAuthCredentials | CommandHandlerExecutionError error) { + setInvalidCredentials(response); + } + } + + private String[] decodeAuth(String authString) { + return new String(Base64.getDecoder().decode(authString.split("\\s+")[1])).split(":"); + } + + private void setInvalidCredentials(ServletResponse response) { + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + httpServletResponse.reset(); + httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN); + } + + private void askForCredentials(ServletResponse response) { + HttpServletResponse httpServletResponse = (HttpServletResponse) response; + httpServletResponse.reset(); + httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + httpServletResponse.setHeader("WWW-Authenticate", "Basic realm=\"CodelyTV\""); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/BackofficeFrontendApplication.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/BackofficeFrontendApplication.java new file mode 100644 index 0000000..ae42787 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/BackofficeFrontendApplication.java @@ -0,0 +1,24 @@ +package tv.codely.apps.backoffice.frontend; + +import java.util.HashMap; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +import tv.codely.shared.domain.Service; + +@SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class) +@ComponentScan( + includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Service.class), + value = { "tv.codely.shared", "tv.codely.backoffice", "tv.codely.mooc", "tv.codely.apps.backoffice.frontend" } +) +public class BackofficeFrontendApplication { + + public static HashMap> commands() { + return new HashMap>() { + {} + }; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendServerPortCustomizer.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendServerPortCustomizer.java new file mode 100644 index 0000000..6cfbd81 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendServerPortCustomizer.java @@ -0,0 +1,28 @@ +package tv.codely.apps.backoffice.frontend.config; + +import org.springframework.boot.web.server.ConfigurableWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; + +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; + +@Component +public final class BackofficeFrontendServerPortCustomizer + implements WebServerFactoryCustomizer { + + private final Parameter param; + + public BackofficeFrontendServerPortCustomizer(Parameter param) { + this.param = param; + } + + @Override + public void customize(ConfigurableWebServerFactory factory) { + try { + factory.setPort(param.getInt("BACKOFFICE_FRONTEND_SERVER_PORT")); + } catch (ParameterNotExist parameterNotExist) { + parameterNotExist.printStackTrace(); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java new file mode 100644 index 0000000..62749d1 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java @@ -0,0 +1,61 @@ +package tv.codely.apps.backoffice.frontend.config; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.web.servlet.ViewResolver; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; +import org.springframework.web.servlet.config.annotation.ViewResolverRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.springframework.web.servlet.view.freemarker.FreeMarkerConfigurer; +import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver; + +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqPublisher; + +@Configuration +@EnableWebMvc +public class BackofficeFrontendWebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + registry.addResourceHandler("/**").addResourceLocations("classpath:/backoffice_frontend/public/"); + } + + @Bean + public ViewResolver getViewResolver() { + FreeMarkerViewResolver resolver = new FreeMarkerViewResolver(); + resolver.setCache(false); + resolver.setSuffix(".ftl"); + return resolver; + } + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("classpath:/backoffice_frontend/templates/"); + configurer.setDefaultEncoding("UTF-8"); + // configurer.setFreemarkerVariables(new HashMap() {{ + // put("flash", new Flash()); + // }}); + + return configurer; + } + + @Primary + @Bean + public RabbitMqEventBus rabbitMqEventBus( + RabbitMqPublisher publisher, + @Qualifier("backofficeMysqlEventBus") MySqlEventBus failoverPublisher + ) { + return new RabbitMqEventBus(publisher, failoverPublisher); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesGetWebController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesGetWebController.java new file mode 100644 index 0000000..95ff91c --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesGetWebController.java @@ -0,0 +1,48 @@ +package tv.codely.apps.backoffice.frontend.controller.courses; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.servlet.ModelAndView; + +import tv.codely.mooc.courses_counter.application.find.CoursesCounterResponse; +import tv.codely.mooc.courses_counter.application.find.FindCoursesCounterQuery; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; + +@Controller +public final class CoursesGetWebController { + + private final QueryBus bus; + + public CoursesGetWebController(QueryBus bus) { + this.bus = bus; + } + + @GetMapping("/courses") + public ModelAndView index( + @ModelAttribute("inputs") HashMap inputs, + @ModelAttribute("errors") HashMap> errors + ) throws QueryHandlerExecutionError { + CoursesCounterResponse counterResponse = bus.ask(new FindCoursesCounterQuery()); + + return new ModelAndView( + "pages/courses/courses", + new HashMap() { + { + put("title", "Courses"); + put("description", "Courses CodelyTV - Backoffice"); + put("courses_counter", counterResponse.total()); + put("inputs", inputs); + put("errors", errors); + put("generated_uuid", UUID.randomUUID().toString()); + } + } + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesPostWebController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesPostWebController.java new file mode 100644 index 0000000..927dd41 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesPostWebController.java @@ -0,0 +1,67 @@ +package tv.codely.apps.backoffice.frontend.controller.courses; + +import java.io.Serializable; +import java.util.HashMap; + +import org.springframework.http.MediaType; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; +import org.springframework.web.servlet.view.RedirectView; + +import tv.codely.mooc.courses.application.create.CreateCourseCommand; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; +import tv.codely.shared.infrastructure.validation.ValidationResponse; +import tv.codely.shared.infrastructure.validation.Validator; + +@Controller +public final class CoursesPostWebController { + + private final CommandBus bus; + private final HashMap rules = new HashMap() { + { + put("id", "required|not_empty|uuid"); + put("name", "required|not_empty|string"); + put("duration", "required|not_empty|string"); + } + }; + + public CoursesPostWebController(CommandBus bus) { + this.bus = bus; + } + + @PostMapping(value = "/courses", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + public RedirectView index(@RequestParam HashMap request, RedirectAttributes attributes) + throws Exception { + ValidationResponse validationResponse = Validator.validate(request, rules); + + return validationResponse.hasErrors() + ? redirectWithErrors(validationResponse, request, attributes) + : createCourse(request); + } + + private RedirectView redirectWithErrors( + ValidationResponse validationResponse, + HashMap request, + RedirectAttributes attributes + ) { + attributes.addFlashAttribute("errors", validationResponse.errors()); + attributes.addFlashAttribute("inputs", request); + + return new RedirectView("/courses"); + } + + private RedirectView createCourse(HashMap request) throws CommandHandlerExecutionError { + bus.dispatch( + new CreateCourseCommand( + request.get("id").toString(), + request.get("name").toString(), + request.get("duration").toString() + ) + ); + + return new RedirectView("/courses"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetController.java new file mode 100644 index 0000000..9c06c15 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetController.java @@ -0,0 +1,19 @@ +package tv.codely.apps.backoffice.frontend.controller.health_check; + +import java.util.HashMap; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public final class HealthCheckGetController { + + @GetMapping("/health-check") + public HashMap index() { + HashMap status = new HashMap<>(); + status.put("application", "backoffice_frontend"); + status.put("status", "ok"); + + return status; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/controller/home/HomeGetWebController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/controller/home/HomeGetWebController.java new file mode 100644 index 0000000..254fba9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/backoffice/frontend/controller/home/HomeGetWebController.java @@ -0,0 +1,25 @@ +package tv.codely.apps.backoffice.frontend.controller.home; + +import java.io.Serializable; +import java.util.HashMap; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.servlet.ModelAndView; + +@Controller +public final class HomeGetWebController { + + @GetMapping("/") + public ModelAndView index() { + return new ModelAndView( + "pages/home", + new HashMap() { + { + put("title", "Welcome"); + put("description", "CodelyTV - Backoffice"); + } + } + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/MoocBackendApplication.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/MoocBackendApplication.java new file mode 100644 index 0000000..b9e0448 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/MoocBackendApplication.java @@ -0,0 +1,29 @@ +package tv.codely.apps.mooc.backend; + +import java.util.HashMap; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; + +import tv.codely.apps.mooc.backend.command.ConsumeMySqlDomainEventsCommand; +import tv.codely.apps.mooc.backend.command.ConsumeRabbitMqDomainEventsCommand; +import tv.codely.shared.domain.Service; + +@SpringBootApplication(exclude = HibernateJpaAutoConfiguration.class) +@ComponentScan( + includeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Service.class), + value = { "tv.codely.shared", "tv.codely.mooc", "tv.codely.apps.mooc.backend" } +) +public class MoocBackendApplication { + + public static HashMap> commands() { + return new HashMap>() { + { + put("domain-events:mysql:consume", ConsumeMySqlDomainEventsCommand.class); + put("domain-events:rabbitmq:consume", ConsumeRabbitMqDomainEventsCommand.class); + } + }; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/command/ConsumeMySqlDomainEventsCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/command/ConsumeMySqlDomainEventsCommand.java new file mode 100644 index 0000000..78eaf41 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/command/ConsumeMySqlDomainEventsCommand.java @@ -0,0 +1,18 @@ +package tv.codely.apps.mooc.backend.command; + +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlDomainEventsConsumer; +import tv.codely.shared.infrastructure.cli.ConsoleCommand; + +public final class ConsumeMySqlDomainEventsCommand extends ConsoleCommand { + + private final MySqlDomainEventsConsumer consumer; + + public ConsumeMySqlDomainEventsCommand(MySqlDomainEventsConsumer consumer) { + this.consumer = consumer; + } + + @Override + public void execute(String[] args) { + consumer.consume(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java new file mode 100644 index 0000000..ce5d7bd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java @@ -0,0 +1,18 @@ +package tv.codely.apps.mooc.backend.command; + +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqDomainEventsConsumer; +import tv.codely.shared.infrastructure.cli.ConsoleCommand; + +public final class ConsumeRabbitMqDomainEventsCommand extends ConsoleCommand { + + private final RabbitMqDomainEventsConsumer consumer; + + public ConsumeRabbitMqDomainEventsCommand(RabbitMqDomainEventsConsumer consumer) { + this.consumer = consumer; + } + + @Override + public void execute(String[] args) { + consumer.consume("mooc"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java new file mode 100644 index 0000000..3041e7b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java @@ -0,0 +1,29 @@ +package tv.codely.apps.mooc.backend.config; + +import java.util.Optional; + +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; + +import tv.codely.shared.infrastructure.spring.ApiExceptionMiddleware; + +@Configuration +public class MoocBackendServerConfiguration { + + private final Optional mapping; + + public MoocBackendServerConfiguration(Optional mapping) { + this.mapping = mapping; + } + + @Bean + public FilterRegistrationBean apiExceptionMiddleware() { + FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); + + mapping.ifPresent(map -> registrationBean.setFilter(new ApiExceptionMiddleware(map))); + + return registrationBean; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerPortCustomizer.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerPortCustomizer.java new file mode 100644 index 0000000..3d61c27 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerPortCustomizer.java @@ -0,0 +1,27 @@ +package tv.codely.apps.mooc.backend.config; + +import org.springframework.boot.web.server.ConfigurableWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; + +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; + +@Component +public final class MoocBackendServerPortCustomizer implements WebServerFactoryCustomizer { + + private final Parameter param; + + public MoocBackendServerPortCustomizer(Parameter param) { + this.param = param; + } + + @Override + public void customize(ConfigurableWebServerFactory factory) { + try { + factory.setPort(param.getInt("MOOC_BACKEND_SERVER_PORT")); + } catch (ParameterNotExist parameterNotExist) { + parameterNotExist.printStackTrace(); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/courses/CourseGetController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/courses/CourseGetController.java new file mode 100644 index 0000000..838783b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/courses/CourseGetController.java @@ -0,0 +1,54 @@ +package tv.codely.apps.mooc.backend.controller.courses; + +import java.io.Serializable; +import java.util.HashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.mooc.courses.application.CourseResponse; +import tv.codely.mooc.courses.application.find.FindCourseQuery; +import tv.codely.mooc.courses.domain.CourseNotExist; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +public final class CourseGetController extends ApiController { + + public CourseGetController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @GetMapping("/courses/{id}") + public ResponseEntity> index(@PathVariable String id) + throws QueryHandlerExecutionError { + CourseResponse course = ask(new FindCourseQuery(id)); + + return ResponseEntity + .ok() + .body( + new HashMap() { + { + put("id", course.id()); + put("name", course.name()); + put("duration", course.duration()); + } + } + ); + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return new HashMap, HttpStatus>() { + { + put(CourseNotExist.class, HttpStatus.NOT_FOUND); + } + }; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java new file mode 100644 index 0000000..54bc204 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java @@ -0,0 +1,60 @@ +package tv.codely.apps.mooc.backend.controller.courses; + +import java.util.HashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.mooc.courses.application.create.CreateCourseCommand; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +public final class CoursesPutController extends ApiController { + + public CoursesPutController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @PutMapping(value = "/courses/{id}") + public ResponseEntity index(@PathVariable String id, @RequestBody Request request) + throws CommandHandlerExecutionError { + dispatch(new CreateCourseCommand(id, request.name(), request.duration())); + + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return null; + } +} + +final class Request { + + private String name; + private String duration; + + public void setDuration(String duration) { + this.duration = duration; + } + + public void setName(String name) { + this.name = name; + } + + String name() { + return name; + } + + String duration() { + return duration; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetController.java new file mode 100644 index 0000000..88a932f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetController.java @@ -0,0 +1,39 @@ +package tv.codely.apps.mooc.backend.controller.courses_counter; + +import java.util.HashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.mooc.courses_counter.application.find.CoursesCounterResponse; +import tv.codely.mooc.courses_counter.application.find.FindCoursesCounterQuery; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +public final class CoursesCounterGetController extends ApiController { + + public CoursesCounterGetController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @GetMapping("/courses-counter") + public HashMap index() throws QueryHandlerExecutionError { + CoursesCounterResponse response = ask(new FindCoursesCounterQuery()); + + return new HashMap() { + { + put("total", response.total()); + } + }; + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return null; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetController.java new file mode 100644 index 0000000..43461f9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetController.java @@ -0,0 +1,34 @@ +package tv.codely.apps.mooc.backend.controller.health_check; + +import java.util.HashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +public final class HealthCheckGetController extends ApiController { + + public HealthCheckGetController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @GetMapping("/health-check") + public HashMap index() { + HashMap status = new HashMap<>(); + status.put("application", "mooc_backend"); + status.put("status", "ok"); + + return status; + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return null; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutController.java new file mode 100644 index 0000000..3fbcd68 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutController.java @@ -0,0 +1,36 @@ +package tv.codely.apps.mooc.backend.controller.notifications; + +import java.util.HashMap; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.mooc.notifications.application.send_new_courses_newsletter.SendNewCoursesNewsletterCommand; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.infrastructure.spring.ApiController; + +@RestController +public final class NewsletterNotificationPutController extends ApiController { + + public NewsletterNotificationPutController(QueryBus queryBus, CommandBus commandBus) { + super(queryBus, commandBus); + } + + @PutMapping(value = "/newsletter/{id}") + public ResponseEntity index(@PathVariable String id) throws CommandHandlerExecutionError { + dispatch(new SendNewCoursesNewsletterCommand(id)); + + return new ResponseEntity<>(HttpStatus.CREATED); + } + + @Override + public HashMap, HttpStatus> errorMapping() { + return null; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java new file mode 100644 index 0000000..dfc9657 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java @@ -0,0 +1,53 @@ +package tv.codely.apps.mooc.backend.controller.playground; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RestController; + +import tv.codely.shared.domain.Utils; + +@RestController +record DomainEventPostController(RabbitTemplate rabbitTemplate) { + @PostMapping(value = "/domain-events") + public ResponseEntity index(@RequestBody Request request) { + System.out.println(request.eventName()); + + var serializedEvent = Utils.jsonEncode(request.eventRaw()); + + Message message = new Message( + serializedEvent.getBytes(), + MessagePropertiesBuilder.newInstance().setContentEncoding("utf-8").setContentType("application/json").build() + ); + + rabbitTemplate.send("domain_events", request.eventName(), message); + + return new ResponseEntity<>(HttpStatus.CREATED); + } +} + +final class Request { + + private String eventName; + private Object eventRaw; + + public void setEventName(String eventName) { + this.eventName = eventName; + } + + public void setEventRaw(Object eventRaw) { + this.eventRaw = eventRaw; + } + + String eventName() { + return eventName; + } + + Object eventRaw() { + return eventRaw; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/resources/log4j2.properties b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/resources/log4j2.properties new file mode 100644 index 0000000..98427f2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/resources/log4j2.properties @@ -0,0 +1,33 @@ +name = CodelyTvJavaDddExample +property.filename = logs +appenders = console, file + +status = warn + +appender.console.name = CONSOLE +appender.console.type = CONSOLE +appender.console.target = SYSTEM_OUT + +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = [%level] [%date{HH:mm:ss.SSS}] [%class{0}#%method:%line] %message \(%mdc\) %n%throwable +appender.console.filter.threshold.type = ThresholdFilter +appender.console.filter.threshold.level = info + +appender.file.type = File +appender.file.name = LOGFILE +appender.file.fileName = var/log/java-ddd-example-test.log +appender.file.logstash.type = LogstashLayout +appender.file.logstash.dateTimeFormatPattern = yyyy-MM-dd'T'HH:mm:ss.SSSZZZ +appender.file.logstash.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.json +appender.file.logstash.prettyPrintEnabled = false +appender.file.logstash.stackTraceEnabled = true + +loggers = file +logger.file.name = tv.codely.java_ddd_example +logger.file.level = info +logger.file.appenderRefs = file +logger.file.appenderRef.file.ref = LOGFILE + +rootLogger.level = info +rootLogger.appenderRefs = stdout +rootLogger.appenderRef.stdout.ref = CONSOLE diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/ApplicationTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/ApplicationTestCase.java new file mode 100644 index 0000000..d356818 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/ApplicationTestCase.java @@ -0,0 +1,67 @@ +package tv.codely.apps; + +import static org.springframework.http.MediaType.APPLICATION_JSON; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.request; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.Arrays; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultMatcher; + +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.domain.bus.event.EventBus; + +@SpringBootTest +@AutoConfigureMockMvc +public abstract class ApplicationTestCase { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private EventBus eventBus; + + protected void assertResponse(String endpoint, Integer expectedStatusCode, String expectedResponse) throws Exception { + ResultMatcher response = expectedResponse.isEmpty() ? content().string("") : content().json(expectedResponse); + + mockMvc.perform(get(endpoint)).andExpect(status().is(expectedStatusCode)).andExpect(response); + } + + protected void assertResponse( + String endpoint, + Integer expectedStatusCode, + String expectedResponse, + HttpHeaders headers + ) throws Exception { + ResultMatcher response = expectedResponse.isEmpty() ? content().string("") : content().json(expectedResponse); + + mockMvc.perform(get(endpoint).headers(headers)).andExpect(status().is(expectedStatusCode)).andExpect(response); + } + + protected void assertRequestWithBody(String method, String endpoint, String body, Integer expectedStatusCode) + throws Exception { + mockMvc + .perform(request(HttpMethod.valueOf(method), endpoint).content(body).contentType(APPLICATION_JSON)) + .andExpect(status().is(expectedStatusCode)) + .andExpect(content().string("")); + } + + protected void assertRequest(String method, String endpoint, Integer expectedStatusCode) throws Exception { + mockMvc + .perform(request(HttpMethod.valueOf(method), endpoint)) + .andExpect(status().is(expectedStatusCode)) + .andExpect(content().string("")); + } + + protected void givenISendEventsToTheBus(DomainEvent... domainEvents) { + eventBus.publish(Arrays.asList(domainEvents)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/backoffice/BackofficeApplicationTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/backoffice/BackofficeApplicationTestCase.java new file mode 100644 index 0000000..195b553 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/backoffice/BackofficeApplicationTestCase.java @@ -0,0 +1,8 @@ +package tv.codely.apps.backoffice; + +import org.springframework.transaction.annotation.Transactional; + +import tv.codely.apps.ApplicationTestCase; + +@Transactional("backoffice-transaction_manager") +public abstract class BackofficeApplicationTestCase extends ApplicationTestCase {} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetControllerShould.java new file mode 100644 index 0000000..add71d8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetControllerShould.java @@ -0,0 +1,38 @@ +package tv.codely.apps.backoffice.backend.controller.health_check; + +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpHeaders; + +import tv.codely.apps.ApplicationTestCase; + +final class HealthCheckGetControllerShould extends ApplicationTestCase { + + @Test + void check_the_app_is_working_ok_with_valid_credentials() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setBasicAuth("javi", "barbitas"); + + assertResponse("/health-check", 200, "{'application':'backoffice_backend','status':'ok'}", headers); + } + + @Test + void not_check_the_app_is_working_ok_with_invalid_credentials() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setBasicAuth("tipo_de_incognito", "homer.sampson"); + + assertResponse("/health-check", 403, "", headers); + } + + @Test + void not_check_the_app_is_working_ok_with_invalid_credentials_of_an_existing_user() throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.setBasicAuth("rafa", "incorrect.password"); + + assertResponse("/health-check", 403, "", headers); + } + + @Test + void not_check_the_app_is_working_ok_with_no_credentials() throws Exception { + assertResponse("/health-check", 401, ""); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetControllerShould.java new file mode 100644 index 0000000..c5a9747 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetControllerShould.java @@ -0,0 +1,13 @@ +package tv.codely.apps.backoffice.frontend.controller.health_check; + +import org.junit.jupiter.api.Test; + +import tv.codely.apps.ApplicationTestCase; + +final class HealthCheckGetControllerShould extends ApplicationTestCase { + + @Test + void check_the_app_is_working_ok() throws Exception { + assertResponse("/health-check", 200, "{'application':'backoffice_frontend','status':'ok'}"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/MoocApplicationTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/MoocApplicationTestCase.java new file mode 100644 index 0000000..90dc231 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/MoocApplicationTestCase.java @@ -0,0 +1,8 @@ +package tv.codely.apps.mooc; + +import org.springframework.transaction.annotation.Transactional; + +import tv.codely.apps.ApplicationTestCase; + +@Transactional("mooc-transaction_manager") +public abstract class MoocApplicationTestCase extends ApplicationTestCase {} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/courses/CourseGetControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/courses/CourseGetControllerShould.java new file mode 100644 index 0000000..0ebbcc4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/courses/CourseGetControllerShould.java @@ -0,0 +1,31 @@ +package tv.codely.apps.mooc.backend.controller.courses; + +import org.junit.jupiter.api.Test; + +import tv.codely.apps.mooc.MoocApplicationTestCase; + +final class CourseGetControllerShould extends MoocApplicationTestCase { + + @Test + void find_an_existing_course() throws Exception { + String id = "99ad55f5-6eab-4d73-b383-c63268e251e8"; + String body = "{\"name\": \"The best course\", \"duration\": \"5 hours\"}"; + + givenThereIsACourse(id, body); + + assertResponse(String.format("/courses/%s", id), 200, body); + } + + @Test + void no_find_a_non_existing_course() throws Exception { + String id = "4ecc0cb3-05b2-4238-b7e1-1fbb0d5d3661"; + String body = + "{\"error_code\": \"course_not_exist\", \"message\": \"The course <4ecc0cb3-05b2-4238-b7e1-1fbb0d5d3661> doesn't exist\"}"; + + assertResponse(String.format("/courses/%s", id), 404, body); + } + + private void givenThereIsACourse(String id, String body) throws Exception { + assertRequestWithBody("PUT", String.format("/courses/%s", id), body, 201); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/courses/CoursesPutControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/courses/CoursesPutControllerShould.java new file mode 100644 index 0000000..9dcbe3d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/courses/CoursesPutControllerShould.java @@ -0,0 +1,18 @@ +package tv.codely.apps.mooc.backend.controller.courses; + +import org.junit.jupiter.api.Test; + +import tv.codely.apps.mooc.MoocApplicationTestCase; + +public final class CoursesPutControllerShould extends MoocApplicationTestCase { + + @Test + void create_a_valid_non_existing_course() throws Exception { + assertRequestWithBody( + "PUT", + "/courses/1aab45ba-3c7a-4344-8936-78466eca77fa", + "{\"name\": \"The best course\", \"duration\": \"5 hours\"}", + 201 + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetControllerShould.java new file mode 100644 index 0000000..2927678 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetControllerShould.java @@ -0,0 +1,46 @@ +package tv.codely.apps.mooc.backend.controller.courses_counter; + +import org.junit.jupiter.api.Test; + +import tv.codely.apps.mooc.MoocApplicationTestCase; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +public final class CoursesCounterGetControllerShould extends MoocApplicationTestCase { + + @Test + void get_the_counter_with_one_course() throws Exception { + givenISendEventsToTheBus( + new CourseCreatedDomainEvent("8f34bc99-e0e2-4296-a008-75f51f03aeb4", "DDD en Java", "7 days") + ); + + assertResponse("/courses-counter", 200, "{'total': 1}"); + } + + @Test + void get_the_counter_with_more_than_one_course() throws Exception { + givenISendEventsToTheBus( + new CourseCreatedDomainEvent("8f34bc99-e0e2-4296-a008-75f51f03aeb4", "DDD en Java", "7 days"), + new CourseCreatedDomainEvent("3642f700-868a-4778-9317-a2d542d01785", "DDD en PHP", "6 days"), + new CourseCreatedDomainEvent("92dd8402-69f3-4900-b569-3f2c2797065f", "DDD en Cobol", "10 years") + ); + + assertResponse("/courses-counter", 200, "{'total': 3}"); + } + + @Test + void get_the_counter_with_more_than_one_course_having_duplicated_events() throws Exception { + givenISendEventsToTheBus( + new CourseCreatedDomainEvent("8f34bc99-e0e2-4296-a008-75f51f03aeb4", "DDD en Java", "7 days"), + new CourseCreatedDomainEvent("8f34bc99-e0e2-4296-a008-75f51f03aeb4", "DDD en Java", "7 days"), + new CourseCreatedDomainEvent("8f34bc99-e0e2-4296-a008-75f51f03aeb4", "DDD en Java", "7 days"), + new CourseCreatedDomainEvent("3642f700-868a-4778-9317-a2d542d01785", "DDD en PHP", "6 days"), + new CourseCreatedDomainEvent("3642f700-868a-4778-9317-a2d542d01785", "DDD en PHP", "6 days"), + new CourseCreatedDomainEvent("3642f700-868a-4778-9317-a2d542d01785", "DDD en PHP", "6 days"), + new CourseCreatedDomainEvent("3642f700-868a-4778-9317-a2d542d01785", "DDD en PHP", "6 days"), + new CourseCreatedDomainEvent("92dd8402-69f3-4900-b569-3f2c2797065f", "DDD en Cobol", "10 years"), + new CourseCreatedDomainEvent("92dd8402-69f3-4900-b569-3f2c2797065f", "DDD en Cobol", "10 years") + ); + + assertResponse("/courses-counter", 200, "{'total': 3}"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetControllerShould.java new file mode 100644 index 0000000..94069bf --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetControllerShould.java @@ -0,0 +1,13 @@ +package tv.codely.apps.mooc.backend.controller.health_check; + +import org.junit.jupiter.api.Test; + +import tv.codely.apps.mooc.MoocApplicationTestCase; + +final class HealthCheckGetControllerShould extends MoocApplicationTestCase { + + @Test + void check_the_app_is_working_ok() throws Exception { + assertResponse("/health-check", 200, "{'application':'mooc_backend','status':'ok'}"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutControllerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutControllerShould.java new file mode 100644 index 0000000..8f011e9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/apps/test/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutControllerShould.java @@ -0,0 +1,13 @@ +package tv.codely.apps.mooc.backend.controller.notifications; + +import org.junit.jupiter.api.Test; + +import tv.codely.apps.mooc.MoocApplicationTestCase; + +final class NewsletterNotificationPutControllerShould extends MoocApplicationTestCase { + + @Test + void create_a_valid_non_existing_course() throws Exception { + assertRequest("PUT", "/newsletter/6eebbe60-50e7-400a-810c-3e0af0943ee6", 201); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/build.gradle b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/build.gradle new file mode 100644 index 0000000..508fb39 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/build.gradle @@ -0,0 +1,171 @@ +// Main project (located in apps/) +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath('org.springframework.boot:spring-boot-gradle-plugin:3.1.5') + } +} + +plugins { + id "com.diffplug.spotless" version "6.22.0" +} + +spotless { + java { + prettier(['prettier': '2.8.8', 'prettier-plugin-java': '2.2.0']) + .config(['parser': 'java', 'useTabs': true, 'printWidth': 120]) + + importOrder('\\#', 'java', '', 'tv.codely') + removeUnusedImports() + + endWithNewline() + + formatAnnotations() + } +} + +// Common for all projects +allprojects { + apply plugin: 'java' + apply plugin: 'io.spring.dependency-management' + apply plugin: 'org.springframework.boot' + + java { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + + repositories { + mavenCentral() + maven { url 'https://repo.spring.io/milestone' } + } + + ext { + set('springCloudVersion', "Hoxton.M3") + set('elasticsearchVersion', '6.8.4') + } + + dependencies { + // Prod + implementation 'org.apache.logging.log4j:log4j-core:2.21.1' + implementation 'org.apache.logging.log4j:log4j-api:2.21.1' + implementation 'com.vlkan.log4j2:log4j2-logstash-layout:1.0.5' + implementation 'io.github.cdimascio:dotenv-java:3.0.0' + implementation 'org.hibernate.orm:hibernate-core:6.3.1.Final' + implementation 'jakarta.persistence:jakarta.persistence-api:3.2.0-B01' + implementation 'org.springframework:spring-orm:6.0.13' + implementation 'org.springframework:spring-context-support:6.0.13' + implementation 'org.apache.tomcat:tomcat-dbcp:10.1.15' + implementation 'com.sun.xml.bind:jaxb-impl:4.0.4' + implementation 'jakarta.xml.bind:jakarta.xml.bind-api:4.0.1' + implementation 'org.freemarker:freemarker-gae:2.3.32' + implementation 'org.reflections:reflections:0.10.2' + implementation 'com.google.guava:guava:31.0.1-jre' + implementation 'org.springframework.boot:spring-boot-starter-amqp' + implementation "org.elasticsearch.client:elasticsearch-rest-client:${elasticsearchVersion}" + implementation "org.elasticsearch.client:elasticsearch-rest-high-level-client:${elasticsearchVersion}" + implementation 'mysql:mysql-connector-java:8.0.28' + + // Test + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2' + testImplementation 'org.mockito:mockito-core:3.3.3' + testImplementation 'com.github.javafaker:javafaker:1.0.1' + testImplementation 'org.junit.jupiter:junit-jupiter-engine:5.5.2' + } + + dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } + } + + test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + } + } + + task view_paths { + doLast { task -> + println "$task.project.name" + println "------------------" + println "Main: $sourceSets.main.java.srcDirTrees" + println " Resources: $sourceSets.main.resources.srcDirTrees" + println "Tests: $sourceSets.test.java.srcDirTrees" + println " Resources: $sourceSets.test.resources.srcDirTrees" + } + } +} + +// All subprojects (located in src/*) +subprojects { + group = "tv.codely.${rootProject.name}" + + sourceSets { + main { + java { srcDirs = ['main'] } + resources { srcDirs = ['main/resources'] } + } + + test { + java { srcDirs = ['test'] } + resources { srcDirs = ['test/resources'] } + } + } + + dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web:3.1.5' + + testImplementation rootProject.sourceSets.main.output + testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.5' + + if (project.name != "shared") { + implementation project(":shared") + testImplementation project(":shared").sourceSets.test.output + } + } + + bootJar { + enabled = false + } + + jar { + enabled = true + } +} + + +sourceSets { + main { + java { srcDirs = ['apps/main'] } + resources { srcDirs = ['apps/main/resources'] } + } + + test { + java { srcDirs = ['apps/test'] } + resources { srcDirs = ['apps/test/resources'] } + } +} + +bootJar { + archiveBaseName.set('java-ddd-example') + archiveVersion.set('0.0.1') + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-web:3.1.5' + + implementation project(":shared") + implementation project(":backoffice") + implementation project(":mooc") + + testImplementation 'org.springframework.boot:spring-boot-starter-test:3.1.5' + testImplementation project(":shared").sourceSets.test.output +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/docker-compose.ci.yml b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/docker-compose.ci.yml new file mode 100755 index 0000000..d2c1c99 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/docker-compose.ci.yml @@ -0,0 +1,55 @@ +version: '3' + +services: + shared_mysql: + container_name: codely-java_ddd_example-mysql + image: mysql:8 + platform: linux/amd64 + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD= + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + entrypoint: + sh -c " + echo 'CREATE DATABASE IF NOT EXISTS mooc;CREATE DATABASE IF NOT EXISTS backoffice;' > /docker-entrypoint-initdb.d/init.sql; + /usr/local/bin/docker-entrypoint.sh --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + " + command: ["--default-authentication-plugin=mysql_native_password"] + + shared_rabbitmq: + container_name: codely-java_ddd_example-rabbitmq + image: 'rabbitmq:3.7-management' + platform: linux/amd64 + restart: unless-stopped + ports: + - "5672:5672" + - "8090:15672" + environment: + - RABBITMQ_DEFAULT_USER=codely + - RABBITMQ_DEFAULT_PASS=c0d3ly + + backoffice_elasticsearch: + container_name: codely-java_ddd_example-elasticsearch + image: 'elasticsearch:6.8.4' + platform: linux/amd64 + restart: unless-stopped + ports: + - "9300:9300" + - "9200:9200" + environment: + - discovery.type=single-node + + test_server_java: + container_name: codely-java_ddd_example-test_server + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + volumes: + - .:/app:delegated + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + tty: true diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/docker-compose.yml b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/docker-compose.yml new file mode 100755 index 0000000..216bacd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/docker-compose.yml @@ -0,0 +1,139 @@ +version: '3' + +services: + shared_mysql: + container_name: codely-java_ddd_example-mysql + image: mysql:8 + platform: linux/amd64 + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD= + - MYSQL_ALLOW_EMPTY_PASSWORD=yes + entrypoint: + sh -c " + echo 'CREATE DATABASE IF NOT EXISTS mooc;CREATE DATABASE IF NOT EXISTS backoffice;' > /docker-entrypoint-initdb.d/init.sql; + /usr/local/bin/docker-entrypoint.sh --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci + " + command: ["--default-authentication-plugin=mysql_native_password"] + + shared_rabbitmq: + container_name: codely-java_ddd_example-rabbitmq + image: 'rabbitmq:3.7-management' + platform: linux/amd64 + restart: unless-stopped + ports: + - "5672:5672" + - "8090:15672" + environment: + - RABBITMQ_DEFAULT_USER=codely + - RABBITMQ_DEFAULT_PASS=c0d3ly + + backoffice_elasticsearch: + container_name: codely-java_ddd_example-elasticsearch + image: 'elasticsearch:6.8.4' + platform: linux/amd64 + restart: unless-stopped + ports: + - "9300:9300" + - "9200:9200" + environment: + - discovery.type=single-node + + backoffice_backend_server_java: + container_name: codely-java_ddd_example-backoffice_backend_server + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "8040:8040" + volumes: + - .:/app:delegated + - backoffice_backend_gradle_cache:/app/.gradle + - backoffice_backend_build:/app/build + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + command: ["./gradlew", "bootRun", "--args", "backoffice_backend server"] + + backoffice_backend_consumers_java: + container_name: codely-java_ddd_example-backoffice_backend_consumers + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + volumes: + - .:/app:delegated + - backoffice_consumers_gradle_cache:/app/.gradle + - backoffice_consumers_build:/app/build + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + command: ["./gradlew", "bootRun", "--args", "backoffice_backend domain-events:rabbitmq:consume"] + + backoffice_frontend_server_java: + container_name: codely-java_ddd_example-backoffice_frontend_server + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "8041:8041" + volumes: + - .:/app:delegated + - backoffice_frontend_gradle_cache:/app/.gradle + - backoffice_frontend_build:/app/build + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + command: ["./gradlew", "bootRun", "--args", "backoffice_frontend server"] + + mooc_backend_server_java: + container_name: codely-java_ddd_example-mooc_backend_server + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + ports: + - "8030:8030" + volumes: + - .:/app:delegated + - mooc_backend_gradle_cache:/app/.gradle + - mooc_backend_build:/app/build + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + command: ["./gradlew", "bootRun", "--args", "mooc_backend server"] + + mooc_backend_consumers_java: + container_name: codely-java_ddd_example-mooc_backend_consumers + build: + context: . + dockerfile: Dockerfile + restart: unless-stopped + volumes: + - .:/app:delegated + - mooc_consumers_gradle_cache:/app/.gradle + - mooc_consumers_build:/app/build + depends_on: + - shared_mysql + - shared_rabbitmq + - backoffice_elasticsearch + command: ["./gradlew", "bootRun", "--args", "mooc_backend domain-events:rabbitmq:consume"] + +volumes: + backoffice_backend_gradle_cache: + backoffice_backend_build: + backoffice_consumers_gradle_cache: + backoffice_consumers_build: + backoffice_frontend_gradle_cache: + backoffice_frontend_build: + mooc_backend_gradle_cache: + mooc_backend_build: + mooc_consumers_gradle_cache: + mooc_consumers_build: diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/etc/http/backoffice_frontend.http b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/etc/http/backoffice_frontend.http new file mode 100644 index 0000000..119764f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/etc/http/backoffice_frontend.http @@ -0,0 +1,80 @@ +# ELASTIC - Search +POST localhost:9200/backoffice_courses/_search +Content-Type: application/json + +{ + "query": { + "term": { + "name": "git avanzado" + } + } +} + +### + +# ELASTIC - debug +POST localhost:9200/backoffice_courses/_search +Content-Type: application/json + +{ + "from": 0, + "size": 1000, + "query": { + "bool": { + "must": [ + { + "term": { + "name": { + "value": "pepe2" + } + } + } + ], + "adjust_pure_negative": true, + "boost": 1.0 + } + } +} + +### +# ELASTIC - Search +POST localhost:9200/backoffice_courses/_search +Content-Type: application/json + +### +# ELASTIC - Mapping +GET localhost:9200/backoffice_courses/_mapping +Content-Type: application/json + +### +# ELASTIC - Change mapping +PUT localhost:9200/backoffice_courses/_mapping/_doc +Content-Type: application/json + +{ + "properties": { + "name": { + "type": "text", + "fielddata": true + } + } +} + +### +# ELASTIC - DELETE +DELETE localhost:9200/backoffice_courses + +### + +PUT localhost:9200/backoffice_courses/_settings +Content-Type: application/json + +{ + "index": { + "blocks": { + "read_only_allow_delete": "false" + } + } +} + +### diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/etc/http/publish_domain_events.http b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/etc/http/publish_domain_events.http new file mode 100644 index 0000000..a27db73 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/etc/http/publish_domain_events.http @@ -0,0 +1,62 @@ +POST http://localhost:8030/domain-events +Content-Type: application/json + +{ + "eventName": "course.created", + "eventRaw": { + "data": { + "id": "{{$random.uuid}}", + "type": "course.created", + "occurred_on": "2023-11-14 10:00:00", + "attributes": { + "id": "9bd0c98a-92cc-4a56-a5a1-7d40839ddc83", + "name": "Demo course", + "duration": "2 days" + } + }, + "meta": { + } + } +} + +### +POST http://localhost:8030/domain-events +Content-Type: application/json + +{ + "eventName": "course.renamed", + "eventRaw": { + "data": { + "id": "{{$random.uuid}}", + "type": "course.renamed", + "occurred_on": "2023-11-14 10:00:00", + "attributes": { + "id": "7b081a3e-f90e-4efe-a3a5-81e853e89c8b", + "name": "Este es el nombre bueno" + } + }, + "meta": { + } + } +} + +### +POST http://localhost:8030/domain-events +Content-Type: application/json + +{ + "eventName": "course.renamed", + "eventRaw": { + "data": { + "id": "{{$random.uuid}}", + "type": "course.renamed", + "occurred_on": "2022-11-14 10:00:00", + "attributes": { + "id": "7b081a3e-f90e-4efe-a3a5-81e853e89c8b", + "name": "Este es el nombre malo" + } + }, + "meta": { + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/gradle/wrapper/gradle-wrapper.jar b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7f93135 Binary files /dev/null and b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/gradle/wrapper/gradle-wrapper.jar differ diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/gradle/wrapper/gradle-wrapper.properties b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3fa8f86 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/gradlew b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/gradlew.bat b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/gradlew.bat new file mode 100755 index 0000000..93e3f59 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/settings.gradle b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/settings.gradle new file mode 100644 index 0000000..4823818 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/settings.gradle @@ -0,0 +1,13 @@ +rootProject.name = 'java-ddd-example' + +include ':shared' +project(':shared').projectDir = new File('src/shared') + +include ':analytics' +project(':analytics').projectDir = new File('src/analytics') + +include ':backoffice' +project(':backoffice').projectDir = new File('src/backoffice') + +include ':mooc' +project(':mooc').projectDir = new File('src/mooc') diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/application/store/DomainEventStorer.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/application/store/DomainEventStorer.java new file mode 100644 index 0000000..674c72e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/application/store/DomainEventStorer.java @@ -0,0 +1,22 @@ +package tv.codely.analytics.domain_events.application.store; + +import tv.codely.analytics.domain_events.domain.*; + +public final class DomainEventStorer { + private DomainEventsRepository repository; + + public DomainEventStorer(DomainEventsRepository repository) { + this.repository = repository; + } + + public void store( + AnalyticsDomainEventId id, + AnalyticsDomainEventAggregateId aggregateId, + AnalyticsDomainEventName name, + AnalyticsDomainEventBody body + ) { + AnalyticsDomainEvent domainEvent = new AnalyticsDomainEvent(id, aggregateId, name, body); + + repository.save(domainEvent); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/application/store/StoreDomainEventOnOccurred.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/application/store/StoreDomainEventOnOccurred.java new file mode 100644 index 0000000..23413ef --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/application/store/StoreDomainEventOnOccurred.java @@ -0,0 +1,28 @@ +package tv.codely.analytics.domain_events.application.store; + +import org.springframework.context.event.EventListener; +import tv.codely.analytics.domain_events.domain.AnalyticsDomainEventAggregateId; +import tv.codely.analytics.domain_events.domain.AnalyticsDomainEventBody; +import tv.codely.analytics.domain_events.domain.AnalyticsDomainEventId; +import tv.codely.analytics.domain_events.domain.AnalyticsDomainEventName; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; + +@DomainEventSubscriber({DomainEvent.class}) +public final class StoreDomainEventOnOccurred { + private final DomainEventStorer storer; + + public StoreDomainEventOnOccurred(DomainEventStorer storer) { + this.storer = storer; + } + + @EventListener + public void on(DomainEvent event) { + AnalyticsDomainEventId id = new AnalyticsDomainEventId(event.eventId()); + AnalyticsDomainEventAggregateId aggregateId = new AnalyticsDomainEventAggregateId(event.aggregateId()); + AnalyticsDomainEventName name = new AnalyticsDomainEventName(event.eventName()); + AnalyticsDomainEventBody body = new AnalyticsDomainEventBody(event.toPrimitives()); + + storer.store(id, aggregateId, name, body); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEvent.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEvent.java new file mode 100644 index 0000000..c2f310e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEvent.java @@ -0,0 +1,37 @@ +package tv.codely.analytics.domain_events.domain; + +public final class AnalyticsDomainEvent { + private final AnalyticsDomainEventId id; + private final AnalyticsDomainEventAggregateId aggregateId; + private final AnalyticsDomainEventName name; + private final AnalyticsDomainEventBody body; + + public AnalyticsDomainEvent( + AnalyticsDomainEventId id, + AnalyticsDomainEventAggregateId aggregateId, + AnalyticsDomainEventName name, + AnalyticsDomainEventBody body + ) { + + this.id = id; + this.aggregateId = aggregateId; + this.name = name; + this.body = body; + } + + public AnalyticsDomainEventId getId() { + return id; + } + + public AnalyticsDomainEventAggregateId getAggregateId() { + return aggregateId; + } + + public AnalyticsDomainEventName getName() { + return name; + } + + public AnalyticsDomainEventBody getBody() { + return body; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventAggregateId.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventAggregateId.java new file mode 100644 index 0000000..beef90d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventAggregateId.java @@ -0,0 +1,9 @@ +package tv.codely.analytics.domain_events.domain; + +import tv.codely.shared.domain.Identifier; + +public final class AnalyticsDomainEventAggregateId extends Identifier { + public AnalyticsDomainEventAggregateId(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventBody.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventBody.java new file mode 100644 index 0000000..a706b7a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventBody.java @@ -0,0 +1,16 @@ +package tv.codely.analytics.domain_events.domain; + +import java.io.Serializable; +import java.util.HashMap; + +public final class AnalyticsDomainEventBody { + private HashMap value; + + public HashMap getValue() { + return value; + } + + public AnalyticsDomainEventBody(HashMap value) { + this.value = value; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventId.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventId.java new file mode 100644 index 0000000..5f730dd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventId.java @@ -0,0 +1,9 @@ +package tv.codely.analytics.domain_events.domain; + +import tv.codely.shared.domain.Identifier; + +public final class AnalyticsDomainEventId extends Identifier { + public AnalyticsDomainEventId(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventName.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventName.java new file mode 100644 index 0000000..d4a018e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventName.java @@ -0,0 +1,9 @@ +package tv.codely.analytics.domain_events.domain; + +import tv.codely.shared.domain.StringValueObject; + +public final class AnalyticsDomainEventName extends StringValueObject { + public AnalyticsDomainEventName(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/DomainEventsRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/DomainEventsRepository.java new file mode 100644 index 0000000..c04ac90 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/analytics/main/tv/codely/analytics/domain_events/domain/DomainEventsRepository.java @@ -0,0 +1,5 @@ +package tv.codely.analytics.domain_events.domain; + +public interface DomainEventsRepository { + void save(AnalyticsDomainEvent event); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/build.gradle b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/build.gradle new file mode 100644 index 0000000..7d82dc7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/resources/database/backoffice.sql b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/resources/database/backoffice.sql new file mode 100644 index 0000000..58f92fa --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/resources/database/backoffice.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS `courses` ( + `id` CHAR(36) NOT NULL, + `name` VARCHAR(255) NOT NULL, + `duration` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/resources/database/backoffice/backoffice_courses.json b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/resources/database/backoffice/backoffice_courses.json new file mode 100644 index 0000000..7891e8b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/resources/database/backoffice/backoffice_courses.json @@ -0,0 +1,22 @@ +{ + "mappings": { + "courses": { + "properties": { + "id": { + "type": "keyword", + "index": true + }, + "name": { + "type": "text", + "index": true, + "fielddata": true + }, + "duration": { + "type": "text", + "index": true, + "fielddata": true + } + } + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommand.java new file mode 100644 index 0000000..5e1e74c --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommand.java @@ -0,0 +1,21 @@ +package tv.codely.backoffice.auth.application.authenticate; + +import tv.codely.shared.domain.bus.command.Command; + +public final class AuthenticateUserCommand implements Command { + private final String username; + private final String password; + + public AuthenticateUserCommand(String username, String password) { + this.username = username; + this.password = password; + } + + public String username() { + return username; + } + + public String password() { + return password; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandler.java new file mode 100644 index 0000000..ec43db6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandler.java @@ -0,0 +1,23 @@ +package tv.codely.backoffice.auth.application.authenticate; + +import tv.codely.backoffice.auth.domain.AuthPassword; +import tv.codely.backoffice.auth.domain.AuthUsername; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.command.CommandHandler; + +@Service +public final class AuthenticateUserCommandHandler implements CommandHandler { + private final UserAuthenticator authenticator; + + public AuthenticateUserCommandHandler(UserAuthenticator authenticator) { + this.authenticator = authenticator; + } + + @Override + public void handle(AuthenticateUserCommand command) { + AuthUsername username = new AuthUsername(command.username()); + AuthPassword password = new AuthPassword(command.password()); + + authenticator.authenticate(username, password); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/UserAuthenticator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/UserAuthenticator.java new file mode 100644 index 0000000..1b43c79 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/UserAuthenticator.java @@ -0,0 +1,34 @@ +package tv.codely.backoffice.auth.application.authenticate; + +import tv.codely.backoffice.auth.domain.*; +import tv.codely.shared.domain.Service; + +import java.util.Optional; + +@Service +public final class UserAuthenticator { + private final AuthRepository repository; + + public UserAuthenticator(AuthRepository repository) { + this.repository = repository; + } + + public void authenticate(AuthUsername username, AuthPassword password) { + Optional auth = repository.search(username); + + ensureUserExist(auth, username); + ensureCredentialsAreValid(auth.get(), password); + } + + private void ensureUserExist(Optional auth, AuthUsername username) { + if (!auth.isPresent()) { + throw new InvalidAuthUsername(username); + } + } + + private void ensureCredentialsAreValid(AuthUser auth, AuthPassword password) { + if (!auth.passwordMatches(password)) { + throw new InvalidAuthCredentials(auth.username()); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthPassword.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthPassword.java new file mode 100644 index 0000000..331588f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthPassword.java @@ -0,0 +1,9 @@ +package tv.codely.backoffice.auth.domain; + +import tv.codely.shared.domain.StringValueObject; + +public final class AuthPassword extends StringValueObject { + public AuthPassword(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthRepository.java new file mode 100644 index 0000000..37152ec --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthRepository.java @@ -0,0 +1,7 @@ +package tv.codely.backoffice.auth.domain; + +import java.util.Optional; + +public interface AuthRepository { + Optional search(AuthUsername username); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUser.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUser.java new file mode 100644 index 0000000..af0d45e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUser.java @@ -0,0 +1,19 @@ +package tv.codely.backoffice.auth.domain; + +public final class AuthUser { + private final AuthUsername username; + private final AuthPassword password; + + public AuthUser(AuthUsername username, AuthPassword password) { + this.username = username; + this.password = password; + } + + public AuthUsername username() { + return username; + } + + public boolean passwordMatches(AuthPassword password) { + return this.password.equals(password); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUsername.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUsername.java new file mode 100644 index 0000000..6d1d48a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUsername.java @@ -0,0 +1,9 @@ +package tv.codely.backoffice.auth.domain; + +import tv.codely.shared.domain.StringValueObject; + +public final class AuthUsername extends StringValueObject { + public AuthUsername(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthCredentials.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthCredentials.java new file mode 100644 index 0000000..3092104 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthCredentials.java @@ -0,0 +1,7 @@ +package tv.codely.backoffice.auth.domain; + +public final class InvalidAuthCredentials extends RuntimeException { + public InvalidAuthCredentials(AuthUsername username) { + super(String.format("The credentials for <%s> are invalid", username.value())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthUsername.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthUsername.java new file mode 100644 index 0000000..857c10f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthUsername.java @@ -0,0 +1,7 @@ +package tv.codely.backoffice.auth.domain; + +public final class InvalidAuthUsername extends RuntimeException { + public InvalidAuthUsername(AuthUsername username) { + super(String.format("The user <%s> does not exist", username.value())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/infrastructure/persistence/InMemoryAuthRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/infrastructure/persistence/InMemoryAuthRepository.java new file mode 100644 index 0000000..e873c4f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/auth/infrastructure/persistence/InMemoryAuthRepository.java @@ -0,0 +1,25 @@ +package tv.codely.backoffice.auth.infrastructure.persistence; + +import tv.codely.backoffice.auth.domain.AuthPassword; +import tv.codely.backoffice.auth.domain.AuthRepository; +import tv.codely.backoffice.auth.domain.AuthUser; +import tv.codely.backoffice.auth.domain.AuthUsername; +import tv.codely.shared.domain.Service; + +import java.util.HashMap; +import java.util.Optional; + +@Service +public final class InMemoryAuthRepository implements AuthRepository { + private final HashMap users = new HashMap() {{ + put(new AuthUsername("javi"), new AuthPassword("barbitas")); + put(new AuthUsername("rafa"), new AuthPassword("pelazo")); + }}; + + @Override + public Optional search(AuthUsername username) { + return users.containsKey(username) + ? Optional.of(new AuthUser(username, users.get(username))) + : Optional.empty(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCourseResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCourseResponse.java new file mode 100644 index 0000000..5871ac9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCourseResponse.java @@ -0,0 +1,32 @@ +package tv.codely.backoffice.courses.application; + +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.shared.domain.bus.query.Response; + +public final class BackofficeCourseResponse implements Response { + private final String id; + private final String name; + private final String duration; + + public BackofficeCourseResponse(String id, String name, String duration) { + this.id = id; + this.name = name; + this.duration = duration; + } + + public static BackofficeCourseResponse fromAggregate(BackofficeCourse course) { + return new BackofficeCourseResponse(course.id(), course.name(), course.duration()); + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public String duration() { + return duration; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCoursesResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCoursesResponse.java new file mode 100644 index 0000000..1225864 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCoursesResponse.java @@ -0,0 +1,17 @@ +package tv.codely.backoffice.courses.application; + +import tv.codely.shared.domain.bus.query.Response; + +import java.util.List; + +public final class BackofficeCoursesResponse implements Response { + private final List courses; + + public BackofficeCoursesResponse(List courses) { + this.courses = courses; + } + + public List courses() { + return courses; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java new file mode 100644 index 0000000..f129d16 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java @@ -0,0 +1,18 @@ +package tv.codely.backoffice.courses.application.create; + +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; + +@Service +public final class BackofficeCourseCreator { + private final BackofficeCourseRepository repository; + + public BackofficeCourseCreator(BackofficeCourseRepository repository) { + this.repository = repository; + } + + public void create(String id, String name, String duration) { + this.repository.save(new BackofficeCourse(id, name, duration)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/create/CreateBackofficeCourseOnCourseCreated.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/create/CreateBackofficeCourseOnCourseCreated.java new file mode 100644 index 0000000..fd06214 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/create/CreateBackofficeCourseOnCourseCreated.java @@ -0,0 +1,21 @@ +package tv.codely.backoffice.courses.application.create; + +import org.springframework.context.event.EventListener; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +@Service +@DomainEventSubscriber({CourseCreatedDomainEvent.class}) +public final class CreateBackofficeCourseOnCourseCreated { + private final BackofficeCourseCreator creator; + + public CreateBackofficeCourseOnCourseCreated(BackofficeCourseCreator creator) { + this.creator = creator; + } + + @EventListener + public void on(CourseCreatedDomainEvent event) { + creator.create(event.aggregateId(), event.name(), event.duration()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java new file mode 100644 index 0000000..e50c42f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java @@ -0,0 +1,26 @@ +package tv.codely.backoffice.courses.application.rename; + +import tv.codely.backoffice.courses.domain.BackofficeCourseNotFound; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; + +@Service +public final class BackofficeCourseRenamer { + private final BackofficeCourseRepository repository; + + public BackofficeCourseRenamer(BackofficeCourseRepository repository) { + this.repository = repository; + } + + public void rename(String id, String name) { + this.repository.search(id) + .ifPresentOrElse(course -> { + course.rename(name); + + this.repository.save(course); + }, + () -> { + throw new BackofficeCourseNotFound(id); + }); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java new file mode 100644 index 0000000..72415cd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java @@ -0,0 +1,43 @@ +package tv.codely.backoffice.courses.application.rename; + +import org.springframework.context.event.EventListener; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; +import tv.codely.shared.domain.course.CourseRenamedDomainEvent; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Service +@DomainEventSubscriber({CourseRenamedDomainEvent.class}) +public final class RenameBackofficeCourseOnCourseRenamed { + private static final Map lastExecutionTimeMap = new ConcurrentHashMap<>(); + + private final BackofficeCourseRenamer renamer; + + public RenameBackofficeCourseOnCourseRenamed(BackofficeCourseRenamer renamer) { + this.renamer = renamer; + } + + @EventListener + public void on(CourseRenamedDomainEvent event) { + Instant eventTime = LocalDateTime.parse( + event.occurredOn(), + DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + ).atZone(ZoneId.systemDefault()).toInstant(); + + if (eventDateIsMoreRecentThanLastTime(event.aggregateId(), eventTime)) { + renamer.rename(event.aggregateId(), event.name()); + + lastExecutionTimeMap.put(event.aggregateId(), eventTime); + } + } + + private boolean eventDateIsMoreRecentThanLastTime(String aggregateId, Instant eventTime) { + return lastExecutionTimeMap.getOrDefault(aggregateId, Instant.MIN).isBefore(eventTime); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/AllBackofficeCoursesSearcher.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/AllBackofficeCoursesSearcher.java new file mode 100644 index 0000000..db65ef0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/AllBackofficeCoursesSearcher.java @@ -0,0 +1,23 @@ +package tv.codely.backoffice.courses.application.search_all; + +import tv.codely.backoffice.courses.application.BackofficeCourseResponse; +import tv.codely.backoffice.courses.application.BackofficeCoursesResponse; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; + +import java.util.stream.Collectors; + +@Service +public final class AllBackofficeCoursesSearcher { + private final BackofficeCourseRepository repository; + + public AllBackofficeCoursesSearcher(BackofficeCourseRepository repository) { + this.repository = repository; + } + + public BackofficeCoursesResponse search() { + return new BackofficeCoursesResponse( + repository.searchAll().stream().map(BackofficeCourseResponse::fromAggregate).collect(Collectors.toList()) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQuery.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQuery.java new file mode 100644 index 0000000..fc00a52 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQuery.java @@ -0,0 +1,6 @@ +package tv.codely.backoffice.courses.application.search_all; + +import tv.codely.shared.domain.bus.query.Query; + +public final class SearchAllBackofficeCoursesQuery implements Query { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQueryHandler.java new file mode 100644 index 0000000..b500126 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQueryHandler.java @@ -0,0 +1,19 @@ +package tv.codely.backoffice.courses.application.search_all; + +import tv.codely.backoffice.courses.application.BackofficeCoursesResponse; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.QueryHandler; + +@Service +public final class SearchAllBackofficeCoursesQueryHandler implements QueryHandler { + private final AllBackofficeCoursesSearcher searcher; + + public SearchAllBackofficeCoursesQueryHandler(AllBackofficeCoursesSearcher searcher) { + this.searcher = searcher; + } + + @Override + public BackofficeCoursesResponse handle(SearchAllBackofficeCoursesQuery query) { + return searcher.search(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/BackofficeCoursesByCriteriaSearcher.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/BackofficeCoursesByCriteriaSearcher.java new file mode 100644 index 0000000..1533079 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/BackofficeCoursesByCriteriaSearcher.java @@ -0,0 +1,37 @@ +package tv.codely.backoffice.courses.application.search_by_criteria; + +import tv.codely.backoffice.courses.application.BackofficeCourseResponse; +import tv.codely.backoffice.courses.application.BackofficeCoursesResponse; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.domain.criteria.Filters; +import tv.codely.shared.domain.criteria.Order; + +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +public final class BackofficeCoursesByCriteriaSearcher { + private final BackofficeCourseRepository repository; + + public BackofficeCoursesByCriteriaSearcher(BackofficeCourseRepository repository) { + this.repository = repository; + } + + public BackofficeCoursesResponse search( + Filters filters, + Order order, + Optional limit, + Optional offset + ) { + Criteria criteria = new Criteria(filters, order, limit, offset); + + return new BackofficeCoursesResponse( + repository.matching(criteria) + .stream() + .map(BackofficeCourseResponse::fromAggregate) + .collect(Collectors.toList()) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQuery.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQuery.java new file mode 100644 index 0000000..2671661 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQuery.java @@ -0,0 +1,49 @@ +package tv.codely.backoffice.courses.application.search_by_criteria; + +import tv.codely.shared.domain.bus.query.Query; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +public final class SearchBackofficeCoursesByCriteriaQuery implements Query { + private final List> filters; + private final Optional orderBy; + private final Optional orderType; + private final Optional limit; + private final Optional offset; + + public SearchBackofficeCoursesByCriteriaQuery( + List> filters, + Optional orderBy, + Optional orderType, + Optional limit, + Optional offset + ) { + this.filters = filters; + this.orderBy = orderBy; + this.orderType = orderType; + this.limit = limit; + this.offset = offset; + } + + public List> filters() { + return filters; + } + + public Optional orderBy() { + return orderBy; + } + + public Optional orderType() { + return orderType; + } + + public Optional limit() { + return limit; + } + + public Optional offset() { + return offset; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQueryHandler.java new file mode 100644 index 0000000..8bf9363 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQueryHandler.java @@ -0,0 +1,24 @@ +package tv.codely.backoffice.courses.application.search_by_criteria; + +import tv.codely.backoffice.courses.application.BackofficeCoursesResponse; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.QueryHandler; +import tv.codely.shared.domain.criteria.Filters; +import tv.codely.shared.domain.criteria.Order; + +@Service +public final class SearchBackofficeCoursesByCriteriaQueryHandler implements QueryHandler { + private final BackofficeCoursesByCriteriaSearcher searcher; + + public SearchBackofficeCoursesByCriteriaQueryHandler(BackofficeCoursesByCriteriaSearcher searcher) { + this.searcher = searcher; + } + + @Override + public BackofficeCoursesResponse handle(SearchBackofficeCoursesByCriteriaQuery query) { + Filters filters = Filters.fromValues(query.filters()); + Order order = Order.fromValues(query.orderBy(), query.orderType()); + + return searcher.search(filters, order, query.limit(), query.offset()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java new file mode 100644 index 0000000..05f88b1 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java @@ -0,0 +1,75 @@ +package tv.codely.backoffice.courses.domain; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +public final class BackofficeCourse { + private final String id; + private String name; + private final String duration; + + public BackofficeCourse() { + id = null; + name = null; + duration = null; + } + + public BackofficeCourse(String id, String name, String duration) { + this.id = id; + this.name = name; + this.duration = duration; + } + + public static BackofficeCourse fromPrimitives(Map plainData) { + return new BackofficeCourse( + (String) plainData.get("id"), + (String) plainData.get("name"), + (String) plainData.get("duration") + ); + } + + public void rename(String newName) { + this.name = newName; + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public String duration() { + return duration; + } + + public HashMap toPrimitives() { + return new HashMap() {{ + put("id", id); + put("name", name); + put("duration", duration); + }}; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + BackofficeCourse that = (BackofficeCourse) o; + return id.equals(that.id) && + name.equals(that.name) && + duration.equals(that.duration); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, duration); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java new file mode 100644 index 0000000..f5596ff --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java @@ -0,0 +1,7 @@ +package tv.codely.backoffice.courses.domain; + +public class BackofficeCourseNotFound extends RuntimeException { + public BackofficeCourseNotFound(String id) { + super(String.format("The course <%s> doesn't exist", id)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java new file mode 100644 index 0000000..3689352 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java @@ -0,0 +1,16 @@ +package tv.codely.backoffice.courses.domain; + +import tv.codely.shared.domain.criteria.Criteria; + +import java.util.List; +import java.util.Optional; + +public interface BackofficeCourseRepository { + void save(BackofficeCourse course); + + Optional search(String id); + + List searchAll(); + + List matching(Criteria criteria); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java new file mode 100644 index 0000000..1206e8f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java @@ -0,0 +1,45 @@ +package tv.codely.backoffice.courses.infrastructure.persistence; + +import org.springframework.context.annotation.Primary; +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.infrastructure.elasticsearch.ElasticsearchClient; +import tv.codely.shared.infrastructure.elasticsearch.ElasticsearchRepository; + +import java.util.List; +import java.util.Optional; + +@Primary +@Service +public final class ElasticsearchBackofficeCourseRepository extends ElasticsearchRepository implements BackofficeCourseRepository { + public ElasticsearchBackofficeCourseRepository(ElasticsearchClient client) { + super(client); + } + + @Override + public void save(BackofficeCourse course) { + persist(course.id(), course.toPrimitives()); + } + + @Override + public Optional search(String id) { + return this.searchById(id, BackofficeCourse::fromPrimitives); + } + + @Override + public List searchAll() { + return searchAllInElastic(BackofficeCourse::fromPrimitives); + } + + @Override + public List matching(Criteria criteria) { + return searchByCriteria(criteria, BackofficeCourse::fromPrimitives); + } + + @Override + protected String moduleName() { + return "courses"; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java new file mode 100644 index 0000000..b645f5b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java @@ -0,0 +1,64 @@ +package tv.codely.backoffice.courses.infrastructure.persistence; + +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.criteria.Criteria; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +public final class InMemoryCacheBackofficeCourseRepository implements BackofficeCourseRepository { + private final BackofficeCourseRepository repository; + private List courses = new ArrayList<>(); + private HashMap> matchingCourses = new HashMap<>(); + + public InMemoryCacheBackofficeCourseRepository(BackofficeCourseRepository repository) { + this.repository = repository; + } + + @Override + public void save(BackofficeCourse course) { + repository.save(course); + } + + @Override + public List searchAll() { + return courses.isEmpty() ? searchAndFillCache() : courses; + } + + public Optional search(String id) { + return courses.stream() + .filter(course -> course.id().equals(id)) + .findFirst() + .or(() -> { + Optional course = repository.search(id); + course.ifPresent(courses::add); + return course; + }); + } + + @Override + public List matching(Criteria criteria) { + return matchingCourses.containsKey(criteria.serialize()) + ? matchingCourses.get(criteria.serialize()) + : searchMatchingAndFillCache(criteria); + } + + private List searchMatchingAndFillCache(Criteria criteria) { + List courses = repository.matching(criteria); + + this.matchingCourses.put(criteria.serialize(), courses); + + return courses; + } + + private List searchAndFillCache() { + List courses = repository.searchAll(); + + this.courses = courses; + + return courses; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java new file mode 100644 index 0000000..a23fd77 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java @@ -0,0 +1,41 @@ +package tv.codely.backoffice.courses.infrastructure.persistence; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.transaction.annotation.Transactional; +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.infrastructure.hibernate.HibernateRepository; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional("backoffice-transaction_manager") +public class MySqlBackofficeCourseRepository extends HibernateRepository implements BackofficeCourseRepository { + public MySqlBackofficeCourseRepository(@Qualifier("backoffice-session_factory") SessionFactory sessionFactory) { + super(sessionFactory, BackofficeCourse.class); + } + + @Override + public void save(BackofficeCourse course) { + persist(course); + } + + @Override + public Optional search(String id) { + return byId(id); + } + + @Override + public List searchAll() { + return all(); + } + + @Override + public List matching(Criteria criteria) { + return byCriteria(criteria); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml new file mode 100644 index 0000000..a8592d0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeElasticsearchConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeElasticsearchConfiguration.java new file mode 100644 index 0000000..98c0f1a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeElasticsearchConfiguration.java @@ -0,0 +1,89 @@ +package tv.codely.backoffice.shared.infrastructure.persistence; + +import org.apache.http.HttpHost; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.indices.GetIndexRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; +import tv.codely.shared.domain.Utils; +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; +import tv.codely.shared.infrastructure.elasticsearch.ElasticsearchClient; + +import java.io.IOException; +import java.util.Objects; +import java.util.Scanner; + +@Configuration +public class BackofficeElasticsearchConfiguration { + private final Parameter config; + private final ResourcePatternResolver resourceResolver; + + public BackofficeElasticsearchConfiguration(Parameter config, ResourcePatternResolver resourceResolver) { + this.config = config; + this.resourceResolver = resourceResolver; + } + + @Bean + public ElasticsearchClient elasticsearchClient() throws ParameterNotExist, Exception { + ElasticsearchClient client = new ElasticsearchClient( + new RestHighLevelClient( + RestClient.builder( + new HttpHost( + config.get("BACKOFFICE_ELASTICSEARCH_HOST"), + config.getInt("BACKOFFICE_ELASTICSEARCH_PORT"), + "http" + ) + ) + ), + RestClient.builder( + new HttpHost( + config.get("BACKOFFICE_ELASTICSEARCH_HOST"), + config.getInt("BACKOFFICE_ELASTICSEARCH_PORT"), + "http" + )).build(), + config.get("BACKOFFICE_ELASTICSEARCH_INDEX_PREFIX") + ); + + Utils.retry(10, 10000, () -> { + try { + generateIndexIfNotExists(client, "backoffice"); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + return client; + } + + private void generateIndexIfNotExists(ElasticsearchClient client, String contextName) throws IOException { + Resource[] jsonsIndexes = resourceResolver.getResources( + String.format("classpath:database/%s/*.json", contextName) + ); + + for (Resource jsonIndex : jsonsIndexes) { + String indexName = Objects.requireNonNull(jsonIndex.getFilename()).replace(".json", ""); + + if (!indexExists(indexName, client)) { + String indexBody = new Scanner( + jsonIndex.getInputStream(), + "UTF-8" + ).useDelimiter("\\A").next(); + + Request request = new Request("PUT", indexName); + request.setJsonEntity(indexBody); + + client.lowLevelClient().performRequest(request); + } + } + } + + private boolean indexExists(String indexName, ElasticsearchClient client) throws IOException { + return client.highLevelClient().indices().exists(new GetIndexRequest(indexName), RequestOptions.DEFAULT); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeHibernateConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeHibernateConfiguration.java new file mode 100644 index 0000000..0615ff7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeHibernateConfiguration.java @@ -0,0 +1,47 @@ +package tv.codely.backoffice.shared.infrastructure.persistence; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; +import tv.codely.shared.infrastructure.hibernate.HibernateConfigurationFactory; + +import javax.sql.DataSource; +import java.io.IOException; + +@Configuration +@EnableTransactionManagement +public class BackofficeHibernateConfiguration { + private final HibernateConfigurationFactory factory; + private final Parameter config; + private final String CONTEXT_NAME = "backoffice"; + + public BackofficeHibernateConfiguration(HibernateConfigurationFactory factory, Parameter config) { + this.factory = factory; + this.config = config; + } + + @Bean("backoffice-transaction_manager") + public PlatformTransactionManager hibernateTransactionManager() throws IOException, ParameterNotExist { + return factory.hibernateTransactionManager(sessionFactory()); + } + + @Bean("backoffice-session_factory") + public LocalSessionFactoryBean sessionFactory() throws IOException, ParameterNotExist { + return factory.sessionFactory(CONTEXT_NAME, dataSource()); + } + + @Bean("backoffice-data_source") + public DataSource dataSource() throws IOException, ParameterNotExist { + return factory.dataSource( + config.get("BACKOFFICE_DATABASE_HOST"), + config.getInt("BACKOFFICE_DATABASE_PORT"), + config.get("BACKOFFICE_DATABASE_NAME"), + config.get("BACKOFFICE_DATABASE_USER"), + config.get("BACKOFFICE_DATABASE_PASSWORD") + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java new file mode 100644 index 0000000..1113298 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java @@ -0,0 +1,38 @@ +package tv.codely.backoffice.shared.infrastructure.persistence; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import tv.codely.shared.infrastructure.bus.event.DomainEventsInformation; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlDomainEventsConsumer; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; +import tv.codely.shared.infrastructure.bus.event.spring.SpringApplicationEventBus; + +@Configuration +public class BackofficeMySqlEventBusConfiguration { + private final SessionFactory sessionFactory; + private final DomainEventsInformation domainEventsInformation; + private final SpringApplicationEventBus bus; + + public BackofficeMySqlEventBusConfiguration( + @Qualifier("backoffice-session_factory") SessionFactory sessionFactory, + DomainEventsInformation domainEventsInformation, + SpringApplicationEventBus bus + ) { + this.sessionFactory = sessionFactory; + this.domainEventsInformation = domainEventsInformation; + this.bus = bus; + } + + @Bean + public MySqlEventBus backofficeMysqlEventBus() { + return new MySqlEventBus(sessionFactory); + } + + @Bean + public MySqlDomainEventsConsumer backofficeMySqlDomainEventsConsumer() { + return new MySqlDomainEventsConsumer(sessionFactory, domainEventsInformation, bus); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeRabbitMqEventBusConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeRabbitMqEventBusConfiguration.java new file mode 100644 index 0000000..b0fae3c --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeRabbitMqEventBusConfiguration.java @@ -0,0 +1,27 @@ +package tv.codely.backoffice.shared.infrastructure.persistence; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqPublisher; + +@Configuration +public class BackofficeRabbitMqEventBusConfiguration { + private final RabbitMqPublisher publisher; + private final MySqlEventBus failoverPublisher; + + public BackofficeRabbitMqEventBusConfiguration( + RabbitMqPublisher publisher, + @Qualifier("backofficeMysqlEventBus") MySqlEventBus failoverPublisher + ) { + this.publisher = publisher; + this.failoverPublisher = failoverPublisher; + } + + @Bean + public RabbitMqEventBus backofficeRabbitMqEventBus() { + return new RabbitMqEventBus(publisher, failoverPublisher); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/BackofficeContextInfrastructureTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/BackofficeContextInfrastructureTestCase.java new file mode 100644 index 0000000..efbad4e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/BackofficeContextInfrastructureTestCase.java @@ -0,0 +1,21 @@ +package tv.codely.backoffice; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import tv.codely.apps.backoffice.frontend.BackofficeFrontendApplication; +import tv.codely.backoffice.courses.ElasticsearchEnvironmentArranger; +import tv.codely.shared.infrastructure.InfrastructureTestCase; + +import java.io.IOException; + +@ContextConfiguration(classes = BackofficeFrontendApplication.class) +@SpringBootTest +public abstract class BackofficeContextInfrastructureTestCase extends InfrastructureTestCase { + @Autowired + private ElasticsearchEnvironmentArranger elasticsearchArranger; + + protected void clearElasticsearch() throws IOException { + elasticsearchArranger.arrange("backoffice", "backoffice_courses"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/AuthModuleUnitTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/AuthModuleUnitTestCase.java new file mode 100644 index 0000000..4ccc749 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/AuthModuleUnitTestCase.java @@ -0,0 +1,31 @@ +package tv.codely.backoffice.auth; + +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mockito; +import tv.codely.backoffice.auth.domain.AuthRepository; +import tv.codely.backoffice.auth.domain.AuthUser; +import tv.codely.backoffice.auth.domain.AuthUsername; +import tv.codely.shared.infrastructure.UnitTestCase; + +import java.util.Optional; + +import static org.mockito.Mockito.mock; + +public abstract class AuthModuleUnitTestCase extends UnitTestCase { + protected AuthRepository repository; + + @BeforeEach + protected void setUp() { + super.setUp(); + + repository = mock(AuthRepository.class); + } + + public void shouldSearch(AuthUsername username, AuthUser user) { + Mockito.when(repository.search(username)).thenReturn(Optional.of(user)); + } + + public void shouldSearch(AuthUsername username) { + Mockito.when(repository.search(username)).thenReturn(Optional.empty()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandlerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandlerShould.java new file mode 100644 index 0000000..a37020e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandlerShould.java @@ -0,0 +1,56 @@ +package tv.codely.backoffice.auth.application.authenticate; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tv.codely.backoffice.auth.AuthModuleUnitTestCase; +import tv.codely.backoffice.auth.domain.AuthUser; +import tv.codely.backoffice.auth.domain.AuthUserMother; +import tv.codely.backoffice.auth.domain.InvalidAuthCredentials; +import tv.codely.backoffice.auth.domain.InvalidAuthUsername; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class AuthenticateUserCommandHandlerShould extends AuthModuleUnitTestCase { + private AuthenticateUserCommandHandler handler; + + @BeforeEach + protected void setUp() { + super.setUp(); + + handler = new AuthenticateUserCommandHandler(new UserAuthenticator(repository)); + } + + @Test + void authenticate_a_valid_user() { + AuthenticateUserCommand command = AuthenticateUserCommandMother.random(); + AuthUser authUser = AuthUserMother.fromCommand(command); + + shouldSearch(authUser.username(), authUser); + + handler.handle(command); + } + + @Test + void throw_an_exception_when_the_user_does_not_exist() { + assertThrows(InvalidAuthUsername.class, () -> { + AuthenticateUserCommand command = AuthenticateUserCommandMother.random(); + AuthUser authUser = AuthUserMother.fromCommand(command); + + shouldSearch(authUser.username()); + + handler.handle(command); + }); + } + + @Test + void throw_an_exception_when_the_password_does_not_math() { + assertThrows(InvalidAuthCredentials.class, () -> { + AuthenticateUserCommand command = AuthenticateUserCommandMother.random(); + AuthUser authUser = AuthUserMother.withUsername(command.username()); + + shouldSearch(authUser.username(), authUser); + + handler.handle(command); + }); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandMother.java new file mode 100644 index 0000000..42746f5 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandMother.java @@ -0,0 +1,16 @@ +package tv.codely.backoffice.auth.application.authenticate; + +import tv.codely.backoffice.auth.domain.AuthPassword; +import tv.codely.backoffice.auth.domain.AuthPasswordMother; +import tv.codely.backoffice.auth.domain.AuthUsername; +import tv.codely.backoffice.auth.domain.AuthUsernameMother; + +public final class AuthenticateUserCommandMother { + public static AuthenticateUserCommand create(AuthUsername username, AuthPassword password) { + return new AuthenticateUserCommand(username.value(), password.value()); + } + + public static AuthenticateUserCommand random() { + return create(AuthUsernameMother.random(), AuthPasswordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthPasswordMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthPasswordMother.java new file mode 100644 index 0000000..8e4f0ab --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthPasswordMother.java @@ -0,0 +1,13 @@ +package tv.codely.backoffice.auth.domain; + +import tv.codely.shared.domain.WordMother; + +public final class AuthPasswordMother { + public static AuthPassword create(String value) { + return new AuthPassword(value); + } + + public static AuthPassword random() { + return create(WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUserMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUserMother.java new file mode 100644 index 0000000..14e26c9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUserMother.java @@ -0,0 +1,21 @@ +package tv.codely.backoffice.auth.domain; + +import tv.codely.backoffice.auth.application.authenticate.AuthenticateUserCommand; + +public final class AuthUserMother { + public static AuthUser create(AuthUsername username, AuthPassword password) { + return new AuthUser(username, password); + } + + public static AuthUser random() { + return create(AuthUsernameMother.random(), AuthPasswordMother.random()); + } + + public static AuthUser fromCommand(AuthenticateUserCommand command) { + return create(AuthUsernameMother.create(command.username()), AuthPasswordMother.create(command.password())); + } + + public static AuthUser withUsername(String username) { + return create(AuthUsernameMother.create(username), AuthPasswordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUsernameMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUsernameMother.java new file mode 100644 index 0000000..e96670c --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUsernameMother.java @@ -0,0 +1,13 @@ +package tv.codely.backoffice.auth.domain; + +import tv.codely.shared.domain.WordMother; + +public final class AuthUsernameMother { + public static AuthUsername create(String value) { + return new AuthUsername(value); + } + + public static AuthUsername random() { + return create(WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/ElasticsearchEnvironmentArranger.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/ElasticsearchEnvironmentArranger.java new file mode 100644 index 0000000..08df2f5 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/ElasticsearchEnvironmentArranger.java @@ -0,0 +1,47 @@ +package tv.codely.backoffice.courses; + +import org.elasticsearch.client.Request; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; +import tv.codely.shared.domain.Service; +import tv.codely.shared.infrastructure.elasticsearch.ElasticsearchClient; + +import java.io.IOException; +import java.util.Objects; +import java.util.Scanner; + +@Service +public final class ElasticsearchEnvironmentArranger { + ResourcePatternResolver resourceResolver; + ElasticsearchClient client; + + public ElasticsearchEnvironmentArranger( + ResourcePatternResolver resourceResolver, + ElasticsearchClient client + ) { + this.resourceResolver = resourceResolver; + this.client = client; + } + + public void arrange(String contextName, String index) throws IOException { + client.delete(index); + + Resource[] jsonsIndexes = resourceResolver.getResources( + String.format("classpath:database/%s/%s.json", contextName, index) + ); + + for (Resource jsonIndex : jsonsIndexes) { + String indexName = Objects.requireNonNull(jsonIndex.getFilename()).replace(".json", ""); + + String indexBody = new Scanner( + jsonIndex.getInputStream(), + "UTF-8" + ).useDelimiter("\\A").next(); + + Request request = new Request("PUT", indexName); + request.setJsonEntity(indexBody); + + client.lowLevelClient().performRequest(request); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseCriteriaMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseCriteriaMother.java new file mode 100644 index 0000000..ee83f69 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseCriteriaMother.java @@ -0,0 +1,17 @@ +package tv.codely.backoffice.courses.domain; + +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.domain.criteria.Filter; +import tv.codely.shared.domain.criteria.Filters; +import tv.codely.shared.domain.criteria.Order; + +import java.util.Arrays; + +public final class BackofficeCourseCriteriaMother { + public static Criteria nameAndDurationContains(String name, String duration) { + Filter nameFilter = Filter.create("name", "contains", name); + Filter durationFilter = Filter.create("duration", "contains", duration); + + return new Criteria(new Filters(Arrays.asList(nameFilter, durationFilter)), Order.asc("name")); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseMother.java new file mode 100644 index 0000000..76a1b4b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseMother.java @@ -0,0 +1,18 @@ +package tv.codely.backoffice.courses.domain; + +import tv.codely.shared.domain.UuidMother; +import tv.codely.shared.domain.WordMother; + +public final class BackofficeCourseMother { + public static BackofficeCourse create(String id, String name, String duration) { + return new BackofficeCourse(id, name, duration); + } + + public static BackofficeCourse create(String name, String duration) { + return new BackofficeCourse(UuidMother.random(), name, duration); + } + + public static BackofficeCourse random() { + return create(UuidMother.random(), WordMother.random(), WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java new file mode 100644 index 0000000..f94748b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java @@ -0,0 +1,114 @@ +package tv.codely.backoffice.courses.infrastructure.persistence; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import tv.codely.backoffice.BackofficeContextInfrastructureTestCase; +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseCriteriaMother; +import tv.codely.backoffice.courses.domain.BackofficeCourseMother; +import tv.codely.shared.domain.UuidMother; +import tv.codely.shared.domain.WordMother; +import tv.codely.shared.domain.criteria.Criteria; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class ElasticsearchBackofficeCourseRepositoryShould extends BackofficeContextInfrastructureTestCase { + @Autowired + private ElasticsearchBackofficeCourseRepository repository; + + @BeforeEach + protected void setUp() throws IOException { + clearElasticsearch(); + } + + @Test + void save_a_course() { + repository.save(BackofficeCourseMother.random()); + } + + @Test + void search_an_existing_course() throws Exception { + BackofficeCourse course = BackofficeCourseMother.random(); + + repository.save(course); + + eventually(() -> assertEquals(Optional.of(course), repository.search(course.id()))); + } + + @Test + void update_an_existing_course() throws Exception { + BackofficeCourse course = BackofficeCourseMother.random(); + + repository.save(course); + course.rename(WordMother.random()); + repository.save(course); + + eventually(() -> assertEquals(Optional.of(course), repository.search(course.id()))); + } + + @Test + void not_find_a_non_existing_course() { + assertEquals(Optional.empty(), repository.search(UuidMother.random())); + } + + @Test + void search_all_existing_courses() throws Exception { + BackofficeCourse course = BackofficeCourseMother.random(); + BackofficeCourse anotherCourse = BackofficeCourseMother.random(); + + List expected = Arrays.asList(course, anotherCourse); + + repository.save(course); + repository.save(anotherCourse); + + eventually(() -> { + List actual = repository.searchAll(); + + List sortedExpected = expected.stream() + .sorted(Comparator.comparing(BackofficeCourse::id)) + .collect(Collectors.toList()); + List sortedActual = actual.stream() + .sorted(Comparator.comparing(BackofficeCourse::id)) + .collect(Collectors.toList()); + + assertEquals(sortedExpected, sortedActual); + }); + } + + @Test + void search_courses_using_a_criteria() throws Exception { + BackofficeCourse matchingCourse = BackofficeCourseMother.create("DDD en Java", "3 days"); + BackofficeCourse anotherMatchingCourse = BackofficeCourseMother.create("DDD en TypeScript", "2.5 days"); + BackofficeCourse intellijCourse = BackofficeCourseMother.create("Exprimiendo Intellij", "48 hours"); + BackofficeCourse cobolCourse = BackofficeCourseMother.create("DDD en Cobol", "5 years"); + + Criteria criteria = BackofficeCourseCriteriaMother.nameAndDurationContains("DDD", "days"); + List expected = Arrays.asList(matchingCourse, anotherMatchingCourse); + + repository.save(matchingCourse); + repository.save(anotherMatchingCourse); + repository.save(intellijCourse); + repository.save(cobolCourse); + + eventually(() -> { + List actual = repository.matching(criteria); + + List sortedExpected = expected.stream() + .sorted(Comparator.comparing(BackofficeCourse::id)) + .collect(Collectors.toList()); + List sortedActual = actual.stream() + .sorted(Comparator.comparing(BackofficeCourse::id)) + .collect(Collectors.toList()); + + assertEquals(sortedExpected, sortedActual); + }); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepositoryShould.java new file mode 100644 index 0000000..e82dffb --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepositoryShould.java @@ -0,0 +1,100 @@ +package tv.codely.backoffice.courses.infrastructure.persistence; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import tv.codely.backoffice.BackofficeContextInfrastructureTestCase; +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseCriteriaMother; +import tv.codely.backoffice.courses.domain.BackofficeCourseMother; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.criteria.Criteria; + +import java.util.Arrays; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.mockito.Mockito.*; + +final class InMemoryCacheBackofficeCourseRepositoryShould extends BackofficeContextInfrastructureTestCase { + private InMemoryCacheBackofficeCourseRepository repository; + private BackofficeCourseRepository innerRepository; + + @BeforeEach + protected void setUp() { + innerRepository = mock(BackofficeCourseRepository.class); + repository = new InMemoryCacheBackofficeCourseRepository(innerRepository); + } + + @Test + void save_a_course() { + BackofficeCourse course = BackofficeCourseMother.random(); + + repository.save(course); + + shouldHaveSaved(course); + } + + @Test + void search_all_existing_courses() { + BackofficeCourse course = BackofficeCourseMother.random(); + BackofficeCourse anotherCourse = BackofficeCourseMother.random(); + List courses = Arrays.asList(course, anotherCourse); + + shouldSearchAll(courses); + + assertThat(courses, containsInAnyOrder(repository.searchAll().toArray())); + } + + @Test + void search_all_existing_courses_without_hitting_the_inner_repository_the_second_time() { + BackofficeCourse course = BackofficeCourseMother.random(); + BackofficeCourse anotherCourse = BackofficeCourseMother.random(); + List courses = Arrays.asList(course, anotherCourse); + + shouldSearchAll(courses); + + assertThat(courses, containsInAnyOrder(repository.searchAll().toArray())); + assertThat(courses, containsInAnyOrder(repository.searchAll().toArray())); + } + + @Test + void search_courses_using_a_criteria() { + BackofficeCourse matchingCourse = BackofficeCourseMother.create("DDD en Java", "3 days"); + BackofficeCourse anotherMatchingCourse = BackofficeCourseMother.create("DDD en TypeScript", "2.5 days"); + List matchingCourses = Arrays.asList(matchingCourse, anotherMatchingCourse); + + Criteria criteria = BackofficeCourseCriteriaMother.nameAndDurationContains("DDD", "days"); + + shouldSearchMatching(criteria, matchingCourses); + + assertThat(matchingCourses, containsInAnyOrder(repository.matching(criteria).toArray())); + } + + @Test + void search_courses_using_a_criteria_without_hitting_the_inner_repository_the_second_time() { + BackofficeCourse matchingCourse = BackofficeCourseMother.create("DDD en Java", "3 days"); + BackofficeCourse anotherMatchingCourse = BackofficeCourseMother.create("DDD en TypeScript", "2.5 days"); + List matchingCourses = Arrays.asList(matchingCourse, anotherMatchingCourse); + + Criteria criteria = BackofficeCourseCriteriaMother.nameAndDurationContains("DDD", "days"); + + shouldSearchMatching(criteria, matchingCourses); + + assertThat(matchingCourses, containsInAnyOrder(repository.matching(criteria).toArray())); + assertThat(matchingCourses, containsInAnyOrder(repository.matching(criteria).toArray())); + } + + private void shouldSearchAll(List courses) { + Mockito.when(innerRepository.searchAll()).thenReturn(courses); + } + + private void shouldSearchMatching(Criteria criteria, List courses) { + Mockito.when(innerRepository.matching(criteria)).thenReturn(courses); + } + + private void shouldHaveSaved(BackofficeCourse course) { + verify(innerRepository, atLeastOnce()).save(course); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepositoryShould.java new file mode 100644 index 0000000..8d9d5ce --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepositoryShould.java @@ -0,0 +1,60 @@ +package tv.codely.backoffice.courses.infrastructure.persistence; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import tv.codely.backoffice.BackofficeContextInfrastructureTestCase; +import tv.codely.backoffice.courses.domain.BackofficeCourse; +import tv.codely.backoffice.courses.domain.BackofficeCourseCriteriaMother; +import tv.codely.backoffice.courses.domain.BackofficeCourseMother; +import tv.codely.backoffice.courses.domain.BackofficeCourseRepository; +import tv.codely.shared.domain.criteria.Criteria; + +import jakarta.transaction.Transactional; +import java.util.Arrays; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; + +@Transactional +class MySqlBackofficeCourseRepositoryShould extends BackofficeContextInfrastructureTestCase { + @Autowired + @Qualifier("mySqlBackofficeCourseRepository") + private BackofficeCourseRepository repository; + + @Test + void save_a_course() { + repository.save(BackofficeCourseMother.random()); + } + + @Test + void search_all_existing_courses() { + BackofficeCourse course = BackofficeCourseMother.random(); + BackofficeCourse anotherCourse = BackofficeCourseMother.random(); + + repository.save(course); + repository.save(anotherCourse); + + assertThat(Arrays.asList(course, anotherCourse), containsInAnyOrder(repository.searchAll().toArray())); + } + + @Test + void search_courses_using_a_criteria() { + BackofficeCourse matchingCourse = BackofficeCourseMother.create("DDD en Java", "3 days"); + BackofficeCourse anotherMatchingCourse = BackofficeCourseMother.create("DDD en TypeScript", "2.5 days"); + BackofficeCourse intellijCourse = BackofficeCourseMother.create("Exprimiendo Intellij", "48 hours"); + BackofficeCourse cobolCourse = BackofficeCourseMother.create("DDD en Cobol", "5 years"); + + Criteria criteria = BackofficeCourseCriteriaMother.nameAndDurationContains("DDD", "days"); + + repository.save(matchingCourse); + repository.save(anotherMatchingCourse); + repository.save(intellijCourse); + repository.save(cobolCourse); + + assertThat( + Arrays.asList(matchingCourse, anotherMatchingCourse), + containsInAnyOrder(repository.matching(criteria).toArray()) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/build.gradle b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/build.gradle new file mode 100644 index 0000000..7d82dc7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/resources/database/mooc.sql b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/resources/database/mooc.sql new file mode 100644 index 0000000..6cc6def --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/resources/database/mooc.sql @@ -0,0 +1,62 @@ +CREATE TABLE IF NOT EXISTS courses ( + id CHAR(36) NOT NULL, + name VARCHAR(255) NOT NULL, + duration VARCHAR(255) NOT NULL, + PRIMARY KEY (id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS courses_counter ( + id CHAR(36) NOT NULL, + total INT NOT NULL, + existing_courses JSON NOT NULL, + PRIMARY KEY (id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; +INSERT IGNORE INTO courses_counter (id, total, existing_courses) VALUES ('efbaff16-8fcd-4689-9fc9-ec545d641c46', 0, '[]'); + +CREATE TABLE IF NOT EXISTS steps ( + id CHAR(36) NOT NULL, + title VARCHAR(155) NOT NULL, + PRIMARY KEY (id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS steps_challenges ( + id CHAR(36) NOT NULL, + statement TEXT NOT NULL, + PRIMARY KEY (id), + CONSTRAINT fk_steps_challenges__step_id FOREIGN KEY (id) REFERENCES steps(id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS steps_videos ( + id CHAR(36) NOT NULL, + url VARCHAR(255) NOT NULL, + text TEXT NOT NULL, + PRIMARY KEY (id), + CONSTRAINT fk_steps_video__step_id FOREIGN KEY (id) REFERENCES steps(id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; + +CREATE TABLE IF NOT EXISTS domain_events ( + id CHAR(36) NOT NULL, + aggregate_id CHAR(36) NOT NULL, + name VARCHAR(255) NOT NULL, + body JSON NOT NULL, + occurred_on TIMESTAMP NOT NULL, + PRIMARY KEY (id) +) + ENGINE = InnoDB + DEFAULT CHARSET = utf8mb4 + COLLATE = utf8mb4_unicode_ci; diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/CourseResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/CourseResponse.java new file mode 100644 index 0000000..22c8798 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/CourseResponse.java @@ -0,0 +1,32 @@ +package tv.codely.mooc.courses.application; + +import tv.codely.mooc.courses.domain.Course; +import tv.codely.shared.domain.bus.query.Response; + +public final class CourseResponse implements Response { + private final String id; + private final String name; + private final String duration; + + public CourseResponse(String id, String name, String duration) { + this.id = id; + this.name = name; + this.duration = duration; + } + + public static CourseResponse fromAggregate(Course course) { + return new CourseResponse(course.id().value(), course.name().value(), course.duration().value()); + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public String duration() { + return duration; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/CoursesResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/CoursesResponse.java new file mode 100644 index 0000000..cd39f43 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/CoursesResponse.java @@ -0,0 +1,17 @@ +package tv.codely.mooc.courses.application; + +import tv.codely.shared.domain.bus.query.Response; + +import java.util.List; + +public final class CoursesResponse implements Response { + private final List courses; + + public CoursesResponse(List courses) { + this.courses = courses; + } + + public List courses() { + return courses; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/create/CourseCreator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/create/CourseCreator.java new file mode 100644 index 0000000..dbbec32 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/create/CourseCreator.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.courses.application.create; + +import tv.codely.mooc.courses.domain.*; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.EventBus; + +@Service +public final class CourseCreator { + private final CourseRepository repository; + private final EventBus eventBus; + + public CourseCreator(CourseRepository repository, EventBus eventBus) { + this.repository = repository; + this.eventBus = eventBus; + } + + public void create(CourseId id, CourseName name, CourseDuration duration) { + Course course = Course.create(id, name, duration); + + repository.save(course); + eventBus.publish(course.pullDomainEvents()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommand.java new file mode 100644 index 0000000..5f6f783 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommand.java @@ -0,0 +1,27 @@ +package tv.codely.mooc.courses.application.create; + +import tv.codely.shared.domain.bus.command.Command; + +public final class CreateCourseCommand implements Command { + private final String id; + private final String name; + private final String duration; + + public CreateCourseCommand(String id, String name, String duration) { + this.id = id; + this.name = name; + this.duration = duration; + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public String duration() { + return duration; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommandHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommandHandler.java new file mode 100644 index 0000000..85127f4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommandHandler.java @@ -0,0 +1,25 @@ +package tv.codely.mooc.courses.application.create; + +import tv.codely.mooc.courses.domain.CourseDuration; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseName; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.command.CommandHandler; + +@Service +public final class CreateCourseCommandHandler implements CommandHandler { + private final CourseCreator creator; + + public CreateCourseCommandHandler(CourseCreator creator) { + this.creator = creator; + } + + @Override + public void handle(CreateCourseCommand command) { + CourseId id = new CourseId(command.id()); + CourseName name = new CourseName(command.name()); + CourseDuration duration = new CourseDuration(command.duration()); + + creator.create(id, name, duration); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java new file mode 100644 index 0000000..b912a3e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java @@ -0,0 +1,22 @@ +package tv.codely.mooc.courses.application.find; + +import tv.codely.mooc.courses.application.CourseResponse; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseNotExist; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.shared.domain.Service; + +@Service +public final class CourseFinder { + private final CourseRepository repository; + + public CourseFinder(CourseRepository repository) { + this.repository = repository; + } + + public CourseResponse find(CourseId id) throws CourseNotExist { + return repository.search(id) + .map(CourseResponse::fromAggregate) + .orElseThrow(() -> new CourseNotExist(id)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQuery.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQuery.java new file mode 100644 index 0000000..187e5e0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQuery.java @@ -0,0 +1,15 @@ +package tv.codely.mooc.courses.application.find; + +import tv.codely.shared.domain.bus.query.Query; + +public final class FindCourseQuery implements Query { + private final String id; + + public FindCourseQuery(String id) { + this.id = id; + } + + public String id() { + return id; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQueryHandler.java new file mode 100644 index 0000000..bc8d380 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQueryHandler.java @@ -0,0 +1,21 @@ +package tv.codely.mooc.courses.application.find; + +import tv.codely.mooc.courses.application.CourseResponse; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseNotExist; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.QueryHandler; + +@Service +public final class FindCourseQueryHandler implements QueryHandler { + private final CourseFinder finder; + + public FindCourseQueryHandler(CourseFinder finder) { + this.finder = finder; + } + + @Override + public CourseResponse handle(FindCourseQuery query) throws CourseNotExist { + return finder.find(new CourseId(query.id())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/search_last/LastCoursesSearcher.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/search_last/LastCoursesSearcher.java new file mode 100644 index 0000000..1e0f6fa --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/search_last/LastCoursesSearcher.java @@ -0,0 +1,34 @@ +package tv.codely.mooc.courses.application.search_last; + +import tv.codely.mooc.courses.application.CourseResponse; +import tv.codely.mooc.courses.application.CoursesResponse; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.domain.criteria.Filters; +import tv.codely.shared.domain.criteria.Order; + +import java.util.Optional; +import java.util.stream.Collectors; + +@Service +public final class LastCoursesSearcher { + private final CourseRepository repository; + + public LastCoursesSearcher(CourseRepository repository) { + this.repository = repository; + } + + public CoursesResponse search(int courses) { + Criteria criteria = new Criteria( + Filters.none(), + Order.none(), + Optional.of(courses), + Optional.empty() + ); + + return new CoursesResponse( + repository.matching(criteria).stream().map(CourseResponse::fromAggregate).collect(Collectors.toList()) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQuery.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQuery.java new file mode 100644 index 0000000..834a847 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQuery.java @@ -0,0 +1,34 @@ +package tv.codely.mooc.courses.application.search_last; + +import tv.codely.shared.domain.bus.query.Query; + +import java.util.Objects; + +public final class SearchLastCoursesQuery implements Query { + private final Integer total; + + public SearchLastCoursesQuery(Integer total) { + this.total = total; + } + + public Integer total() { + return total; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SearchLastCoursesQuery that = (SearchLastCoursesQuery) o; + return total.equals(that.total); + } + + @Override + public int hashCode() { + return Objects.hash(total); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryHandler.java new file mode 100644 index 0000000..91bea0f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryHandler.java @@ -0,0 +1,19 @@ +package tv.codely.mooc.courses.application.search_last; + +import tv.codely.mooc.courses.application.CoursesResponse; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.QueryHandler; + +@Service +public final class SearchLastCoursesQueryHandler implements QueryHandler { + private final LastCoursesSearcher searcher; + + public SearchLastCoursesQueryHandler(LastCoursesSearcher searcher) { + this.searcher = searcher; + } + + @Override + public CoursesResponse handle(SearchLastCoursesQuery query) { + return searcher.search(query.total()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/Course.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/Course.java new file mode 100644 index 0000000..ef44a89 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/Course.java @@ -0,0 +1,63 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.AggregateRoot; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +import java.util.Objects; + +public final class Course extends AggregateRoot { + private final CourseId id; + private final CourseName name; + private final CourseDuration duration; + + public Course(CourseId id, CourseName name, CourseDuration duration) { + this.id = id; + this.name = name; + this.duration = duration; + } + + private Course() { + id = null; + name = null; + duration = null; + } + + public static Course create(CourseId id, CourseName name, CourseDuration duration) { + Course course = new Course(id, name, duration); + + course.record(new CourseCreatedDomainEvent(id.value(), name.value(), duration.value())); + + return course; + } + + public CourseId id() { + return id; + } + + public CourseName name() { + return name; + } + + public CourseDuration duration() { + return duration; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Course course = (Course) o; + return id.equals(course.id) && + name.equals(course.name) && + duration.equals(course.duration); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, duration); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseDuration.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseDuration.java new file mode 100644 index 0000000..42c4482 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseDuration.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.StringValueObject; + +public final class CourseDuration extends StringValueObject { + public CourseDuration(String value) { + super(value); + } + + private CourseDuration() { + super(""); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseId.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseId.java new file mode 100644 index 0000000..dd6c3c2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseId.java @@ -0,0 +1,12 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.Identifier; + +public final class CourseId extends Identifier { + public CourseId(String value) { + super(value); + } + + public CourseId() { + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseName.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseName.java new file mode 100644 index 0000000..67e0a06 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseName.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.StringValueObject; + +public final class CourseName extends StringValueObject { + public CourseName(String value) { + super(value); + } + + public CourseName() { + super(""); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseNotExist.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseNotExist.java new file mode 100644 index 0000000..d3490be --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseNotExist.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.DomainError; + +public final class CourseNotExist extends DomainError { + public CourseNotExist(CourseId id) { + super("course_not_exist", String.format("The course <%s> doesn't exist", id.value())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseRepository.java new file mode 100644 index 0000000..4d2e696 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/domain/CourseRepository.java @@ -0,0 +1,14 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.criteria.Criteria; + +import java.util.List; +import java.util.Optional; + +public interface CourseRepository { + void save(Course course); + + Optional search(CourseId id); + + List matching(Criteria criteria); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepository.java new file mode 100644 index 0000000..622d60a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepository.java @@ -0,0 +1,28 @@ +package tv.codely.mooc.courses.infrastructure.persistence; + +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.shared.domain.criteria.Criteria; + +import java.util.HashMap; +import java.util.List; +import java.util.Optional; + +public final class InMemoryCourseRepository implements CourseRepository { + private HashMap courses = new HashMap<>(); + + @Override + public void save(Course course) { + courses.put(course.id().value(), course); + } + + public Optional search(CourseId id) { + return Optional.ofNullable(courses.get(id.value())); + } + + @Override + public List matching(Criteria criteria) { + return null; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepository.java new file mode 100644 index 0000000..b90c0cf --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepository.java @@ -0,0 +1,37 @@ +package tv.codely.mooc.courses.infrastructure.persistence; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.transaction.annotation.Transactional; +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.infrastructure.hibernate.HibernateRepository; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional("mooc-transaction_manager") +public class MySqlCourseRepository extends HibernateRepository implements CourseRepository { + public MySqlCourseRepository(@Qualifier("mooc-session_factory") SessionFactory sessionFactory) { + super(sessionFactory, Course.class); + } + + @Override + public void save(Course course) { + persist(course); + } + + @Override + public Optional search(CourseId id) { + return byId(id); + } + + @Override + public List matching(Criteria criteria) { + return byCriteria(criteria); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml new file mode 100644 index 0000000..88809da --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterFinder.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterFinder.java new file mode 100644 index 0000000..e0ccfe7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterFinder.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.courses_counter.application.find; + +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterNotInitialized; +import tv.codely.mooc.courses_counter.domain.CoursesCounterRepository; +import tv.codely.shared.domain.Service; + +@Service +public final class CoursesCounterFinder { + private CoursesCounterRepository repository; + + public CoursesCounterFinder(CoursesCounterRepository repository) { + this.repository = repository; + } + + public CoursesCounterResponse find() { + CoursesCounter coursesCounter = repository.search().orElseGet(() -> { + throw new CoursesCounterNotInitialized(); + }); + + return new CoursesCounterResponse(coursesCounter.total().value()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponse.java new file mode 100644 index 0000000..a4ee91a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponse.java @@ -0,0 +1,34 @@ +package tv.codely.mooc.courses_counter.application.find; + +import tv.codely.shared.domain.bus.query.Response; + +import java.util.Objects; + +public final class CoursesCounterResponse implements Response { + private Integer total; + + public CoursesCounterResponse(Integer total) { + this.total = total; + } + + public Integer total() { + return total; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CoursesCounterResponse that = (CoursesCounterResponse) o; + return total.equals(that.total); + } + + @Override + public int hashCode() { + return Objects.hash(total); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQuery.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQuery.java new file mode 100644 index 0000000..7586b4b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQuery.java @@ -0,0 +1,6 @@ +package tv.codely.mooc.courses_counter.application.find; + +import tv.codely.shared.domain.bus.query.Query; + +public final class FindCoursesCounterQuery implements Query { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandler.java new file mode 100644 index 0000000..1eefb82 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandler.java @@ -0,0 +1,18 @@ +package tv.codely.mooc.courses_counter.application.find; + +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.QueryHandler; + +@Service +public final class FindCoursesCounterQueryHandler implements QueryHandler { + private final CoursesCounterFinder finder; + + public FindCoursesCounterQueryHandler(CoursesCounterFinder finder) { + this.finder = finder; + } + + @Override + public CoursesCounterResponse handle(FindCoursesCounterQuery query) { + return finder.find(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java new file mode 100644 index 0000000..c4433c0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java @@ -0,0 +1,29 @@ +package tv.codely.mooc.courses_counter.application.increment; + +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.UuidGenerator; + +@Service +public final class CoursesCounterIncrementer { + private CoursesCounterRepository repository; + private UuidGenerator uuidGenerator; + + public CoursesCounterIncrementer(CoursesCounterRepository repository, UuidGenerator uuidGenerator) { + this.repository = repository; + this.uuidGenerator = uuidGenerator; + } + + public void increment(CourseId id) { + CoursesCounter counter = repository.search() + .orElseGet(() -> CoursesCounter.initialize(uuidGenerator.generate())); + + if (!counter.hasIncremented(id)) { + counter.increment(id); + + repository.save(counter); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreated.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreated.java new file mode 100644 index 0000000..f8465fb --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreated.java @@ -0,0 +1,24 @@ +package tv.codely.mooc.courses_counter.application.increment; + +import org.springframework.context.event.EventListener; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +@Service +@DomainEventSubscriber({CourseCreatedDomainEvent.class}) +public final class IncrementCoursesCounterOnCourseCreated { + private final CoursesCounterIncrementer incrementer; + + public IncrementCoursesCounterOnCourseCreated(CoursesCounterIncrementer incrementer) { + this.incrementer = incrementer; + } + + @EventListener + public void on(CourseCreatedDomainEvent event) { + CourseId courseId = new CourseId(event.aggregateId()); + + incrementer.increment(courseId); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounter.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounter.java new file mode 100644 index 0000000..2a13583 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounter.java @@ -0,0 +1,69 @@ +package tv.codely.mooc.courses_counter.domain; + +import tv.codely.mooc.courses.domain.CourseId; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public final class CoursesCounter { + private CoursesCounterId id; + private CoursesCounterTotal total; + private List existingCourses; + + public CoursesCounter(CoursesCounterId id, CoursesCounterTotal total, List existingCourses) { + this.id = id; + this.total = total; + this.existingCourses = existingCourses; + } + + private CoursesCounter() { + this.id = null; + this.total = null; + this.existingCourses = null; + } + + public static CoursesCounter initialize(String id) { + return new CoursesCounter(new CoursesCounterId(id), CoursesCounterTotal.initialize(), new ArrayList<>()); + } + + public CoursesCounterId id() { + return id; + } + + public CoursesCounterTotal total() { + return total; + } + + public List existingCourses() { + return existingCourses; + } + + public boolean hasIncremented(CourseId id) { + return existingCourses.contains(id); + } + + public void increment(CourseId id) { + total = total.increment(); + existingCourses.add(id); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CoursesCounter that = (CoursesCounter) o; + return id.equals(that.id) && + total.equals(that.total) && + existingCourses.equals(that.existingCourses); + } + + @Override + public int hashCode() { + return Objects.hash(id, total, existingCourses); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterId.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterId.java new file mode 100644 index 0000000..3899c15 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterId.java @@ -0,0 +1,12 @@ +package tv.codely.mooc.courses_counter.domain; + +import tv.codely.shared.domain.Identifier; + +public final class CoursesCounterId extends Identifier { + public CoursesCounterId(String value) { + super(value); + } + + private CoursesCounterId() { + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterNotInitialized.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterNotInitialized.java new file mode 100644 index 0000000..b20ca87 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterNotInitialized.java @@ -0,0 +1,4 @@ +package tv.codely.mooc.courses_counter.domain; + +public final class CoursesCounterNotInitialized extends RuntimeException { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterRepository.java new file mode 100644 index 0000000..ccbbb71 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterRepository.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.courses_counter.domain; + +import java.util.Optional; + +public interface CoursesCounterRepository { + void save(CoursesCounter counter); + + Optional search(); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterTotal.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterTotal.java new file mode 100644 index 0000000..65a299d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterTotal.java @@ -0,0 +1,21 @@ +package tv.codely.mooc.courses_counter.domain; + +import tv.codely.shared.domain.IntValueObject; + +public final class CoursesCounterTotal extends IntValueObject { + public CoursesCounterTotal(Integer value) { + super(value); + } + + public CoursesCounterTotal() { + super(null); + } + + public static CoursesCounterTotal initialize() { + return new CoursesCounterTotal(0); + } + + public CoursesCounterTotal increment() { + return new CoursesCounterTotal(value() + 1); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepository.java new file mode 100644 index 0000000..f9cb245 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepository.java @@ -0,0 +1,32 @@ +package tv.codely.mooc.courses_counter.infrastructure.persistence; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.transaction.annotation.Transactional; +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.infrastructure.hibernate.HibernateRepository; + +import java.util.List; +import java.util.Optional; + +@Service +@Transactional("mooc-transaction_manager") +public class MySqlCoursesCounterRepository extends HibernateRepository implements CoursesCounterRepository { + public MySqlCoursesCounterRepository(@Qualifier("mooc-session_factory") SessionFactory sessionFactory) { + super(sessionFactory, CoursesCounter.class); + } + + @Override + public void save(CoursesCounter counter) { + persist(counter); + } + + @Override + public Optional search() { + List coursesCounter = all(); + + return 0 == coursesCounter.size() ? Optional.empty() : Optional.ofNullable(coursesCounter.get(0)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/hibernate/CoursesCounter.hbm.xml b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/hibernate/CoursesCounter.hbm.xml new file mode 100644 index 0000000..8ea7482 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/hibernate/CoursesCounter.hbm.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + tv.codely.mooc.courses.domain.CourseId + + + + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/NewCoursesNewsletterSender.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/NewCoursesNewsletterSender.java new file mode 100644 index 0000000..0e2ea20 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/NewCoursesNewsletterSender.java @@ -0,0 +1,52 @@ +package tv.codely.mooc.notifications.application.send_new_courses_newsletter; + +import tv.codely.mooc.courses.application.CoursesResponse; +import tv.codely.mooc.courses.application.search_last.SearchLastCoursesQuery; +import tv.codely.mooc.notifications.domain.EmailSender; +import tv.codely.mooc.notifications.domain.NewCoursesNewsletter; +import tv.codely.mooc.students.application.StudentResponse; +import tv.codely.mooc.students.application.StudentsResponse; +import tv.codely.mooc.students.application.search_all.SearchAllStudentsQuery; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.UuidGenerator; +import tv.codely.shared.domain.bus.event.EventBus; +import tv.codely.shared.domain.bus.query.QueryBus; + +@Service +public final class NewCoursesNewsletterSender { + private final static Integer TOTAL_COURSES = 3; + private final QueryBus queryBus; + private final EmailSender sender; + private final UuidGenerator uuidGenerator; + private final EventBus eventBus; + + public NewCoursesNewsletterSender( + QueryBus queryBus, + UuidGenerator uuidGenerator, + EmailSender sender, + EventBus eventBus + ) { + this.queryBus = queryBus; + this.uuidGenerator = uuidGenerator; + this.sender = sender; + this.eventBus = eventBus; + } + + public void send() { + CoursesResponse courses = queryBus.ask(new SearchLastCoursesQuery(TOTAL_COURSES)); + + if (courses.courses().size() > 0) { + StudentsResponse students = queryBus.ask(new SearchAllStudentsQuery()); + + students.students().forEach(student -> send(student, courses)); + } + } + + public void send(StudentResponse student, CoursesResponse courses) { + NewCoursesNewsletter newsletter = NewCoursesNewsletter.send(uuidGenerator.generate(), student, courses); + + sender.send(newsletter); + + eventBus.publish(newsletter.pullDomainEvents()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommand.java new file mode 100644 index 0000000..51f6c52 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommand.java @@ -0,0 +1,15 @@ +package tv.codely.mooc.notifications.application.send_new_courses_newsletter; + +import tv.codely.shared.domain.bus.command.Command; + +public final class SendNewCoursesNewsletterCommand implements Command { + private final String id; + + public SendNewCoursesNewsletterCommand(String id) { + this.id = id; + } + + public String id() { + return id; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandler.java new file mode 100644 index 0000000..2c6dad7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandler.java @@ -0,0 +1,18 @@ +package tv.codely.mooc.notifications.application.send_new_courses_newsletter; + +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.command.CommandHandler; + +@Service +public final class SendNewCoursesNewsletterCommandHandler implements CommandHandler { + private final NewCoursesNewsletterSender sender; + + public SendNewCoursesNewsletterCommandHandler(NewCoursesNewsletterSender sender) { + this.sender = sender; + } + + @Override + public void handle(SendNewCoursesNewsletterCommand command) { + sender.send(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/Email.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/Email.java new file mode 100644 index 0000000..120feff --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/Email.java @@ -0,0 +1,62 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.shared.domain.AggregateRoot; + +import java.util.Objects; + +public abstract class Email extends AggregateRoot { + private final EmailId id; + private final String from; + private final String to; + private final String subject; + private final String body; + + public Email(EmailId id, String from, String to, String subject, String body) { + this.id = id; + this.from = from; + this.to = to; + this.subject = subject; + this.body = body; + } + + public EmailId id() { + return id; + } + + public String from() { + return from; + } + + public String to() { + return to; + } + + public String subject() { + return subject; + } + + public String body() { + return body; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Email email = (Email) o; + return id.equals(email.id) && + from.equals(email.from) && + to.equals(email.to) && + subject.equals(email.subject) && + body.equals(email.body); + } + + @Override + public int hashCode() { + return Objects.hash(id, from, to, subject, body); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/EmailId.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/EmailId.java new file mode 100644 index 0000000..dd03323 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/EmailId.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.shared.domain.Identifier; + +public final class EmailId extends Identifier { + public EmailId(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/EmailSender.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/EmailSender.java new file mode 100644 index 0000000..b393b2d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/EmailSender.java @@ -0,0 +1,5 @@ +package tv.codely.mooc.notifications.domain; + +public interface EmailSender { + void send(Email email); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletter.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletter.java new file mode 100644 index 0000000..0fc60cd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletter.java @@ -0,0 +1,55 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.mooc.courses.application.CoursesResponse; +import tv.codely.mooc.students.application.StudentResponse; + +import java.util.Objects; + +public final class NewCoursesNewsletter extends Email { + private final StudentResponse student; + private final CoursesResponse courses; + + public NewCoursesNewsletter(EmailId id, StudentResponse student, CoursesResponse courses) { + super(id, "news@codely.tv", student.email(), "Último cursos en CodelyTV", formatBody(student, courses)); + + this.student = student; + this.courses = courses; + } + + private static String formatBody(StudentResponse student, CoursesResponse courses) { + return String.format( + "Hoy es tu día de suerte... %s vas a ver %s nuevos cursos", + student.name(), + courses.courses().size() + ); + } + + public static NewCoursesNewsletter send(String id, StudentResponse student, CoursesResponse courses) { + NewCoursesNewsletter newsletter = new NewCoursesNewsletter(new EmailId(id), student, courses); + + newsletter.record(new NewCoursesNewsletterEmailSent(id, student.id())); + + return newsletter; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + NewCoursesNewsletter that = (NewCoursesNewsletter) o; + return student.equals(that.student) && + courses.equals(that.courses); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), student, courses); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSent.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSent.java new file mode 100644 index 0000000..b03f209 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSent.java @@ -0,0 +1,78 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Objects; + +public final class NewCoursesNewsletterEmailSent extends DomainEvent { + private final String studentId; + + public NewCoursesNewsletterEmailSent() { + super(null); + + this.studentId = null; + } + + public NewCoursesNewsletterEmailSent(String aggregateId, String studentId) { + super(aggregateId); + + this.studentId = studentId; + } + + public NewCoursesNewsletterEmailSent( + String aggregateId, + String eventId, + String occurredOn, + String studentId + ) { + super(aggregateId, eventId, occurredOn); + + this.studentId = studentId; + } + + @Override + public String eventName() { + return "new_courses_newsletter_email.sent"; + } + + @Override + public HashMap toPrimitives() { + return new HashMap() {{ + put("student_id", studentId); + }}; + } + + @Override + public NewCoursesNewsletterEmailSent fromPrimitives( + String aggregateId, + HashMap body, + String eventId, + String occurredOn + ) { + return new NewCoursesNewsletterEmailSent( + aggregateId, + eventId, + occurredOn, + (String) body.get("student_id") + ); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NewCoursesNewsletterEmailSent that = (NewCoursesNewsletterEmailSent) o; + return studentId.equals(that.studentId); + } + + @Override + public int hashCode() { + return Objects.hash(studentId); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/infrastructure/FakeEmailSender.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/infrastructure/FakeEmailSender.java new file mode 100644 index 0000000..17501f3 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/notifications/infrastructure/FakeEmailSender.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.notifications.infrastructure; + +import tv.codely.mooc.notifications.domain.Email; +import tv.codely.mooc.notifications.domain.EmailSender; +import tv.codely.shared.domain.Service; + +@Service +public final class FakeEmailSender implements EmailSender { + @Override + public void send(Email email) { + // In the future... + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocHibernateConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocHibernateConfiguration.java new file mode 100644 index 0000000..88f915d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocHibernateConfiguration.java @@ -0,0 +1,47 @@ +package tv.codely.mooc.shared.infrastructure.persistence; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.EnableTransactionManagement; +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; +import tv.codely.shared.infrastructure.hibernate.HibernateConfigurationFactory; + +import javax.sql.DataSource; +import java.io.IOException; + +@Configuration +@EnableTransactionManagement +public class MoocHibernateConfiguration { + private final HibernateConfigurationFactory factory; + private final Parameter config; + private final String CONTEXT_NAME = "mooc"; + + public MoocHibernateConfiguration(HibernateConfigurationFactory factory, Parameter config) { + this.factory = factory; + this.config = config; + } + + @Bean("mooc-transaction_manager") + public PlatformTransactionManager hibernateTransactionManager() throws IOException, ParameterNotExist { + return factory.hibernateTransactionManager(sessionFactory()); + } + + @Bean("mooc-session_factory") + public LocalSessionFactoryBean sessionFactory() throws IOException, ParameterNotExist { + return factory.sessionFactory(CONTEXT_NAME, dataSource()); + } + + @Bean("mooc-data_source") + public DataSource dataSource() throws IOException, ParameterNotExist { + return factory.dataSource( + config.get("MOOC_DATABASE_HOST"), + config.getInt("MOOC_DATABASE_PORT"), + config.get("MOOC_DATABASE_NAME"), + config.get("MOOC_DATABASE_USER"), + config.get("MOOC_DATABASE_PASSWORD") + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocMySqlEventBusConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocMySqlEventBusConfiguration.java new file mode 100644 index 0000000..dc8f346 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocMySqlEventBusConfiguration.java @@ -0,0 +1,37 @@ +package tv.codely.mooc.shared.infrastructure.persistence; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tv.codely.shared.infrastructure.bus.event.DomainEventsInformation; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlDomainEventsConsumer; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; +import tv.codely.shared.infrastructure.bus.event.spring.SpringApplicationEventBus; + +@Configuration +public class MoocMySqlEventBusConfiguration { + private final SessionFactory sessionFactory; + private final DomainEventsInformation domainEventsInformation; + private final SpringApplicationEventBus bus; + + public MoocMySqlEventBusConfiguration( + @Qualifier("mooc-session_factory") SessionFactory sessionFactory, + DomainEventsInformation domainEventsInformation, + SpringApplicationEventBus bus + ) { + this.sessionFactory = sessionFactory; + this.domainEventsInformation = domainEventsInformation; + this.bus = bus; + } + + @Bean + public MySqlEventBus moocMysqlEventBus() { + return new MySqlEventBus(sessionFactory); + } + + @Bean + public MySqlDomainEventsConsumer moocMySqlDomainEventsConsumer() { + return new MySqlDomainEventsConsumer(sessionFactory, domainEventsInformation, bus); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocRabbitMqEventBusConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocRabbitMqEventBusConfiguration.java new file mode 100644 index 0000000..df81787 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocRabbitMqEventBusConfiguration.java @@ -0,0 +1,27 @@ +package tv.codely.mooc.shared.infrastructure.persistence; + +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqEventBus; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqPublisher; + +@Configuration +public class MoocRabbitMqEventBusConfiguration { + private final RabbitMqPublisher publisher; + private final MySqlEventBus failoverPublisher; + + public MoocRabbitMqEventBusConfiguration( + RabbitMqPublisher publisher, + @Qualifier("moocMysqlEventBus") MySqlEventBus failoverPublisher + ) { + this.publisher = publisher; + this.failoverPublisher = failoverPublisher; + } + + @Bean + public RabbitMqEventBus moocRabbitMqEventBus() { + return new RabbitMqEventBus(publisher, failoverPublisher); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/Step.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/Step.java new file mode 100644 index 0000000..838d110 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/Step.java @@ -0,0 +1,35 @@ +package tv.codely.mooc.steps.domain; + +import java.util.Objects; + +public abstract class Step { + private final StepId id; + private final StepTitle title; + + public Step(StepId id, StepTitle title) { + this.id = id; + this.title = title; + } + + public StepId id() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Step step = (Step) o; + return id.equals(step.id) && + title.equals(step.title); + } + + @Override + public int hashCode() { + return Objects.hash(id, title); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/StepId.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/StepId.java new file mode 100644 index 0000000..7f5d07b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/StepId.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.steps.domain; + +import tv.codely.shared.domain.Identifier; + +public final class StepId extends Identifier { + public StepId(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/StepRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/StepRepository.java new file mode 100644 index 0000000..419f53d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/StepRepository.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.steps.domain; + +import java.util.Optional; + +public interface StepRepository { + void save(Step step); + + Optional search(StepId id); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/StepTitle.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/StepTitle.java new file mode 100644 index 0000000..b1a14a5 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/StepTitle.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain; + +import tv.codely.shared.domain.StringValueObject; + +public final class StepTitle extends StringValueObject { + public StepTitle(String value) { + super(value); + } + + private StepTitle() { + super(null); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStep.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStep.java new file mode 100644 index 0000000..9d2be39 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStep.java @@ -0,0 +1,43 @@ +package tv.codely.mooc.steps.domain.challenge; + +import tv.codely.mooc.steps.domain.Step; +import tv.codely.mooc.steps.domain.StepId; +import tv.codely.mooc.steps.domain.StepTitle; + +import java.util.Objects; + +public final class ChallengeStep extends Step { + private final ChallengeStepStatement statement; + + public ChallengeStep(StepId id, StepTitle title, ChallengeStepStatement statement) { + super(id, title); + + this.statement = statement; + } + + private ChallengeStep() { + super(null, null); + + this.statement = null; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + if (!super.equals(o)) { + return false; + } + ChallengeStep that = (ChallengeStep) o; + return statement.equals(that.statement); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), statement); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatement.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatement.java new file mode 100644 index 0000000..bfc8a40 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatement.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain.challenge; + +import tv.codely.shared.domain.StringValueObject; + +public final class ChallengeStepStatement extends StringValueObject { + public ChallengeStepStatement(String value) { + super(value); + } + + public ChallengeStepStatement() { + super(null); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStep.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStep.java new file mode 100644 index 0000000..81f6c2d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStep.java @@ -0,0 +1,25 @@ +package tv.codely.mooc.steps.domain.video; + +import tv.codely.mooc.steps.domain.Step; +import tv.codely.mooc.steps.domain.StepId; +import tv.codely.mooc.steps.domain.StepTitle; +import tv.codely.shared.domain.VideoUrl; + +public final class VideoStep extends Step { + private final VideoUrl videoUrl; + private final VideoStepText text; + + public VideoStep(StepId id, StepTitle title, VideoUrl videoUrl, VideoStepText text) { + super(id, title); + + this.videoUrl = videoUrl; + this.text = text; + } + + private VideoStep() { + super(null, null); + + this.videoUrl = null; + this.text = null; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStepText.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStepText.java new file mode 100644 index 0000000..99e36c9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStepText.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain.video; + +import tv.codely.shared.domain.StringValueObject; + +public final class VideoStepText extends StringValueObject { + public VideoStepText(String value) { + super(value); + } + + private VideoStepText() { + super(null); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepository.java new file mode 100644 index 0000000..6b4b103 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepository.java @@ -0,0 +1,30 @@ +package tv.codely.mooc.steps.infrastructure.persistence; + +import org.hibernate.SessionFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.transaction.annotation.Transactional; +import tv.codely.mooc.steps.domain.Step; +import tv.codely.mooc.steps.domain.StepId; +import tv.codely.mooc.steps.domain.StepRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.infrastructure.hibernate.HibernateRepository; + +import java.util.Optional; + +@Service +@Transactional("mooc-transaction_manager") +public class MySqlStepRepository extends HibernateRepository implements StepRepository { + public MySqlStepRepository(@Qualifier("mooc-session_factory") SessionFactory sessionFactory) { + super(sessionFactory, Step.class); + } + + @Override + public void save(Step step) { + persist(step); + } + + @Override + public Optional search(StepId id) { + return byId(id); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml new file mode 100644 index 0000000..31cd919 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/StudentResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/StudentResponse.java new file mode 100644 index 0000000..9b8bc05 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/StudentResponse.java @@ -0,0 +1,38 @@ +package tv.codely.mooc.students.application; + +import tv.codely.mooc.students.domain.Student; +import tv.codely.shared.domain.bus.query.Response; + +public final class StudentResponse implements Response { + private final String id; + private final String name; + private final String surname; + private final String email; + + public StudentResponse(String id, String name, String surname, String email) { + this.id = id; + this.name = name; + this.surname = surname; + this.email = email; + } + + public static StudentResponse fromAggregate(Student student) { + return new StudentResponse(student.id().value(), student.name(), student.surname(), student.email()); + } + + public String id() { + return id; + } + + public String name() { + return name; + } + + public String surname() { + return surname; + } + + public String email() { + return email; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/StudentsResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/StudentsResponse.java new file mode 100644 index 0000000..9a7e035 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/StudentsResponse.java @@ -0,0 +1,17 @@ +package tv.codely.mooc.students.application; + +import tv.codely.shared.domain.bus.query.Response; + +import java.util.List; + +public final class StudentsResponse implements Response { + private final List students; + + public StudentsResponse(List students) { + this.students = students; + } + + public List students() { + return students; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/search_all/AllStudentsSearcher.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/search_all/AllStudentsSearcher.java new file mode 100644 index 0000000..a079520 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/search_all/AllStudentsSearcher.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.students.application.search_all; + +import tv.codely.mooc.students.application.StudentResponse; +import tv.codely.mooc.students.application.StudentsResponse; +import tv.codely.mooc.students.domain.StudentRepository; +import tv.codely.shared.domain.Service; + +import java.util.stream.Collectors; + +@Service +public final class AllStudentsSearcher { + private final StudentRepository repository; + + public AllStudentsSearcher(StudentRepository repository) { + this.repository = repository; + } + + public StudentsResponse search() { + return new StudentsResponse( + repository.searchAll().stream().map(StudentResponse::fromAggregate).collect(Collectors.toList()) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQuery.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQuery.java new file mode 100644 index 0000000..618a9fa --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQuery.java @@ -0,0 +1,20 @@ +package tv.codely.mooc.students.application.search_all; + +import tv.codely.shared.domain.bus.query.Query; + +import java.util.Objects; + +public final class SearchAllStudentsQuery implements Query { + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + return o != null && getClass() == o.getClass(); + } + + @Override + public int hashCode() { + return Objects.hash("SearchAllStudentsQuery"); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryHandler.java new file mode 100644 index 0000000..c9e3fa4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryHandler.java @@ -0,0 +1,19 @@ +package tv.codely.mooc.students.application.search_all; + +import tv.codely.mooc.students.application.StudentsResponse; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.QueryHandler; + +@Service +public final class SearchAllStudentsQueryHandler implements QueryHandler { + private final AllStudentsSearcher searcher; + + public SearchAllStudentsQueryHandler(AllStudentsSearcher searcher) { + this.searcher = searcher; + } + + @Override + public StudentsResponse handle(SearchAllStudentsQuery query) { + return searcher.search(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/domain/Student.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/domain/Student.java new file mode 100644 index 0000000..eb0353d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/domain/Student.java @@ -0,0 +1,31 @@ +package tv.codely.mooc.students.domain; + +public final class Student { + private final StudentId id; + private final String name; + private final String surname; + private final String email; + + public Student(StudentId id, String name, String surname, String email) { + this.id = id; + this.name = name; + this.surname = surname; + this.email = email; + } + + public StudentId id() { + return id; + } + + public String name() { + return name; + } + + public String surname() { + return surname; + } + + public String email() { + return email; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/domain/StudentId.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/domain/StudentId.java new file mode 100644 index 0000000..3e6735e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/domain/StudentId.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.students.domain; + +import tv.codely.shared.domain.Identifier; + +public final class StudentId extends Identifier { + public StudentId(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/domain/StudentRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/domain/StudentRepository.java new file mode 100644 index 0000000..0fbd2e6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/domain/StudentRepository.java @@ -0,0 +1,7 @@ +package tv.codely.mooc.students.domain; + +import java.util.List; + +public interface StudentRepository { + List searchAll(); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/infrastructure/InMemoryStudentRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/infrastructure/InMemoryStudentRepository.java new file mode 100644 index 0000000..aabd3ab --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/main/tv/codely/mooc/students/infrastructure/InMemoryStudentRepository.java @@ -0,0 +1,27 @@ +package tv.codely.mooc.students.infrastructure; + +import tv.codely.mooc.students.domain.Student; +import tv.codely.mooc.students.domain.StudentId; +import tv.codely.mooc.students.domain.StudentRepository; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.UuidGenerator; + +import java.util.Arrays; +import java.util.List; + +@Service +public final class InMemoryStudentRepository implements StudentRepository { + private UuidGenerator generator; + + public InMemoryStudentRepository(UuidGenerator generator) { + this.generator = generator; + } + + @Override + public List searchAll() { + return Arrays.asList( + new Student(new StudentId(generator.generate()), "name", "surname", "email@mail.com"), + new Student(new StudentId(generator.generate()), "Other name", "Other surname", "another@mail.com") + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/MoocContextInfrastructureTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/MoocContextInfrastructureTestCase.java new file mode 100644 index 0000000..eb574c4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/MoocContextInfrastructureTestCase.java @@ -0,0 +1,11 @@ +package tv.codely.mooc; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import tv.codely.apps.mooc.backend.MoocBackendApplication; +import tv.codely.shared.infrastructure.InfrastructureTestCase; + +@ContextConfiguration(classes = MoocBackendApplication.class) +@SpringBootTest +public abstract class MoocContextInfrastructureTestCase extends InfrastructureTestCase { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/CoursesModuleInfrastructureTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/CoursesModuleInfrastructureTestCase.java new file mode 100644 index 0000000..a52cf56 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/CoursesModuleInfrastructureTestCase.java @@ -0,0 +1,12 @@ +package tv.codely.mooc.courses; + +import org.springframework.beans.factory.annotation.Autowired; +import tv.codely.mooc.MoocContextInfrastructureTestCase; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.mooc.courses.infrastructure.persistence.InMemoryCourseRepository; + +public abstract class CoursesModuleInfrastructureTestCase extends MoocContextInfrastructureTestCase { + protected InMemoryCourseRepository inMemoryCourseRepository = new InMemoryCourseRepository(); + @Autowired + protected CourseRepository mySqlCourseRepository; +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java new file mode 100644 index 0000000..eb7454d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.courses; + +import org.junit.jupiter.api.BeforeEach; +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseRepository; +import tv.codely.shared.infrastructure.UnitTestCase; + +import static org.mockito.Mockito.*; + +public abstract class CoursesModuleUnitTestCase extends UnitTestCase { + protected CourseRepository repository; + + @BeforeEach + protected void setUp() { + super.setUp(); + + repository = mock(CourseRepository.class); + } + + public void shouldHaveSaved(Course course) { + verify(repository, atLeastOnce()).save(course); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/CourseResponseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/CourseResponseMother.java new file mode 100644 index 0000000..564e527 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/CourseResponseMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.application; + +import tv.codely.mooc.courses.domain.*; + +public final class CourseResponseMother { + public static CourseResponse create(CourseId id, CourseName name, CourseDuration duration) { + return new CourseResponse(id.value(), name.value(), duration.value()); + } + + public static CourseResponse random() { + return create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/CoursesResponseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/CoursesResponseMother.java new file mode 100644 index 0000000..1067e22 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/CoursesResponseMother.java @@ -0,0 +1,24 @@ +package tv.codely.mooc.courses.application; + +import tv.codely.shared.domain.ListMother; + +import java.util.Collections; +import java.util.List; + +public final class CoursesResponseMother { + public static CoursesResponse create(List courses) { + return new CoursesResponse(courses); + } + + public static CoursesResponse random() { + return create(ListMother.random(CourseResponseMother::random)); + } + + public static CoursesResponse times(int times) { + return create(ListMother.create(times, CourseResponseMother::random)); + } + + public static CoursesResponse empty() { + return create(Collections.emptyList()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandHandlerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandHandlerShould.java new file mode 100644 index 0000000..efdd3f7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandHandlerShould.java @@ -0,0 +1,33 @@ +package tv.codely.mooc.courses.application.create; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses.CoursesModuleUnitTestCase; +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseCreatedDomainEventMother; +import tv.codely.mooc.courses.domain.CourseMother; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +final class CreateCourseCommandHandlerShould extends CoursesModuleUnitTestCase { + private CreateCourseCommandHandler handler; + + @BeforeEach + protected void setUp() { + super.setUp(); + + handler = new CreateCourseCommandHandler(new CourseCreator(repository, eventBus)); + } + + @Test + void create_a_valid_course() { + CreateCourseCommand command = CreateCourseCommandMother.random(); + + Course course = CourseMother.fromRequest(command); + CourseCreatedDomainEvent domainEvent = CourseCreatedDomainEventMother.fromCourse(course); + + handler.handle(command); + + shouldHaveSaved(course); + shouldHavePublished(domainEvent); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandMother.java new file mode 100644 index 0000000..f4dad05 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.application.create; + +import tv.codely.mooc.courses.domain.*; + +public final class CreateCourseCommandMother { + public static CreateCourseCommand create(CourseId id, CourseName name, CourseDuration duration) { + return new CreateCourseCommand(id.value(), name.value(), duration.value()); + } + + public static CreateCourseCommand random() { + return create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryMother.java new file mode 100644 index 0000000..c589326 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.application.search_last; + +import tv.codely.shared.domain.IntegerMother; + +public final class SearchLastCoursesQueryMother { + public static SearchLastCoursesQuery create(Integer total) { + return new SearchLastCoursesQuery(total); + } + + public static SearchLastCoursesQuery random() { + return create(IntegerMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseCreatedDomainEventMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseCreatedDomainEventMother.java new file mode 100644 index 0000000..e73f2d4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseCreatedDomainEventMother.java @@ -0,0 +1,17 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +public final class CourseCreatedDomainEventMother { + public static CourseCreatedDomainEvent create(CourseId id, CourseName name, CourseDuration duration) { + return new CourseCreatedDomainEvent(id.value(), name.value(), duration.value()); + } + + public static CourseCreatedDomainEvent fromCourse(Course course) { + return create(course.id(), course.name(), course.duration()); + } + + public static CourseCreatedDomainEvent random() { + return create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseDurationMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseDurationMother.java new file mode 100644 index 0000000..d9ed986 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseDurationMother.java @@ -0,0 +1,20 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.IntegerMother; +import tv.codely.shared.domain.RandomElementPicker; + +public final class CourseDurationMother { + public static CourseDuration create(String value) { + return new CourseDuration(value); + } + + public static CourseDuration random() { + return create( + String.format( + "%s %s", + IntegerMother.random(), + RandomElementPicker.from("months", "years", "days", "hours", "minutes", "seconds") + ) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseIdMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseIdMother.java new file mode 100644 index 0000000..76bf0d9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseIdMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.UuidMother; + +public final class CourseIdMother { + public static CourseId create(String value) { + return new CourseId(value); + } + + public static CourseId random() { + return create(UuidMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseMother.java new file mode 100644 index 0000000..20f225e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseMother.java @@ -0,0 +1,21 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.mooc.courses.application.create.CreateCourseCommand; + +public final class CourseMother { + public static Course create(CourseId id, CourseName name, CourseDuration duration) { + return new Course(id, name, duration); + } + + public static Course fromRequest(CreateCourseCommand request) { + return create( + CourseIdMother.create(request.id()), + CourseNameMother.create(request.name()), + CourseDurationMother.create(request.duration()) + ); + } + + public static Course random() { + return create(CourseIdMother.random(), CourseNameMother.random(), CourseDurationMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseNameMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseNameMother.java new file mode 100644 index 0000000..1ca25f8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/domain/CourseNameMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses.domain; + +import tv.codely.shared.domain.WordMother; + +public final class CourseNameMother { + public static CourseName create(String value) { + return new CourseName(value); + } + + public static CourseName random() { + return create(WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepositoryShould.java new file mode 100644 index 0000000..04e3d6a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepositoryShould.java @@ -0,0 +1,35 @@ +package tv.codely.mooc.courses.infrastructure.persistence; + +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses.CoursesModuleInfrastructureTestCase; +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseIdMother; +import tv.codely.mooc.courses.domain.CourseMother; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +final class InMemoryCourseRepositoryShould extends CoursesModuleInfrastructureTestCase { + @Test + void save_a_course() { + Course course = CourseMother.random(); + + inMemoryCourseRepository.save(course); + } + + @Test + void return_an_existing_course() { + Course course = CourseMother.random(); + + inMemoryCourseRepository.save(course); + + assertEquals(Optional.of(course), inMemoryCourseRepository.search(course.id())); + } + + @Test + void not_return_a_non_existing_course() { + assertFalse(inMemoryCourseRepository.search(CourseIdMother.random()).isPresent()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepositoryShould.java new file mode 100644 index 0000000..45b8a1b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepositoryShould.java @@ -0,0 +1,37 @@ +package tv.codely.mooc.courses.infrastructure.persistence; + +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses.CoursesModuleInfrastructureTestCase; +import tv.codely.mooc.courses.domain.Course; +import tv.codely.mooc.courses.domain.CourseIdMother; +import tv.codely.mooc.courses.domain.CourseMother; + +import jakarta.transaction.Transactional; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@Transactional +class MySqlCourseRepositoryShould extends CoursesModuleInfrastructureTestCase { + @Test + void save_a_course() { + Course course = CourseMother.random(); + + mySqlCourseRepository.save(course); + } + + @Test + void return_an_existing_course() { + Course course = CourseMother.random(); + + mySqlCourseRepository.save(course); + + assertEquals(Optional.of(course), mySqlCourseRepository.search(course.id())); + } + + @Test + void not_return_a_non_existing_course() { + assertFalse(mySqlCourseRepository.search(CourseIdMother.random()).isPresent()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleInfrastructureTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleInfrastructureTestCase.java new file mode 100644 index 0000000..18978bb --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleInfrastructureTestCase.java @@ -0,0 +1,10 @@ +package tv.codely.mooc.courses_counter; + +import org.springframework.beans.factory.annotation.Autowired; +import tv.codely.mooc.MoocContextInfrastructureTestCase; +import tv.codely.mooc.courses_counter.domain.CoursesCounterRepository; + +public abstract class CoursesCounterModuleInfrastructureTestCase extends MoocContextInfrastructureTestCase { + @Autowired + protected CoursesCounterRepository repository; +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleUnitTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleUnitTestCase.java new file mode 100644 index 0000000..8c06e7f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleUnitTestCase.java @@ -0,0 +1,34 @@ +package tv.codely.mooc.courses_counter; + +import org.junit.jupiter.api.BeforeEach; +import org.mockito.Mockito; +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterRepository; +import tv.codely.shared.infrastructure.UnitTestCase; + +import java.util.Optional; + +import static org.mockito.Mockito.*; + +public abstract class CoursesCounterModuleUnitTestCase extends UnitTestCase { + protected CoursesCounterRepository repository; + + @BeforeEach + protected void setUp() { + super.setUp(); + + repository = mock(CoursesCounterRepository.class); + } + + public void shouldSearch(CoursesCounter course) { + Mockito.when(repository.search()).thenReturn(Optional.of(course)); + } + + public void shouldSearch() { + Mockito.when(repository.search()).thenReturn(Optional.empty()); + } + + public void shouldHaveSaved(CoursesCounter course) { + verify(repository, atLeastOnce()).save(course); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponseMother.java new file mode 100644 index 0000000..4b38949 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponseMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses_counter.application.find; + +import tv.codely.shared.domain.IntegerMother; + +final class CoursesCounterResponseMother { + public static CoursesCounterResponse create(Integer value) { + return new CoursesCounterResponse(value); + } + + public static CoursesCounterResponse random() { + return create(IntegerMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandlerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandlerShould.java new file mode 100644 index 0000000..1065514 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandlerShould.java @@ -0,0 +1,42 @@ +package tv.codely.mooc.courses_counter.application.find; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses_counter.CoursesCounterModuleUnitTestCase; +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterMother; +import tv.codely.mooc.courses_counter.domain.CoursesCounterNotInitialized; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class FindCoursesCounterQueryHandlerShould extends CoursesCounterModuleUnitTestCase { + FindCoursesCounterQueryHandler handler; + + @BeforeEach + protected void setUp() { + super.setUp(); + + handler = new FindCoursesCounterQueryHandler(new CoursesCounterFinder(repository)); + } + + @Test + void it_should_find_an_existing_courses_counter() { + CoursesCounter counter = CoursesCounterMother.random(); + FindCoursesCounterQuery query = new FindCoursesCounterQuery(); + CoursesCounterResponse response = CoursesCounterResponseMother.create(counter.total().value()); + + shouldSearch(counter); + + assertEquals(response, handler.handle(query)); + } + + @Test + void it_should_throw_an_exception_when_courses_counter_does_not_exists() { + FindCoursesCounterQuery query = new FindCoursesCounterQuery(); + + shouldSearch(); + + assertThrows(CoursesCounterNotInitialized.class, () -> handler.handle(query)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreatedShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreatedShould.java new file mode 100644 index 0000000..13d6218 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreatedShould.java @@ -0,0 +1,66 @@ +package tv.codely.mooc.courses_counter.application.increment; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses.domain.CourseCreatedDomainEventMother; +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseIdMother; +import tv.codely.mooc.courses_counter.CoursesCounterModuleUnitTestCase; +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterMother; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +final class IncrementCoursesCounterOnCourseCreatedShould extends CoursesCounterModuleUnitTestCase { + IncrementCoursesCounterOnCourseCreated subscriber; + + @BeforeEach + protected void setUp() { + super.setUp(); + + subscriber = new IncrementCoursesCounterOnCourseCreated( + new CoursesCounterIncrementer(repository, uuidGenerator) + ); + } + + @Test + void it_should_initialize_a_new_counter() { + CourseCreatedDomainEvent event = CourseCreatedDomainEventMother.random(); + + CourseId courseId = CourseIdMother.create(event.aggregateId()); + CoursesCounter newCounter = CoursesCounterMother.withOne(courseId); + + shouldSearch(); + shouldGenerateUuid(newCounter.id().value()); + + subscriber.on(event); + + shouldHaveSaved(newCounter); + } + + @Test + void it_should_increment_an_existing_counter() { + CourseCreatedDomainEvent event = CourseCreatedDomainEventMother.random(); + + CourseId courseId = CourseIdMother.create(event.aggregateId()); + CoursesCounter existingCounter = CoursesCounterMother.random(); + CoursesCounter incrementedCounter = CoursesCounterMother.incrementing(existingCounter, courseId); + + shouldSearch(existingCounter); + + subscriber.on(event); + + shouldHaveSaved(incrementedCounter); + } + + @Test + void it_should_not_increment_an_already_incremented_course() { + CourseCreatedDomainEvent event = CourseCreatedDomainEventMother.random(); + + CourseId courseId = CourseIdMother.create(event.aggregateId()); + CoursesCounter existingCounter = CoursesCounterMother.withOne(courseId); + + shouldSearch(existingCounter); + + subscriber.on(event); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterIdMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterIdMother.java new file mode 100644 index 0000000..596a118 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterIdMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.courses_counter.domain; + +import tv.codely.shared.domain.UuidMother; + +public final class CoursesCounterIdMother { + public static CoursesCounterId create(String value) { + return new CoursesCounterId(value); + } + + public static CoursesCounterId random() { + return create(UuidMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterMother.java new file mode 100644 index 0000000..9e5a27f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterMother.java @@ -0,0 +1,43 @@ +package tv.codely.mooc.courses_counter.domain; + +import tv.codely.mooc.courses.domain.CourseId; +import tv.codely.mooc.courses.domain.CourseIdMother; +import tv.codely.shared.domain.ListMother; + +import java.util.ArrayList; +import java.util.List; + +public final class CoursesCounterMother { + public static CoursesCounter create( + CoursesCounterId id, + CoursesCounterTotal total, + List existingCourses + ) { + return new CoursesCounter(id, total, existingCourses); + } + + public static CoursesCounter withOne(CourseId courseId) { + return create(CoursesCounterIdMother.random(), CoursesCounterTotalMother.one(), ListMother.one(courseId)); + } + + public static CoursesCounter random() { + List existingCourses = ListMother.random(CourseIdMother::random); + + return create( + CoursesCounterIdMother.random(), + CoursesCounterTotalMother.create(existingCourses.size()), + existingCourses + ); + } + + public static CoursesCounter incrementing(CoursesCounter existingCounter, CourseId courseId) { + List existingCourses = new ArrayList<>(existingCounter.existingCourses()); + existingCourses.add(courseId); + + return create( + existingCounter.id(), + CoursesCounterTotalMother.create(existingCounter.total().value() + 1), + existingCourses + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterTotalMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterTotalMother.java new file mode 100644 index 0000000..b8c22cb --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterTotalMother.java @@ -0,0 +1,17 @@ +package tv.codely.mooc.courses_counter.domain; + +import tv.codely.shared.domain.IntegerMother; + +public final class CoursesCounterTotalMother { + public static CoursesCounterTotal create(Integer value) { + return new CoursesCounterTotal(value); + } + + public static CoursesCounterTotal random() { + return create(IntegerMother.random()); + } + + public static CoursesCounterTotal one() { + return create(1); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepositoryShould.java new file mode 100644 index 0000000..fabdfe8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepositoryShould.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.courses_counter.infrastructure.persistence; + +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses_counter.CoursesCounterModuleInfrastructureTestCase; +import tv.codely.mooc.courses_counter.domain.CoursesCounter; +import tv.codely.mooc.courses_counter.domain.CoursesCounterMother; + +import jakarta.transaction.Transactional; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Transactional +class MySqlCoursesCounterRepositoryShould extends CoursesCounterModuleInfrastructureTestCase { + @Test + void return_an_existing_courses_counter() { + CoursesCounter counter = CoursesCounterMother.random(); + + repository.save(counter); + + assertEquals(Optional.of(counter), repository.search()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/application/NotificationsModuleUnitTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/application/NotificationsModuleUnitTestCase.java new file mode 100644 index 0000000..6b12480 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/application/NotificationsModuleUnitTestCase.java @@ -0,0 +1,33 @@ +package tv.codely.mooc.notifications.application; + +import org.junit.jupiter.api.BeforeEach; +import org.mockito.ArgumentCaptor; +import tv.codely.mooc.notifications.domain.Email; +import tv.codely.mooc.notifications.domain.EmailSender; +import tv.codely.shared.infrastructure.UnitTestCase; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.*; + +public abstract class NotificationsModuleUnitTestCase extends UnitTestCase { + protected EmailSender sender; + + @BeforeEach + protected void setUp() { + super.setUp(); + + sender = mock(EmailSender.class); + } + + public void shouldHaveSentEmail(Email email) { + ArgumentCaptor argument = ArgumentCaptor.forClass(Email.class); + + verify(sender, atLeastOnce()).send(argument.capture()); + + List emails = argument.getAllValues(); + + assertTrue(emails.contains(email)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandlerShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandlerShould.java new file mode 100644 index 0000000..8134c05 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandlerShould.java @@ -0,0 +1,102 @@ +package tv.codely.mooc.notifications.application.send_new_courses_newsletter; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tv.codely.mooc.courses.application.CoursesResponse; +import tv.codely.mooc.courses.application.CoursesResponseMother; +import tv.codely.mooc.courses.application.search_last.SearchLastCoursesQuery; +import tv.codely.mooc.courses.application.search_last.SearchLastCoursesQueryMother; +import tv.codely.mooc.notifications.application.NotificationsModuleUnitTestCase; +import tv.codely.mooc.notifications.domain.NewCoursesNewsletter; +import tv.codely.mooc.notifications.domain.NewCoursesNewsletterEmailSent; +import tv.codely.mooc.notifications.domain.NewCoursesNewsletterEmailSentMother; +import tv.codely.mooc.notifications.domain.NewCoursesNewsletterMother; +import tv.codely.mooc.students.application.StudentResponse; +import tv.codely.mooc.students.application.StudentResponseMother; +import tv.codely.mooc.students.application.StudentsResponse; +import tv.codely.mooc.students.application.StudentsResponseMother; +import tv.codely.mooc.students.application.search_all.SearchAllStudentsQuery; +import tv.codely.mooc.students.application.search_all.SearchAllStudentsQueryMother; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; + +import java.util.Arrays; + +final class SendNewCoursesNewsletterCommandHandlerShould extends NotificationsModuleUnitTestCase { + SendNewCoursesNewsletterCommandHandler handler; + + @BeforeEach + protected void setUp() { + super.setUp(); + + handler = new SendNewCoursesNewsletterCommandHandler( + new NewCoursesNewsletterSender(queryBus, uuidGenerator, sender, eventBus) + ); + } + + @Test + void not_send_the_newsletter_when_there_are_no_courses() throws QueryHandlerExecutionError, CommandHandlerExecutionError { + SendNewCoursesNewsletterCommand command = SendNewCoursesNewsletterCommandMother.random(); + + SearchLastCoursesQuery coursesQuery = SearchLastCoursesQueryMother.create(3); + CoursesResponse coursesResponse = CoursesResponseMother.empty(); + + shouldAsk(coursesQuery, coursesResponse); + + handler.handle(command); + } + + @Test + void not_send_the_newsletter_when_there_are_no_students() throws QueryHandlerExecutionError, CommandHandlerExecutionError { + SendNewCoursesNewsletterCommand command = SendNewCoursesNewsletterCommandMother.random(); + + SearchLastCoursesQuery coursesQuery = SearchLastCoursesQueryMother.create(3); + CoursesResponse coursesResponse = CoursesResponseMother.random(); + + SearchAllStudentsQuery studentsQuery = SearchAllStudentsQueryMother.random(); + StudentsResponse studentsResponse = StudentsResponseMother.empty(); + + shouldAsk(coursesQuery, coursesResponse); + shouldAsk(studentsQuery, studentsResponse); + + handler.handle(command); + } + + @Test + void send_the_new_courses_newsletter() throws QueryHandlerExecutionError, CommandHandlerExecutionError { + SendNewCoursesNewsletterCommand command = SendNewCoursesNewsletterCommandMother.random(); + + SearchLastCoursesQuery coursesQuery = SearchLastCoursesQueryMother.create(3); + CoursesResponse coursesResponse = CoursesResponseMother.times(3); + + SearchAllStudentsQuery studentsQuery = SearchAllStudentsQueryMother.random(); + StudentResponse student = StudentResponseMother.random(); + StudentResponse otherStudent = StudentResponseMother.random(); + StudentsResponse studentsResponse = StudentsResponseMother.create(Arrays.asList(student, otherStudent)); + + NewCoursesNewsletter newsletter = NewCoursesNewsletterMother.create(student, coursesResponse); + NewCoursesNewsletter otherNewsletter = NewCoursesNewsletterMother.create(otherStudent, coursesResponse); + + NewCoursesNewsletterEmailSent domainEvent = NewCoursesNewsletterEmailSentMother.create( + newsletter.id(), + student.id() + ); + NewCoursesNewsletterEmailSent otherDomainEvent = NewCoursesNewsletterEmailSentMother.create( + otherNewsletter.id(), + otherStudent.id() + ); + + shouldAsk(coursesQuery, coursesResponse); + shouldAsk(studentsQuery, studentsResponse); + + shouldGenerateUuids(newsletter.id().value(), otherNewsletter.id().value()); + + handler.handle(command); + + shouldHaveSentEmail(newsletter); + shouldHavePublished(domainEvent); + + shouldHaveSentEmail(otherNewsletter); + shouldHavePublished(otherDomainEvent); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandMother.java new file mode 100644 index 0000000..f91caff --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandMother.java @@ -0,0 +1,9 @@ +package tv.codely.mooc.notifications.application.send_new_courses_newsletter; + +import tv.codely.shared.domain.UuidMother; + +public final class SendNewCoursesNewsletterCommandMother { + public static SendNewCoursesNewsletterCommand random() { + return new SendNewCoursesNewsletterCommand(UuidMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/domain/EmailIdMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/domain/EmailIdMother.java new file mode 100644 index 0000000..0211444 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/domain/EmailIdMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.shared.domain.UuidMother; + +public final class EmailIdMother { + public static EmailId create(String value) { + return new EmailId(value); + } + + public static EmailId random() { + return create(UuidMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSentMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSentMother.java new file mode 100644 index 0000000..97afd0f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSentMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.mooc.students.domain.StudentIdMother; + +public final class NewCoursesNewsletterEmailSentMother { + public static NewCoursesNewsletterEmailSent create(EmailId id, String studentId) { + return new NewCoursesNewsletterEmailSent(id.value(), studentId); + } + + public static NewCoursesNewsletterEmailSent random() { + return create(EmailIdMother.random(), StudentIdMother.random().value()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterMother.java new file mode 100644 index 0000000..a5ff7f5 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterMother.java @@ -0,0 +1,20 @@ +package tv.codely.mooc.notifications.domain; + +import tv.codely.mooc.courses.application.CoursesResponse; +import tv.codely.mooc.courses.application.CoursesResponseMother; +import tv.codely.mooc.students.application.StudentResponse; +import tv.codely.mooc.students.application.StudentResponseMother; + +public final class NewCoursesNewsletterMother { + public static NewCoursesNewsletter create(EmailId id, StudentResponse student, CoursesResponse courses) { + return new NewCoursesNewsletter(id, student, courses); + } + + public static NewCoursesNewsletter create(StudentResponse student, CoursesResponse courses) { + return new NewCoursesNewsletter(EmailIdMother.random(), student, courses); + } + + public static NewCoursesNewsletter random() { + return create(EmailIdMother.random(), StudentResponseMother.random(), CoursesResponseMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/mysql/MySqlEventBusShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/mysql/MySqlEventBusShould.java new file mode 100644 index 0000000..17dc703 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/mysql/MySqlEventBusShould.java @@ -0,0 +1,34 @@ +package tv.codely.mooc.shared.infrastructure.bus.event.mysql; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import tv.codely.mooc.MoocContextInfrastructureTestCase; +import tv.codely.mooc.courses.domain.CourseCreatedDomainEventMother; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlDomainEventsConsumer; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; + +import jakarta.transaction.Transactional; +import java.util.Collections; + +@Transactional +class MySqlEventBusShould extends MoocContextInfrastructureTestCase { + @Autowired + private MySqlEventBus eventBus; + @Autowired + private MySqlDomainEventsConsumer consumer; + + @Test + void publish_and_consume_domain_events_from_msql() throws InterruptedException { + CourseCreatedDomainEvent domainEvent = CourseCreatedDomainEventMother.random(); + + eventBus.publish(Collections.singletonList(domainEvent)); + + Thread consumerProcess = new Thread(() -> consumer.consume()); + consumerProcess.start(); + + Thread.sleep(100); + + consumer.stop(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java new file mode 100644 index 0000000..70c8de2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java @@ -0,0 +1,53 @@ +package tv.codely.mooc.shared.infrastructure.bus.event.rabbitmq; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import tv.codely.mooc.MoocContextInfrastructureTestCase; +import tv.codely.mooc.courses.domain.CourseCreatedDomainEventMother; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; +import tv.codely.shared.infrastructure.bus.event.DomainEventSubscriberInformation; +import tv.codely.shared.infrastructure.bus.event.DomainEventSubscribersInformation; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqDomainEventsConsumer; +import tv.codely.shared.infrastructure.bus.event.rabbitmq.RabbitMqEventBus; + +import java.util.Collections; +import java.util.HashMap; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public final class RabbitMqEventBusShould extends MoocContextInfrastructureTestCase { + @Autowired + private RabbitMqEventBus eventBus; + @Autowired + private RabbitMqDomainEventsConsumer consumer; + @Autowired + private TestAllWorksOnRabbitMqEventsPublished subscriber; + + @BeforeEach + protected void setUp() { + subscriber.hasBeenExecuted = false; + + consumer.withSubscribersInformation( + new DomainEventSubscribersInformation( + new HashMap, DomainEventSubscriberInformation>() {{ + put(TestAllWorksOnRabbitMqEventsPublished.class, new DomainEventSubscriberInformation( + TestAllWorksOnRabbitMqEventsPublished.class, + Collections.singletonList(CourseCreatedDomainEvent.class) + )); + }} + ) + ); + } + + @Test + void publish_and_consume_domain_events_from_rabbitmq() throws Exception { + CourseCreatedDomainEvent domainEvent = CourseCreatedDomainEventMother.random(); + + eventBus.publish(Collections.singletonList(domainEvent)); + + consumer.consume("mooc"); + + eventually(() -> assertTrue(subscriber.hasBeenExecuted)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/TestAllWorksOnRabbitMqEventsPublished.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/TestAllWorksOnRabbitMqEventsPublished.java new file mode 100644 index 0000000..8b1981d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/TestAllWorksOnRabbitMqEventsPublished.java @@ -0,0 +1,15 @@ +package tv.codely.mooc.shared.infrastructure.bus.event.rabbitmq; + +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; +import tv.codely.shared.domain.course.CourseCreatedDomainEvent; + +@Service +@DomainEventSubscriber({CourseCreatedDomainEvent.class}) +public final class TestAllWorksOnRabbitMqEventsPublished { + public Boolean hasBeenExecuted = false; + + public void on(CourseCreatedDomainEvent event) { + hasBeenExecuted = true; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/StepsModuleInfrastructureTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/StepsModuleInfrastructureTestCase.java new file mode 100644 index 0000000..cc0c90d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/StepsModuleInfrastructureTestCase.java @@ -0,0 +1,10 @@ +package tv.codely.mooc.steps; + +import org.springframework.beans.factory.annotation.Autowired; +import tv.codely.mooc.MoocContextInfrastructureTestCase; +import tv.codely.mooc.steps.domain.StepRepository; + +public abstract class StepsModuleInfrastructureTestCase extends MoocContextInfrastructureTestCase { + @Autowired + protected StepRepository repository; +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/StepIdMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/StepIdMother.java new file mode 100644 index 0000000..7747f37 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/StepIdMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain; + +import tv.codely.shared.domain.UuidMother; + +public final class StepIdMother { + public static StepId create(String value) { + return new StepId(value); + } + + public static StepId random() { + return create(UuidMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/StepTitleMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/StepTitleMother.java new file mode 100644 index 0000000..571bc13 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/StepTitleMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain; + +import tv.codely.shared.domain.WordMother; + +public final class StepTitleMother { + public static StepTitle create(String value) { + return new StepTitle(value); + } + + public static StepTitle random() { + return create(WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepMother.java new file mode 100644 index 0000000..46fcb75 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepMother.java @@ -0,0 +1,16 @@ +package tv.codely.mooc.steps.domain.challenge; + +import tv.codely.mooc.steps.domain.StepId; +import tv.codely.mooc.steps.domain.StepIdMother; +import tv.codely.mooc.steps.domain.StepTitle; +import tv.codely.mooc.steps.domain.StepTitleMother; + +public final class ChallengeStepMother { + public static ChallengeStep create(StepId id, StepTitle title, ChallengeStepStatement statement) { + return new ChallengeStep(id, title, statement); + } + + public static ChallengeStep random() { + return create(StepIdMother.random(), StepTitleMother.random(), ChallengeStepStatementMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatementMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatementMother.java new file mode 100644 index 0000000..8e85970 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatementMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain.challenge; + +import tv.codely.shared.domain.WordMother; + +public final class ChallengeStepStatementMother { + public static ChallengeStepStatement create(String value) { + return new ChallengeStepStatement(value); + } + + public static ChallengeStepStatement random() { + return create(WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepMother.java new file mode 100644 index 0000000..3cb9432 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepMother.java @@ -0,0 +1,23 @@ +package tv.codely.mooc.steps.domain.video; + +import tv.codely.mooc.steps.domain.StepId; +import tv.codely.mooc.steps.domain.StepIdMother; +import tv.codely.mooc.steps.domain.StepTitle; +import tv.codely.mooc.steps.domain.StepTitleMother; +import tv.codely.shared.domain.VideoUrl; +import tv.codely.shared.domain.VideoUrlMother; + +public final class VideoStepMother { + public static VideoStep create(StepId id, StepTitle title, VideoUrl videoUrl, VideoStepText text) { + return new VideoStep(id, title, videoUrl, text); + } + + public static VideoStep random() { + return create( + StepIdMother.random(), + StepTitleMother.random(), + VideoUrlMother.random(), + VideoStepTextMother.random() + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepTextMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepTextMother.java new file mode 100644 index 0000000..7dca6fa --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepTextMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.steps.domain.video; + +import tv.codely.shared.domain.WordMother; + +public final class VideoStepTextMother { + public static VideoStepText create(String value) { + return new VideoStepText(value); + } + + public static VideoStepText random() { + return create(WordMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepositoryShould.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepositoryShould.java new file mode 100644 index 0000000..88c684e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepositoryShould.java @@ -0,0 +1,44 @@ +package tv.codely.mooc.steps.infrastructure.persistence; + +import org.junit.jupiter.api.Test; +import tv.codely.mooc.steps.StepsModuleInfrastructureTestCase; +import tv.codely.mooc.steps.domain.Step; +import tv.codely.mooc.steps.domain.StepIdMother; +import tv.codely.mooc.steps.domain.challenge.ChallengeStepMother; +import tv.codely.mooc.steps.domain.video.VideoStepMother; + +import jakarta.transaction.Transactional; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@Transactional +class MySqlStepRepositoryShould extends StepsModuleInfrastructureTestCase { + @Test + void save_a_step() { + for (Step step : steps()) { + repository.save(step); + } + } + + @Test + void return_an_existing_step() { + for (Step step : steps()) { + repository.save(step); + + assertEquals(Optional.of(step), repository.search(step.id())); + } + } + + @Test + void not_return_a_non_existing_course() { + assertFalse(repository.search(StepIdMother.random()).isPresent()); + } + + private List steps() { + return Arrays.asList(ChallengeStepMother.random(), VideoStepMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/students/application/StudentResponseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/students/application/StudentResponseMother.java new file mode 100644 index 0000000..b29fb25 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/students/application/StudentResponseMother.java @@ -0,0 +1,16 @@ +package tv.codely.mooc.students.application; + +import tv.codely.mooc.students.domain.StudentId; +import tv.codely.mooc.students.domain.StudentIdMother; +import tv.codely.shared.domain.EmailMother; +import tv.codely.shared.domain.WordMother; + +public final class StudentResponseMother { + public static StudentResponse create(StudentId id, String name, String surname, String email) { + return new StudentResponse(id.value(), name, surname, email); + } + + public static StudentResponse random() { + return create(StudentIdMother.random(), WordMother.random(), WordMother.random(), EmailMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/students/application/StudentsResponseMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/students/application/StudentsResponseMother.java new file mode 100644 index 0000000..89e879a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/students/application/StudentsResponseMother.java @@ -0,0 +1,20 @@ +package tv.codely.mooc.students.application; + +import tv.codely.shared.domain.ListMother; + +import java.util.Collections; +import java.util.List; + +public final class StudentsResponseMother { + public static StudentsResponse create(List courses) { + return new StudentsResponse(courses); + } + + public static StudentsResponse random() { + return create(ListMother.random(StudentResponseMother::random)); + } + + public static StudentsResponse empty() { + return create(Collections.emptyList()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryMother.java new file mode 100644 index 0000000..93a0e28 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryMother.java @@ -0,0 +1,7 @@ +package tv.codely.mooc.students.application.search_all; + +public final class SearchAllStudentsQueryMother { + public static SearchAllStudentsQuery random() { + return new SearchAllStudentsQuery(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/students/domain/StudentIdMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/students/domain/StudentIdMother.java new file mode 100644 index 0000000..b98ca0e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/mooc/test/tv/codely/mooc/students/domain/StudentIdMother.java @@ -0,0 +1,13 @@ +package tv.codely.mooc.students.domain; + +import tv.codely.shared.domain.UuidMother; + +public final class StudentIdMother { + public static StudentId create(String value) { + return new StudentId(value); + } + + public static StudentId random() { + return create(UuidMother.random()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/build.gradle b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/build.gradle new file mode 100644 index 0000000..7d82dc7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/AggregateRoot.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/AggregateRoot.java new file mode 100644 index 0000000..796e327 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/AggregateRoot.java @@ -0,0 +1,23 @@ +package tv.codely.shared.domain; + +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public abstract class AggregateRoot { + private List domainEvents = new ArrayList<>(); + + final public List pullDomainEvents() { + List events = domainEvents; + + domainEvents = Collections.emptyList(); + + return events; + } + + final protected void record(DomainEvent event) { + domainEvents.add(event); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/DomainError.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/DomainError.java new file mode 100644 index 0000000..1d65456 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/DomainError.java @@ -0,0 +1,21 @@ +package tv.codely.shared.domain; + +public abstract class DomainError extends RuntimeException { + private final String errorCode; + private final String errorMessage; + + public DomainError(String errorCode, String errorMessage) { + super(errorMessage); + + this.errorCode = errorCode; + this.errorMessage = errorMessage; + } + + public String errorCode() { + return errorCode; + } + + public String errorMessage() { + return errorMessage; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Identifier.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Identifier.java new file mode 100644 index 0000000..b25970b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Identifier.java @@ -0,0 +1,44 @@ +package tv.codely.shared.domain; + +import java.io.Serializable; +import java.util.Objects; +import java.util.UUID; + +public abstract class Identifier implements Serializable { + final protected String value; + + public Identifier(String value) { + ensureValidUuid(value); + + this.value = value; + } + + protected Identifier() { + this.value = null; + } + + public String value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Identifier that = (Identifier) o; + return value.equals(that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } + + private void ensureValidUuid(String value) throws IllegalArgumentException { + UUID.fromString(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/IntValueObject.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/IntValueObject.java new file mode 100644 index 0000000..4e943e5 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/IntValueObject.java @@ -0,0 +1,32 @@ +package tv.codely.shared.domain; + +import java.util.Objects; + +public abstract class IntValueObject { + private Integer value; + + public IntValueObject(Integer value) { + this.value = value; + } + + public Integer value() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + IntValueObject that = (IntValueObject) o; + return value.equals(that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Logger.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Logger.java new file mode 100644 index 0000000..efeab9f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Logger.java @@ -0,0 +1,15 @@ +package tv.codely.shared.domain; + +import java.io.Serializable; +import java.util.HashMap; + +public interface Logger { + void info(String $message); + void info(String $message, HashMap $context); + + void warning(String $message); + void warning(String $message, HashMap $context); + + void critical(String $message); + void critical(String $message, HashMap $context); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Monitoring.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Monitoring.java new file mode 100644 index 0000000..0958579 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Monitoring.java @@ -0,0 +1,13 @@ +package tv.codely.shared.domain; + +import java.util.HashMap; + +public interface Monitoring { + void incrementCounter(int times); + + void incrementGauge(int times); + void decrementGauge(int times); + void setGauge(int value); + + void observeHistogram(int value, HashMap labels); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Service.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Service.java new file mode 100644 index 0000000..d3f9566 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Service.java @@ -0,0 +1,9 @@ +package tv.codely.shared.domain; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface Service { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/StringValueObject.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/StringValueObject.java new file mode 100644 index 0000000..45525e5 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/StringValueObject.java @@ -0,0 +1,37 @@ +package tv.codely.shared.domain; + +import java.util.Objects; + +public abstract class StringValueObject { + private String value; + + public StringValueObject(String value) { + this.value = value; + } + + public String value() { + return value; + } + + @Override + public String toString() { + return this.value(); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof StringValueObject)) { + return false; + } + StringValueObject that = (StringValueObject) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Utils.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Utils.java new file mode 100644 index 0000000..a3a56a6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/Utils.java @@ -0,0 +1,81 @@ +package tv.codely.shared.domain; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.base.CaseFormat; + +import java.io.IOException; +import java.io.Serializable; +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; + +public final class Utils { + public static String dateToString(LocalDateTime dateTime) { + return dateTime.format(DateTimeFormatter.ISO_LOCAL_DATE); + } + + public static String dateToString(Timestamp timestamp) { + return dateToString(timestamp.toLocalDateTime()); + } + + public static String jsonEncode(HashMap map) { + try { + return new ObjectMapper().writeValueAsString(map); + } catch (JsonProcessingException e) { + return ""; + } + } + + public static String jsonEncode(Object map) { + try { + return new ObjectMapper().writeValueAsString(map); + } catch (JsonProcessingException e) { + return ""; + } + } + + public static HashMap jsonDecode(String body) { + try { + return new ObjectMapper().readValue(body, HashMap.class); + } catch (IOException e) { + return null; + } + } + + public static String toSnake(String text) { + return CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, text); + } + + public static String toCamel(String text) { + return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, text); + } + + public static String toCamelFirstLower(String text) { + return CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, text); + } + + public static void retry(int numberOfRetries, long waitTimeInMillis, Runnable operation) throws Exception { + for (int i = 0; i < numberOfRetries; i++) { + try { + operation.run(); + return; // Success, exit the method + } catch (Exception ex) { + System.out.println("Retry " + (i + 1) + "/" + numberOfRetries + " fail. Retrying…"); + if (i >= numberOfRetries - 1) { + throw ex; + } + + try { + Thread.sleep(waitTimeInMillis); + } catch (InterruptedException ie) { + Thread.currentThread().interrupt(); + + throw new Exception("Operation interrupted while retrying", ie); + } + } + } + } + +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/UuidGenerator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/UuidGenerator.java new file mode 100644 index 0000000..8348a24 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/UuidGenerator.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain; + +public interface UuidGenerator { + String generate(); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/VideoUrl.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/VideoUrl.java new file mode 100644 index 0000000..47aaccc --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/VideoUrl.java @@ -0,0 +1,11 @@ +package tv.codely.shared.domain; + +public final class VideoUrl extends StringValueObject { + public VideoUrl(String value) { + super(value); + } + + public VideoUrl() { + super(null); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/Command.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/Command.java new file mode 100644 index 0000000..da5a342 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/Command.java @@ -0,0 +1,4 @@ +package tv.codely.shared.domain.bus.command; + +public interface Command { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/CommandBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/CommandBus.java new file mode 100644 index 0000000..dabddf2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/CommandBus.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain.bus.command; + +public interface CommandBus { + void dispatch(Command command) throws CommandHandlerExecutionError; +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandler.java new file mode 100644 index 0000000..177e09b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandler.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain.bus.command; + +public interface CommandHandler { + void handle(T command); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandlerExecutionError.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandlerExecutionError.java new file mode 100644 index 0000000..60d0d74 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandlerExecutionError.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain.bus.command; + +public final class CommandHandlerExecutionError extends RuntimeException { + public CommandHandlerExecutionError(Throwable cause) { + super(cause); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/CommandNotRegisteredError.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/CommandNotRegisteredError.java new file mode 100644 index 0000000..2d3af85 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/command/CommandNotRegisteredError.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain.bus.command; + +public final class CommandNotRegisteredError extends Exception { + public CommandNotRegisteredError(Class command) { + super(String.format("The command <%s> hasn't a command handler associated", command.toString())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/event/DomainEvent.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/event/DomainEvent.java new file mode 100644 index 0000000..901955c --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/event/DomainEvent.java @@ -0,0 +1,52 @@ +package tv.codely.shared.domain.bus.event; + +import tv.codely.shared.domain.Utils; + +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.UUID; + +public abstract class DomainEvent { + private String aggregateId; + private String eventId; + private String occurredOn; + + public DomainEvent(String aggregateId) { + this.aggregateId = aggregateId; + this.eventId = UUID.randomUUID().toString(); + this.occurredOn = Utils.dateToString(LocalDateTime.now()); + } + + public DomainEvent(String aggregateId, String eventId, String occurredOn) { + this.aggregateId = aggregateId; + this.eventId = eventId; + this.occurredOn = occurredOn; + } + + protected DomainEvent() { + } + + public abstract String eventName(); + + public abstract HashMap toPrimitives(); + + public abstract DomainEvent fromPrimitives( + String aggregateId, + HashMap body, + String eventId, + String occurredOn + ); + + public String aggregateId() { + return aggregateId; + } + + public String eventId() { + return eventId; + } + + public String occurredOn() { + return occurredOn; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/event/DomainEventSubscriber.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/event/DomainEventSubscriber.java new file mode 100644 index 0000000..9895464 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/event/DomainEventSubscriber.java @@ -0,0 +1,10 @@ +package tv.codely.shared.domain.bus.event; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +@Inherited +public @interface DomainEventSubscriber { + Class[] value(); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/event/EventBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/event/EventBus.java new file mode 100644 index 0000000..cd13e0b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/event/EventBus.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain.bus.event; + +import java.util.List; + +public interface EventBus { + void publish(final List events); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/Query.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/Query.java new file mode 100644 index 0000000..cdc5477 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/Query.java @@ -0,0 +1,4 @@ +package tv.codely.shared.domain.bus.query; + +public interface Query { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/QueryBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/QueryBus.java new file mode 100644 index 0000000..197945f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/QueryBus.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain.bus.query; + +public interface QueryBus { + R ask(Query query) throws QueryHandlerExecutionError; +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandler.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandler.java new file mode 100644 index 0000000..4b56bd8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandler.java @@ -0,0 +1,5 @@ +package tv.codely.shared.domain.bus.query; + +public interface QueryHandler { + R handle(Q query); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandlerExecutionError.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandlerExecutionError.java new file mode 100644 index 0000000..2e5135b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandlerExecutionError.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain.bus.query; + +public final class QueryHandlerExecutionError extends RuntimeException { + public QueryHandlerExecutionError(Throwable cause) { + super(cause); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/QueryNotRegisteredError.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/QueryNotRegisteredError.java new file mode 100644 index 0000000..d2d380f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/QueryNotRegisteredError.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain.bus.query; + +public final class QueryNotRegisteredError extends Exception { + public QueryNotRegisteredError(Class query) { + super(String.format("The query <%s> hasn't a query handler associated", query.toString())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/Response.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/Response.java new file mode 100644 index 0000000..ac3eefc --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/bus/query/Response.java @@ -0,0 +1,4 @@ +package tv.codely.shared.domain.bus.query; + +public interface Response { +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/course/CourseCreatedDomainEvent.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/course/CourseCreatedDomainEvent.java new file mode 100644 index 0000000..23181f0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/course/CourseCreatedDomainEvent.java @@ -0,0 +1,94 @@ +package tv.codely.shared.domain.course; + +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Objects; + +public final class CourseCreatedDomainEvent extends DomainEvent { + private final String name; + private final String duration; + + public CourseCreatedDomainEvent() { + super(null); + + this.name = null; + this.duration = null; + } + + public CourseCreatedDomainEvent(String aggregateId, String name, String duration) { + super(aggregateId); + + this.name = name; + this.duration = duration; + } + + public CourseCreatedDomainEvent( + String aggregateId, + String eventId, + String occurredOn, + String name, + String duration + ) { + super(aggregateId, eventId, occurredOn); + + this.name = name; + this.duration = duration; + } + + @Override + public String eventName() { + return "course.created"; + } + + @Override + public HashMap toPrimitives() { + return new HashMap() {{ + put("name", name); + put("duration", duration); + }}; + } + + @Override + public CourseCreatedDomainEvent fromPrimitives( + String aggregateId, + HashMap body, + String eventId, + String occurredOn + ) { + return new CourseCreatedDomainEvent( + aggregateId, + eventId, + occurredOn, + (String) body.get("name"), + (String) body.get("duration") + ); + } + + public String name() { + return name; + } + + public String duration() { + return duration; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + CourseCreatedDomainEvent that = (CourseCreatedDomainEvent) o; + return name.equals(that.name) && + duration.equals(that.duration); + } + + @Override + public int hashCode() { + return Objects.hash(name, duration); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java new file mode 100644 index 0000000..2fb4097 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java @@ -0,0 +1,78 @@ +package tv.codely.shared.domain.course; + +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.Objects; + +public final class CourseRenamedDomainEvent extends DomainEvent { + private final String name; + + public CourseRenamedDomainEvent() { + super(null); + + this.name = null; + } + + public CourseRenamedDomainEvent(String aggregateId, String name) { + super(aggregateId); + + this.name = name; + } + + public CourseRenamedDomainEvent( + String aggregateId, + String eventId, + String occurredOn, + String name + ) { + super(aggregateId, eventId, occurredOn); + + this.name = name; + } + + @Override + public String eventName() { + return "course.renamed"; + } + + @Override + public HashMap toPrimitives() { + return new HashMap<>() {{ + put("name", name); + }}; + } + + @Override + public CourseRenamedDomainEvent fromPrimitives( + String aggregateId, + HashMap body, + String eventId, + String occurredOn + ) { + return new CourseRenamedDomainEvent( + aggregateId, + eventId, + occurredOn, + (String) body.get("name") + ); + } + + public String name() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CourseRenamedDomainEvent that = (CourseRenamedDomainEvent) o; + return Objects.equals(name, that.name); + } + + @Override + public int hashCode() { + return Objects.hash(name); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/Criteria.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/Criteria.java new file mode 100644 index 0000000..aea18af --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/Criteria.java @@ -0,0 +1,54 @@ +package tv.codely.shared.domain.criteria; + +import java.util.Optional; + +public final class Criteria { + private final Filters filters; + private final Order order; + private final Optional limit; + private final Optional offset; + + public Criteria(Filters filters, Order order, Optional limit, Optional offset) { + this.filters = filters; + this.order = order; + this.limit = limit; + this.offset = offset; + } + + public Criteria(Filters filters, Order order) { + this.filters = filters; + this.order = order; + this.limit = Optional.empty(); + this.offset = Optional.empty(); + } + + public Filters filters() { + return filters; + } + + public Order order() { + return order; + } + + public Optional limit() { + return limit; + } + + public Optional offset() { + return offset; + } + + public boolean hasFilters() { + return filters.filters().size() > 0; + } + + public String serialize() { + return String.format( + "%s~~%s~~%s~~%s", + filters.serialize(), + order.serialize(), + offset.orElse(0), + limit.orElse(0) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/Filter.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/Filter.java new file mode 100644 index 0000000..b3244a6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/Filter.java @@ -0,0 +1,47 @@ +package tv.codely.shared.domain.criteria; + +import java.util.HashMap; + +public final class Filter { + private final FilterField field; + private final FilterOperator operator; + private final FilterValue value; + + public Filter(FilterField field, FilterOperator operator, FilterValue value) { + this.field = field; + this.operator = operator; + this.value = value; + } + + public static Filter create(String field, String operator, String value) { + return new Filter( + new FilterField(field), + FilterOperator.fromValue(operator.toUpperCase()), + new FilterValue(value) + ); + } + + public static Filter fromValues(HashMap values) { + return new Filter( + new FilterField(values.get("field")), + FilterOperator.fromValue(values.get("operator")), + new FilterValue(values.get("value")) + ); + } + + public FilterField field() { + return field; + } + + public FilterOperator operator() { + return operator; + } + + public FilterValue value() { + return value; + } + + public String serialize() { + return String.format("%s.%s.%s", field.value(), operator.value(), value.value()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/FilterField.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/FilterField.java new file mode 100644 index 0000000..c5f230d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/FilterField.java @@ -0,0 +1,9 @@ +package tv.codely.shared.domain.criteria; + +import tv.codely.shared.domain.StringValueObject; + +public final class FilterField extends StringValueObject { + public FilterField(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/FilterOperator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/FilterOperator.java new file mode 100644 index 0000000..8614f12 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/FilterOperator.java @@ -0,0 +1,36 @@ +package tv.codely.shared.domain.criteria; + +public enum FilterOperator { + EQUAL("="), + NOT_EQUAL("!="), + GT(">"), + LT("<"), + CONTAINS("CONTAINS"), + NOT_CONTAINS("NOT_CONTAINS"); + + private final String operator; + + FilterOperator(String operator) { + this.operator = operator; + } + + public static FilterOperator fromValue(String value) { + switch (value) { + case "=": return FilterOperator.EQUAL; + case "!=": return FilterOperator.NOT_EQUAL; + case ">": return FilterOperator.GT; + case "<": return FilterOperator.LT; + case "CONTAINS": return FilterOperator.CONTAINS; + case "NOT_CONTAINS": return FilterOperator.NOT_CONTAINS; + default: return null; + } + } + + public boolean isPositive() { + return this != NOT_EQUAL && this != NOT_CONTAINS; + } + + public String value() { + return operator; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/FilterValue.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/FilterValue.java new file mode 100644 index 0000000..28a4f48 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/FilterValue.java @@ -0,0 +1,9 @@ +package tv.codely.shared.domain.criteria; + +import tv.codely.shared.domain.StringValueObject; + +public final class FilterValue extends StringValueObject { + public FilterValue(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/Filters.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/Filters.java new file mode 100644 index 0000000..83e36ae --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/Filters.java @@ -0,0 +1,30 @@ +package tv.codely.shared.domain.criteria; + +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +public final class Filters { + private final List filters; + + public Filters(List filters) { + this.filters = filters; + } + + public static Filters fromValues(List> filters) { + return new Filters(filters.stream().map(Filter::fromValues).collect(Collectors.toList())); + } + + public static Filters none() { + return new Filters(Collections.emptyList()); + } + + public List filters() { + return filters; + } + + public String serialize() { + return filters.stream().map(Filter::serialize).collect(Collectors.joining("^")); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/Order.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/Order.java new file mode 100644 index 0000000..f95eaf8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/Order.java @@ -0,0 +1,46 @@ +package tv.codely.shared.domain.criteria; + +import java.util.Optional; + +public final class Order { + private final OrderBy orderBy; + private final OrderType orderType; + + public Order(OrderBy orderBy, OrderType orderType) { + this.orderBy = orderBy; + this.orderType = orderType; + } + + public static Order fromValues(Optional orderBy, Optional orderType) { + return orderBy.map(order -> new Order(new OrderBy(order), OrderType.valueOf(orderType.orElse("ASC")))) + .orElseGet(Order::none); + } + + public static Order none() { + return new Order(new OrderBy(""), OrderType.NONE); + } + + public static Order desc(String orderBy) { + return new Order(new OrderBy(orderBy), OrderType.DESC); + } + + public static Order asc(String orderBy) { + return new Order(new OrderBy(orderBy), OrderType.ASC); + } + + public OrderBy orderBy() { + return orderBy; + } + + public OrderType orderType() { + return orderType; + } + + public boolean hasOrder() { + return !orderType.isNone(); + } + + public String serialize() { + return String.format("%s.%s", orderBy.value(), orderType.value()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/OrderBy.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/OrderBy.java new file mode 100644 index 0000000..2c1450d --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/OrderBy.java @@ -0,0 +1,9 @@ +package tv.codely.shared.domain.criteria; + +import tv.codely.shared.domain.StringValueObject; + +public final class OrderBy extends StringValueObject { + public OrderBy(String value) { + super(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/OrderType.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/OrderType.java new file mode 100644 index 0000000..52deae0 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/domain/criteria/OrderType.java @@ -0,0 +1,25 @@ +package tv.codely.shared.domain.criteria; + +public enum OrderType { + ASC("asc"), + DESC("desc"), + NONE("none"); + private final String type; + + OrderType(String type) { + this.type = type; + } + + public boolean isNone() { + return this == NONE; + } + + public boolean isAsc() { + return this == ASC; + } + + public String value() { + return type; + } +} + diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/JavaUuidGenerator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/JavaUuidGenerator.java new file mode 100644 index 0000000..21667a8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/JavaUuidGenerator.java @@ -0,0 +1,14 @@ +package tv.codely.shared.infrastructure; + +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.UuidGenerator; + +import java.util.UUID; + +@Service +public final class JavaUuidGenerator implements UuidGenerator { + @Override + public String generate() { + return UUID.randomUUID().toString(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/command/CommandHandlersInformation.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/command/CommandHandlersInformation.java new file mode 100644 index 0000000..5bdd8fc --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/command/CommandHandlersInformation.java @@ -0,0 +1,48 @@ +package tv.codely.shared.infrastructure.bus.command; + +import org.reflections.Reflections; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.command.Command; +import tv.codely.shared.domain.bus.command.CommandHandler; +import tv.codely.shared.domain.bus.command.CommandNotRegisteredError; + +import java.lang.reflect.ParameterizedType; +import java.util.HashMap; +import java.util.Set; + +@Service +public final class CommandHandlersInformation { + HashMap, Class> indexedCommandHandlers; + + public CommandHandlersInformation() { + Reflections reflections = new Reflections("tv.codely"); + Set> classes = reflections.getSubTypesOf(CommandHandler.class); + + indexedCommandHandlers = formatHandlers(classes); + } + + public Class search(Class commandClass) throws CommandNotRegisteredError { + Class commandHandlerClass = indexedCommandHandlers.get(commandClass); + + if (null == commandHandlerClass) { + throw new CommandNotRegisteredError(commandClass); + } + + return commandHandlerClass; + } + + private HashMap, Class> formatHandlers( + Set> commandHandlers + ) { + HashMap, Class> handlers = new HashMap<>(); + + for (Class handler : commandHandlers) { + ParameterizedType paramType = (ParameterizedType) handler.getGenericInterfaces()[0]; + Class commandClass = (Class) paramType.getActualTypeArguments()[0]; + + handlers.put(commandClass, handler); + } + + return handlers; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/command/InMemoryCommandBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/command/InMemoryCommandBus.java new file mode 100644 index 0000000..d8b23c7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/command/InMemoryCommandBus.java @@ -0,0 +1,32 @@ +package tv.codely.shared.infrastructure.bus.command; + +import org.springframework.context.ApplicationContext; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.command.Command; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.command.CommandHandler; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; + +@Service +public final class InMemoryCommandBus implements CommandBus { + private final CommandHandlersInformation information; + private final ApplicationContext context; + + public InMemoryCommandBus(CommandHandlersInformation information, ApplicationContext context) { + this.information = information; + this.context = context; + } + + @Override + public void dispatch(Command command) throws CommandHandlerExecutionError { + try { + Class commandHandlerClass = information.search(command.getClass()); + + CommandHandler handler = context.getBean(commandHandlerClass); + + handler.handle(command); + } catch (Throwable error) { + throw new CommandHandlerExecutionError(error); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonDeserializer.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonDeserializer.java new file mode 100644 index 0000000..ccfa660 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonDeserializer.java @@ -0,0 +1,46 @@ +package tv.codely.shared.infrastructure.bus.event; + +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.io.Serializable; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; + +@Service +public final class DomainEventJsonDeserializer { + private final DomainEventsInformation information; + + public DomainEventJsonDeserializer(DomainEventsInformation information) { + this.information = information; + } + + public DomainEvent deserialize(String body) throws InvocationTargetException, IllegalAccessException, NoSuchMethodException, InstantiationException { + HashMap eventData = Utils.jsonDecode(body); + HashMap data = (HashMap) eventData.get("data"); + HashMap attributes = (HashMap) data.get("attributes"); + Class domainEventClass = information.forName((String) data.get("type")); + + DomainEvent nullInstance = domainEventClass.getConstructor().newInstance(); + + Method fromPrimitivesMethod = domainEventClass.getMethod( + "fromPrimitives", + String.class, + HashMap.class, + String.class, + String.class + ); + + Object domainEvent = fromPrimitivesMethod.invoke( + nullInstance, + (String) attributes.get("id"), + attributes, + (String) data.get("id"), + (String) data.get("occurred_on") + ); + + return (DomainEvent) domainEvent; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonSerializer.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonSerializer.java new file mode 100644 index 0000000..adbedb9 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonSerializer.java @@ -0,0 +1,24 @@ +package tv.codely.shared.infrastructure.bus.event; + +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.io.Serializable; +import java.util.HashMap; + +public final class DomainEventJsonSerializer { + public static String serialize(DomainEvent domainEvent) { + HashMap attributes = domainEvent.toPrimitives(); + attributes.put("id", domainEvent.aggregateId()); + + return Utils.jsonEncode(new HashMap() {{ + put("data", new HashMap() {{ + put("id", domainEvent.eventId()); + put("type", domainEvent.eventName()); + put("occurred_on", domainEvent.occurredOn()); + put("attributes", attributes); + }}); + put("meta", new HashMap()); + }}); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscriberInformation.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscriberInformation.java new file mode 100644 index 0000000..e14d3d2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscriberInformation.java @@ -0,0 +1,49 @@ +package tv.codely.shared.infrastructure.bus.event; + +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.util.List; + +public final class DomainEventSubscriberInformation { + private final Class subscriberClass; + private final List> subscribedEvents; + + public DomainEventSubscriberInformation( + Class subscriberClass, + List> subscribedEvents + ) { + this.subscriberClass = subscriberClass; + this.subscribedEvents = subscribedEvents; + } + + public Class subscriberClass() { + return subscriberClass; + } + + public String contextName() { + String[] nameParts = subscriberClass.getName().split("\\."); + + return nameParts[2]; + } + + public String moduleName() { + String[] nameParts = subscriberClass.getName().split("\\."); + + return nameParts[3]; + } + + public String className() { + String[] nameParts = subscriberClass.getName().split("\\."); + + return nameParts[nameParts.length - 1]; + } + + public List> subscribedEvents() { + return subscribedEvents; + } + + public String formatRabbitMqQueueName() { + return String.format("codely.%s.%s.%s", contextName(), moduleName(), Utils.toSnake(className())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java new file mode 100644 index 0000000..88556f1 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java @@ -0,0 +1,54 @@ +package tv.codely.shared.infrastructure.bus.event; + +import org.reflections.Reflections; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEventSubscriber; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Set; + +@Service +public final class DomainEventSubscribersInformation { + HashMap, DomainEventSubscriberInformation> information; + + public DomainEventSubscribersInformation(HashMap, DomainEventSubscriberInformation> information) { + this.information = information; + } + + public DomainEventSubscribersInformation() { + this(scanDomainEventSubscribers()); + } + + private static HashMap, DomainEventSubscriberInformation> scanDomainEventSubscribers() { + Reflections reflections = new Reflections("tv.codely"); + Set> subscribers = reflections.getTypesAnnotatedWith(DomainEventSubscriber.class); + + HashMap, DomainEventSubscriberInformation> subscribersInformation = new HashMap<>(); + + for (Class subscriberClass : subscribers) { + DomainEventSubscriber annotation = subscriberClass.getAnnotation(DomainEventSubscriber.class); + + subscribersInformation.put( + subscriberClass, + new DomainEventSubscriberInformation(subscriberClass, Arrays.asList(annotation.value())) + ); + } + + return subscribersInformation; + } + + public Collection all() { + return information.values(); + } + + public String[] rabbitMqFormattedNamesFor(String contextName) { + return information.values() + .stream() + .map(DomainEventSubscriberInformation::formatRabbitMqQueueName) + .distinct() + .filter(queueName -> queueName.contains("." + contextName + ".")) + .toArray(String[]::new); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventsInformation.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventsInformation.java new file mode 100644 index 0000000..9498913 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventsInformation.java @@ -0,0 +1,53 @@ +package tv.codely.shared.infrastructure.bus.event; + +import org.reflections.Reflections; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEvent; + +import java.lang.reflect.InvocationTargetException; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +@Service +public final class DomainEventsInformation { + HashMap> indexedDomainEvents; + + public DomainEventsInformation() { + Reflections reflections = new Reflections("tv.codely"); + Set> classes = reflections.getSubTypesOf(DomainEvent.class); + + try { + indexedDomainEvents = formatEvents(classes); + } catch (NoSuchMethodException | IllegalAccessException | InstantiationException | InvocationTargetException e) { + e.printStackTrace(); + } + } + + public Class forName(String name) { + return indexedDomainEvents.get(name); + } + + public String forClass(Class domainEventClass) { + return indexedDomainEvents.entrySet() + .stream() + .filter(entry -> Objects.equals(entry.getValue(), domainEventClass)) + .map(Map.Entry::getKey) + .findFirst().orElse(""); + } + + private HashMap> formatEvents( + Set> domainEvents + ) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + HashMap> events = new HashMap<>(); + + for (Class domainEvent : domainEvents) { + DomainEvent nullInstance = domainEvent.getConstructor().newInstance(); + + events.put((String) domainEvent.getMethod("eventName").invoke(nullInstance), domainEvent); + } + + return events; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlDomainEventsConsumer.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlDomainEventsConsumer.java new file mode 100644 index 0000000..263da44 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlDomainEventsConsumer.java @@ -0,0 +1,95 @@ +package tv.codely.shared.infrastructure.bus.event.mysql; + +import jakarta.transaction.Transactional; +import org.hibernate.SessionFactory; +import org.hibernate.query.NativeQuery; +import org.springframework.beans.factory.annotation.Qualifier; +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.infrastructure.bus.event.DomainEventsInformation; +import tv.codely.shared.infrastructure.bus.event.spring.SpringApplicationEventBus; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.sql.Timestamp; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; + +public class MySqlDomainEventsConsumer { + private final SessionFactory sessionFactory; + private final DomainEventsInformation domainEventsInformation; + private final SpringApplicationEventBus bus; + private final Integer CHUNKS = 200; + private Boolean shouldStop = false; + + public MySqlDomainEventsConsumer( + @Qualifier("mooc-session_factory") SessionFactory sessionFactory, + DomainEventsInformation domainEventsInformation, + SpringApplicationEventBus bus + ) { + this.sessionFactory = sessionFactory; + this.domainEventsInformation = domainEventsInformation; + this.bus = bus; + } + + @Transactional + public void consume() { + while (!shouldStop) { + NativeQuery query = sessionFactory.getCurrentSession().createNativeQuery( + "SELECT * FROM domain_events ORDER BY occurred_on ASC LIMIT :chunk" + ); + + query.setParameter("chunk", CHUNKS); + + List events = query.list(); + + try { + for (Object[] event : events) { + executeSubscribers( + (String) event[0], + (String) event[1], + (String) event[2], + (String) event[3], + (Timestamp) event[4] + ); + } + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException | InstantiationException e) { + e.printStackTrace(); + } + + sessionFactory.getCurrentSession().clear(); + } + } + + public void stop() { + shouldStop = true; + } + + private void executeSubscribers( + String id, String aggregateId, String eventName, String body, Timestamp occurredOn + ) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { + + Class domainEventClass = domainEventsInformation.forName(eventName); + + DomainEvent nullInstance = domainEventClass.getConstructor().newInstance(); + + Method fromPrimitivesMethod = domainEventClass.getMethod( + "fromPrimitives", + String.class, + HashMap.class, + String.class, + String.class + ); + + Object domainEvent = fromPrimitivesMethod.invoke( + nullInstance, + aggregateId, + Utils.jsonDecode(body), + id, + Utils.dateToString(occurredOn) + ); + + bus.publish(Collections.singletonList((DomainEvent) domainEvent)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlEventBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlEventBus.java new file mode 100644 index 0000000..231ab21 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlEventBus.java @@ -0,0 +1,45 @@ +package tv.codely.shared.infrastructure.bus.event.mysql; + +import org.hibernate.SessionFactory; +import org.hibernate.query.NativeQuery; +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.domain.bus.event.EventBus; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.List; + +public final class MySqlEventBus implements EventBus { + private final SessionFactory sessionFactory; + + public MySqlEventBus(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + } + + @Override + public void publish(List events) { + events.forEach(this::publish); + } + + private void publish(DomainEvent domainEvent) { + String id = domainEvent.eventId(); + String aggregateId = domainEvent.aggregateId(); + String name = domainEvent.eventName(); + HashMap body = domainEvent.toPrimitives(); + String occurredOn = domainEvent.occurredOn(); + + NativeQuery query = sessionFactory.getCurrentSession().createNativeQuery( + "INSERT INTO domain_events (id, aggregate_id, name, body, occurred_on) " + + "VALUES (:id, :aggregateId, :name, :body, :occurredOn);" + ); + + query.setParameter("id", id) + .setParameter("aggregateId", aggregateId) + .setParameter("name", name) + .setParameter("body", Utils.jsonEncode(body)) + .setParameter("occurredOn", occurredOn); + + query.executeUpdate(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java new file mode 100644 index 0000000..1311278 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java @@ -0,0 +1,134 @@ +package tv.codely.shared.infrastructure.bus.event.rabbitmq; + +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessageBuilder; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.amqp.rabbit.listener.AbstractMessageListenerContainer; +import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry; +import org.springframework.context.ApplicationContext; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.infrastructure.bus.event.DomainEventJsonDeserializer; +import tv.codely.shared.infrastructure.bus.event.DomainEventSubscribersInformation; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +@Service +public final class RabbitMqDomainEventsConsumer { + private final String CONSUMER_NAME = "domain_events_consumer"; + private final int MAX_RETRIES = 10; + private final DomainEventJsonDeserializer deserializer; + private final ApplicationContext context; + private final RabbitMqPublisher publisher; + private final HashMap domainEventSubscribers = new HashMap<>(); + RabbitListenerEndpointRegistry registry; + private DomainEventSubscribersInformation information; + private String contextName; + + public RabbitMqDomainEventsConsumer( + RabbitListenerEndpointRegistry registry, + DomainEventSubscribersInformation information, + DomainEventJsonDeserializer deserializer, + ApplicationContext context, + RabbitMqPublisher publisher + ) { + this.registry = registry; + this.information = information; + this.deserializer = deserializer; + this.context = context; + this.publisher = publisher; + } + + public void consume(String contextName) { + this.contextName = contextName; + + AbstractMessageListenerContainer container = (AbstractMessageListenerContainer) registry.getListenerContainer( + CONSUMER_NAME + ); + + container.addQueueNames(information.rabbitMqFormattedNamesFor(contextName)); + + container.start(); + } + + @RabbitListener(id = CONSUMER_NAME, autoStartup = "false") + public void consumer(Message message) throws Exception { + String serializedMessage = new String(message.getBody()); + DomainEvent domainEvent = deserializer.deserialize(serializedMessage); + + String queue = message.getMessageProperties().getConsumerQueue(); + + Object subscriber = domainEventSubscribers.containsKey(queue) + ? domainEventSubscribers.get(queue) + : subscriberFor(queue); + + Method subscriberOnMethod = subscriber.getClass().getMethod("on", domainEvent.getClass()); + + try { + subscriberOnMethod.invoke(subscriber, domainEvent); + } catch (Exception error) { + handleConsumptionError(message, queue); + } + } + + public void withSubscribersInformation(DomainEventSubscribersInformation information) { + this.information = information; + } + + private void handleConsumptionError(Message message, String queue) { + if (hasBeenRedeliveredTooMuch(message)) { + sendToDeadLetter(message, queue); + } else { + sendToRetry(message, queue); + } + } + + private void sendToRetry(Message message, String queue) { + System.out.println("SENDING TO RETRY: " + contextName + " - " + queue); + + sendMessageTo(RabbitMqExchangeNameFormatter.retry("domain_events"), message, queue); + } + + private void sendToDeadLetter(Message message, String queue) { + System.out.println("SENDING TO DEAD LETTER: " + contextName + " - " + queue); + + sendMessageTo(RabbitMqExchangeNameFormatter.deadLetter("domain_events"), message, queue); + } + + private void sendMessageTo(String exchange, Message message, String queue) { + Map headers = message.getMessageProperties().getHeaders(); + + headers.put("redelivery_count", (int) headers.getOrDefault("redelivery_count", 0) + 1); + + MessageBuilder.fromMessage(message).andProperties( + MessagePropertiesBuilder.newInstance() + .setContentEncoding("utf-8") + .setContentType("application/json") + .copyHeaders(headers) + .build()); + + publisher.publish(message, exchange, queue); + } + + private boolean hasBeenRedeliveredTooMuch(Message message) { + return (int) message.getMessageProperties().getHeaders().getOrDefault("redelivery_count", 0) >= MAX_RETRIES; + } + + private Object subscriberFor(String queue) throws Exception { + String[] queueParts = queue.split("\\."); + String subscriberName = Utils.toCamelFirstLower(queueParts[queueParts.length - 1]); + + try { + Object subscriber = context.getBean(subscriberName); + domainEventSubscribers.put(queue, subscriber); + + return subscriber; + } catch (Exception e) { + throw new Exception(String.format("There are not registered subscribers for <%s> queue", queue)); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java new file mode 100644 index 0000000..2bad6fd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java @@ -0,0 +1,38 @@ +package tv.codely.shared.infrastructure.bus.event.rabbitmq; + +import org.springframework.amqp.AmqpException; +import org.springframework.context.annotation.Primary; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.domain.bus.event.EventBus; +import tv.codely.shared.infrastructure.bus.event.mysql.MySqlEventBus; + +import java.util.Collections; +import java.util.List; + +@Primary +@Service +public class RabbitMqEventBus implements EventBus { + private final RabbitMqPublisher publisher; + private final MySqlEventBus failoverPublisher; + private final String exchangeName; + + public RabbitMqEventBus(RabbitMqPublisher publisher, MySqlEventBus failoverPublisher) { + this.publisher = publisher; + this.failoverPublisher = failoverPublisher; + this.exchangeName = "domain_events"; + } + + @Override + public void publish(List events) { + events.forEach(this::publish); + } + + private void publish(DomainEvent domainEvent) { + try { + this.publisher.publish(domainEvent, exchangeName); + } catch (AmqpException error) { + failoverPublisher.publish(Collections.singletonList(domainEvent)); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java new file mode 100644 index 0000000..f95e188 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java @@ -0,0 +1,134 @@ +package tv.codely.shared.infrastructure.bus.event.rabbitmq; + +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.connection.CachingConnectionFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tv.codely.shared.infrastructure.bus.event.DomainEventSubscribersInformation; +import tv.codely.shared.infrastructure.bus.event.DomainEventsInformation; +import tv.codely.shared.infrastructure.config.Parameter; +import tv.codely.shared.infrastructure.config.ParameterNotExist; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.stream.Collectors; + +@Configuration +public class RabbitMqEventBusConfiguration { + private final DomainEventSubscribersInformation domainEventSubscribersInformation; + private final DomainEventsInformation domainEventsInformation; + private final Parameter config; + private final String exchangeName; + + public RabbitMqEventBusConfiguration( + DomainEventSubscribersInformation domainEventSubscribersInformation, + DomainEventsInformation domainEventsInformation, + Parameter config + ) throws ParameterNotExist { + this.domainEventSubscribersInformation = domainEventSubscribersInformation; + this.domainEventsInformation = domainEventsInformation; + this.config = config; + this.exchangeName = config.get("RABBITMQ_EXCHANGE"); + } + + @Bean + public CachingConnectionFactory connection() throws ParameterNotExist { + CachingConnectionFactory factory = new CachingConnectionFactory(); + + factory.setHost(config.get("RABBITMQ_HOST")); + factory.setPort(config.getInt("RABBITMQ_PORT")); + factory.setUsername(config.get("RABBITMQ_LOGIN")); + factory.setPassword(config.get("RABBITMQ_PASSWORD")); + + return factory; + } + + @Bean + public Declarables declaration() { + String retryExchangeName = RabbitMqExchangeNameFormatter.retry(exchangeName); + String deadLetterExchangeName = RabbitMqExchangeNameFormatter.deadLetter(exchangeName); + + TopicExchange domainEventsExchange = new TopicExchange(exchangeName, true, false); + TopicExchange retryDomainEventsExchange = new TopicExchange(retryExchangeName, true, false); + TopicExchange deadLetterDomainEventsExchange = new TopicExchange(deadLetterExchangeName, true, false); + List declarables = new ArrayList<>(); + declarables.add(domainEventsExchange); + declarables.add(retryDomainEventsExchange); + declarables.add(deadLetterDomainEventsExchange); + + Collection queuesAndBindings = declareQueuesAndBindings( + domainEventsExchange, + retryDomainEventsExchange, + deadLetterDomainEventsExchange + ).stream().flatMap(Collection::stream).collect(Collectors.toList()); + + declarables.addAll(queuesAndBindings); + + return new Declarables(declarables); + } + + private Collection> declareQueuesAndBindings( + TopicExchange domainEventsExchange, + TopicExchange retryDomainEventsExchange, + TopicExchange deadLetterDomainEventsExchange + ) { + return domainEventSubscribersInformation.all().stream().map(information -> { + String queueName = RabbitMqQueueNameFormatter.format(information); + String retryQueueName = RabbitMqQueueNameFormatter.formatRetry(information); + String deadLetterQueueName = RabbitMqQueueNameFormatter.formatDeadLetter(information); + + Queue queue = QueueBuilder.durable(queueName).build(); + Queue retryQueue = QueueBuilder.durable(retryQueueName).withArguments( + retryQueueArguments(domainEventsExchange, queueName) + ).build(); + Queue deadLetterQueue = QueueBuilder.durable(deadLetterQueueName).build(); + + Binding fromExchangeSameQueueBinding = BindingBuilder + .bind(queue) + .to(domainEventsExchange) + .with(queueName); + + Binding fromRetryExchangeSameQueueBinding = BindingBuilder + .bind(retryQueue) + .to(retryDomainEventsExchange) + .with(queueName); + + Binding fromDeadLetterExchangeSameQueueBinding = BindingBuilder + .bind(deadLetterQueue) + .to(deadLetterDomainEventsExchange) + .with(queueName); + + List fromExchangeDomainEventsBindings = information.subscribedEvents().stream().map( + domainEventClass -> { + String eventName = domainEventsInformation.forClass(domainEventClass); + return BindingBuilder + .bind(queue) + .to(domainEventsExchange) + .with(eventName); + }).collect(Collectors.toList()); + + List queuesAndBindings = new ArrayList<>(); + queuesAndBindings.add(queue); + queuesAndBindings.add(fromExchangeSameQueueBinding); + queuesAndBindings.addAll(fromExchangeDomainEventsBindings); + + queuesAndBindings.add(retryQueue); + queuesAndBindings.add(fromRetryExchangeSameQueueBinding); + + queuesAndBindings.add(deadLetterQueue); + queuesAndBindings.add(fromDeadLetterExchangeSameQueueBinding); + + return queuesAndBindings; + }).collect(Collectors.toList()); + } + + private HashMap retryQueueArguments(TopicExchange exchange, String routingKey) { + return new HashMap() {{ + put("x-dead-letter-exchange", exchange.getName()); + put("x-dead-letter-routing-key", routingKey); + put("x-message-ttl", 3000); + }}; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqExchangeNameFormatter.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqExchangeNameFormatter.java new file mode 100644 index 0000000..f399cac --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqExchangeNameFormatter.java @@ -0,0 +1,11 @@ +package tv.codely.shared.infrastructure.bus.event.rabbitmq; + +public final class RabbitMqExchangeNameFormatter { + public static String retry(String exchangeName) { + return String.format("retry-%s", exchangeName); + } + + public static String deadLetter(String exchangeName) { + return String.format("dead_letter-%s", exchangeName); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqPublisher.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqPublisher.java new file mode 100644 index 0000000..61f7fe4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqPublisher.java @@ -0,0 +1,36 @@ +package tv.codely.shared.infrastructure.bus.event.rabbitmq; + +import org.springframework.amqp.AmqpException; +import org.springframework.amqp.core.Message; +import org.springframework.amqp.core.MessagePropertiesBuilder; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.infrastructure.bus.event.DomainEventJsonSerializer; + +@Service +public final class RabbitMqPublisher { + private final RabbitTemplate rabbitTemplate; + + public RabbitMqPublisher(RabbitTemplate rabbitTemplate) { + this.rabbitTemplate = rabbitTemplate; + } + + public void publish(DomainEvent domainEvent, String exchangeName) throws AmqpException { + String serializedDomainEvent = DomainEventJsonSerializer.serialize(domainEvent); + + Message message = new Message( + serializedDomainEvent.getBytes(), + MessagePropertiesBuilder.newInstance() + .setContentEncoding("utf-8") + .setContentType("application/json") + .build() + ); + + rabbitTemplate.send(exchangeName, domainEvent.eventName(), message); + } + + public void publish(Message domainEvent, String exchangeName, String routingKey) throws AmqpException { + rabbitTemplate.send(exchangeName, routingKey, domainEvent); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqQueueNameFormatter.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqQueueNameFormatter.java new file mode 100644 index 0000000..13c091f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqQueueNameFormatter.java @@ -0,0 +1,17 @@ +package tv.codely.shared.infrastructure.bus.event.rabbitmq; + +import tv.codely.shared.infrastructure.bus.event.DomainEventSubscriberInformation; + +public final class RabbitMqQueueNameFormatter { + public static String format(DomainEventSubscriberInformation information) { + return information.formatRabbitMqQueueName(); + } + + public static String formatRetry(DomainEventSubscriberInformation information) { + return String.format("retry.%s", format(information)); + } + + public static String formatDeadLetter(DomainEventSubscriberInformation information) { + return String.format("dead_letter.%s", format(information)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java new file mode 100644 index 0000000..56a6205 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java @@ -0,0 +1,26 @@ +package tv.codely.shared.infrastructure.bus.event.spring; + +import org.springframework.context.ApplicationEventPublisher; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.domain.bus.event.EventBus; + +import java.util.List; + +@Service +public class SpringApplicationEventBus implements EventBus { + private final ApplicationEventPublisher publisher; + + public SpringApplicationEventBus(ApplicationEventPublisher publisher) { + this.publisher = publisher; + } + + @Override + public void publish(final List events) { + events.forEach(this::publish); + } + + private void publish(final DomainEvent event) { + this.publisher.publishEvent(event); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/query/InMemoryQueryBus.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/query/InMemoryQueryBus.java new file mode 100644 index 0000000..8d448c6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/query/InMemoryQueryBus.java @@ -0,0 +1,29 @@ +package tv.codely.shared.infrastructure.bus.query; + +import org.springframework.context.ApplicationContext; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.*; + +@Service +public final class InMemoryQueryBus implements QueryBus { + private final QueryHandlersInformation information; + private final ApplicationContext context; + + public InMemoryQueryBus(QueryHandlersInformation information, ApplicationContext context) { + this.information = information; + this.context = context; + } + + @Override + public Response ask(Query query) throws QueryHandlerExecutionError { + try { + Class queryHandlerClass = information.search(query.getClass()); + + QueryHandler handler = context.getBean(queryHandlerClass); + + return handler.handle(query); + } catch (Throwable error) { + throw new QueryHandlerExecutionError(error); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/query/QueryHandlersInformation.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/query/QueryHandlersInformation.java new file mode 100644 index 0000000..e9d7606 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/bus/query/QueryHandlersInformation.java @@ -0,0 +1,48 @@ +package tv.codely.shared.infrastructure.bus.query; + +import org.reflections.Reflections; +import tv.codely.shared.domain.Service; +import tv.codely.shared.domain.bus.query.Query; +import tv.codely.shared.domain.bus.query.QueryHandler; +import tv.codely.shared.domain.bus.query.QueryNotRegisteredError; + +import java.lang.reflect.ParameterizedType; +import java.util.HashMap; +import java.util.Set; + +@Service +public final class QueryHandlersInformation { + HashMap, Class> indexedQueryHandlers; + + public QueryHandlersInformation() { + Reflections reflections = new Reflections("tv.codely"); + Set> classes = reflections.getSubTypesOf(QueryHandler.class); + + indexedQueryHandlers = formatHandlers(classes); + } + + public Class search(Class queryClass) throws QueryNotRegisteredError { + Class queryHandlerClass = indexedQueryHandlers.get(queryClass); + + if (null == queryHandlerClass) { + throw new QueryNotRegisteredError(queryClass); + } + + return queryHandlerClass; + } + + private HashMap, Class> formatHandlers( + Set> queryHandlers + ) { + HashMap, Class> handlers = new HashMap<>(); + + for (Class handler : queryHandlers) { + ParameterizedType paramType = (ParameterizedType) handler.getGenericInterfaces()[0]; + Class queryClass = (Class) paramType.getActualTypeArguments()[0]; + + handlers.put(queryClass, handler); + } + + return handlers; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java new file mode 100644 index 0000000..dd90a88 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java @@ -0,0 +1,25 @@ +package tv.codely.shared.infrastructure.cli; + +import tv.codely.shared.domain.Service; + +@Service +public abstract class ConsoleCommand { + private static final String ANSI_RESET = "\u001B[0m"; + private static final String ANSI_RED = "\u001B[31m"; + private static final String ANSI_CYAN = "\u001B[36m"; + private static final String ANSI_GREEN = "\u001B[32m"; + + abstract public void execute(String[] args); + + protected void log(String text) { + System.out.println(String.format("%s%s%s", ANSI_GREEN, text, ANSI_RESET)); + } + + protected void info(String text) { + System.out.println(String.format("%s%s%s", ANSI_CYAN, text, ANSI_RESET)); + } + + protected void error(String text) { + System.out.println(String.format("%s%s%s", ANSI_RED, text, ANSI_RESET)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/config/EnvironmentConfig.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/config/EnvironmentConfig.java new file mode 100644 index 0000000..c9d898f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/config/EnvironmentConfig.java @@ -0,0 +1,27 @@ +package tv.codely.shared.infrastructure.config; + +import io.github.cdimascio.dotenv.Dotenv; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +@Configuration +public class EnvironmentConfig { + ResourceLoader resourceLoader; + + public EnvironmentConfig(ResourceLoader resourceLoader) { + this.resourceLoader = resourceLoader; + } + + @Bean + public Dotenv dotenv() { + Resource resource = resourceLoader.getResource("classpath:/.env.local"); + + return Dotenv + .configure() + .directory("/") + .filename(resource.exists() ? ".env.local" : ".env") + .load(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/config/Parameter.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/config/Parameter.java new file mode 100644 index 0000000..ac521bb --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/config/Parameter.java @@ -0,0 +1,29 @@ +package tv.codely.shared.infrastructure.config; + +import io.github.cdimascio.dotenv.Dotenv; +import tv.codely.shared.domain.Service; + +@Service +public final class Parameter { + private final Dotenv dotenv; + + public Parameter(Dotenv dotenv) { + this.dotenv = dotenv; + } + + public String get(String key) throws ParameterNotExist { + String value = dotenv.get(key); + + if (null == value) { + throw new ParameterNotExist(key); + } + + return value; + } + + public Integer getInt(String key) throws ParameterNotExist { + String value = get(key); + + return Integer.parseInt(value); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/config/ParameterNotExist.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/config/ParameterNotExist.java new file mode 100644 index 0000000..aa329d1 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/config/ParameterNotExist.java @@ -0,0 +1,7 @@ +package tv.codely.shared.infrastructure.config; + +public final class ParameterNotExist extends Throwable { + public ParameterNotExist(String key) { + super(String.format("The parameter <%s> does not exist in the environment file", key)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchClient.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchClient.java new file mode 100644 index 0000000..78a458f --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchClient.java @@ -0,0 +1,49 @@ +package tv.codely.shared.infrastructure.elasticsearch; + +import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestClient; +import org.elasticsearch.client.RestHighLevelClient; + +import java.io.IOException; +import java.io.Serializable; +import java.util.HashMap; + +public final class ElasticsearchClient { + private final RestHighLevelClient highLevelClient; + private final RestClient lowLevelClient; + private final String indexPrefix; + + public ElasticsearchClient(RestHighLevelClient highLevelClient, RestClient lowLevelClient, String indexPrefix) { + this.highLevelClient = highLevelClient; + this.lowLevelClient = lowLevelClient; + this.indexPrefix = indexPrefix; + } + + public RestHighLevelClient highLevelClient() { + return highLevelClient; + } + + public RestClient lowLevelClient() { + return lowLevelClient; + } + + public String indexPrefix() { + return indexPrefix; + } + + public void persist(String moduleName, String id, HashMap plainBody) throws IOException { + IndexRequest request = new IndexRequest(indexFor(moduleName), moduleName, id).source(plainBody); + + highLevelClient().index(request, RequestOptions.DEFAULT); + } + + public String indexFor(String moduleName) { + return String.format("%s_%s", indexPrefix(), moduleName); + } + + public void delete(String index) throws IOException { + highLevelClient.indices().delete(new DeleteIndexRequest(index), RequestOptions.DEFAULT); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchCriteriaConverter.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchCriteriaConverter.java new file mode 100644 index 0000000..99e2585 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchCriteriaConverter.java @@ -0,0 +1,92 @@ +package tv.codely.shared.infrastructure.elasticsearch; + +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import org.elasticsearch.search.sort.SortOrder; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.domain.criteria.Filter; +import tv.codely.shared.domain.criteria.FilterOperator; +import tv.codely.shared.domain.criteria.Filters; + +import java.util.HashMap; +import java.util.function.Function; + +public final class ElasticsearchCriteriaConverter { + private final HashMap> queryTransformers = new HashMap>() {{ + put(FilterOperator.EQUAL, ElasticsearchCriteriaConverter.this::termQuery); + put(FilterOperator.NOT_EQUAL, ElasticsearchCriteriaConverter.this::termQuery); + put(FilterOperator.GT, ElasticsearchCriteriaConverter.this::greaterThanQueryTransformer); + put(FilterOperator.LT, ElasticsearchCriteriaConverter.this::lowerThanQueryTransformer); + put(FilterOperator.CONTAINS, ElasticsearchCriteriaConverter.this::wildcardTransformer); + put(FilterOperator.NOT_CONTAINS, ElasticsearchCriteriaConverter.this::wildcardTransformer); + }}; + + public SearchSourceBuilder convert(Criteria criteria) { + SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); + + sourceBuilder.from(criteria.offset().orElse(0)); + sourceBuilder.size(criteria.limit().orElse(1000)); + + if (criteria.order().hasOrder()) { + sourceBuilder.sort( + criteria.order().orderBy().value(), + SortOrder.fromString(criteria.order().orderType().value()) + ); + } + + if (criteria.hasFilters()) { + QueryBuilder queryBuilder = generateQueryBuilder(criteria.filters()); + + sourceBuilder.query(queryBuilder); + } + + return sourceBuilder; + } + + private QueryBuilder generateQueryBuilder(Filters filters) { + BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); + + for (Filter filter : filters.filters()) { + QueryBuilder query = queryForFilter(filter); + + if (isPositiveOperator(filter.operator())) { + boolQueryBuilder.must(query); + } else { + boolQueryBuilder.mustNot(query); + } + } + + return boolQueryBuilder; + } + + private boolean isPositiveOperator(FilterOperator operator) { + return operator.isPositive(); + } + + private QueryBuilder queryForFilter(Filter filter) { + Function transformer = queryTransformers.get(filter.operator()); + + return transformer.apply(filter); + } + + private QueryBuilder termQuery(Filter filter) { + return QueryBuilders.termQuery(filter.field().value(), filter.value().value().toLowerCase()); + } + + private QueryBuilder greaterThanQueryTransformer(Filter filter) { + return QueryBuilders.rangeQuery(filter.field().value()).gt(filter.value().value().toLowerCase()); + } + + private QueryBuilder lowerThanQueryTransformer(Filter filter) { + return QueryBuilders.rangeQuery(filter.field().value()).lt(filter.value().value().toLowerCase()); + } + + private QueryBuilder wildcardTransformer(Filter filter) { + return QueryBuilders.wildcardQuery( + filter.field().value(), + String.format("*%s*", filter.value().value().toLowerCase()) + ); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java new file mode 100644 index 0000000..c87903b --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java @@ -0,0 +1,79 @@ +package tv.codely.shared.infrastructure.elasticsearch; + +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.search.builder.SearchSourceBuilder; +import tv.codely.shared.domain.criteria.Criteria; + +import java.io.IOException; +import java.io.Serializable; +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +public abstract class ElasticsearchRepository { + private final ElasticsearchClient client; + private final ElasticsearchCriteriaConverter criteriaConverter; + + public ElasticsearchRepository(ElasticsearchClient client) { + this.client = client; + this.criteriaConverter = new ElasticsearchCriteriaConverter(); + } + + abstract protected String moduleName(); + + protected List searchAllInElastic(Function, T> unserializer) { + return searchAllInElastic(unserializer, new SearchSourceBuilder()); + } + + protected Optional searchById(String id, Function, T> unserializer) { + GetRequest request = new GetRequest(client.indexFor(moduleName()), "_doc", id); + + try { + GetResponse getResponse = client.highLevelClient().get(request, RequestOptions.DEFAULT); + + if (!getResponse.isExists()) { + return Optional.empty(); + } + + return Optional.of(unserializer.apply(getResponse.getSourceAsMap())); + } catch (IOException e) { + e.printStackTrace(); + return Optional.empty(); + } + } + + + protected List searchAllInElastic( + Function, T> unserializer, + SearchSourceBuilder sourceBuilder + ) { + SearchRequest request = new SearchRequest(client.indexFor(moduleName())).source(sourceBuilder); + try { + SearchResponse response = client.highLevelClient().search(request, RequestOptions.DEFAULT); + + return Arrays.stream(response.getHits().getHits()) + .map(hit -> unserializer.apply(hit.getSourceAsMap())) + .collect(Collectors.toList()); + } catch (IOException e) { + e.printStackTrace(); + } + + return Collections.emptyList(); + } + + protected List searchByCriteria(Criteria criteria, Function, T> unserializer) { + return searchAllInElastic(unserializer, criteriaConverter.convert(criteria)); + } + + protected void persist(String id, HashMap plainBody) { + try { + client.persist(moduleName(), id, plainBody); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateConfigurationFactory.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateConfigurationFactory.java new file mode 100644 index 0000000..2dbd1ba --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateConfigurationFactory.java @@ -0,0 +1,137 @@ +package tv.codely.shared.infrastructure.hibernate; + +import org.apache.tomcat.dbcp.dbcp2.BasicDataSource; +import org.hibernate.cfg.AvailableSettings; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.core.io.support.ResourcePatternResolver; +import org.springframework.orm.hibernate5.HibernateTransactionManager; +import org.springframework.orm.hibernate5.LocalSessionFactoryBean; +import org.springframework.transaction.PlatformTransactionManager; +import tv.codely.shared.domain.Service; + +import javax.sql.DataSource; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.stream.Collectors; + +@Service +public final class HibernateConfigurationFactory { + private final ResourcePatternResolver resourceResolver; + + public HibernateConfigurationFactory(ResourcePatternResolver resourceResolver) { + this.resourceResolver = resourceResolver; + } + + public PlatformTransactionManager hibernateTransactionManager(LocalSessionFactoryBean sessionFactory) { + HibernateTransactionManager transactionManager = new HibernateTransactionManager(); + transactionManager.setSessionFactory(sessionFactory.getObject()); + + return transactionManager; + } + + public LocalSessionFactoryBean sessionFactory(String contextName, DataSource dataSource) { + LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean(); + sessionFactory.setDataSource(dataSource); + sessionFactory.setHibernateProperties(hibernateProperties()); + + List mappingFiles = searchMappingFiles(contextName); + + sessionFactory.setMappingLocations(mappingFiles.toArray(new Resource[mappingFiles.size()])); + + return sessionFactory; + } + + public DataSource dataSource( + String host, + Integer port, + String databaseName, + String username, + String password + ) throws IOException { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); + dataSource.setUrl( + String.format( + "jdbc:mysql://%s:%s/%s?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC", + host, + port, + databaseName + ) + ); + dataSource.setUsername(username); + dataSource.setPassword(password); + + Resource mysqlResource = resourceResolver.getResource(String.format( + "classpath:database/%s.sql", + databaseName + )); + String mysqlSentences = new Scanner(mysqlResource.getInputStream(), StandardCharsets.UTF_8).useDelimiter("\\A").next(); + + dataSource.setConnectionInitSqls(new ArrayList<>(Arrays.asList(mysqlSentences.split(";")))); + + return dataSource; + } + + private List searchMappingFiles(String contextName) { + List modules = subdirectoriesFor(contextName); + List goodPaths = new ArrayList<>(); + + for (String module : modules) { + String[] files = mappingFilesIn(module + "/infrastructure/persistence/hibernate/"); + + for (String file : files) { + goodPaths.add(module + "/infrastructure/persistence/hibernate/" + file); + } + } + + return goodPaths.stream().map(FileSystemResource::new).collect(Collectors.toList()); + } + + private List subdirectoriesFor(String contextName) { + String path = "./src/" + contextName + "/main/tv/codely/" + contextName + "/"; + + String[] files = new File(path).list((current, name) -> new File(current, name).isDirectory()); + + if (null == files) { + path = "./main/tv/codely/" + contextName + "/"; + files = new File(path).list((current, name) -> new File(current, name).isDirectory()); + } + + if (null == files) { + return Collections.emptyList(); + } + + String finalPath = path; + + return Arrays.stream(files).map(file -> finalPath + file).collect(Collectors.toList()); + } + + private String[] mappingFilesIn(String path) { + List fileList = new ArrayList<>(); + + String[] hbmFiles = new File(path).list((current, name) -> new File(current, name).getName().contains(".hbm.xml")); + String[] ormFiles = new File(path).list((current, name) -> new File(current, name).getName().contains(".orm.xml")); + + if (hbmFiles != null) { + fileList.addAll(Arrays.asList(hbmFiles)); + } + if (ormFiles != null) { + fileList.addAll(Arrays.asList(ormFiles)); + } + + return fileList.toArray(new String[0]); + } + + private Properties hibernateProperties() { + Properties hibernateProperties = new Properties(); + hibernateProperties.put(AvailableSettings.HBM2DDL_AUTO, "none"); + hibernateProperties.put(AvailableSettings.SHOW_SQL, "false"); + hibernateProperties.put(AvailableSettings.DIALECT, "org.hibernate.dialect.MySQLDialect"); + hibernateProperties.put(AvailableSettings.TRANSFORM_HBM_XML, true); + + return hibernateProperties; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateCriteriaConverter.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateCriteriaConverter.java new file mode 100644 index 0000000..5679bbd --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateCriteriaConverter.java @@ -0,0 +1,85 @@ +package tv.codely.shared.infrastructure.hibernate; + +import jakarta.persistence.criteria.*; +import tv.codely.shared.domain.criteria.Criteria; +import tv.codely.shared.domain.criteria.Filter; +import tv.codely.shared.domain.criteria.FilterOperator; + +import java.util.HashMap; +import java.util.List; +import java.util.function.BiFunction; +import java.util.stream.Collectors; + +public final class HibernateCriteriaConverter { + private final CriteriaBuilder builder; + private final HashMap, Predicate>> predicateTransformers = new HashMap, Predicate>>() {{ + put(FilterOperator.EQUAL, HibernateCriteriaConverter.this::equalsPredicateTransformer); + put(FilterOperator.NOT_EQUAL, HibernateCriteriaConverter.this::notEqualsPredicateTransformer); + put(FilterOperator.GT, HibernateCriteriaConverter.this::greaterThanPredicateTransformer); + put(FilterOperator.LT, HibernateCriteriaConverter.this::lowerThanPredicateTransformer); + put(FilterOperator.CONTAINS, HibernateCriteriaConverter.this::containsPredicateTransformer); + put(FilterOperator.NOT_CONTAINS, HibernateCriteriaConverter.this::notContainsPredicateTransformer); + }}; + + public HibernateCriteriaConverter(CriteriaBuilder builder) { + this.builder = builder; + } + + public CriteriaQuery convert(Criteria criteria, Class aggregateClass) { + CriteriaQuery hibernateCriteria = builder.createQuery(aggregateClass); + Root root = hibernateCriteria.from(aggregateClass); + + hibernateCriteria.where(formatPredicates(criteria.filters().filters(), root)); + + if (criteria.order().hasOrder()) { + Path orderBy = root.get(criteria.order().orderBy().value()); + Order order = criteria.order().orderType().isAsc() ? builder.asc(orderBy) : builder.desc(orderBy); + + hibernateCriteria.orderBy(order); + } + + return hibernateCriteria; + } + + private Predicate[] formatPredicates(List filters, Root root) { + List predicates = filters.stream().map(filter -> formatPredicate( + filter, + root + )).collect(Collectors.toList()); + + Predicate[] predicatesArray = new Predicate[predicates.size()]; + predicatesArray = predicates.toArray(predicatesArray); + + return predicatesArray; + } + + private Predicate formatPredicate(Filter filter, Root root) { + BiFunction, Predicate> transformer = predicateTransformers.get(filter.operator()); + + return transformer.apply(filter, root); + } + + private Predicate equalsPredicateTransformer(Filter filter, Root root) { + return builder.equal(root.get(filter.field().value()), filter.value().value()); + } + + private Predicate notEqualsPredicateTransformer(Filter filter, Root root) { + return builder.notEqual(root.get(filter.field().value()), filter.value().value()); + } + + private Predicate greaterThanPredicateTransformer(Filter filter, Root root) { + return builder.greaterThan(root.get(filter.field().value()), filter.value().value()); + } + + private Predicate lowerThanPredicateTransformer(Filter filter, Root root) { + return builder.lessThan(root.get(filter.field().value()), filter.value().value()); + } + + private Predicate containsPredicateTransformer(Filter filter, Root root) { + return builder.like(root.get(filter.field().value()), String.format("%%%s%%", filter.value().value())); + } + + private Predicate notContainsPredicateTransformer(Filter filter, Root root) { + return builder.notLike(root.get(filter.field().value()), String.format("%%%s%%", filter.value().value())); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java new file mode 100644 index 0000000..b6ae61e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java @@ -0,0 +1,49 @@ +package tv.codely.shared.infrastructure.hibernate; + +import jakarta.persistence.criteria.CriteriaQuery; +import org.hibernate.SessionFactory; +import tv.codely.shared.domain.Identifier; +import tv.codely.shared.domain.criteria.Criteria; + +import java.util.List; +import java.util.Optional; + +public abstract class HibernateRepository { + protected final SessionFactory sessionFactory; + protected final Class aggregateClass; + protected final HibernateCriteriaConverter criteriaConverter; + + public HibernateRepository(SessionFactory sessionFactory, Class aggregateClass) { + this.sessionFactory = sessionFactory; + this.aggregateClass = aggregateClass; + this.criteriaConverter = new HibernateCriteriaConverter<>(sessionFactory.getCriteriaBuilder()); + } + + protected void persist(T entity) { + sessionFactory.getCurrentSession().saveOrUpdate(entity); + sessionFactory.getCurrentSession().flush(); + sessionFactory.getCurrentSession().clear(); + } + + protected Optional byId(Identifier id) { + return Optional.ofNullable(sessionFactory.getCurrentSession().byId(aggregateClass).load(id)); + } + + protected Optional byId(String id) { + return Optional.ofNullable(sessionFactory.getCurrentSession().byId(aggregateClass).load(id)); + } + + protected List byCriteria(Criteria criteria) { + CriteriaQuery hibernateCriteria = criteriaConverter.convert(criteria, aggregateClass); + + return sessionFactory.getCurrentSession().createQuery(hibernateCriteria).getResultList(); + } + + protected List all() { + CriteriaQuery criteria = sessionFactory.getCriteriaBuilder().createQuery(aggregateClass); + + criteria.from(aggregateClass); + + return sessionFactory.getCurrentSession().createQuery(criteria).getResultList(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/hibernate/JsonListType.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/hibernate/JsonListType.java new file mode 100644 index 0000000..188dd98 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/hibernate/JsonListType.java @@ -0,0 +1,140 @@ +package tv.codely.shared.infrastructure.hibernate; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.hibernate.HibernateException; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.usertype.DynamicParameterizedType; +import org.hibernate.usertype.UserType; + +import java.io.IOException; +import java.io.Serializable; +import java.io.StringWriter; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.*; + +public class JsonListType implements UserType, DynamicParameterizedType { + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + private JavaType valueType = null; + private Class classType = null; + + @Override + public int getSqlType() { + return Types.LONGVARCHAR; + } + + @Override + public Class returnedClass() { + return classType; + } + + @Override + public boolean equals(Object x, Object y) throws HibernateException { + return Objects.equals(x, y); + } + + @Override + public int hashCode(Object x) throws HibernateException { + return Objects.hashCode(x); + } + + @Override + public void nullSafeSet( + PreparedStatement st, + Object value, + int index, + SharedSessionContractImplementor session + ) throws HibernateException, SQLException { + nullSafeSet(st, value, index); + } + + @Override + public Object nullSafeGet( + ResultSet resultSet, + int index, + SharedSessionContractImplementor session, + Object owner + ) throws HibernateException, SQLException { + + String value = resultSet.getString(index).replace("\"value\"", "").replace("{:", "").replace("}", ""); + Object result = null; + if (valueType == null) { + throw new HibernateException("Value type not set."); + } + if (value != null && !value.equals("")) { + try { + result = OBJECT_MAPPER.readValue(value, valueType); + } catch (IOException e) { + throw new HibernateException("Exception deserializing value " + value, e); + } + } + return result; + } + + public void nullSafeSet(PreparedStatement st, Object value, int index) throws HibernateException, SQLException { + StringWriter sw = new StringWriter(); + OBJECT_MAPPER.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + + if (value == null) { + st.setNull(index, Types.VARCHAR); + } else { + try { + OBJECT_MAPPER.writeValue(sw, value); + st.setString(index, sw.toString()); + } catch (IOException e) { + throw new HibernateException("Exception serializing value " + value, e); + } + } + } + + @Override + public Object deepCopy(Object value) throws HibernateException { + if (value == null) { + return null; + } else if (valueType.isCollectionLikeType()) { + Object newValue = new ArrayList<>(); + Collection newValueCollection = (Collection) newValue; + newValueCollection.addAll((Collection) value); + return newValueCollection; + } + + return null; + } + + @Override + public boolean isMutable() { + return true; + } + + @Override + public Serializable disassemble(Object value) throws HibernateException { + return (Serializable) deepCopy(value); + } + + @Override + public Object assemble(Serializable cached, Object owner) throws HibernateException { + return deepCopy(cached); + } + + @Override + public Object replace(Object original, Object target, Object owner) throws HibernateException { + return deepCopy(original); + } + + @Override + public void setParameterValues(Properties parameters) { + try { + Class entityClass = Class.forName(parameters.getProperty("list_of")); + + valueType = OBJECT_MAPPER.getTypeFactory().constructCollectionType(ArrayList.class, entityClass); + classType = List.class; + } catch (ClassNotFoundException e) { + throw new IllegalArgumentException(e); + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/spring/ApiController.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/spring/ApiController.java new file mode 100644 index 0000000..fafc2a8 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/spring/ApiController.java @@ -0,0 +1,32 @@ +package tv.codely.shared.infrastructure.spring; + +import org.springframework.http.HttpStatus; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.bus.command.Command; +import tv.codely.shared.domain.bus.command.CommandBus; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; +import tv.codely.shared.domain.bus.query.Query; +import tv.codely.shared.domain.bus.query.QueryBus; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; + +import java.util.HashMap; + +public abstract class ApiController { + private final QueryBus queryBus; + private final CommandBus commandBus; + + public ApiController(QueryBus queryBus, CommandBus commandBus) { + this.queryBus = queryBus; + this.commandBus = commandBus; + } + + protected void dispatch(Command command) throws CommandHandlerExecutionError { + commandBus.dispatch(command); + } + + protected R ask(Query query) throws QueryHandlerExecutionError { + return queryBus.ask(query); + } + + abstract public HashMap, HttpStatus> errorMapping(); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/spring/ApiExceptionMiddleware.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/spring/ApiExceptionMiddleware.java new file mode 100644 index 0000000..ac1db97 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/spring/ApiExceptionMiddleware.java @@ -0,0 +1,95 @@ +package tv.codely.shared.infrastructure.spring; + +import org.springframework.http.HttpStatus; +import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; +import org.springframework.web.util.NestedServletException; +import tv.codely.shared.domain.DomainError; +import tv.codely.shared.domain.Utils; +import tv.codely.shared.domain.bus.command.CommandHandlerExecutionError; +import tv.codely.shared.domain.bus.query.QueryHandlerExecutionError; + +import jakarta.servlet.*; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.HashMap; +import java.util.Objects; + +public final class ApiExceptionMiddleware implements Filter { + private RequestMappingHandlerMapping mapping; + + public ApiExceptionMiddleware(RequestMappingHandlerMapping mapping) { + this.mapping = mapping; + } + + @Override + public void doFilter( + ServletRequest request, + ServletResponse response, + FilterChain chain + ) throws ServletException { + HttpServletRequest httpRequest = ((HttpServletRequest) request); + HttpServletResponse httpResponse = ((HttpServletResponse) response); + + try { + Object possibleController = ( + (HandlerMethod) Objects.requireNonNull( + mapping.getHandler(httpRequest)).getHandler() + ).getBean(); + + try { + chain.doFilter(request, response); + } catch (Exception exception) { + if (possibleController instanceof ApiController) { + handleCustomError(response, httpResponse, (ApiController) possibleController, exception); + } + } + } catch (Exception e) { + throw new ServletException(e); + } + } + + private void handleCustomError( + ServletResponse response, + HttpServletResponse httpResponse, + ApiController possibleController, + Exception exception + ) throws IOException { + HashMap, HttpStatus> errorMapping = possibleController + .errorMapping(); + Throwable error = ( + exception.getCause() instanceof CommandHandlerExecutionError || + exception.getCause() instanceof QueryHandlerExecutionError + ) + ? exception.getCause().getCause() : exception.getCause(); + + int statusCode = statusFor(errorMapping, error); + String errorCode = errorCodeFor(error); + String errorMessage = error.getMessage(); + + httpResponse.reset(); + httpResponse.setHeader("Content-Type", "application/json"); + httpResponse.setStatus(statusCode); + PrintWriter writer = response.getWriter(); + writer.write(String.format( + "{\"error_code\": \"%s\", \"message\": \"%s\"}", + errorCode, + errorMessage + )); + writer.close(); + } + + private String errorCodeFor(Throwable error) { + if (error instanceof DomainError) { + return ((DomainError) error).errorCode(); + } + + return Utils.toSnake(error.getClass().toString()); + } + + private int statusFor(HashMap, HttpStatus> errorMapping, Throwable error) { + return errorMapping.getOrDefault(error.getClass(), HttpStatus.INTERNAL_SERVER_ERROR).value(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/ValidationResponse.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/ValidationResponse.java new file mode 100644 index 0000000..6137031 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/ValidationResponse.java @@ -0,0 +1,20 @@ +package tv.codely.shared.infrastructure.validation; + +import java.util.HashMap; +import java.util.List; + +public final class ValidationResponse { + private HashMap> validationErrors; + + public ValidationResponse(HashMap> validationErrors) { + this.validationErrors = validationErrors; + } + + public Boolean hasErrors() { + return !validationErrors.isEmpty(); + } + + public HashMap> errors() { + return validationErrors; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/Validator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/Validator.java new file mode 100644 index 0000000..d08cc93 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/Validator.java @@ -0,0 +1,46 @@ +package tv.codely.shared.infrastructure.validation; + +import tv.codely.shared.infrastructure.validation.validators.*; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public final class Validator { + private static final HashMap validators = new HashMap() {{ + put("required", new RequiredValidator()); + put("string", new StringValidator()); + put("not_empty", new NotEmptyValidator()); + put("uuid", new UuidValidator()); + }}; + + public static ValidationResponse validate( + HashMap input, + HashMap combinedRules + ) throws ValidatorNotExist { + HashMap> validationErrors = new HashMap<>(); + + for (Map.Entry entry : combinedRules.entrySet()) { + String[] rules = entry.getValue().split("\\|"); + + for (String rule : rules) { + FieldValidator validator = validators.get(rule); + + if (null == validator) { + throw new ValidatorNotExist(rule); + } + + if (!validator.isValid(entry.getKey(), input)) { + List existingErrors = validationErrors.getOrDefault(entry.getKey(), new ArrayList<>()); + existingErrors.add(validator.errorMessage(entry.getKey())); + + validationErrors.put(entry.getKey(), existingErrors); + } + } + } + + return new ValidationResponse(validationErrors); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/ValidatorNotExist.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/ValidatorNotExist.java new file mode 100644 index 0000000..c75d631 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/ValidatorNotExist.java @@ -0,0 +1,7 @@ +package tv.codely.shared.infrastructure.validation; + +public final class ValidatorNotExist extends Exception { + public ValidatorNotExist(String name) { + super(String.format("The validator <%s> does not exist", name)); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/FieldValidator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/FieldValidator.java new file mode 100644 index 0000000..65ace91 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/FieldValidator.java @@ -0,0 +1,10 @@ +package tv.codely.shared.infrastructure.validation.validators; + +import java.io.Serializable; +import java.util.HashMap; + +public interface FieldValidator { + Boolean isValid(String fieldName, HashMap fields); + + String errorMessage(String fieldName); +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/NotEmptyValidator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/NotEmptyValidator.java new file mode 100644 index 0000000..106e7ff --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/NotEmptyValidator.java @@ -0,0 +1,16 @@ +package tv.codely.shared.infrastructure.validation.validators; + +import java.io.Serializable; +import java.util.HashMap; + +public final class NotEmptyValidator implements FieldValidator { + @Override + public Boolean isValid(String fieldName, HashMap fields) { + return !fields.get(fieldName).toString().isEmpty(); + } + + @Override + public String errorMessage(String fieldName) { + return String.format("The field <%s> should not be empty", fieldName); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/RequiredValidator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/RequiredValidator.java new file mode 100644 index 0000000..862b5a2 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/RequiredValidator.java @@ -0,0 +1,16 @@ +package tv.codely.shared.infrastructure.validation.validators; + +import java.io.Serializable; +import java.util.HashMap; + +public final class RequiredValidator implements FieldValidator { + @Override + public Boolean isValid(String fieldName, HashMap fields) { + return fields.containsKey(fieldName); + } + + @Override + public String errorMessage(String fieldName) { + return String.format("The field <%s> is required", fieldName); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/StringValidator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/StringValidator.java new file mode 100644 index 0000000..f653ac7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/StringValidator.java @@ -0,0 +1,16 @@ +package tv.codely.shared.infrastructure.validation.validators; + +import java.io.Serializable; +import java.util.HashMap; + +public final class StringValidator implements FieldValidator { + @Override + public Boolean isValid(String fieldName, HashMap fields) { + return true; + } + + @Override + public String errorMessage(String fieldName) { + return String.format("The field <%s> should be of type string", fieldName); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/UuidValidator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/UuidValidator.java new file mode 100644 index 0000000..c39d679 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/main/tv/codely/shared/infrastructure/validation/validators/UuidValidator.java @@ -0,0 +1,19 @@ +package tv.codely.shared.infrastructure.validation.validators; + +import java.io.Serializable; +import java.util.HashMap; +import java.util.regex.Pattern; + +public final class UuidValidator implements FieldValidator { + @Override + public Boolean isValid(String fieldName, HashMap fields) { + Pattern uuidPattern = Pattern.compile("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"); + + return uuidPattern.matcher((String) fields.get(fieldName)).matches(); + } + + @Override + public String errorMessage(String fieldName) { + return String.format("The field <%s> is not a valid uuid", fieldName); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/EmailMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/EmailMother.java new file mode 100644 index 0000000..b8d4077 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/EmailMother.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain; + +public final class EmailMother { + public static String random() { + return MotherCreator.random().internet().emailAddress(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/IntegerMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/IntegerMother.java new file mode 100644 index 0000000..fe197ce --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/IntegerMother.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain; + +public final class IntegerMother { + public static Integer random() { + return MotherCreator.random().number().randomDigit(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/ListMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/ListMother.java new file mode 100644 index 0000000..5dc7622 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/ListMother.java @@ -0,0 +1,26 @@ +package tv.codely.shared.domain; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.function.Supplier; + +public final class ListMother { + public static List create(Integer size, Supplier creator) { + ArrayList list = new ArrayList<>(); + + for (int i = 0; i < size; i++) { + list.add(creator.get()); + } + + return list; + } + + public static List random(Supplier creator) { + return create(IntegerMother.random(), creator); + } + + public static List one(T element) { + return Collections.singletonList(element); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/MotherCreator.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/MotherCreator.java new file mode 100644 index 0000000..eef2ac4 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/MotherCreator.java @@ -0,0 +1,11 @@ +package tv.codely.shared.domain; + +import com.github.javafaker.Faker; + +public final class MotherCreator { + private final static Faker faker = new Faker(); + + public static Faker random() { + return faker; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/RandomElementPicker.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/RandomElementPicker.java new file mode 100644 index 0000000..a69497e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/RandomElementPicker.java @@ -0,0 +1,12 @@ +package tv.codely.shared.domain; + +import java.util.Random; + +public final class RandomElementPicker { + @SafeVarargs + public static T from(T... elements) { + Random rand = new Random(); + + return elements[rand.nextInt(elements.length)]; + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/UuidMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/UuidMother.java new file mode 100644 index 0000000..1075dd7 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/UuidMother.java @@ -0,0 +1,9 @@ +package tv.codely.shared.domain; + +import java.util.UUID; + +public final class UuidMother { + public static String random() { + return UUID.randomUUID().toString(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/VideoUrlMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/VideoUrlMother.java new file mode 100644 index 0000000..1588ec6 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/VideoUrlMother.java @@ -0,0 +1,11 @@ +package tv.codely.shared.domain; + +public final class VideoUrlMother { + public static VideoUrl create(String value) { + return new VideoUrl(value); + } + + public static VideoUrl random() { + return create(MotherCreator.random().internet().url()); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/WordMother.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/WordMother.java new file mode 100644 index 0000000..4bad17a --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/domain/WordMother.java @@ -0,0 +1,7 @@ +package tv.codely.shared.domain; + +public final class WordMother { + public static String random() { + return MotherCreator.random().lorem().word(); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java new file mode 100644 index 0000000..af93e67 --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java @@ -0,0 +1,28 @@ +package tv.codely.shared.infrastructure; + +public abstract class InfrastructureTestCase { + private final int MAX_ATTEMPTS = 3; + private final int MILLIS_TO_WAIT_BETWEEN_RETRIES = 700; + + protected void eventually(Runnable assertion) throws Exception { + int attempts = 0; + + while (true) { + try { + assertion.run(); + return; + } catch (Throwable error) { + attempts++; + + if (attempts >= MAX_ATTEMPTS) { + throw new Exception( + String.format("Could not assert after %d retries. Last error: %s", MAX_ATTEMPTS, error.getMessage()), + error + ); + } + + Thread.sleep(MILLIS_TO_WAIT_BETWEEN_RETRIES); + } + } + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/infrastructure/UnitTestCase.java b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/infrastructure/UnitTestCase.java new file mode 100644 index 0000000..96e007e --- /dev/null +++ b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/src/shared/test/tv/codely/shared/infrastructure/UnitTestCase.java @@ -0,0 +1,45 @@ +package tv.codely.shared.infrastructure; + +import org.junit.jupiter.api.BeforeEach; +import tv.codely.shared.domain.UuidGenerator; +import tv.codely.shared.domain.bus.event.DomainEvent; +import tv.codely.shared.domain.bus.event.EventBus; +import tv.codely.shared.domain.bus.query.*; + +import java.util.Collections; +import java.util.List; + +import static org.mockito.Mockito.*; + +public abstract class UnitTestCase { + protected EventBus eventBus; + protected QueryBus queryBus; + protected UuidGenerator uuidGenerator; + + @BeforeEach + protected void setUp() { + eventBus = mock(EventBus.class); + queryBus = mock(QueryBus.class); + uuidGenerator = mock(UuidGenerator.class); + } + + public void shouldHavePublished(List domainEvents) { + verify(eventBus, atLeastOnce()).publish(domainEvents); + } + + public void shouldHavePublished(DomainEvent domainEvent) { + shouldHavePublished(Collections.singletonList(domainEvent)); + } + + public void shouldGenerateUuid(String uuid) { + when(uuidGenerator.generate()).thenReturn(uuid); + } + + public void shouldGenerateUuids(String uuid, String... others) { + when(uuidGenerator.generate()).thenReturn(uuid, others); + } + + public void shouldAsk(Query query, Response response) { + when(queryBus.ask(query)).thenReturn(response); + } +} diff --git a/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/var/log/.gitkeep b/01-unordered_events/3-consume_unordered_courses_renamed/2-preference_by_recent_date/var/log/.gitkeep new file mode 100644 index 0000000..e69de29