From d8769860bd2af88e16da3ab93f13879eb57b9b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rafa=20G=C3=B3mez?= Date: Mon, 13 Nov 2023 19:57:38 +0100 Subject: [PATCH] feat(2-3): add example --- .../.editorconfig | 11 + .../1-counter_incrementing_always/.env | 1 + .../.github/workflows/ci.yml | 31 +++ .../1-counter_incrementing_always/.gitignore | 12 + .../1-counter_incrementing_always/Dockerfile | 15 ++ .../1-counter_incrementing_always/Makefile | 29 ++ .../1-counter_incrementing_always/README.md | 52 ++++ .../apps/main/resources/.env | 34 +++ .../apps/main/resources/.no.env.local | 34 +++ .../main/resources/application.properties | 1 + .../public/images/logo.png | Bin 0 -> 4186 bytes .../backoffice_frontend/templates/master.ftl | 27 ++ .../templates/pages/courses/courses.ftl | 20 ++ .../pages/courses/partials/list_courses.ftl | 153 +++++++++++ .../courses/partials/new_course_form.ftl | 54 ++++ .../templates/pages/home.ftl | 7 + .../templates/partials/footer.ftl | 7 + .../templates/partials/header.ftl | 28 ++ .../apps/main/resources/log4j2.properties | 34 +++ .../apps/main/tv/codely/apps/Starter.java | 96 +++++++ .../backend/BackofficeBackendApplication.java | 27 ++ .../ConsumeRabbitMqDomainEventsCommand.java | 18 ++ .../BackofficeBackendServerConfiguration.java | 28 ++ ...BackofficeBackendServerPortCustomizer.java | 28 ++ .../courses/CoursesGetController.java | 85 ++++++ .../HealthCheckGetController.java | 34 +++ .../middleware/BasicHttpAuthMiddleware.java | 77 ++++++ .../BackofficeFrontendApplication.java | 24 ++ ...ackofficeFrontendServerPortCustomizer.java | 28 ++ .../config/BackofficeFrontendWebConfig.java | 61 +++++ .../courses/CoursesGetWebController.java | 48 ++++ .../courses/CoursesPostWebController.java | 67 +++++ .../HealthCheckGetController.java | 19 ++ .../controller/home/HomeGetWebController.java | 25 ++ .../mooc/backend/MoocBackendApplication.java | 29 ++ .../ConsumeMySqlDomainEventsCommand.java | 18 ++ .../ConsumeRabbitMqDomainEventsCommand.java | 18 ++ .../MoocBackendServerConfiguration.java | 29 ++ .../MoocBackendServerPortCustomizer.java | 27 ++ .../courses/CourseGetController.java | 54 ++++ .../courses/CoursesPutController.java | 60 +++++ .../CoursesCounterGetController.java | 39 +++ .../HealthCheckGetController.java | 34 +++ .../NewsletterNotificationPutController.java | 36 +++ .../playground/DomainEventPostController.java | 53 ++++ .../apps/test/resources/log4j2.properties | 33 +++ .../tv/codely/apps/ApplicationTestCase.java | 67 +++++ .../BackofficeApplicationTestCase.java | 8 + .../HealthCheckGetControllerShould.java | 38 +++ .../HealthCheckGetControllerShould.java | 13 + .../apps/mooc/MoocApplicationTestCase.java | 8 + .../courses/CourseGetControllerShould.java | 31 +++ .../courses/CoursesPutControllerShould.java | 18 ++ .../CoursesCounterGetControllerShould.java | 46 ++++ .../HealthCheckGetControllerShould.java | 13 + ...letterNotificationPutControllerShould.java | 13 + .../build.gradle | 171 ++++++++++++ .../docker-compose.ci.yml | 55 ++++ .../docker-compose.yml | 139 ++++++++++ .../etc/http/backoffice_frontend.http | 80 ++++++ .../etc/http/publish_domain_events.http | 62 +++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 63721 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + .../1-counter_incrementing_always/gradlew | 249 ++++++++++++++++++ .../1-counter_incrementing_always/gradlew.bat | 92 +++++++ .../settings.gradle | 13 + .../application/store/DomainEventStorer.java | 22 ++ .../store/StoreDomainEventOnOccurred.java | 28 ++ .../domain/AnalyticsDomainEvent.java | 37 +++ .../AnalyticsDomainEventAggregateId.java | 9 + .../domain/AnalyticsDomainEventBody.java | 16 ++ .../domain/AnalyticsDomainEventId.java | 9 + .../domain/AnalyticsDomainEventName.java | 9 + .../domain/DomainEventsRepository.java | 5 + .../src/backoffice/build.gradle | 2 + .../main/resources/database/backoffice.sql | 6 + .../backoffice/backoffice_courses.json | 22 ++ .../authenticate/AuthenticateUserCommand.java | 21 ++ .../AuthenticateUserCommandHandler.java | 23 ++ .../authenticate/UserAuthenticator.java | 34 +++ .../backoffice/auth/domain/AuthPassword.java | 9 + .../auth/domain/AuthRepository.java | 7 + .../backoffice/auth/domain/AuthUser.java | 19 ++ .../backoffice/auth/domain/AuthUsername.java | 9 + .../auth/domain/InvalidAuthCredentials.java | 7 + .../auth/domain/InvalidAuthUsername.java | 7 + .../persistence/InMemoryAuthRepository.java | 25 ++ .../application/BackofficeCourseResponse.java | 32 +++ .../BackofficeCoursesResponse.java | 17 ++ .../create/BackofficeCourseCreator.java | 20 ++ ...CreateBackofficeCourseOnCourseCreated.java | 21 ++ .../rename/BackofficeCourseRenamer.java | 26 ++ ...RenameBackofficeCourseOnCourseRenamed.java | 21 ++ .../AllBackofficeCoursesSearcher.java | 23 ++ .../SearchAllBackofficeCoursesQuery.java | 6 + ...earchAllBackofficeCoursesQueryHandler.java | 19 ++ .../BackofficeCoursesByCriteriaSearcher.java | 37 +++ ...earchBackofficeCoursesByCriteriaQuery.java | 49 ++++ ...ckofficeCoursesByCriteriaQueryHandler.java | 24 ++ .../courses/domain/BackofficeCourse.java | 75 ++++++ .../domain/BackofficeCourseNotFound.java | 7 + .../domain/BackofficeCourseRepository.java | 16 ++ ...asticsearchBackofficeCourseRepository.java | 45 ++++ ...MemoryCacheBackofficeCourseRepository.java | 64 +++++ .../MySqlBackofficeCourseRepository.java | 41 +++ .../hibernate/BackofficeCourse.hbm.xml | 14 + .../BackofficeElasticsearchConfiguration.java | 89 +++++++ .../BackofficeHibernateConfiguration.java | 47 ++++ .../BackofficeMySqlEventBusConfiguration.java | 38 +++ ...ckofficeRabbitMqEventBusConfiguration.java | 27 ++ ...ckofficeContextInfrastructureTestCase.java | 21 ++ .../auth/AuthModuleUnitTestCase.java | 31 +++ .../AuthenticateUserCommandHandlerShould.java | 56 ++++ .../AuthenticateUserCommandMother.java | 16 ++ .../auth/domain/AuthPasswordMother.java | 13 + .../auth/domain/AuthUserMother.java | 21 ++ .../auth/domain/AuthUsernameMother.java | 13 + .../ElasticsearchEnvironmentArranger.java | 47 ++++ .../BackofficeCourseCriteriaMother.java | 17 ++ .../domain/BackofficeCourseMother.java | 18 ++ ...earchBackofficeCourseRepositoryShould.java | 114 ++++++++ ...CacheBackofficeCourseRepositoryShould.java | 100 +++++++ ...MySqlBackofficeCourseRepositoryShould.java | 60 +++++ .../src/mooc/build.gradle | 2 + .../src/mooc/main/resources/database/mooc.sql | 62 +++++ .../courses/application/CourseResponse.java | 32 +++ .../courses/application/CoursesResponse.java | 17 ++ .../application/create/CourseCreator.java | 23 ++ .../create/CreateCourseCommand.java | 27 ++ .../create/CreateCourseCommandHandler.java | 25 ++ .../application/find/CourseFinder.java | 22 ++ .../application/find/FindCourseQuery.java | 15 ++ .../find/FindCourseQueryHandler.java | 21 ++ .../search_last/LastCoursesSearcher.java | 34 +++ .../search_last/SearchLastCoursesQuery.java | 34 +++ .../SearchLastCoursesQueryHandler.java | 19 ++ .../tv/codely/mooc/courses/domain/Course.java | 63 +++++ .../mooc/courses/domain/CourseDuration.java | 13 + .../codely/mooc/courses/domain/CourseId.java | 12 + .../mooc/courses/domain/CourseName.java | 13 + .../mooc/courses/domain/CourseNotExist.java | 9 + .../mooc/courses/domain/CourseRepository.java | 14 + .../persistence/InMemoryCourseRepository.java | 28 ++ .../persistence/MySqlCourseRepository.java | 37 +++ .../persistence/hibernate/Course.hbm.xml | 20 ++ .../find/CoursesCounterFinder.java | 23 ++ .../find/CoursesCounterResponse.java | 34 +++ .../find/FindCoursesCounterQuery.java | 6 + .../find/FindCoursesCounterQueryHandler.java | 18 ++ .../increment/CoursesCounterIncrementer.java | 27 ++ ...ncrementCoursesCounterOnCourseCreated.java | 24 ++ .../domain/CoursesCounter.java | 69 +++++ .../domain/CoursesCounterId.java | 12 + .../domain/CoursesCounterNotInitialized.java | 4 + .../domain/CoursesCounterRepository.java | 9 + .../domain/CoursesCounterTotal.java | 21 ++ .../MySqlCoursesCounterRepository.java | 32 +++ .../hibernate/CoursesCounter.hbm.xml | 24 ++ .../NewCoursesNewsletterSender.java | 52 ++++ .../SendNewCoursesNewsletterCommand.java | 15 ++ ...endNewCoursesNewsletterCommandHandler.java | 18 ++ .../mooc/notifications/domain/Email.java | 62 +++++ .../mooc/notifications/domain/EmailId.java | 9 + .../notifications/domain/EmailSender.java | 5 + .../domain/NewCoursesNewsletter.java | 55 ++++ .../domain/NewCoursesNewsletterEmailSent.java | 78 ++++++ .../infrastructure/FakeEmailSender.java | 13 + .../MoocHibernateConfiguration.java | 47 ++++ .../MoocMySqlEventBusConfiguration.java | 37 +++ .../MoocRabbitMqEventBusConfiguration.java | 27 ++ .../tv/codely/mooc/steps/domain/Step.java | 35 +++ .../tv/codely/mooc/steps/domain/StepId.java | 9 + .../mooc/steps/domain/StepRepository.java | 9 + .../codely/mooc/steps/domain/StepTitle.java | 13 + .../steps/domain/challenge/ChallengeStep.java | 43 +++ .../challenge/ChallengeStepStatement.java | 13 + .../mooc/steps/domain/video/VideoStep.java | 25 ++ .../steps/domain/video/VideoStepText.java | 13 + .../persistence/MySqlStepRepository.java | 30 +++ .../persistence/hibernate/VideoStep.hbm.xml | 37 +++ .../students/application/StudentResponse.java | 38 +++ .../application/StudentsResponse.java | 17 ++ .../search_all/AllStudentsSearcher.java | 23 ++ .../search_all/SearchAllStudentsQuery.java | 20 ++ .../SearchAllStudentsQueryHandler.java | 19 ++ .../codely/mooc/students/domain/Student.java | 31 +++ .../mooc/students/domain/StudentId.java | 9 + .../students/domain/StudentRepository.java | 7 + .../InMemoryStudentRepository.java | 27 ++ .../MoocContextInfrastructureTestCase.java | 11 + .../CoursesModuleInfrastructureTestCase.java | 12 + .../courses/CoursesModuleUnitTestCase.java | 23 ++ .../application/CourseResponseMother.java | 13 + .../application/CoursesResponseMother.java | 24 ++ .../CreateCourseCommandHandlerShould.java | 33 +++ .../create/CreateCourseCommandMother.java | 13 + .../SearchLastCoursesQueryMother.java | 13 + .../CourseCreatedDomainEventMother.java | 17 ++ .../courses/domain/CourseDurationMother.java | 20 ++ .../mooc/courses/domain/CourseIdMother.java | 13 + .../mooc/courses/domain/CourseMother.java | 21 ++ .../mooc/courses/domain/CourseNameMother.java | 13 + .../InMemoryCourseRepositoryShould.java | 35 +++ .../MySqlCourseRepositoryShould.java | 37 +++ ...esCounterModuleInfrastructureTestCase.java | 10 + .../CoursesCounterModuleUnitTestCase.java | 34 +++ .../find/CoursesCounterResponseMother.java | 13 + .../FindCoursesCounterQueryHandlerShould.java | 42 +++ ...ntCoursesCounterOnCourseCreatedShould.java | 66 +++++ .../domain/CoursesCounterIdMother.java | 13 + .../domain/CoursesCounterMother.java | 43 +++ .../domain/CoursesCounterTotalMother.java | 17 ++ .../MySqlCoursesCounterRepositoryShould.java | 23 ++ .../NotificationsModuleUnitTestCase.java | 33 +++ ...CoursesNewsletterCommandHandlerShould.java | 102 +++++++ ...SendNewCoursesNewsletterCommandMother.java | 9 + .../notifications/domain/EmailIdMother.java | 13 + .../NewCoursesNewsletterEmailSentMother.java | 13 + .../domain/NewCoursesNewsletterMother.java | 20 ++ .../bus/event/mysql/MySqlEventBusShould.java | 34 +++ .../rabbitmq/RabbitMqEventBusShould.java | 53 ++++ ...TestAllWorksOnRabbitMqEventsPublished.java | 15 ++ .../StepsModuleInfrastructureTestCase.java | 10 + .../mooc/steps/domain/StepIdMother.java | 13 + .../mooc/steps/domain/StepTitleMother.java | 13 + .../domain/challenge/ChallengeStepMother.java | 16 ++ .../ChallengeStepStatementMother.java | 13 + .../steps/domain/video/VideoStepMother.java | 23 ++ .../domain/video/VideoStepTextMother.java | 13 + .../MySqlStepRepositoryShould.java | 44 ++++ .../application/StudentResponseMother.java | 16 ++ .../application/StudentsResponseMother.java | 20 ++ .../SearchAllStudentsQueryMother.java | 7 + .../mooc/students/domain/StudentIdMother.java | 13 + .../src/shared/build.gradle | 2 + .../codely/shared/domain/AggregateRoot.java | 23 ++ .../tv/codely/shared/domain/DomainError.java | 21 ++ .../tv/codely/shared/domain/Identifier.java | 44 ++++ .../codely/shared/domain/IntValueObject.java | 32 +++ .../main/tv/codely/shared/domain/Logger.java | 15 ++ .../tv/codely/shared/domain/Monitoring.java | 13 + .../main/tv/codely/shared/domain/Service.java | 9 + .../shared/domain/StringValueObject.java | 37 +++ .../main/tv/codely/shared/domain/Utils.java | 81 ++++++ .../codely/shared/domain/UuidGenerator.java | 5 + .../tv/codely/shared/domain/VideoUrl.java | 11 + .../shared/domain/bus/command/Command.java | 4 + .../shared/domain/bus/command/CommandBus.java | 5 + .../domain/bus/command/CommandHandler.java | 5 + .../command/CommandHandlerExecutionError.java | 7 + .../command/CommandNotRegisteredError.java | 7 + .../shared/domain/bus/event/DomainEvent.java | 52 ++++ .../bus/event/DomainEventSubscriber.java | 10 + .../shared/domain/bus/event/EventBus.java | 7 + .../codely/shared/domain/bus/query/Query.java | 4 + .../shared/domain/bus/query/QueryBus.java | 5 + .../shared/domain/bus/query/QueryHandler.java | 5 + .../bus/query/QueryHandlerExecutionError.java | 7 + .../bus/query/QueryNotRegisteredError.java | 7 + .../shared/domain/bus/query/Response.java | 4 + .../course/CourseCreatedDomainEvent.java | 94 +++++++ .../course/CourseRenamedDomainEvent.java | 78 ++++++ .../shared/domain/criteria/Criteria.java | 54 ++++ .../codely/shared/domain/criteria/Filter.java | 47 ++++ .../shared/domain/criteria/FilterField.java | 9 + .../domain/criteria/FilterOperator.java | 36 +++ .../shared/domain/criteria/FilterValue.java | 9 + .../shared/domain/criteria/Filters.java | 30 +++ .../codely/shared/domain/criteria/Order.java | 46 ++++ .../shared/domain/criteria/OrderBy.java | 9 + .../shared/domain/criteria/OrderType.java | 25 ++ .../infrastructure/JavaUuidGenerator.java | 14 + .../command/CommandHandlersInformation.java | 48 ++++ .../bus/command/InMemoryCommandBus.java | 32 +++ .../event/DomainEventJsonDeserializer.java | 46 ++++ .../bus/event/DomainEventJsonSerializer.java | 24 ++ .../DomainEventSubscriberInformation.java | 49 ++++ .../DomainEventSubscribersInformation.java | 54 ++++ .../bus/event/DomainEventsInformation.java | 53 ++++ .../mysql/MySqlDomainEventsConsumer.java | 95 +++++++ .../bus/event/mysql/MySqlEventBus.java | 45 ++++ .../RabbitMqDomainEventsConsumer.java | 138 ++++++++++ .../bus/event/rabbitmq/RabbitMqEventBus.java | 38 +++ .../RabbitMqEventBusConfiguration.java | 134 ++++++++++ .../RabbitMqExchangeNameFormatter.java | 11 + .../bus/event/rabbitmq/RabbitMqPublisher.java | 36 +++ .../rabbitmq/RabbitMqQueueNameFormatter.java | 17 ++ .../spring/SpringApplicationEventBus.java | 26 ++ .../bus/query/InMemoryQueryBus.java | 29 ++ .../bus/query/QueryHandlersInformation.java | 48 ++++ .../infrastructure/cli/ConsoleCommand.java | 25 ++ .../config/EnvironmentConfig.java | 27 ++ .../infrastructure/config/Parameter.java | 29 ++ .../config/ParameterNotExist.java | 7 + .../elasticsearch/ElasticsearchClient.java | 49 ++++ .../ElasticsearchCriteriaConverter.java | 92 +++++++ .../ElasticsearchRepository.java | 79 ++++++ .../HibernateConfigurationFactory.java | 137 ++++++++++ .../hibernate/HibernateCriteriaConverter.java | 85 ++++++ .../hibernate/HibernateRepository.java | 49 ++++ .../hibernate/JsonListType.java | 140 ++++++++++ .../infrastructure/spring/ApiController.java | 32 +++ .../spring/ApiExceptionMiddleware.java | 95 +++++++ .../validation/ValidationResponse.java | 20 ++ .../infrastructure/validation/Validator.java | 46 ++++ .../validation/ValidatorNotExist.java | 7 + .../validation/validators/FieldValidator.java | 10 + .../validators/NotEmptyValidator.java | 16 ++ .../validators/RequiredValidator.java | 16 ++ .../validators/StringValidator.java | 16 ++ .../validation/validators/UuidValidator.java | 19 ++ .../tv/codely/shared/domain/EmailMother.java | 7 + .../codely/shared/domain/IntegerMother.java | 7 + .../tv/codely/shared/domain/ListMother.java | 26 ++ .../codely/shared/domain/MotherCreator.java | 11 + .../shared/domain/RandomElementPicker.java | 12 + .../tv/codely/shared/domain/UuidMother.java | 9 + .../codely/shared/domain/VideoUrlMother.java | 11 + .../tv/codely/shared/domain/WordMother.java | 7 + .../InfrastructureTestCase.java | 28 ++ .../shared/infrastructure/UnitTestCase.java | 45 ++++ .../var/log/.gitkeep | 0 .../.editorconfig | 11 + .../.env | 1 + .../.github/workflows/ci.yml | 31 +++ .../.gitignore | 12 + .../Dockerfile | 15 ++ .../Makefile | 29 ++ .../README.md | 52 ++++ .../apps/main/resources/.env | 34 +++ .../apps/main/resources/.no.env.local | 34 +++ .../main/resources/application.properties | 1 + .../public/images/logo.png | Bin 0 -> 4186 bytes .../backoffice_frontend/templates/master.ftl | 27 ++ .../templates/pages/courses/courses.ftl | 20 ++ .../pages/courses/partials/list_courses.ftl | 153 +++++++++++ .../courses/partials/new_course_form.ftl | 54 ++++ .../templates/pages/home.ftl | 7 + .../templates/partials/footer.ftl | 7 + .../templates/partials/header.ftl | 28 ++ .../apps/main/resources/log4j2.properties | 34 +++ .../apps/main/tv/codely/apps/Starter.java | 96 +++++++ .../backend/BackofficeBackendApplication.java | 27 ++ .../ConsumeRabbitMqDomainEventsCommand.java | 18 ++ .../BackofficeBackendServerConfiguration.java | 28 ++ ...BackofficeBackendServerPortCustomizer.java | 28 ++ .../courses/CoursesGetController.java | 85 ++++++ .../HealthCheckGetController.java | 34 +++ .../middleware/BasicHttpAuthMiddleware.java | 77 ++++++ .../BackofficeFrontendApplication.java | 24 ++ ...ackofficeFrontendServerPortCustomizer.java | 28 ++ .../config/BackofficeFrontendWebConfig.java | 61 +++++ .../courses/CoursesGetWebController.java | 48 ++++ .../courses/CoursesPostWebController.java | 67 +++++ .../HealthCheckGetController.java | 19 ++ .../controller/home/HomeGetWebController.java | 25 ++ .../mooc/backend/MoocBackendApplication.java | 29 ++ .../ConsumeMySqlDomainEventsCommand.java | 18 ++ .../ConsumeRabbitMqDomainEventsCommand.java | 18 ++ .../MoocBackendServerConfiguration.java | 29 ++ .../MoocBackendServerPortCustomizer.java | 27 ++ .../courses/CourseGetController.java | 54 ++++ .../courses/CoursesPutController.java | 60 +++++ .../CoursesCounterGetController.java | 39 +++ .../HealthCheckGetController.java | 34 +++ .../NewsletterNotificationPutController.java | 36 +++ .../playground/DomainEventPostController.java | 53 ++++ .../apps/test/resources/log4j2.properties | 33 +++ .../tv/codely/apps/ApplicationTestCase.java | 67 +++++ .../BackofficeApplicationTestCase.java | 8 + .../HealthCheckGetControllerShould.java | 38 +++ .../HealthCheckGetControllerShould.java | 13 + .../apps/mooc/MoocApplicationTestCase.java | 8 + .../courses/CourseGetControllerShould.java | 31 +++ .../courses/CoursesPutControllerShould.java | 18 ++ .../CoursesCounterGetControllerShould.java | 46 ++++ .../HealthCheckGetControllerShould.java | 13 + ...letterNotificationPutControllerShould.java | 13 + .../build.gradle | 171 ++++++++++++ .../docker-compose.ci.yml | 55 ++++ .../docker-compose.yml | 139 ++++++++++ .../etc/http/backoffice_frontend.http | 80 ++++++ .../etc/http/publish_domain_events.http | 62 +++++ .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 63721 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + .../gradlew | 249 ++++++++++++++++++ .../gradlew.bat | 92 +++++++ .../settings.gradle | 13 + .../application/store/DomainEventStorer.java | 22 ++ .../store/StoreDomainEventOnOccurred.java | 28 ++ .../domain/AnalyticsDomainEvent.java | 37 +++ .../AnalyticsDomainEventAggregateId.java | 9 + .../domain/AnalyticsDomainEventBody.java | 16 ++ .../domain/AnalyticsDomainEventId.java | 9 + .../domain/AnalyticsDomainEventName.java | 9 + .../domain/DomainEventsRepository.java | 5 + .../src/backoffice/build.gradle | 2 + .../main/resources/database/backoffice.sql | 6 + .../backoffice/backoffice_courses.json | 22 ++ .../authenticate/AuthenticateUserCommand.java | 21 ++ .../AuthenticateUserCommandHandler.java | 23 ++ .../authenticate/UserAuthenticator.java | 34 +++ .../backoffice/auth/domain/AuthPassword.java | 9 + .../auth/domain/AuthRepository.java | 7 + .../backoffice/auth/domain/AuthUser.java | 19 ++ .../backoffice/auth/domain/AuthUsername.java | 9 + .../auth/domain/InvalidAuthCredentials.java | 7 + .../auth/domain/InvalidAuthUsername.java | 7 + .../persistence/InMemoryAuthRepository.java | 25 ++ .../application/BackofficeCourseResponse.java | 32 +++ .../BackofficeCoursesResponse.java | 17 ++ .../create/BackofficeCourseCreator.java | 20 ++ ...CreateBackofficeCourseOnCourseCreated.java | 21 ++ .../rename/BackofficeCourseRenamer.java | 26 ++ ...RenameBackofficeCourseOnCourseRenamed.java | 21 ++ .../AllBackofficeCoursesSearcher.java | 23 ++ .../SearchAllBackofficeCoursesQuery.java | 6 + ...earchAllBackofficeCoursesQueryHandler.java | 19 ++ .../BackofficeCoursesByCriteriaSearcher.java | 37 +++ ...earchBackofficeCoursesByCriteriaQuery.java | 49 ++++ ...ckofficeCoursesByCriteriaQueryHandler.java | 24 ++ .../courses/domain/BackofficeCourse.java | 75 ++++++ .../domain/BackofficeCourseNotFound.java | 7 + .../domain/BackofficeCourseRepository.java | 16 ++ ...asticsearchBackofficeCourseRepository.java | 45 ++++ ...MemoryCacheBackofficeCourseRepository.java | 64 +++++ .../MySqlBackofficeCourseRepository.java | 41 +++ .../hibernate/BackofficeCourse.hbm.xml | 14 + .../BackofficeElasticsearchConfiguration.java | 89 +++++++ .../BackofficeHibernateConfiguration.java | 47 ++++ .../BackofficeMySqlEventBusConfiguration.java | 38 +++ ...ckofficeRabbitMqEventBusConfiguration.java | 27 ++ ...ckofficeContextInfrastructureTestCase.java | 21 ++ .../auth/AuthModuleUnitTestCase.java | 31 +++ .../AuthenticateUserCommandHandlerShould.java | 56 ++++ .../AuthenticateUserCommandMother.java | 16 ++ .../auth/domain/AuthPasswordMother.java | 13 + .../auth/domain/AuthUserMother.java | 21 ++ .../auth/domain/AuthUsernameMother.java | 13 + .../ElasticsearchEnvironmentArranger.java | 47 ++++ .../BackofficeCourseCriteriaMother.java | 17 ++ .../domain/BackofficeCourseMother.java | 18 ++ ...earchBackofficeCourseRepositoryShould.java | 114 ++++++++ ...CacheBackofficeCourseRepositoryShould.java | 100 +++++++ ...MySqlBackofficeCourseRepositoryShould.java | 60 +++++ .../src/mooc/build.gradle | 2 + .../src/mooc/main/resources/database/mooc.sql | 62 +++++ .../courses/application/CourseResponse.java | 32 +++ .../courses/application/CoursesResponse.java | 17 ++ .../application/create/CourseCreator.java | 23 ++ .../create/CreateCourseCommand.java | 27 ++ .../create/CreateCourseCommandHandler.java | 25 ++ .../application/find/CourseFinder.java | 22 ++ .../application/find/FindCourseQuery.java | 15 ++ .../find/FindCourseQueryHandler.java | 21 ++ .../search_last/LastCoursesSearcher.java | 34 +++ .../search_last/SearchLastCoursesQuery.java | 34 +++ .../SearchLastCoursesQueryHandler.java | 19 ++ .../tv/codely/mooc/courses/domain/Course.java | 63 +++++ .../mooc/courses/domain/CourseDuration.java | 13 + .../codely/mooc/courses/domain/CourseId.java | 12 + .../mooc/courses/domain/CourseName.java | 13 + .../mooc/courses/domain/CourseNotExist.java | 9 + .../mooc/courses/domain/CourseRepository.java | 14 + .../persistence/InMemoryCourseRepository.java | 28 ++ .../persistence/MySqlCourseRepository.java | 37 +++ .../persistence/hibernate/Course.hbm.xml | 20 ++ .../find/CoursesCounterFinder.java | 23 ++ .../find/CoursesCounterResponse.java | 34 +++ .../find/FindCoursesCounterQuery.java | 6 + .../find/FindCoursesCounterQueryHandler.java | 18 ++ .../increment/CoursesCounterIncrementer.java | 29 ++ ...ncrementCoursesCounterOnCourseCreated.java | 24 ++ .../domain/CoursesCounter.java | 69 +++++ .../domain/CoursesCounterId.java | 12 + .../domain/CoursesCounterNotInitialized.java | 4 + .../domain/CoursesCounterRepository.java | 9 + .../domain/CoursesCounterTotal.java | 21 ++ .../MySqlCoursesCounterRepository.java | 32 +++ .../hibernate/CoursesCounter.hbm.xml | 24 ++ .../NewCoursesNewsletterSender.java | 52 ++++ .../SendNewCoursesNewsletterCommand.java | 15 ++ ...endNewCoursesNewsletterCommandHandler.java | 18 ++ .../mooc/notifications/domain/Email.java | 62 +++++ .../mooc/notifications/domain/EmailId.java | 9 + .../notifications/domain/EmailSender.java | 5 + .../domain/NewCoursesNewsletter.java | 55 ++++ .../domain/NewCoursesNewsletterEmailSent.java | 78 ++++++ .../infrastructure/FakeEmailSender.java | 13 + .../MoocHibernateConfiguration.java | 47 ++++ .../MoocMySqlEventBusConfiguration.java | 37 +++ .../MoocRabbitMqEventBusConfiguration.java | 27 ++ .../tv/codely/mooc/steps/domain/Step.java | 35 +++ .../tv/codely/mooc/steps/domain/StepId.java | 9 + .../mooc/steps/domain/StepRepository.java | 9 + .../codely/mooc/steps/domain/StepTitle.java | 13 + .../steps/domain/challenge/ChallengeStep.java | 43 +++ .../challenge/ChallengeStepStatement.java | 13 + .../mooc/steps/domain/video/VideoStep.java | 25 ++ .../steps/domain/video/VideoStepText.java | 13 + .../persistence/MySqlStepRepository.java | 30 +++ .../persistence/hibernate/VideoStep.hbm.xml | 37 +++ .../students/application/StudentResponse.java | 38 +++ .../application/StudentsResponse.java | 17 ++ .../search_all/AllStudentsSearcher.java | 23 ++ .../search_all/SearchAllStudentsQuery.java | 20 ++ .../SearchAllStudentsQueryHandler.java | 19 ++ .../codely/mooc/students/domain/Student.java | 31 +++ .../mooc/students/domain/StudentId.java | 9 + .../students/domain/StudentRepository.java | 7 + .../InMemoryStudentRepository.java | 27 ++ .../MoocContextInfrastructureTestCase.java | 11 + .../CoursesModuleInfrastructureTestCase.java | 12 + .../courses/CoursesModuleUnitTestCase.java | 23 ++ .../application/CourseResponseMother.java | 13 + .../application/CoursesResponseMother.java | 24 ++ .../CreateCourseCommandHandlerShould.java | 33 +++ .../create/CreateCourseCommandMother.java | 13 + .../SearchLastCoursesQueryMother.java | 13 + .../CourseCreatedDomainEventMother.java | 17 ++ .../courses/domain/CourseDurationMother.java | 20 ++ .../mooc/courses/domain/CourseIdMother.java | 13 + .../mooc/courses/domain/CourseMother.java | 21 ++ .../mooc/courses/domain/CourseNameMother.java | 13 + .../InMemoryCourseRepositoryShould.java | 35 +++ .../MySqlCourseRepositoryShould.java | 37 +++ ...esCounterModuleInfrastructureTestCase.java | 10 + .../CoursesCounterModuleUnitTestCase.java | 34 +++ .../find/CoursesCounterResponseMother.java | 13 + .../FindCoursesCounterQueryHandlerShould.java | 42 +++ ...ntCoursesCounterOnCourseCreatedShould.java | 66 +++++ .../domain/CoursesCounterIdMother.java | 13 + .../domain/CoursesCounterMother.java | 43 +++ .../domain/CoursesCounterTotalMother.java | 17 ++ .../MySqlCoursesCounterRepositoryShould.java | 23 ++ .../NotificationsModuleUnitTestCase.java | 33 +++ ...CoursesNewsletterCommandHandlerShould.java | 102 +++++++ ...SendNewCoursesNewsletterCommandMother.java | 9 + .../notifications/domain/EmailIdMother.java | 13 + .../NewCoursesNewsletterEmailSentMother.java | 13 + .../domain/NewCoursesNewsletterMother.java | 20 ++ .../bus/event/mysql/MySqlEventBusShould.java | 34 +++ .../rabbitmq/RabbitMqEventBusShould.java | 53 ++++ ...TestAllWorksOnRabbitMqEventsPublished.java | 15 ++ .../StepsModuleInfrastructureTestCase.java | 10 + .../mooc/steps/domain/StepIdMother.java | 13 + .../mooc/steps/domain/StepTitleMother.java | 13 + .../domain/challenge/ChallengeStepMother.java | 16 ++ .../ChallengeStepStatementMother.java | 13 + .../steps/domain/video/VideoStepMother.java | 23 ++ .../domain/video/VideoStepTextMother.java | 13 + .../MySqlStepRepositoryShould.java | 44 ++++ .../application/StudentResponseMother.java | 16 ++ .../application/StudentsResponseMother.java | 20 ++ .../SearchAllStudentsQueryMother.java | 7 + .../mooc/students/domain/StudentIdMother.java | 13 + .../src/shared/build.gradle | 2 + .../codely/shared/domain/AggregateRoot.java | 23 ++ .../tv/codely/shared/domain/DomainError.java | 21 ++ .../tv/codely/shared/domain/Identifier.java | 44 ++++ .../codely/shared/domain/IntValueObject.java | 32 +++ .../main/tv/codely/shared/domain/Logger.java | 15 ++ .../tv/codely/shared/domain/Monitoring.java | 13 + .../main/tv/codely/shared/domain/Service.java | 9 + .../shared/domain/StringValueObject.java | 37 +++ .../main/tv/codely/shared/domain/Utils.java | 81 ++++++ .../codely/shared/domain/UuidGenerator.java | 5 + .../tv/codely/shared/domain/VideoUrl.java | 11 + .../shared/domain/bus/command/Command.java | 4 + .../shared/domain/bus/command/CommandBus.java | 5 + .../domain/bus/command/CommandHandler.java | 5 + .../command/CommandHandlerExecutionError.java | 7 + .../command/CommandNotRegisteredError.java | 7 + .../shared/domain/bus/event/DomainEvent.java | 52 ++++ .../bus/event/DomainEventSubscriber.java | 10 + .../shared/domain/bus/event/EventBus.java | 7 + .../codely/shared/domain/bus/query/Query.java | 4 + .../shared/domain/bus/query/QueryBus.java | 5 + .../shared/domain/bus/query/QueryHandler.java | 5 + .../bus/query/QueryHandlerExecutionError.java | 7 + .../bus/query/QueryNotRegisteredError.java | 7 + .../shared/domain/bus/query/Response.java | 4 + .../course/CourseCreatedDomainEvent.java | 94 +++++++ .../course/CourseRenamedDomainEvent.java | 78 ++++++ .../shared/domain/criteria/Criteria.java | 54 ++++ .../codely/shared/domain/criteria/Filter.java | 47 ++++ .../shared/domain/criteria/FilterField.java | 9 + .../domain/criteria/FilterOperator.java | 36 +++ .../shared/domain/criteria/FilterValue.java | 9 + .../shared/domain/criteria/Filters.java | 30 +++ .../codely/shared/domain/criteria/Order.java | 46 ++++ .../shared/domain/criteria/OrderBy.java | 9 + .../shared/domain/criteria/OrderType.java | 25 ++ .../infrastructure/JavaUuidGenerator.java | 14 + .../command/CommandHandlersInformation.java | 48 ++++ .../bus/command/InMemoryCommandBus.java | 32 +++ .../event/DomainEventJsonDeserializer.java | 46 ++++ .../bus/event/DomainEventJsonSerializer.java | 24 ++ .../DomainEventSubscriberInformation.java | 49 ++++ .../DomainEventSubscribersInformation.java | 54 ++++ .../bus/event/DomainEventsInformation.java | 53 ++++ .../mysql/MySqlDomainEventsConsumer.java | 95 +++++++ .../bus/event/mysql/MySqlEventBus.java | 45 ++++ .../RabbitMqDomainEventsConsumer.java | 138 ++++++++++ .../bus/event/rabbitmq/RabbitMqEventBus.java | 38 +++ .../RabbitMqEventBusConfiguration.java | 134 ++++++++++ .../RabbitMqExchangeNameFormatter.java | 11 + .../bus/event/rabbitmq/RabbitMqPublisher.java | 36 +++ .../rabbitmq/RabbitMqQueueNameFormatter.java | 17 ++ .../spring/SpringApplicationEventBus.java | 26 ++ .../bus/query/InMemoryQueryBus.java | 29 ++ .../bus/query/QueryHandlersInformation.java | 48 ++++ .../infrastructure/cli/ConsoleCommand.java | 25 ++ .../config/EnvironmentConfig.java | 27 ++ .../infrastructure/config/Parameter.java | 29 ++ .../config/ParameterNotExist.java | 7 + .../elasticsearch/ElasticsearchClient.java | 49 ++++ .../ElasticsearchCriteriaConverter.java | 92 +++++++ .../ElasticsearchRepository.java | 79 ++++++ .../HibernateConfigurationFactory.java | 137 ++++++++++ .../hibernate/HibernateCriteriaConverter.java | 85 ++++++ .../hibernate/HibernateRepository.java | 49 ++++ .../hibernate/JsonListType.java | 140 ++++++++++ .../infrastructure/spring/ApiController.java | 32 +++ .../spring/ApiExceptionMiddleware.java | 95 +++++++ .../validation/ValidationResponse.java | 20 ++ .../infrastructure/validation/Validator.java | 46 ++++ .../validation/ValidatorNotExist.java | 7 + .../validation/validators/FieldValidator.java | 10 + .../validators/NotEmptyValidator.java | 16 ++ .../validators/RequiredValidator.java | 16 ++ .../validators/StringValidator.java | 16 ++ .../validation/validators/UuidValidator.java | 19 ++ .../tv/codely/shared/domain/EmailMother.java | 7 + .../codely/shared/domain/IntegerMother.java | 7 + .../tv/codely/shared/domain/ListMother.java | 26 ++ .../codely/shared/domain/MotherCreator.java | 11 + .../shared/domain/RandomElementPicker.java | 12 + .../tv/codely/shared/domain/UuidMother.java | 9 + .../codely/shared/domain/VideoUrlMother.java | 11 + .../tv/codely/shared/domain/WordMother.java | 7 + .../InfrastructureTestCase.java | 28 ++ .../shared/infrastructure/UnitTestCase.java | 45 ++++ .../var/log/.gitkeep | 0 644 files changed, 20136 insertions(+) create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.editorconfig create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.env create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.github/workflows/ci.yml create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.gitignore create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/Dockerfile create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/Makefile create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/README.md create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/.env create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/.no.env.local create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/application.properties create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/public/images/logo.png create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/master.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/pages/courses/courses.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/new_course_form.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/pages/home.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/partials/header.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/log4j2.properties create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/Starter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerPortCustomizer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/controller/courses/CoursesGetController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/middleware/BasicHttpAuthMiddleware.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/BackofficeFrontendApplication.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendServerPortCustomizer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesGetWebController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesPostWebController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/controller/home/HomeGetWebController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/MoocBackendApplication.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/command/ConsumeMySqlDomainEventsCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerPortCustomizer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/courses/CourseGetController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/resources/log4j2.properties create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/ApplicationTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/backoffice/BackofficeApplicationTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/MoocApplicationTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/courses/CourseGetControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/courses/CoursesPutControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/build.gradle create mode 100755 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/docker-compose.ci.yml create mode 100755 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/docker-compose.yml create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/etc/http/backoffice_frontend.http create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/etc/http/publish_domain_events.http create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/gradle/wrapper/gradle-wrapper.jar create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/gradle/wrapper/gradle-wrapper.properties create mode 100755 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/gradlew create mode 100755 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/gradlew.bat create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/settings.gradle create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/application/store/DomainEventStorer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/application/store/StoreDomainEventOnOccurred.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEvent.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventAggregateId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventBody.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventName.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/DomainEventsRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/build.gradle create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/resources/database/backoffice.sql create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/resources/database/backoffice/backoffice_courses.json create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/UserAuthenticator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthPassword.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUser.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUsername.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthCredentials.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthUsername.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/infrastructure/persistence/InMemoryAuthRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCourseResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCoursesResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/create/CreateBackofficeCourseOnCourseCreated.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/AllBackofficeCoursesSearcher.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQuery.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/BackofficeCoursesByCriteriaSearcher.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQuery.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeElasticsearchConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeHibernateConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeRabbitMqEventBusConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/BackofficeContextInfrastructureTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/AuthModuleUnitTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandlerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthPasswordMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUserMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUsernameMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/ElasticsearchEnvironmentArranger.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseCriteriaMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/build.gradle create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/resources/database/mooc.sql create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/CourseResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/CoursesResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/create/CourseCreator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommandHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQuery.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/search_last/LastCoursesSearcher.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQuery.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/Course.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseDuration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseName.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseNotExist.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterFinder.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQuery.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreated.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterNotInitialized.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterTotal.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/hibernate/CoursesCounter.hbm.xml create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/NewCoursesNewsletterSender.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/Email.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/EmailId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/EmailSender.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSent.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/infrastructure/FakeEmailSender.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocHibernateConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocMySqlEventBusConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocRabbitMqEventBusConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/Step.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/StepId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/StepRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/StepTitle.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStep.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatement.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStep.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStepText.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/StudentResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/StudentsResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/search_all/AllStudentsSearcher.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQuery.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/domain/Student.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/domain/StudentId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/domain/StudentRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/infrastructure/InMemoryStudentRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/MoocContextInfrastructureTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/CoursesModuleInfrastructureTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/CourseResponseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/CoursesResponseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandHandlerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseCreatedDomainEventMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseDurationMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseIdMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseNameMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleInfrastructureTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleUnitTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandlerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreatedShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterIdMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterTotalMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/application/NotificationsModuleUnitTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandlerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/domain/EmailIdMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSentMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/mysql/MySqlEventBusShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/TestAllWorksOnRabbitMqEventsPublished.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/StepsModuleInfrastructureTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/StepIdMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/StepTitleMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatementMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepTextMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/students/application/StudentResponseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/students/application/StudentsResponseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/students/domain/StudentIdMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/build.gradle create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/AggregateRoot.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/DomainError.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Identifier.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/IntValueObject.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Logger.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Monitoring.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Service.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/StringValueObject.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Utils.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/UuidGenerator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/VideoUrl.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/Command.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/CommandBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandlerExecutionError.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/CommandNotRegisteredError.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/event/DomainEvent.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/event/DomainEventSubscriber.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/event/EventBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/Query.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/QueryBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandlerExecutionError.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/QueryNotRegisteredError.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/Response.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/course/CourseCreatedDomainEvent.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/Criteria.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/Filter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/FilterField.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/FilterOperator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/FilterValue.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/Filters.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/Order.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/OrderBy.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/OrderType.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/JavaUuidGenerator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/command/CommandHandlersInformation.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/command/InMemoryCommandBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonDeserializer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonSerializer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscriberInformation.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventsInformation.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlDomainEventsConsumer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlEventBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqExchangeNameFormatter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqPublisher.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqQueueNameFormatter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/query/InMemoryQueryBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/query/QueryHandlersInformation.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/config/EnvironmentConfig.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/config/Parameter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/config/ParameterNotExist.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchClient.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchCriteriaConverter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateConfigurationFactory.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateCriteriaConverter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/hibernate/JsonListType.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/spring/ApiController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/spring/ApiExceptionMiddleware.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/ValidationResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/Validator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/ValidatorNotExist.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/FieldValidator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/NotEmptyValidator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/RequiredValidator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/StringValidator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/UuidValidator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/EmailMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/IntegerMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/ListMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/MotherCreator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/RandomElementPicker.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/UuidMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/VideoUrlMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/WordMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/infrastructure/UnitTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/var/log/.gitkeep create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.editorconfig create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.env create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.github/workflows/ci.yml create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.gitignore create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/Dockerfile create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/Makefile create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/README.md create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/.env create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/.no.env.local create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/application.properties create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/public/images/logo.png create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/master.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/pages/courses/courses.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/new_course_form.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/pages/home.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/partials/header.ftl create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/log4j2.properties create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/Starter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerPortCustomizer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/controller/courses/CoursesGetController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/middleware/BasicHttpAuthMiddleware.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/BackofficeFrontendApplication.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendServerPortCustomizer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesGetWebController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesPostWebController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/controller/home/HomeGetWebController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/MoocBackendApplication.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/command/ConsumeMySqlDomainEventsCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerPortCustomizer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/courses/CourseGetController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/resources/log4j2.properties create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/ApplicationTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/backoffice/BackofficeApplicationTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/MoocApplicationTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/courses/CourseGetControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/courses/CoursesPutControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutControllerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/build.gradle create mode 100755 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/docker-compose.ci.yml create mode 100755 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/docker-compose.yml create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/etc/http/backoffice_frontend.http create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/etc/http/publish_domain_events.http create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/gradle/wrapper/gradle-wrapper.jar create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/gradle/wrapper/gradle-wrapper.properties create mode 100755 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/gradlew create mode 100755 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/gradlew.bat create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/settings.gradle create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/application/store/DomainEventStorer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/application/store/StoreDomainEventOnOccurred.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEvent.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventAggregateId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventBody.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventName.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/DomainEventsRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/build.gradle create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/resources/database/backoffice.sql create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/resources/database/backoffice/backoffice_courses.json create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/UserAuthenticator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthPassword.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUser.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUsername.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthCredentials.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthUsername.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/infrastructure/persistence/InMemoryAuthRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCourseResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCoursesResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/create/CreateBackofficeCourseOnCourseCreated.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/AllBackofficeCoursesSearcher.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQuery.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/BackofficeCoursesByCriteriaSearcher.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQuery.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeElasticsearchConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeHibernateConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeRabbitMqEventBusConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/BackofficeContextInfrastructureTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/AuthModuleUnitTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandlerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthPasswordMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUserMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUsernameMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/ElasticsearchEnvironmentArranger.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseCriteriaMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/build.gradle create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/resources/database/mooc.sql create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/CourseResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/CoursesResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/create/CourseCreator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommandHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQuery.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/search_last/LastCoursesSearcher.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQuery.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/Course.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseDuration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseName.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseNotExist.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterFinder.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQuery.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreated.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterNotInitialized.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterTotal.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/hibernate/CoursesCounter.hbm.xml create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/NewCoursesNewsletterSender.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/Email.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/EmailId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/EmailSender.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSent.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/infrastructure/FakeEmailSender.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocHibernateConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocMySqlEventBusConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocRabbitMqEventBusConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/Step.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/StepId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/StepRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/StepTitle.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStep.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatement.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStep.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStepText.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/StudentResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/StudentsResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/search_all/AllStudentsSearcher.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQuery.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/domain/Student.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/domain/StudentId.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/domain/StudentRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/infrastructure/InMemoryStudentRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/MoocContextInfrastructureTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/CoursesModuleInfrastructureTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/CourseResponseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/CoursesResponseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandHandlerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseCreatedDomainEventMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseDurationMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseIdMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseNameMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleInfrastructureTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleUnitTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandlerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreatedShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterIdMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterTotalMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/application/NotificationsModuleUnitTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandlerShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/domain/EmailIdMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSentMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/mysql/MySqlEventBusShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/TestAllWorksOnRabbitMqEventsPublished.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/StepsModuleInfrastructureTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/StepIdMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/StepTitleMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatementMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepTextMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepositoryShould.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/students/application/StudentResponseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/students/application/StudentsResponseMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/students/domain/StudentIdMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/build.gradle create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/AggregateRoot.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/DomainError.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Identifier.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/IntValueObject.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Logger.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Monitoring.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Service.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/StringValueObject.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Utils.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/UuidGenerator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/VideoUrl.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/Command.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/CommandBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandlerExecutionError.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/CommandNotRegisteredError.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/event/DomainEvent.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/event/DomainEventSubscriber.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/event/EventBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/Query.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/QueryBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandler.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandlerExecutionError.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/QueryNotRegisteredError.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/Response.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/course/CourseCreatedDomainEvent.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/Criteria.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/Filter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/FilterField.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/FilterOperator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/FilterValue.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/Filters.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/Order.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/OrderBy.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/OrderType.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/JavaUuidGenerator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/command/CommandHandlersInformation.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/command/InMemoryCommandBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonDeserializer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonSerializer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscriberInformation.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventsInformation.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlDomainEventsConsumer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlEventBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqExchangeNameFormatter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqPublisher.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqQueueNameFormatter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/query/InMemoryQueryBus.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/query/QueryHandlersInformation.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/config/EnvironmentConfig.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/config/Parameter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/config/ParameterNotExist.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchClient.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchCriteriaConverter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateConfigurationFactory.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateCriteriaConverter.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/hibernate/JsonListType.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/spring/ApiController.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/spring/ApiExceptionMiddleware.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/ValidationResponse.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/Validator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/ValidatorNotExist.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/FieldValidator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/NotEmptyValidator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/RequiredValidator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/StringValidator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/UuidValidator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/EmailMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/IntegerMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/ListMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/MotherCreator.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/RandomElementPicker.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/UuidMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/VideoUrlMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/WordMother.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/infrastructure/UnitTestCase.java create mode 100644 02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/var/log/.gitkeep diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.editorconfig b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.editorconfig new file mode 100644 index 0000000..e6e6a12 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.env b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.env new file mode 100644 index 0000000..b934e03 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.env @@ -0,0 +1 @@ +# See apps/main/resources/.env diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.github/workflows/ci.yml b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.github/workflows/ci.yml new file mode 100644 index 0000000..8dcdf5b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.gitignore b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.gitignore new file mode 100644 index 0000000..8d89f10 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/.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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/Dockerfile b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/Dockerfile new file mode 100644 index 0000000..98f0b27 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/Makefile b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/Makefile new file mode 100644 index 0000000..4855cd1 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/README.md b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/README.md new file mode 100644 index 0000000..6c6bb26 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/.env b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/.env new file mode 100644 index 0000000..cd58c25 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/.no.env.local b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/.no.env.local new file mode 100644 index 0000000..8f100bc --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/application.properties b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/application.properties new file mode 100644 index 0000000..e439ebd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/application.properties @@ -0,0 +1 @@ +spring.main.allow-bean-definition-overriding=true diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/public/images/logo.png b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..759395922beee82928cbb06c3bfe0d415ceb70f5 GIT binary patch literal 4186 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 literal 0 HcmV?d00001 diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/master.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/master.ftl new file mode 100644 index 0000000..0a8bc23 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/pages/courses/courses.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/pages/courses/courses.ftl new file mode 100644 index 0000000..8b4c99c --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl new file mode 100644 index 0000000..aa64bee --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/new_course_form.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/new_course_form.ftl new file mode 100644 index 0000000..b7ecd5f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/pages/home.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/pages/home.ftl new file mode 100644 index 0000000..8ee5f2b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl new file mode 100644 index 0000000..27a640f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl @@ -0,0 +1,7 @@ +
+
+

+ 🤙 CodelyTV - El mejor backoffice de la historia +

+
+
diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/partials/header.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/partials/header.ftl new file mode 100644 index 0000000..3f2f738 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/backoffice_frontend/templates/partials/header.ftl @@ -0,0 +1,28 @@ +
+ +
diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/log4j2.properties b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/resources/log4j2.properties new file mode 100644 index 0000000..b577541 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/Starter.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/Starter.java new file mode 100644 index 0000000..64ad963 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java new file mode 100644 index 0000000..0a9374e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java new file mode 100644 index 0000000..018972f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerConfiguration.java new file mode 100644 index 0000000..80314c2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerPortCustomizer.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerPortCustomizer.java new file mode 100644 index 0000000..8c46d55 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/controller/courses/CoursesGetController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/controller/courses/CoursesGetController.java new file mode 100644 index 0000000..48bb8b4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetController.java new file mode 100644 index 0000000..bef9ec0 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/middleware/BasicHttpAuthMiddleware.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/backend/middleware/BasicHttpAuthMiddleware.java new file mode 100644 index 0000000..2ce6a90 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/BackofficeFrontendApplication.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/BackofficeFrontendApplication.java new file mode 100644 index 0000000..ae42787 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendServerPortCustomizer.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendServerPortCustomizer.java new file mode 100644 index 0000000..6cfbd81 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java new file mode 100644 index 0000000..62749d1 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesGetWebController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesGetWebController.java new file mode 100644 index 0000000..95ff91c --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesPostWebController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesPostWebController.java new file mode 100644 index 0000000..927dd41 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetController.java new file mode 100644 index 0000000..9c06c15 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/controller/home/HomeGetWebController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/backoffice/frontend/controller/home/HomeGetWebController.java new file mode 100644 index 0000000..254fba9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/MoocBackendApplication.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/MoocBackendApplication.java new file mode 100644 index 0000000..b9e0448 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/command/ConsumeMySqlDomainEventsCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/command/ConsumeMySqlDomainEventsCommand.java new file mode 100644 index 0000000..78eaf41 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java new file mode 100644 index 0000000..ce5d7bd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java new file mode 100644 index 0000000..3041e7b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerPortCustomizer.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerPortCustomizer.java new file mode 100644 index 0000000..3d61c27 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/courses/CourseGetController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/courses/CourseGetController.java new file mode 100644 index 0000000..838783b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java new file mode 100644 index 0000000..54bc204 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetController.java new file mode 100644 index 0000000..88a932f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetController.java new file mode 100644 index 0000000..43461f9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutController.java new file mode 100644 index 0000000..3fbcd68 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java new file mode 100644 index 0000000..dfc9657 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/resources/log4j2.properties b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/resources/log4j2.properties new file mode 100644 index 0000000..98427f2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/ApplicationTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/ApplicationTestCase.java new file mode 100644 index 0000000..d356818 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/backoffice/BackofficeApplicationTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/backoffice/BackofficeApplicationTestCase.java new file mode 100644 index 0000000..195b553 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetControllerShould.java new file mode 100644 index 0000000..add71d8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetControllerShould.java new file mode 100644 index 0000000..c5a9747 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/MoocApplicationTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/MoocApplicationTestCase.java new file mode 100644 index 0000000..90dc231 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/courses/CourseGetControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/courses/CourseGetControllerShould.java new file mode 100644 index 0000000..0ebbcc4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/courses/CoursesPutControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/courses/CoursesPutControllerShould.java new file mode 100644 index 0000000..9dcbe3d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetControllerShould.java new file mode 100644 index 0000000..2927678 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetControllerShould.java new file mode 100644 index 0000000..94069bf --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/apps/test/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutControllerShould.java new file mode 100644 index 0000000..8f011e9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/build.gradle b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/build.gradle new file mode 100644 index 0000000..508fb39 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/docker-compose.ci.yml b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/docker-compose.ci.yml new file mode 100755 index 0000000..d2c1c99 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/docker-compose.yml b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/docker-compose.yml new file mode 100755 index 0000000..216bacd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/etc/http/backoffice_frontend.http b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/etc/http/backoffice_frontend.http new file mode 100644 index 0000000..119764f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/etc/http/publish_domain_events.http b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/etc/http/publish_domain_events.http new file mode 100644 index 0000000..309c2fd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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": "c3a11f1d-512e-420b-aeae-e687a3c449aa", + "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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/gradle/wrapper/gradle-wrapper.jar b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7f93135c49b765f8051ef9d0a6055ff8e46073d8 GIT binary patch literal 63721 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鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc literal 0 HcmV?d00001 diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/gradle/wrapper/gradle-wrapper.properties b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3fa8f86 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/gradlew b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/gradlew.bat b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/gradlew.bat new file mode 100755 index 0000000..93e3f59 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/settings.gradle b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/settings.gradle new file mode 100644 index 0000000..4823818 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/application/store/DomainEventStorer.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/application/store/DomainEventStorer.java new file mode 100644 index 0000000..674c72e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/application/store/StoreDomainEventOnOccurred.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/application/store/StoreDomainEventOnOccurred.java new file mode 100644 index 0000000..23413ef --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEvent.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEvent.java new file mode 100644 index 0000000..c2f310e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventAggregateId.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventAggregateId.java new file mode 100644 index 0000000..beef90d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventBody.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventBody.java new file mode 100644 index 0000000..a706b7a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventId.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventId.java new file mode 100644 index 0000000..5f730dd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventName.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventName.java new file mode 100644 index 0000000..d4a018e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/DomainEventsRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/analytics/main/tv/codely/analytics/domain_events/domain/DomainEventsRepository.java new file mode 100644 index 0000000..c04ac90 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/build.gradle b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/build.gradle new file mode 100644 index 0000000..7d82dc7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/resources/database/backoffice.sql b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/resources/database/backoffice.sql new file mode 100644 index 0000000..58f92fa --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/resources/database/backoffice/backoffice_courses.json b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/resources/database/backoffice/backoffice_courses.json new file mode 100644 index 0000000..7891e8b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommand.java new file mode 100644 index 0000000..5e1e74c --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandler.java new file mode 100644 index 0000000..ec43db6 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/UserAuthenticator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/UserAuthenticator.java new file mode 100644 index 0000000..1b43c79 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthPassword.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthPassword.java new file mode 100644 index 0000000..331588f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthRepository.java new file mode 100644 index 0000000..37152ec --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUser.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUser.java new file mode 100644 index 0000000..af0d45e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUsername.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUsername.java new file mode 100644 index 0000000..6d1d48a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthCredentials.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthCredentials.java new file mode 100644 index 0000000..3092104 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthUsername.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthUsername.java new file mode 100644 index 0000000..857c10f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/infrastructure/persistence/InMemoryAuthRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/auth/infrastructure/persistence/InMemoryAuthRepository.java new file mode 100644 index 0000000..e873c4f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCourseResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCourseResponse.java new file mode 100644 index 0000000..5871ac9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCoursesResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCoursesResponse.java new file mode 100644 index 0000000..1225864 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java new file mode 100644 index 0000000..496b8cb --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java @@ -0,0 +1,20 @@ +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) { + if (this.repository.search(id).isEmpty()) { + this.repository.save(new BackofficeCourse(id, name, duration)); + } + } +} diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/create/CreateBackofficeCourseOnCourseCreated.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/create/CreateBackofficeCourseOnCourseCreated.java new file mode 100644 index 0000000..fd06214 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java new file mode 100644 index 0000000..e50c42f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java new file mode 100644 index 0000000..c92e48d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/AllBackofficeCoursesSearcher.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/AllBackofficeCoursesSearcher.java new file mode 100644 index 0000000..db65ef0 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQuery.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQuery.java new file mode 100644 index 0000000..fc00a52 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQueryHandler.java new file mode 100644 index 0000000..b500126 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/BackofficeCoursesByCriteriaSearcher.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/BackofficeCoursesByCriteriaSearcher.java new file mode 100644 index 0000000..1533079 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQuery.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQuery.java new file mode 100644 index 0000000..2671661 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQueryHandler.java new file mode 100644 index 0000000..8bf9363 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java new file mode 100644 index 0000000..05f88b1 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java new file mode 100644 index 0000000..f5596ff --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java new file mode 100644 index 0000000..3689352 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java new file mode 100644 index 0000000..c97dd85 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java new file mode 100644 index 0000000..b645f5b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java new file mode 100644 index 0000000..a23fd77 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml new file mode 100644 index 0000000..a8592d0 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeElasticsearchConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeElasticsearchConfiguration.java new file mode 100644 index 0000000..98c0f1a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeHibernateConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeHibernateConfiguration.java new file mode 100644 index 0000000..0615ff7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java new file mode 100644 index 0000000..1113298 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeRabbitMqEventBusConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeRabbitMqEventBusConfiguration.java new file mode 100644 index 0000000..b0fae3c --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/BackofficeContextInfrastructureTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/BackofficeContextInfrastructureTestCase.java new file mode 100644 index 0000000..efbad4e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/AuthModuleUnitTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/AuthModuleUnitTestCase.java new file mode 100644 index 0000000..4ccc749 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandlerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandlerShould.java new file mode 100644 index 0000000..a37020e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandMother.java new file mode 100644 index 0000000..42746f5 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthPasswordMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthPasswordMother.java new file mode 100644 index 0000000..8e4f0ab --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUserMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUserMother.java new file mode 100644 index 0000000..14e26c9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUsernameMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUsernameMother.java new file mode 100644 index 0000000..e96670c --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/ElasticsearchEnvironmentArranger.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/ElasticsearchEnvironmentArranger.java new file mode 100644 index 0000000..08df2f5 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseCriteriaMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseCriteriaMother.java new file mode 100644 index 0000000..ee83f69 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseMother.java new file mode 100644 index 0000000..76a1b4b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java new file mode 100644 index 0000000..f94748b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepositoryShould.java new file mode 100644 index 0000000..e82dffb --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepositoryShould.java new file mode 100644 index 0000000..8d9d5ce --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/build.gradle b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/build.gradle new file mode 100644 index 0000000..7d82dc7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/resources/database/mooc.sql b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/resources/database/mooc.sql new file mode 100644 index 0000000..6cc6def --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/CourseResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/CourseResponse.java new file mode 100644 index 0000000..22c8798 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/CoursesResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/CoursesResponse.java new file mode 100644 index 0000000..cd39f43 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/create/CourseCreator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/create/CourseCreator.java new file mode 100644 index 0000000..dbbec32 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommand.java new file mode 100644 index 0000000..5f6f783 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommandHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommandHandler.java new file mode 100644 index 0000000..85127f4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java new file mode 100644 index 0000000..b912a3e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQuery.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQuery.java new file mode 100644 index 0000000..187e5e0 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQueryHandler.java new file mode 100644 index 0000000..bc8d380 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/search_last/LastCoursesSearcher.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/search_last/LastCoursesSearcher.java new file mode 100644 index 0000000..1e0f6fa --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQuery.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQuery.java new file mode 100644 index 0000000..834a847 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryHandler.java new file mode 100644 index 0000000..91bea0f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/Course.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/Course.java new file mode 100644 index 0000000..ef44a89 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseDuration.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseDuration.java new file mode 100644 index 0000000..42c4482 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseId.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseId.java new file mode 100644 index 0000000..dd6c3c2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseName.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseName.java new file mode 100644 index 0000000..67e0a06 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseNotExist.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseNotExist.java new file mode 100644 index 0000000..d3490be --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/domain/CourseRepository.java new file mode 100644 index 0000000..4d2e696 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepository.java new file mode 100644 index 0000000..622d60a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepository.java new file mode 100644 index 0000000..b90c0cf --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml new file mode 100644 index 0000000..88809da --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterFinder.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterFinder.java new file mode 100644 index 0000000..e0ccfe7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponse.java new file mode 100644 index 0000000..a4ee91a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQuery.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQuery.java new file mode 100644 index 0000000..7586b4b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandler.java new file mode 100644 index 0000000..1eefb82 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java new file mode 100644 index 0000000..87aec94 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java @@ -0,0 +1,27 @@ +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())); + + counter.increment(id); + + repository.save(counter); + } +} diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreated.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreated.java new file mode 100644 index 0000000..f8465fb --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounter.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounter.java new file mode 100644 index 0000000..2a13583 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterId.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterId.java new file mode 100644 index 0000000..3899c15 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterNotInitialized.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterNotInitialized.java new file mode 100644 index 0000000..b20ca87 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterRepository.java new file mode 100644 index 0000000..ccbbb71 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterTotal.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterTotal.java new file mode 100644 index 0000000..65a299d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepository.java new file mode 100644 index 0000000..f9cb245 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/hibernate/CoursesCounter.hbm.xml b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/hibernate/CoursesCounter.hbm.xml new file mode 100644 index 0000000..8ea7482 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/NewCoursesNewsletterSender.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/NewCoursesNewsletterSender.java new file mode 100644 index 0000000..0e2ea20 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommand.java new file mode 100644 index 0000000..51f6c52 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandler.java new file mode 100644 index 0000000..2c6dad7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/Email.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/Email.java new file mode 100644 index 0000000..120feff --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/EmailId.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/EmailId.java new file mode 100644 index 0000000..dd03323 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/EmailSender.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/EmailSender.java new file mode 100644 index 0000000..b393b2d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletter.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletter.java new file mode 100644 index 0000000..0fc60cd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSent.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSent.java new file mode 100644 index 0000000..b03f209 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/infrastructure/FakeEmailSender.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/notifications/infrastructure/FakeEmailSender.java new file mode 100644 index 0000000..17501f3 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocHibernateConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocHibernateConfiguration.java new file mode 100644 index 0000000..88f915d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocMySqlEventBusConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocMySqlEventBusConfiguration.java new file mode 100644 index 0000000..dc8f346 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocRabbitMqEventBusConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocRabbitMqEventBusConfiguration.java new file mode 100644 index 0000000..df81787 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/Step.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/Step.java new file mode 100644 index 0000000..838d110 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/StepId.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/StepId.java new file mode 100644 index 0000000..7f5d07b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/StepRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/StepRepository.java new file mode 100644 index 0000000..419f53d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/StepTitle.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/StepTitle.java new file mode 100644 index 0000000..b1a14a5 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStep.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStep.java new file mode 100644 index 0000000..9d2be39 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatement.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatement.java new file mode 100644 index 0000000..bfc8a40 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStep.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStep.java new file mode 100644 index 0000000..81f6c2d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStepText.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStepText.java new file mode 100644 index 0000000..99e36c9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepository.java new file mode 100644 index 0000000..6b4b103 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml new file mode 100644 index 0000000..31cd919 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/StudentResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/StudentResponse.java new file mode 100644 index 0000000..9b8bc05 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/StudentsResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/StudentsResponse.java new file mode 100644 index 0000000..9a7e035 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/search_all/AllStudentsSearcher.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/search_all/AllStudentsSearcher.java new file mode 100644 index 0000000..a079520 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQuery.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQuery.java new file mode 100644 index 0000000..618a9fa --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryHandler.java new file mode 100644 index 0000000..c9e3fa4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/domain/Student.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/domain/Student.java new file mode 100644 index 0000000..eb0353d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/domain/StudentId.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/domain/StudentId.java new file mode 100644 index 0000000..3e6735e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/domain/StudentRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/domain/StudentRepository.java new file mode 100644 index 0000000..0fbd2e6 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/infrastructure/InMemoryStudentRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/main/tv/codely/mooc/students/infrastructure/InMemoryStudentRepository.java new file mode 100644 index 0000000..aabd3ab --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/MoocContextInfrastructureTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/MoocContextInfrastructureTestCase.java new file mode 100644 index 0000000..eb574c4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/CoursesModuleInfrastructureTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/CoursesModuleInfrastructureTestCase.java new file mode 100644 index 0000000..a52cf56 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java new file mode 100644 index 0000000..eb7454d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/CourseResponseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/CourseResponseMother.java new file mode 100644 index 0000000..564e527 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/CoursesResponseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/CoursesResponseMother.java new file mode 100644 index 0000000..1067e22 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandHandlerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandHandlerShould.java new file mode 100644 index 0000000..efdd3f7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandMother.java new file mode 100644 index 0000000..f4dad05 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryMother.java new file mode 100644 index 0000000..c589326 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseCreatedDomainEventMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseCreatedDomainEventMother.java new file mode 100644 index 0000000..e73f2d4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseDurationMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseDurationMother.java new file mode 100644 index 0000000..d9ed986 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseIdMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseIdMother.java new file mode 100644 index 0000000..76bf0d9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseMother.java new file mode 100644 index 0000000..20f225e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseNameMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/domain/CourseNameMother.java new file mode 100644 index 0000000..1ca25f8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepositoryShould.java new file mode 100644 index 0000000..04e3d6a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepositoryShould.java new file mode 100644 index 0000000..45b8a1b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleInfrastructureTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleInfrastructureTestCase.java new file mode 100644 index 0000000..18978bb --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleUnitTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleUnitTestCase.java new file mode 100644 index 0000000..8c06e7f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponseMother.java new file mode 100644 index 0000000..4b38949 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandlerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandlerShould.java new file mode 100644 index 0000000..1065514 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreatedShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreatedShould.java new file mode 100644 index 0000000..13d6218 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterIdMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterIdMother.java new file mode 100644 index 0000000..596a118 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterMother.java new file mode 100644 index 0000000..9e5a27f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterTotalMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterTotalMother.java new file mode 100644 index 0000000..b8c22cb --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepositoryShould.java new file mode 100644 index 0000000..fabdfe8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/application/NotificationsModuleUnitTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/application/NotificationsModuleUnitTestCase.java new file mode 100644 index 0000000..6b12480 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandlerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandlerShould.java new file mode 100644 index 0000000..8134c05 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandMother.java new file mode 100644 index 0000000..f91caff --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/domain/EmailIdMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/domain/EmailIdMother.java new file mode 100644 index 0000000..0211444 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSentMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSentMother.java new file mode 100644 index 0000000..97afd0f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterMother.java new file mode 100644 index 0000000..a5ff7f5 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/mysql/MySqlEventBusShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/mysql/MySqlEventBusShould.java new file mode 100644 index 0000000..17dc703 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java new file mode 100644 index 0000000..70c8de2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/TestAllWorksOnRabbitMqEventsPublished.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/TestAllWorksOnRabbitMqEventsPublished.java new file mode 100644 index 0000000..8b1981d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/StepsModuleInfrastructureTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/StepsModuleInfrastructureTestCase.java new file mode 100644 index 0000000..cc0c90d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/StepIdMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/StepIdMother.java new file mode 100644 index 0000000..7747f37 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/StepTitleMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/StepTitleMother.java new file mode 100644 index 0000000..571bc13 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepMother.java new file mode 100644 index 0000000..46fcb75 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatementMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatementMother.java new file mode 100644 index 0000000..8e85970 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepMother.java new file mode 100644 index 0000000..3cb9432 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepTextMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepTextMother.java new file mode 100644 index 0000000..7dca6fa --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepositoryShould.java new file mode 100644 index 0000000..88c684e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/students/application/StudentResponseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/students/application/StudentResponseMother.java new file mode 100644 index 0000000..b29fb25 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/students/application/StudentsResponseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/students/application/StudentsResponseMother.java new file mode 100644 index 0000000..89e879a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryMother.java new file mode 100644 index 0000000..93a0e28 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/students/domain/StudentIdMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/mooc/test/tv/codely/mooc/students/domain/StudentIdMother.java new file mode 100644 index 0000000..b98ca0e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/build.gradle b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/build.gradle new file mode 100644 index 0000000..7d82dc7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/AggregateRoot.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/AggregateRoot.java new file mode 100644 index 0000000..796e327 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/DomainError.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/DomainError.java new file mode 100644 index 0000000..1d65456 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Identifier.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Identifier.java new file mode 100644 index 0000000..b25970b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/IntValueObject.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/IntValueObject.java new file mode 100644 index 0000000..4e943e5 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Logger.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Logger.java new file mode 100644 index 0000000..efeab9f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Monitoring.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Monitoring.java new file mode 100644 index 0000000..0958579 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Service.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Service.java new file mode 100644 index 0000000..d3f9566 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/StringValueObject.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/StringValueObject.java new file mode 100644 index 0000000..45525e5 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Utils.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/Utils.java new file mode 100644 index 0000000..a3a56a6 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/UuidGenerator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/UuidGenerator.java new file mode 100644 index 0000000..8348a24 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/VideoUrl.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/VideoUrl.java new file mode 100644 index 0000000..47aaccc --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/Command.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/Command.java new file mode 100644 index 0000000..da5a342 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/CommandBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/CommandBus.java new file mode 100644 index 0000000..dabddf2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandler.java new file mode 100644 index 0000000..177e09b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandlerExecutionError.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandlerExecutionError.java new file mode 100644 index 0000000..60d0d74 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/CommandNotRegisteredError.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/command/CommandNotRegisteredError.java new file mode 100644 index 0000000..2d3af85 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/event/DomainEvent.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/event/DomainEvent.java new file mode 100644 index 0000000..901955c --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/event/DomainEventSubscriber.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/event/DomainEventSubscriber.java new file mode 100644 index 0000000..9895464 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/event/EventBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/event/EventBus.java new file mode 100644 index 0000000..cd13e0b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/Query.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/Query.java new file mode 100644 index 0000000..cdc5477 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/QueryBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/QueryBus.java new file mode 100644 index 0000000..197945f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandler.java new file mode 100644 index 0000000..4b56bd8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandlerExecutionError.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandlerExecutionError.java new file mode 100644 index 0000000..2e5135b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/QueryNotRegisteredError.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/QueryNotRegisteredError.java new file mode 100644 index 0000000..d2d380f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/Response.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/bus/query/Response.java new file mode 100644 index 0000000..ac3eefc --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/course/CourseCreatedDomainEvent.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/course/CourseCreatedDomainEvent.java new file mode 100644 index 0000000..23181f0 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java new file mode 100644 index 0000000..2fb4097 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/Criteria.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/Criteria.java new file mode 100644 index 0000000..aea18af --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/Filter.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/Filter.java new file mode 100644 index 0000000..b3244a6 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/FilterField.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/FilterField.java new file mode 100644 index 0000000..c5f230d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/FilterOperator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/FilterOperator.java new file mode 100644 index 0000000..8614f12 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/FilterValue.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/FilterValue.java new file mode 100644 index 0000000..28a4f48 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/Filters.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/Filters.java new file mode 100644 index 0000000..83e36ae --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/Order.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/Order.java new file mode 100644 index 0000000..f95eaf8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/OrderBy.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/OrderBy.java new file mode 100644 index 0000000..2c1450d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/OrderType.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/domain/criteria/OrderType.java new file mode 100644 index 0000000..52deae0 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/JavaUuidGenerator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/JavaUuidGenerator.java new file mode 100644 index 0000000..21667a8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/command/CommandHandlersInformation.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/command/CommandHandlersInformation.java new file mode 100644 index 0000000..5bdd8fc --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/command/InMemoryCommandBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/command/InMemoryCommandBus.java new file mode 100644 index 0000000..d8b23c7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonDeserializer.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonDeserializer.java new file mode 100644 index 0000000..ccfa660 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonSerializer.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonSerializer.java new file mode 100644 index 0000000..adbedb9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscriberInformation.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscriberInformation.java new file mode 100644 index 0000000..e14d3d2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java new file mode 100644 index 0000000..88556f1 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventsInformation.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventsInformation.java new file mode 100644 index 0000000..9498913 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlDomainEventsConsumer.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlDomainEventsConsumer.java new file mode 100644 index 0000000..263da44 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlEventBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlEventBus.java new file mode 100644 index 0000000..231ab21 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java new file mode 100644 index 0000000..d1620f3 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java @@ -0,0 +1,138 @@ +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); + + System.out.println("ACK: Consumed correctly!"); + } catch (Exception error) { + System.out.println("Error consuming"); + + 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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java new file mode 100644 index 0000000..2bad6fd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java new file mode 100644 index 0000000..f95e188 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqExchangeNameFormatter.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqExchangeNameFormatter.java new file mode 100644 index 0000000..f399cac --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqPublisher.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqPublisher.java new file mode 100644 index 0000000..61f7fe4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqQueueNameFormatter.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqQueueNameFormatter.java new file mode 100644 index 0000000..13c091f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java new file mode 100644 index 0000000..56a6205 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/query/InMemoryQueryBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/query/InMemoryQueryBus.java new file mode 100644 index 0000000..8d448c6 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/query/QueryHandlersInformation.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/bus/query/QueryHandlersInformation.java new file mode 100644 index 0000000..e9d7606 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java new file mode 100644 index 0000000..dd90a88 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/config/EnvironmentConfig.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/config/EnvironmentConfig.java new file mode 100644 index 0000000..c9d898f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/config/Parameter.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/config/Parameter.java new file mode 100644 index 0000000..ac521bb --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/config/ParameterNotExist.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/config/ParameterNotExist.java new file mode 100644 index 0000000..aa329d1 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchClient.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchClient.java new file mode 100644 index 0000000..78a458f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchCriteriaConverter.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchCriteriaConverter.java new file mode 100644 index 0000000..99e2585 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java new file mode 100644 index 0000000..c87903b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateConfigurationFactory.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateConfigurationFactory.java new file mode 100644 index 0000000..2dbd1ba --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateCriteriaConverter.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateCriteriaConverter.java new file mode 100644 index 0000000..5679bbd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java new file mode 100644 index 0000000..b6ae61e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/hibernate/JsonListType.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/hibernate/JsonListType.java new file mode 100644 index 0000000..188dd98 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/spring/ApiController.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/spring/ApiController.java new file mode 100644 index 0000000..fafc2a8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/spring/ApiExceptionMiddleware.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/spring/ApiExceptionMiddleware.java new file mode 100644 index 0000000..ac1db97 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/ValidationResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/ValidationResponse.java new file mode 100644 index 0000000..6137031 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/Validator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/Validator.java new file mode 100644 index 0000000..d08cc93 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/ValidatorNotExist.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/ValidatorNotExist.java new file mode 100644 index 0000000..c75d631 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/FieldValidator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/FieldValidator.java new file mode 100644 index 0000000..65ace91 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/NotEmptyValidator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/NotEmptyValidator.java new file mode 100644 index 0000000..106e7ff --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/RequiredValidator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/RequiredValidator.java new file mode 100644 index 0000000..862b5a2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/StringValidator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/StringValidator.java new file mode 100644 index 0000000..f653ac7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/UuidValidator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/main/tv/codely/shared/infrastructure/validation/validators/UuidValidator.java new file mode 100644 index 0000000..c39d679 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/EmailMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/EmailMother.java new file mode 100644 index 0000000..b8d4077 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/IntegerMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/IntegerMother.java new file mode 100644 index 0000000..fe197ce --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/ListMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/ListMother.java new file mode 100644 index 0000000..5dc7622 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/MotherCreator.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/MotherCreator.java new file mode 100644 index 0000000..eef2ac4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/RandomElementPicker.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/RandomElementPicker.java new file mode 100644 index 0000000..a69497e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/UuidMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/UuidMother.java new file mode 100644 index 0000000..1075dd7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/VideoUrlMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/VideoUrlMother.java new file mode 100644 index 0000000..1588ec6 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/WordMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/domain/WordMother.java new file mode 100644 index 0000000..4bad17a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java new file mode 100644 index 0000000..af93e67 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/infrastructure/UnitTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/src/shared/test/tv/codely/shared/infrastructure/UnitTestCase.java new file mode 100644 index 0000000..96e007e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/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/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/var/log/.gitkeep b/02-duplicated_events/3-counter_handle_duplicated_events/1-counter_incrementing_always/var/log/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.editorconfig b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.editorconfig new file mode 100644 index 0000000..e6e6a12 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.env b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.env new file mode 100644 index 0000000..b934e03 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.env @@ -0,0 +1 @@ +# See apps/main/resources/.env diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.github/workflows/ci.yml b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.github/workflows/ci.yml new file mode 100644 index 0000000..8dcdf5b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.gitignore b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.gitignore new file mode 100644 index 0000000..8d89f10 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/.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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/Dockerfile b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/Dockerfile new file mode 100644 index 0000000..98f0b27 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/Makefile b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/Makefile new file mode 100644 index 0000000..4855cd1 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/README.md b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/README.md new file mode 100644 index 0000000..6c6bb26 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/.env b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/.env new file mode 100644 index 0000000..cd58c25 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/.no.env.local b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/.no.env.local new file mode 100644 index 0000000..8f100bc --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/application.properties b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/application.properties new file mode 100644 index 0000000..e439ebd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/application.properties @@ -0,0 +1 @@ +spring.main.allow-bean-definition-overriding=true diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/public/images/logo.png b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/public/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..759395922beee82928cbb06c3bfe0d415ceb70f5 GIT binary patch literal 4186 zcmV-g5T);lP){7FPXRCwC#T~CM{Ss8z`EP{BL2`GA4 z?PA0|NG~bLE(oIOVD>08i@M&N$t6(~(u;>(5H>yP!37~56k&5o=cMc^+X3~GNV>gv z*d3{62lte*hFt_tnFhs!i}Cx$uhz0s_5RiOs%j?R2anFAtLwdb^}gTxec$`u_f{S} zc%Vc?L`05*JSJTrA|fJyB_bjsB7h|#A|ik#A|fIpfF&X#BEln&yIlOtdsoiD$N9p4 z-+$|qKe*m>!=HZl zp?#^(ePorrSy{2^a004Vp`5A04*;#*rWy@kh3$B&;%jWjC0pR)0Ib5`R~(9PjDaeG z4+N*x!dFY^i?t54ZwMb(OTM}Z?b~h(ut3MK-)2yH4R^@_lo<5)kaVl6e^7Sdx)HN3 zW1o*p#tX_E^myRJu%IJP!N+YVr!<$HYPgei^&yzO2JL@x3ilWh=$%xFH8Xh^^aK)OjdEn63DFD>C!c06J&?FrBelLJk zE>laeniH_iN)#8BW7Fb3Lna2<219gkALvU^Q{QFZ^bt^l?bX9SC1)R zCC8|%2Reqr6x11NrBFu(4gjo7WT6q0@J~>lgz`QN>TB?M1r+!n9UTlhKf>Gxta37~ zFb$VB3PXUfNdq>j@G)c#SFCGV$C*zi)V86%tnH?HW)AhnROdid?<6>&db8)WQPL_U zTUPy^`k)AO&if+w_V)7oDn9^)e!uSnF2R8pK@Zr_foAe{W?00odNnMY#sKTdTwULT z@>LiJ-+`C@yAA@@M%@l|0IXX!3P+&=BV`v@*rRe3Sr5NOU)#_n){B4<>$3@xU8TBA zZLz^ZC*-rT^JeAo@AX`65Y zmO+0Oz^<&wKpQ?R?C{^hOaCR5Pjtwn(G8vh7@)NV1O5I5G%KN;g1a8Pot7SDAS}R}rl6edt#H=2bzWzJRqJ5G3$n zFm25rU{LjI-s^tNc1I1(TsFR=nc|JGU)2p70*)Jj9x2h(Ib#OM(-pI|&%3PxhlOop zz#NPe??Qy`Inei%gnn-xX5rBAj-5h!9y1{N`4YtsvC;VpPf3;5S zuR9ZR`vz=meH*YkAU}w0jMJ8eX-pm3w@_}G7peyS!~(3)&$p8+*DL^I=)#^grhdf0 zVxk&=;CjCW=OCoj##$@e#IAIJgj;Z&`r+{skjk=(B2Zl{`OWKr6!k+;z7OSjP^@pm zK>EGo@3CD!F8tqZcpl7#dX!zlkkqd!-++!?p<-P&fD{7SLymf4Eco4q!R(!sn1Vge zIwKmmLruMD$#a@O8G&h}@$U^(^=^b!+EhNXn2dA^$q{5aUqS!hGO&6Hls@$4=g%O};0hO%kJ|o-+VxV&R~PAdUNo3QRkCbwuiv(AHo`$t z%3Oe$MmAK4`+8Ge;8ENWF2^bPb2BWJJW8JhXn{-bXXSNetGAzK%80tOfYDj~_ zv&J>q6>cF)IRNWag?|Xa>Xn8R7jxEp6Uv`q@O;Suv|b_5x^MYhp{TpB?`liK1Z=|- zh7NT`8m6;wNEvL+V4zOj{>mNOSU1O2Ti28~5fhVPi~I*mVa;Gdhj3feSt%SD*p=6Q zN88moRQB_fF1bnhn88FUGiQA=q;??+S~shYY-6b$+2Fu1;6gB%O^!kA5USzxZX+hP zeVP$mOwSv_3h;8gQ|e=#-i~}D^2-3L%H%`8*6oG?>wPG{a=89ARh?;c-*YfDWe)0= z(I*YpiHbR8P|U;uB&Uwa3`dsl%;((>>P}u1-Of3tw1DMfUYUgMx%6n0?%f>J$tF<- zSZGo`3*}OYlAi$MhE%lVG#eql3iiD)=NO@5siZ1f*zBvBm~<@7zcLRuwteTZM#ZGaHY9Y2I{6D z)3N15Fs}kc#S=SK`KOj~lAKq^Khv1Sy8Iuq{tEd4O~y^ub#C^4TS$A0Bv+boPZCudu7 zTO_uiY;mx}sBU9yobLA<(AmF2`5FwiuXuu0opwhp4Fp>yabVKDr9TYm1?oQxG%Y)U$Y&ymnAkRFcGI{9sMHb*XMRlV%4Du@bF`<;6v=ob@+BB# ze{2~vgRT(fqQMi*DwuAyE_JBOg;CU2Ob=6*^XxrrC$41b4)O3Y?`p?R?d|Oim?uHq za~yD;#H?dS7Vl1H?%m{C#cTtx{t4xWP<{df?O&>WSqlYN1Xf9=)bKY(p-)Oe2Dr=z z7e>KRr*(aX)ywCo-Y8efkYYR@Bfb};h zAE$rgs^hm{u-sF}Lk2a?!byRF;8OesDDMH8FsXrgZ~|6h$##-J^=T&t>I-h=^H~Mu z$R1G%l>L668?c&iTO@`FpM48Xn}Br=%9o++cQnhW39x+G)ldTpJyo(1xq?w}w0G$c zl$kOpQdxbKeHriwFs_L}2SQvxsgs^3m^1-cDBBu#HRfzcaHl&A#&@9ntixcXo1xAc z!XB!?3SZz=%_U3nzbK#kYs}9LOY>~Nq?l0~N`(CjuZ0Yuf4!Qkna?F=`5KU>ru8ktCQGRh#GQ52L@zi7HdZ=Ed&uKMvU~Vn|>cU`B zP5Z4gFBCGPBJ;S5eO{7jG-75YS?1QtwxYA0a=0JYq^Ma23$`~lJgi2F@wxJ9SxuM3IfwL?IQ=$o`sS{b{ z#FVMQV^gtCSrKp&-HNE;P}<9M|FNYBIx{Gy&Xj+H`fg4jl`$$C-xv3moJE6r;+o1j z$l<_`JYXRZ(CIR!{OKQ@4x>?alPoJ((?SC{TV#$*3rfF6N@>bYI;xrhHNKH8ViHqT zzP%<=jtLJDB;cOQ3dP2DBm~Y=Vt^^twdODx*eJRwY-3`nn9d;@{eFMuF=o1Qo}x=# z7$iCk(Fh;FI!_yokG8!${qD>A(764kfd=ZutvIKkE%+zyBRpT@c0&(^@Bynqg@f5- zMy@Kq11az<#j(aryd0oT9fO%_?zu|sx$IHpLOr~I5qX+rQv&%O^ZWPR!O?|GBCd6f zLN&t1hgFut9S@GUP30bd$9jVur>MTSa&36kvfY&|OEE>_dVp1yK8Bm#FO)tj=UC}e zhlEhfVSqZ1SZj&N?3Ig(?z0)7ozCFWQqsOsaoARKi(?B;9l+YPxTaCSvaD~{<%Sf@ z`CNd3ioTA>V(EUHvF?Q`->Q;--ZYS(VbtKnP};YiN&E8cOvv2koJ6?I!KN9Ovh0sx z$Oaez>&+*2kf~f($4jnLyIoD0DGic2)60Dh?m4iDfnrh;1UyVL99!0j%fHB@QV40rbT8Kn~d9m_g zLC0L8G&AR^D5#s(I1*AQvj2?<$j(e1|`Zva+&bye7AS zIWC#KQ8|wGxVeIbjZoM^RHA$dDb0+zzBu=+c{$A0nK0LxJ>Q_II=Q3(Sg5d9%C778 z`zt>8uLvAH%+B+(8NZxDIj_nXY~m9n>WV)CSPj2$k&`6_z{1I+EZkuMfE7z`HDnTs zh&)PSnG)##1i)%dRmWz{q*n#7L`1lOD)U=reww+m->Rfr9TULfvWUba6q8Wo2&~Yd zT(-z@PUj~?{h@??cPlQzQ=TK~VIGmq&msa=|6P@>T9KVns-Uvpt&yYsuIBa=>0x15 z!mvcR0n3zO17occ^}kpoWL2h3&%&+*umrF~f`C@4`1cQ<^W5+(A||1Th*r3_lw1(7 z62KA>2_m#*=mNv7mIPK4=~V$N5s{Ffg&S$wZaWoJ^%m!07*qoM6N<$g25l>LjV8( literal 0 HcmV?d00001 diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/master.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/master.ftl new file mode 100644 index 0000000..0a8bc23 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/pages/courses/courses.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/pages/courses/courses.ftl new file mode 100644 index 0000000..8b4c99c --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/list_courses.ftl new file mode 100644 index 0000000..aa64bee --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/new_course_form.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/pages/courses/partials/new_course_form.ftl new file mode 100644 index 0000000..b7ecd5f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/pages/home.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/pages/home.ftl new file mode 100644 index 0000000..8ee5f2b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl new file mode 100644 index 0000000..27a640f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/partials/footer.ftl @@ -0,0 +1,7 @@ +
+
+

+ 🤙 CodelyTV - El mejor backoffice de la historia +

+
+
diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/partials/header.ftl b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/partials/header.ftl new file mode 100644 index 0000000..3f2f738 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/backoffice_frontend/templates/partials/header.ftl @@ -0,0 +1,28 @@ +
+ +
diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/log4j2.properties b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/resources/log4j2.properties new file mode 100644 index 0000000..b577541 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/Starter.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/Starter.java new file mode 100644 index 0000000..64ad963 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/BackofficeBackendApplication.java new file mode 100644 index 0000000..0a9374e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/command/ConsumeRabbitMqDomainEventsCommand.java new file mode 100644 index 0000000..018972f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerConfiguration.java new file mode 100644 index 0000000..80314c2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerPortCustomizer.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/config/BackofficeBackendServerPortCustomizer.java new file mode 100644 index 0000000..8c46d55 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/controller/courses/CoursesGetController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/controller/courses/CoursesGetController.java new file mode 100644 index 0000000..48bb8b4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetController.java new file mode 100644 index 0000000..bef9ec0 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/middleware/BasicHttpAuthMiddleware.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/backend/middleware/BasicHttpAuthMiddleware.java new file mode 100644 index 0000000..2ce6a90 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/BackofficeFrontendApplication.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/BackofficeFrontendApplication.java new file mode 100644 index 0000000..ae42787 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendServerPortCustomizer.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendServerPortCustomizer.java new file mode 100644 index 0000000..6cfbd81 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/config/BackofficeFrontendWebConfig.java new file mode 100644 index 0000000..62749d1 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesGetWebController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesGetWebController.java new file mode 100644 index 0000000..95ff91c --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesPostWebController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/controller/courses/CoursesPostWebController.java new file mode 100644 index 0000000..927dd41 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetController.java new file mode 100644 index 0000000..9c06c15 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/controller/home/HomeGetWebController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/backoffice/frontend/controller/home/HomeGetWebController.java new file mode 100644 index 0000000..254fba9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/MoocBackendApplication.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/MoocBackendApplication.java new file mode 100644 index 0000000..b9e0448 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/command/ConsumeMySqlDomainEventsCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/command/ConsumeMySqlDomainEventsCommand.java new file mode 100644 index 0000000..78eaf41 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/command/ConsumeRabbitMqDomainEventsCommand.java new file mode 100644 index 0000000..ce5d7bd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerConfiguration.java new file mode 100644 index 0000000..3041e7b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerPortCustomizer.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/config/MoocBackendServerPortCustomizer.java new file mode 100644 index 0000000..3d61c27 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/courses/CourseGetController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/courses/CourseGetController.java new file mode 100644 index 0000000..838783b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/courses/CoursesPutController.java new file mode 100644 index 0000000..54bc204 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetController.java new file mode 100644 index 0000000..88a932f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetController.java new file mode 100644 index 0000000..43461f9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutController.java new file mode 100644 index 0000000..3fbcd68 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/main/tv/codely/apps/mooc/backend/controller/playground/DomainEventPostController.java new file mode 100644 index 0000000..dfc9657 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/resources/log4j2.properties b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/resources/log4j2.properties new file mode 100644 index 0000000..98427f2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/ApplicationTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/ApplicationTestCase.java new file mode 100644 index 0000000..d356818 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/backoffice/BackofficeApplicationTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/backoffice/BackofficeApplicationTestCase.java new file mode 100644 index 0000000..195b553 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/backoffice/backend/controller/health_check/HealthCheckGetControllerShould.java new file mode 100644 index 0000000..add71d8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/backoffice/frontend/controller/health_check/HealthCheckGetControllerShould.java new file mode 100644 index 0000000..c5a9747 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/MoocApplicationTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/MoocApplicationTestCase.java new file mode 100644 index 0000000..90dc231 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/courses/CourseGetControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/courses/CourseGetControllerShould.java new file mode 100644 index 0000000..0ebbcc4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/courses/CoursesPutControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/courses/CoursesPutControllerShould.java new file mode 100644 index 0000000..9dcbe3d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/courses_counter/CoursesCounterGetControllerShould.java new file mode 100644 index 0000000..2927678 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/health_check/HealthCheckGetControllerShould.java new file mode 100644 index 0000000..94069bf --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutControllerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/apps/test/tv/codely/apps/mooc/backend/controller/notifications/NewsletterNotificationPutControllerShould.java new file mode 100644 index 0000000..8f011e9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/build.gradle b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/build.gradle new file mode 100644 index 0000000..508fb39 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/docker-compose.ci.yml b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/docker-compose.ci.yml new file mode 100755 index 0000000..d2c1c99 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/docker-compose.yml b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/docker-compose.yml new file mode 100755 index 0000000..216bacd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/etc/http/backoffice_frontend.http b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/etc/http/backoffice_frontend.http new file mode 100644 index 0000000..119764f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/etc/http/publish_domain_events.http b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/etc/http/publish_domain_events.http new file mode 100644 index 0000000..309c2fd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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": "c3a11f1d-512e-420b-aeae-e687a3c449aa", + "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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/gradle/wrapper/gradle-wrapper.jar b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..7f93135c49b765f8051ef9d0a6055ff8e46073d8 GIT binary patch literal 63721 zcmb5Wb9gP!wgnp7wrv|bwr$&XvSZt}Z6`anZSUAlc9NHKf9JdJ;NJVr`=eI(_pMp0 zy1VAAG3FfAOI`{X1O)&90s;U4K;XLp008~hCjbEC_fbYfS%6kTR+JtXK>nW$ZR+`W ze|#J8f4A@M|F5BpfUJb5h>|j$jOe}0oE!`Zf6fM>CR?!y@zU(cL8NsKk`a z6tx5mAkdjD;J=LcJ;;Aw8p!v#ouk>mUDZF@ zK>yvw%+bKu+T{Nk@LZ;zkYy0HBKw06_IWcMHo*0HKpTsEFZhn5qCHH9j z)|XpN&{`!0a>Vl+PmdQc)Yg4A(AG-z!+@Q#eHr&g<9D?7E)_aEB?s_rx>UE9TUq|? z;(ggJt>9l?C|zoO@5)tu?EV0x_7T17q4fF-q3{yZ^ipUbKcRZ4Qftd!xO(#UGhb2y>?*@{xq%`(-`2T^vc=#< zx!+@4pRdk&*1ht2OWk^Z5IAQ0YTAXLkL{(D*$gENaD)7A%^XXrCchN&z2x+*>o2FwPFjWpeaL=!tzv#JOW#( z$B)Nel<+$bkH1KZv3&-}=SiG~w2sbDbAWarg%5>YbC|}*d9hBjBkR(@tyM0T)FO$# zPtRXukGPnOd)~z=?avu+4Co@wF}1T)-uh5jI<1$HLtyDrVak{gw`mcH@Q-@wg{v^c zRzu}hMKFHV<8w}o*yg6p@Sq%=gkd~;`_VGTS?L@yVu`xuGy+dH6YOwcP6ZE`_0rK% zAx5!FjDuss`FQ3eF|mhrWkjux(Pny^k$u_)dyCSEbAsecHsq#8B3n3kDU(zW5yE|( zgc>sFQywFj5}U*qtF9Y(bi*;>B7WJykcAXF86@)z|0-Vm@jt!EPoLA6>r)?@DIobIZ5Sx zsc@OC{b|3%vaMbyeM|O^UxEYlEMHK4r)V-{r)_yz`w1*xV0|lh-LQOP`OP`Pk1aW( z8DSlGN>Ts|n*xj+%If~+E_BxK)~5T#w6Q1WEKt{!Xtbd`J;`2a>8boRo;7u2M&iOop4qcy<)z023=oghSFV zST;?S;ye+dRQe>ygiJ6HCv4;~3DHtJ({fWeE~$H@mKn@Oh6Z(_sO>01JwH5oA4nvK zr5Sr^g+LC zLt(i&ecdmqsIJGNOSUyUpglvhhrY8lGkzO=0USEKNL%8zHshS>Qziu|`eyWP^5xL4 zRP122_dCJl>hZc~?58w~>`P_s18VoU|7(|Eit0-lZRgLTZKNq5{k zE?V=`7=R&ro(X%LTS*f+#H-mGo_j3dm@F_krAYegDLk6UV{`UKE;{YSsn$ z(yz{v1@p|p!0>g04!eRSrSVb>MQYPr8_MA|MpoGzqyd*$@4j|)cD_%^Hrd>SorF>@ zBX+V<@vEB5PRLGR(uP9&U&5=(HVc?6B58NJT_igiAH*q~Wb`dDZpJSKfy5#Aag4IX zj~uv74EQ_Q_1qaXWI!7Vf@ZrdUhZFE;L&P_Xr8l@GMkhc#=plV0+g(ki>+7fO%?Jb zl+bTy7q{w^pTb{>(Xf2q1BVdq?#f=!geqssXp z4pMu*q;iiHmA*IjOj4`4S&|8@gSw*^{|PT}Aw~}ZXU`6=vZB=GGeMm}V6W46|pU&58~P+?LUs%n@J}CSrICkeng6YJ^M? zS(W?K4nOtoBe4tvBXs@@`i?4G$S2W&;$z8VBSM;Mn9 zxcaEiQ9=vS|bIJ>*tf9AH~m&U%2+Dim<)E=}KORp+cZ^!@wI`h1NVBXu{@%hB2Cq(dXx_aQ9x3mr*fwL5!ZryQqi|KFJuzvP zK1)nrKZ7U+B{1ZmJub?4)Ln^J6k!i0t~VO#=q1{?T)%OV?MN}k5M{}vjyZu#M0_*u z8jwZKJ#Df~1jcLXZL7bnCEhB6IzQZ-GcoQJ!16I*39iazoVGugcKA{lhiHg4Ta2fD zk1Utyc5%QzZ$s3;p0N+N8VX{sd!~l*Ta3|t>lhI&G`sr6L~G5Lul`>m z{!^INm?J|&7X=;{XveF!(b*=?9NAp4y&r&N3(GKcW4rS(Ejk|Lzs1PrxPI_owB-`H zg3(Rruh^&)`TKA6+_!n>RdI6pw>Vt1_j&+bKIaMTYLiqhZ#y_=J8`TK{Jd<7l9&sY z^^`hmi7^14s16B6)1O;vJWOF$=$B5ONW;;2&|pUvJlmeUS&F;DbSHCrEb0QBDR|my zIs+pE0Y^`qJTyH-_mP=)Y+u^LHcuZhsM3+P||?+W#V!_6E-8boP#R-*na4!o-Q1 zVthtYhK{mDhF(&7Okzo9dTi03X(AE{8cH$JIg%MEQca`S zy@8{Fjft~~BdzWC(di#X{ny;!yYGK9b@=b|zcKZ{vv4D8i+`ilOPl;PJl{!&5-0!w z^fOl#|}vVg%=n)@_e1BrP)`A zKPgs`O0EO}Y2KWLuo`iGaKu1k#YR6BMySxQf2V++Wo{6EHmK>A~Q5o73yM z-RbxC7Qdh0Cz!nG+7BRZE>~FLI-?&W_rJUl-8FDIaXoNBL)@1hwKa^wOr1($*5h~T zF;%f^%<$p8Y_yu(JEg=c_O!aZ#)Gjh$n(hfJAp$C2he555W5zdrBqjFmo|VY+el;o z=*D_w|GXG|p0**hQ7~9-n|y5k%B}TAF0iarDM!q-jYbR^us(>&y;n^2l0C%@2B}KM zyeRT9)oMt97Agvc4sEKUEy%MpXr2vz*lb zh*L}}iG>-pqDRw7ud{=FvTD?}xjD)w{`KzjNom-$jS^;iw0+7nXSnt1R@G|VqoRhE%12nm+PH?9`(4rM0kfrZzIK9JU=^$YNyLvAIoxl#Q)xxDz!^0@zZ zSCs$nfcxK_vRYM34O<1}QHZ|hp4`ioX3x8(UV(FU$J@o%tw3t4k1QPmlEpZa2IujG&(roX_q*%e`Hq|);0;@k z0z=fZiFckp#JzW0p+2A+D$PC~IsakhJJkG(c;CqAgFfU0Z`u$PzG~-9I1oPHrCw&)@s^Dc~^)#HPW0Ra}J^=|h7Fs*<8|b13ZzG6MP*Q1dkoZ6&A^!}|hbjM{2HpqlSXv_UUg1U4gn z3Q)2VjU^ti1myodv+tjhSZp%D978m~p& z43uZUrraHs80Mq&vcetqfQpQP?m!CFj)44t8Z}k`E798wxg&~aCm+DBoI+nKq}&j^ zlPY3W$)K;KtEajks1`G?-@me7C>{PiiBu+41#yU_c(dITaqE?IQ(DBu+c^Ux!>pCj zLC|HJGU*v+!it1(;3e`6igkH(VA)-S+k(*yqxMgUah3$@C zz`7hEM47xr>j8^g`%*f=6S5n>z%Bt_Fg{Tvmr+MIsCx=0gsu_sF`q2hlkEmisz#Fy zj_0;zUWr;Gz}$BS%Y`meb(=$d%@Crs(OoJ|}m#<7=-A~PQbyN$x%2iXP2@e*nO0b7AwfH8cCUa*Wfu@b)D_>I*%uE4O3 z(lfnB`-Xf*LfC)E}e?%X2kK7DItK6Tf<+M^mX0Ijf_!IP>7c8IZX%8_#0060P{QMuV^B9i<^E`_Qf0pv9(P%_s8D`qvDE9LK9u-jB}J2S`(mCO&XHTS04Z5Ez*vl^T%!^$~EH8M-UdwhegL>3IQ*)(MtuH2Xt1p!fS4o~*rR?WLxlA!sjc2(O znjJn~wQ!Fp9s2e^IWP1C<4%sFF}T4omr}7+4asciyo3DntTgWIzhQpQirM$9{EbQd z3jz9vS@{aOqTQHI|l#aUV@2Q^Wko4T0T04Me4!2nsdrA8QY1%fnAYb~d2GDz@lAtfcHq(P7 zaMBAGo}+NcE-K*@9y;Vt3*(aCaMKXBB*BJcD_Qnxpt75r?GeAQ}*|>pYJE=uZb73 zC>sv)18)q#EGrTG6io*}JLuB_jP3AU1Uiu$D7r|2_zlIGb9 zjhst#ni)Y`$)!fc#reM*$~iaYoz~_Cy7J3ZTiPm)E?%`fbk`3Tu-F#`{i!l5pNEn5 zO-Tw-=TojYhzT{J=?SZj=Z8#|eoF>434b-DXiUsignxXNaR3 zm_}4iWU$gt2Mw5NvZ5(VpF`?X*f2UZDs1TEa1oZCif?Jdgr{>O~7}-$|BZ7I(IKW`{f;@|IZFX*R8&iT= zoWstN8&R;}@2Ka%d3vrLtR|O??ben;k8QbS-WB0VgiCz;<$pBmIZdN!aalyCSEm)crpS9dcD^Y@XT1a3+zpi-`D}e#HV<} z$Y(G&o~PvL-xSVD5D?JqF3?B9rxGWeb=oEGJ3vRp5xfBPlngh1O$yI95EL+T8{GC@ z98i1H9KhZGFl|;`)_=QpM6H?eDPpw~^(aFQWwyXZ8_EEE4#@QeT_URray*mEOGsGc z6|sdXtq!hVZo=d#+9^@lm&L5|q&-GDCyUx#YQiccq;spOBe3V+VKdjJA=IL=Zn%P} zNk=_8u}VhzFf{UYZV0`lUwcD&)9AFx0@Fc6LD9A6Rd1=ga>Mi0)_QxM2ddCVRmZ0d z+J=uXc(?5JLX3=)e)Jm$HS2yF`44IKhwRnm2*669_J=2LlwuF5$1tAo@ROSU@-y+;Foy2IEl2^V1N;fk~YR z?&EP8#t&m0B=?aJeuz~lHjAzRBX>&x=A;gIvb>MD{XEV zV%l-+9N-)i;YH%nKP?>f`=?#`>B(`*t`aiPLoQM(a6(qs4p5KFjDBN?8JGrf3z8>= zi7sD)c)Nm~x{e<^jy4nTx${P~cwz_*a>%0_;ULou3kHCAD7EYkw@l$8TN#LO9jC( z1BeFW`k+bu5e8Ns^a8dPcjEVHM;r6UX+cN=Uy7HU)j-myRU0wHd$A1fNI~`4;I~`zC)3ul#8#^rXVSO*m}Ag>c%_;nj=Nv$rCZ z*~L@C@OZg%Q^m)lc-kcX&a*a5`y&DaRxh6O*dfhLfF+fU5wKs(1v*!TkZidw*)YBP za@r`3+^IHRFeO%!ai%rxy;R;;V^Fr=OJlpBX;(b*3+SIw}7= zIq$*Thr(Zft-RlY)D3e8V;BmD&HOfX+E$H#Y@B3?UL5L~_fA-@*IB-!gItK7PIgG9 zgWuGZK_nuZjHVT_Fv(XxtU%)58;W39vzTI2n&)&4Dmq7&JX6G>XFaAR{7_3QB6zsT z?$L8c*WdN~nZGiscY%5KljQARN;`w$gho=p006z;n(qIQ*Zu<``TMO3n0{ARL@gYh zoRwS*|Niw~cR!?hE{m*y@F`1)vx-JRfqET=dJ5_(076st(=lFfjtKHoYg`k3oNmo_ zNbQEw8&sO5jAYmkD|Zaz_yUb0rC})U!rCHOl}JhbYIDLzLvrZVw0~JO`d*6f;X&?V=#T@ND*cv^I;`sFeq4 z##H5;gpZTb^0Hz@3C*~u0AqqNZ-r%rN3KD~%Gw`0XsIq$(^MEb<~H(2*5G^<2(*aI z%7}WB+TRlMIrEK#s0 z93xn*Ohb=kWFc)BNHG4I(~RPn-R8#0lqyBBz5OM6o5|>x9LK@%HaM}}Y5goCQRt2C z{j*2TtT4ne!Z}vh89mjwiSXG=%DURar~=kGNNaO_+Nkb+tRi~Rkf!7a$*QlavziD( z83s4GmQ^Wf*0Bd04f#0HX@ua_d8 z23~z*53ePD6@xwZ(vdl0DLc=>cPIOPOdca&MyR^jhhKrdQO?_jJh`xV3GKz&2lvP8 zEOwW6L*ufvK;TN{=S&R@pzV^U=QNk^Ec}5H z+2~JvEVA{`uMAr)?Kf|aW>33`)UL@bnfIUQc~L;TsTQ6>r-<^rB8uoNOJ>HWgqMI8 zSW}pZmp_;z_2O5_RD|fGyTxaxk53Hg_3Khc<8AUzV|ZeK{fp|Ne933=1&_^Dbv5^u zB9n=*)k*tjHDRJ@$bp9mrh}qFn*s}npMl5BMDC%Hs0M0g-hW~P*3CNG06G!MOPEQ_ zi}Qs-6M8aMt;sL$vlmVBR^+Ry<64jrm1EI1%#j?c?4b*7>)a{aDw#TfTYKq+SjEFA z(aJ&z_0?0JB83D-i3Vh+o|XV4UP+YJ$9Boid2^M2en@APw&wx7vU~t$r2V`F|7Qfo z>WKgI@eNBZ-+Og<{u2ZiG%>YvH2L3fNpV9J;WLJoBZda)01Rn;o@){01{7E#ke(7U zHK>S#qZ(N=aoae*4X!0A{)nu0R_sKpi1{)u>GVjC+b5Jyl6#AoQ-1_3UDovNSo`T> z?c-@7XX*2GMy?k?{g)7?Sv;SJkmxYPJPs!&QqB12ejq`Lee^-cDveVWL^CTUldb(G zjDGe(O4P=S{4fF=#~oAu>LG>wrU^z_?3yt24FOx>}{^lCGh8?vtvY$^hbZ)9I0E3r3NOlb9I?F-Yc=r$*~l`4N^xzlV~N zl~#oc>U)Yjl0BxV>O*Kr@lKT{Z09OXt2GlvE38nfs+DD7exl|&vT;)>VFXJVZp9Np zDK}aO;R3~ag$X*|hRVY3OPax|PG`@_ESc8E!mHRByJbZQRS38V2F__7MW~sgh!a>98Q2%lUNFO=^xU52|?D=IK#QjwBky-C>zOWlsiiM&1n z;!&1((Xn1$9K}xabq~222gYvx3hnZPg}VMF_GV~5ocE=-v>V=T&RsLBo&`)DOyIj* zLV{h)JU_y*7SdRtDajP_Y+rBkNN*1_TXiKwHH2&p51d(#zv~s#HwbNy?<+(=9WBvo zw2hkk2Dj%kTFhY+$T+W-b7@qD!bkfN#Z2ng@Pd=i3-i?xYfs5Z*1hO?kd7Sp^9`;Y zM2jeGg<-nJD1er@Pc_cSY7wo5dzQX44=%6rn}P_SRbpzsA{6B+!$3B0#;}qwO37G^ zL(V_5JK`XT?OHVk|{_$vQ|oNEpab*BO4F zUTNQ7RUhnRsU`TK#~`)$icsvKh~(pl=3p6m98@k3P#~upd=k*u20SNcb{l^1rUa)>qO997)pYRWMncC8A&&MHlbW?7i^7M`+B$hH~Y|J zd>FYOGQ;j>Zc2e7R{KK7)0>>nn_jYJy&o@sK!4G>-rLKM8Hv)f;hi1D2fAc$+six2 zyVZ@wZ6x|fJ!4KrpCJY=!Mq0;)X)OoS~{Lkh6u8J`eK%u0WtKh6B>GW_)PVc zl}-k`p09qwGtZ@VbYJC!>29V?Dr>>vk?)o(x?!z*9DJ||9qG-&G~#kXxbw{KKYy}J zQKa-dPt~M~E}V?PhW0R26xdA%1T*%ra6SguGu50YHngOTIv)@N|YttEXo#OZfgtP7;H?EeZZxo<}3YlYxtBq znJ!WFR^tmGf0Py}N?kZ(#=VtpC@%xJkDmfcCoBTxq zr_|5gP?u1@vJZbxPZ|G0AW4=tpb84gM2DpJU||(b8kMOV1S3|(yuwZJ&rIiFW(U;5 zUtAW`O6F6Zy+eZ1EDuP~AAHlSY-+A_eI5Gx)%*uro5tljy}kCZU*_d7)oJ>oQSZ3* zneTn`{gnNC&uJd)0aMBzAg021?YJ~b(fmkwZAd696a=0NzBAqBN54KuNDwa*no(^O z6p05bioXUR^uXjpTol*ppHp%1v9e)vkoUAUJyBx3lw0UO39b0?^{}yb!$yca(@DUn zCquRF?t=Zb9`Ed3AI6|L{eX~ijVH`VzSMheKoP7LSSf4g>md>`yi!TkoG5P>Ofp+n z(v~rW+(5L96L{vBb^g51B=(o)?%%xhvT*A5btOpw(TKh^g^4c zw>0%X!_0`{iN%RbVk+A^f{w-4-SSf*fu@FhruNL##F~sF24O~u zyYF<3el2b$$wZ_|uW#@Ak+VAGk#e|kS8nL1g>2B-SNMjMp^8;-FfeofY2fphFHO!{ z*!o4oTb{4e;S<|JEs<1_hPsmAlVNk?_5-Fp5KKU&d#FiNW~Y+pVFk@Cua1I{T+1|+ zHx6rFMor)7L)krbilqsWwy@T+g3DiH5MyVf8Wy}XbEaoFIDr~y;@r&I>FMW{ z?Q+(IgyebZ)-i4jNoXQhq4Muy9Fv+OxU;9_Jmn+<`mEC#%2Q_2bpcgzcinygNI!&^ z=V$)o2&Yz04~+&pPWWn`rrWxJ&}8khR)6B(--!9Q zubo}h+1T)>a@c)H^i``@<^j?|r4*{;tQf78(xn0g39IoZw0(CwY1f<%F>kEaJ zp9u|IeMY5mRdAlw*+gSN^5$Q)ShM<~E=(c8QM+T-Qk)FyKz#Sw0EJ*edYcuOtO#~Cx^(M7w5 z3)rl#L)rF|(Vun2LkFr!rg8Q@=r>9p>(t3Gf_auiJ2Xx9HmxYTa|=MH_SUlYL`mz9 zTTS$`%;D-|Jt}AP1&k7PcnfFNTH0A-*FmxstjBDiZX?}%u%Yq94$fUT&z6od+(Uk> zuqsld#G(b$G8tus=M!N#oPd|PVFX)?M?tCD0tS%2IGTfh}3YA3f&UM)W$_GNV8 zQo+a(ml2Km4o6O%gKTCSDNq+#zCTIQ1*`TIJh~k6Gp;htHBFnne))rlFdGqwC6dx2+La1&Mnko*352k0y z+tQcwndQlX`nc6nb$A9?<-o|r*%aWXV#=6PQic0Ok_D;q>wbv&j7cKc!w4~KF#-{6 z(S%6Za)WpGIWf7jZ3svNG5OLs0>vCL9{V7cgO%zevIVMH{WgP*^D9ws&OqA{yr|m| zKD4*07dGXshJHd#e%x%J+qmS^lS|0Bp?{drv;{@{l9ArPO&?Q5=?OO9=}h$oVe#3b z3Yofj&Cb}WC$PxmRRS)H%&$1-)z7jELS}!u!zQ?A^Y{Tv4QVt*vd@uj-^t2fYRzQj zfxGR>-q|o$3sGn^#VzZ!QQx?h9`njeJry}@x?|k0-GTTA4y3t2E`3DZ!A~D?GiJup z)8%PK2^9OVRlP(24P^4_<|D=H^7}WlWu#LgsdHzB%cPy|f8dD3|A^mh4WXxhLTVu_ z@abE{6Saz|Y{rXYPd4$tfPYo}ef(oQWZ=4Bct-=_9`#Qgp4ma$n$`tOwq#&E18$B; z@Bp)bn3&rEi0>fWWZ@7k5WazfoX`SCO4jQWwVuo+$PmSZn^Hz?O(-tW@*DGxuf)V1 zO_xm&;NVCaHD4dqt(-MlszI3F-p?0!-e$fbiCeuaw66h^TTDLWuaV<@C-`=Xe5WL) zwooG7h>4&*)p3pKMS3O!4>-4jQUN}iAMQ)2*70?hP~)TzzR?-f@?Aqy$$1Iy8VGG$ zMM?8;j!pUX7QQD$gRc_#+=raAS577ga-w?jd`vCiN5lu)dEUkkUPl9!?{$IJNxQys z*E4e$eF&n&+AMRQR2gcaFEjAy*r)G!s(P6D&TfoApMFC_*Ftx0|D0@E-=B7tezU@d zZ{hGiN;YLIoSeRS;9o%dEua4b%4R3;$SugDjP$x;Z!M!@QibuSBb)HY!3zJ7M;^jw zlx6AD50FD&p3JyP*>o+t9YWW8(7P2t!VQQ21pHJOcG_SXQD;(5aX#M6x##5H_Re>6lPyDCjxr*R(+HE%c&QN+b^tbT zXBJk?p)zhJj#I?&Y2n&~XiytG9!1ox;bw5Rbj~)7c(MFBb4>IiRATdhg zmiEFlj@S_hwYYI(ki{}&<;_7(Z0Qkfq>am z&LtL=2qc7rWguk3BtE4zL41@#S;NN*-jWw|7Kx7H7~_%7fPt;TIX}Ubo>;Rmj94V> zNB1=;-9AR7s`Pxn}t_6^3ahlq53e&!Lh85uG zec0vJY_6e`tg7LgfrJ3k!DjR)Bi#L@DHIrZ`sK=<5O0Ip!fxGf*OgGSpP@Hbbe&$9 z;ZI}8lEoC2_7;%L2=w?tb%1oL0V+=Z`7b=P&lNGY;yVBazXRYu;+cQDKvm*7NCxu&i;zub zAJh#11%?w>E2rf2e~C4+rAb-&$^vsdACs7 z@|Ra!OfVM(ke{vyiqh7puf&Yp6cd6{DptUteYfIRWG3pI+5< zBVBI_xkBAc<(pcb$!Y%dTW(b;B;2pOI-(QCsLv@U-D1XJ z(Gk8Q3l7Ws46Aktuj>|s{$6zA&xCPuXL-kB`CgYMs}4IeyG*P51IDwW?8UNQd+$i~ zlxOPtSi5L|gJcF@DwmJA5Ju8HEJ>o{{upwIpb!f{2(vLNBw`7xMbvcw<^{Fj@E~1( z?w`iIMieunS#>nXlmUcSMU+D3rX28f?s7z;X=se6bo8;5vM|O^(D6{A9*ChnGH!RG zP##3>LDC3jZPE4PH32AxrqPk|yIIrq~`aL-=}`okhNu9aT%q z1b)7iJ)CN=V#Ly84N_r7U^SH2FGdE5FpTO2 z630TF$P>GNMu8`rOytb(lB2};`;P4YNwW1<5d3Q~AX#P0aX}R2b2)`rgkp#zTxcGj zAV^cvFbhP|JgWrq_e`~exr~sIR$6p5V?o4Wym3kQ3HA+;Pr$bQ0(PmADVO%MKL!^q z?zAM8j1l4jrq|5X+V!8S*2Wl@=7*pPgciTVK6kS1Ge zMsd_u6DFK$jTnvVtE;qa+8(1sGBu~n&F%dh(&c(Zs4Fc#A=gG^^%^AyH}1^?|8quj zl@Z47h$){PlELJgYZCIHHL= z{U8O>Tw4x3<1{?$8>k-P<}1y9DmAZP_;(3Y*{Sk^H^A=_iSJ@+s5ktgwTXz_2$~W9>VVZsfwCm@s0sQ zeB50_yu@uS+e7QoPvdCwDz{prjo(AFwR%C?z`EL{1`|coJHQTk^nX=tvs1<0arUOJ z!^`*x&&BvTYmemyZ)2p~{%eYX=JVR?DYr(rNgqRMA5E1PR1Iw=prk=L2ldy3r3Vg@27IZx43+ywyzr-X*p*d@tZV+!U#~$-q=8c zgdSuh#r?b4GhEGNai)ayHQpk>5(%j5c@C1K3(W1pb~HeHpaqijJZa-e6vq_8t-^M^ zBJxq|MqZc?pjXPIH}70a5vt!IUh;l}<>VX<-Qcv^u@5(@@M2CHSe_hD$VG-eiV^V( zj7*9T0?di?P$FaD6oo?)<)QT>Npf6Og!GO^GmPV(Km0!=+dE&bk#SNI+C9RGQ|{~O*VC+tXK3!n`5 zHfl6>lwf_aEVV3`0T!aHNZLsj$paS$=LL(?b!Czaa5bbSuZ6#$_@LK<(7yrrl+80| z{tOFd=|ta2Z`^ssozD9BINn45NxUeCQis?-BKmU*Kt=FY-NJ+)8S1ecuFtN-M?&42 zl2$G>u!iNhAk*HoJ^4v^9#ORYp5t^wDj6|lx~5w45#E5wVqI1JQ~9l?nPp1YINf++ zMAdSif~_ETv@Er(EFBI^@L4BULFW>)NI+ejHFP*T}UhWNN`I)RRS8za? z*@`1>9ZB}An%aT5K=_2iQmfE;GcBVHLF!$`I99o5GO`O%O_zLr9AG18>&^HkG(;=V z%}c!OBQ~?MX(9h~tajX{=x)+!cbM7$YzTlmsPOdp2L-?GoW`@{lY9U3f;OUo*BwRB z8A+nv(br0-SH#VxGy#ZrgnGD(=@;HME;yd46EgWJ`EL%oXc&lFpc@Y}^>G(W>h_v_ zlN!`idhX+OjL+~T?19sroAFVGfa5tX-D49w$1g2g_-T|EpHL6}K_aX4$K=LTvwtlF zL*z}j{f+Uoe7{-px3_5iKPA<_7W=>Izkk)!l9ez2w%vi(?Y;i8AxRNLSOGDzNoqoI zP!1uAl}r=_871(G?y`i&)-7{u=%nxk7CZ_Qh#!|ITec zwQn`33GTUM`;D2POWnkqngqJhJRlM>CTONzTG}>^Q0wUunQyn|TAiHzyX2_%ATx%P z%7gW)%4rA9^)M<_%k@`Y?RbC<29sWU&5;@|9thf2#zf8z12$hRcZ!CSb>kUp=4N#y zl3hE#y6>kkA8VY2`W`g5Ip?2qC_BY$>R`iGQLhz2-S>x(RuWv)SPaGdl^)gGw7tjR zH@;jwk!jIaCgSg_*9iF|a);sRUTq30(8I(obh^|}S~}P4U^BIGYqcz;MPpC~Y@k_m zaw4WG1_vz2GdCAX!$_a%GHK**@IrHSkGoN>)e}>yzUTm52on`hYot7cB=oA-h1u|R ztH$11t?54Qg2L+i33FPFKKRm1aOjKST{l1*(nps`>sv%VqeVMWjl5+Gh+9);hIP8? zA@$?}Sc z3qIRpba+y5yf{R6G(u8Z^vkg0Fu&D-7?1s=QZU`Ub{-!Y`I?AGf1VNuc^L3v>)>i# z{DV9W$)>34wnzAXUiV^ZpYKw>UElrN_5Xj6{r_3| z$X5PK`e5$7>~9Dj7gK5ash(dvs`vwfk}&RD`>04;j62zoXESkFBklYaKm5seyiX(P zqQ-;XxlV*yg?Dhlx%xt!b0N3GHp@(p$A;8|%# zZ5m2KL|{on4nr>2_s9Yh=r5ScQ0;aMF)G$-9-Ca6%wA`Pa)i?NGFA|#Yi?{X-4ZO_ z^}%7%vkzvUHa$-^Y#aA+aiR5sa%S|Ebyn`EV<3Pc?ax_f>@sBZF1S;7y$CXd5t5=WGsTKBk8$OfH4v|0?0I=Yp}7c=WBSCg!{0n)XmiU;lfx)**zZaYqmDJelxk$)nZyx5`x$6R|fz(;u zEje5Dtm|a%zK!!tk3{i9$I2b{vXNFy%Bf{50X!x{98+BsDr_u9i>G5%*sqEX|06J0 z^IY{UcEbj6LDwuMh7cH`H@9sVt1l1#8kEQ(LyT@&+K}(ReE`ux8gb0r6L_#bDUo^P z3Ka2lRo52Hdtl_%+pwVs14=q`{d^L58PsU@AMf(hENumaxM{7iAT5sYmWh@hQCO^ zK&}ijo=`VqZ#a3vE?`7QW0ZREL17ZvDfdqKGD?0D4fg{7v%|Yj&_jcKJAB)>=*RS* zto8p6@k%;&^ZF>hvXm&$PCuEp{uqw3VPG$9VMdW5$w-fy2CNNT>E;>ejBgy-m_6`& z97L1p{%srn@O_JQgFpa_#f(_)eb#YS>o>q3(*uB;uZb605(iqM$=NK{nHY=+X2*G) zO3-_Xh%aG}fHWe*==58zBwp%&`mge<8uq8;xIxOd=P%9EK!34^E9sk|(Zq1QSz-JVeP12Fp)-`F|KY$LPwUE?rku zY@OJ)Z9A!ojfzfeyJ9;zv2EM7ZQB)AR5xGa-tMn^bl)FmoIiVyJ@!~@%{}qXXD&Ns zPnfe5U+&ohKefILu_1mPfLGuapX@btta5C#gPB2cjk5m4T}Nfi+Vfka!Yd(L?-c~5 z#ZK4VeQEXNPc4r$K00Fg>g#_W!YZ)cJ?JTS<&68_$#cZT-ME`}tcwqg3#``3M3UPvn+pi}(VNNx6y zFIMVb6OwYU(2`at$gHba*qrMVUl8xk5z-z~fb@Q3Y_+aXuEKH}L+>eW__!IAd@V}L zkw#s%H0v2k5-=vh$^vPCuAi22Luu3uKTf6fPo?*nvj$9(u)4$6tvF-%IM+3pt*cgs z_?wW}J7VAA{_~!?))?s6{M=KPpVhg4fNuU*|3THp@_(q!b*hdl{fjRVFWtu^1dV(f z6iOux9hi&+UK=|%M*~|aqFK{Urfl!TA}UWY#`w(0P!KMe1Si{8|o))Gy6d7;!JQYhgMYmXl?3FfOM2nQGN@~Ap6(G z3+d_5y@=nkpKAhRqf{qQ~k7Z$v&l&@m7Ppt#FSNzKPZM z8LhihcE6i=<(#87E|Wr~HKvVWhkll4iSK$^mUHaxgy8*K$_Zj;zJ`L$naPj+^3zTi z-3NTaaKnD5FPY-~?Tq6QHnmDDRxu0mh0D|zD~Y=vv_qig5r-cIbCpxlju&8Sya)@{ zsmv6XUSi)@(?PvItkiZEeN*)AE~I_?#+Ja-r8$(XiXei2d@Hi7Rx8+rZZb?ZLa{;@*EHeRQ-YDadz~M*YCM4&F-r;E#M+@CSJMJ0oU|PQ^ z=E!HBJDMQ2TN*Y(Ag(ynAL8%^v;=~q?s4plA_hig&5Z0x_^Oab!T)@6kRN$)qEJ6E zNuQjg|G7iwU(N8pI@_6==0CL;lRh1dQF#wePhmu@hADFd3B5KIH#dx(2A zp~K&;Xw}F_N6CU~0)QpQk7s$a+LcTOj1%=WXI(U=Dv!6 z{#<#-)2+gCyyv=Jw?Ab#PVkxPDeH|sAxyG`|Ys}A$PW4TdBv%zDz z^?lwrxWR<%Vzc8Sgt|?FL6ej_*e&rhqJZ3Y>k=X(^dytycR;XDU16}Pc9Vn0>_@H+ zQ;a`GSMEG64=JRAOg%~L)x*w{2re6DVprNp+FcNra4VdNjiaF0M^*>CdPkt(m150rCue?FVdL0nFL$V%5y6N z%eLr5%YN7D06k5ji5*p4v$UMM)G??Q%RB27IvH7vYr_^3>1D-M66#MN8tWGw>WED} z5AhlsanO=STFYFs)Il_0i)l)f<8qn|$DW7ZXhf5xI;m+7M5-%P63XFQrG9>DMqHc} zsgNU9nR`b}E^mL5=@7<1_R~j@q_2U^3h|+`7YH-?C=vme1C3m`Fe0HC>pjt6f_XMh zy~-i-8R46QNYneL4t@)<0VU7({aUO?aH`z4V2+kxgH5pYD5)wCh75JqQY)jIPN=U6 z+qi8cGiOtXG2tXm;_CfpH9ESCz#i5B(42}rBJJF$jh<1sbpj^8&L;gzGHb8M{of+} zzF^8VgML2O9nxBW7AvdEt90vp+#kZxWf@A)o9f9}vKJy9NDBjBW zSt=Hcs=YWCwnfY1UYx*+msp{g!w0HC<_SM!VL1(I2PE?CS}r(eh?{I)mQixmo5^p# zV?2R!R@3GV6hwTCrfHiK#3Orj>I!GS2kYhk1S;aFBD_}u2v;0HYFq}Iz1Z(I4oca4 zxquja8$+8JW_EagDHf$a1OTk5S97umGSDaj)gH=fLs9>_=XvVj^Xj9a#gLdk=&3tl zfmK9MNnIX9v{?%xdw7568 zNrZ|roYs(vC4pHB5RJ8>)^*OuyNC>x7ad)tB_}3SgQ96+-JT^Qi<`xi=)_=$Skwv~ zdqeT9Pa`LYvCAn&rMa2aCDV(TMI#PA5g#RtV|CWpgDYRA^|55LLN^uNh*gOU>Z=a06qJ;$C9z8;n-Pq=qZnc1zUwJ@t)L;&NN+E5m zRkQ(SeM8=l-aoAKGKD>!@?mWTW&~)uF2PYUJ;tB^my`r9n|Ly~0c%diYzqs9W#FTjy?h&X3TnH zXqA{QI82sdjPO->f=^K^f>N`+B`q9&rN0bOXO79S&a9XX8zund(kW7O76f4dcWhIu zER`XSMSFbSL>b;Rp#`CuGJ&p$s~G|76){d?xSA5wVg##_O0DrmyEYppyBr%fyWbbv zp`K84JwRNP$d-pJ!Qk|(RMr?*!wi1if-9G#0p>>1QXKXWFy)eB3ai)l3601q8!9JC zvU#ZWWDNKq9g6fYs?JQ)Q4C_cgTy3FhgKb8s&m)DdmL5zhNK#8wWg!J*7G7Qhe9VU zha?^AQTDpYcuN!B+#1dE*X{<#!M%zfUQbj=zLE{dW0XeQ7-oIsGY6RbkP2re@Q{}r_$iiH0xU%iN*ST`A)-EH6eaZB$GA#v)cLi z*MpA(3bYk$oBDKAzu^kJoSUsDd|856DApz={3u8sbQV@JnRkp2nC|)m;#T=DvIL-O zI4vh;g7824l}*`_p@MT4+d`JZ2%6NQh=N9bmgJ#q!hK@_<`HQq3}Z8Ij>3%~<*= zcv=!oT#5xmeGI92lqm9sGVE%#X$ls;St|F#u!?5Y7syhx6q#MVRa&lBmmn%$C0QzU z);*ldgwwCmzM3uglr}!Z2G+?& zf%Dpo&mD%2ZcNFiN-Z0f;c_Q;A%f@>26f?{d1kxIJD}LxsQkB47SAdwinfMILZdN3 zfj^HmTzS3Ku5BxY>ANutS8WPQ-G>v4^_Qndy==P3pDm+Xc?>rUHl-4+^%Sp5atOja z2oP}ftw-rqnb}+khR3CrRg^ibi6?QYk1*i^;kQGirQ=uB9Sd1NTfT-Rbv;hqnY4neE5H1YUrjS2m+2&@uXiAo- zrKUX|Ohg7(6F(AoP~tj;NZlV#xsfo-5reuQHB$&EIAhyZk;bL;k9ouDmJNBAun;H& zn;Of1z_Qj`x&M;5X;{s~iGzBQTY^kv-k{ksbE*Dl%Qf%N@hQCfY~iUw!=F-*$cpf2 z3wix|aLBV0b;W@z^%7S{>9Z^T^fLOI68_;l@+Qzaxo`nAI8emTV@rRhEKZ z?*z_{oGdI~R*#<2{bkz$G~^Qef}$*4OYTgtL$e9q!FY7EqxJ2`zk6SQc}M(k(_MaV zSLJnTXw&@djco1~a(vhBl^&w=$fa9{Sru>7g8SHahv$&Bl(D@(Zwxo_3r=;VH|uc5 zi1Ny)J!<(KN-EcQ(xlw%PNwK8U>4$9nVOhj(y0l9X^vP1TA>r_7WtSExIOsz`nDOP zs}d>Vxb2Vo2e5x8p(n~Y5ggAyvib>d)6?)|E@{FIz?G3PVGLf7-;BxaP;c?7ddH$z zA+{~k^V=bZuXafOv!RPsE1GrR3J2TH9uB=Z67gok+u`V#}BR86hB1xl}H4v`F+mRfr zYhortD%@IGfh!JB(NUNSDh+qDz?4ztEgCz&bIG-Wg7w-ua4ChgQR_c+z8dT3<1?uX z*G(DKy_LTl*Ea!%v!RhpCXW1WJO6F`bgS-SB;Xw9#! z<*K}=#wVu9$`Yo|e!z-CPYH!nj7s9dEPr-E`DXUBu0n!xX~&|%#G=BeM?X@shQQMf zMvr2!y7p_gD5-!Lnm|a@z8Of^EKboZsTMk%5VsJEm>VsJ4W7Kv{<|#4f-qDE$D-W>gWT%z-!qXnDHhOvLk=?^a1*|0j z{pW{M0{#1VcR5;F!!fIlLVNh_Gj zbnW(_j?0c2q$EHIi@fSMR{OUKBcLr{Y&$hrM8XhPByyZaXy|dd&{hYQRJ9@Fn%h3p7*VQolBIV@Eq`=y%5BU~3RPa^$a?ixp^cCg z+}Q*X+CW9~TL29@OOng(#OAOd!)e$d%sr}^KBJ-?-X&|4HTmtemxmp?cT3uA?md4% zT8yZ0U;6Rg6JHy3fJae{6TMGS?ZUX6+gGTT{Q{)SI85$5FD{g-eR%O0KMpWPY`4@O zx!hen1*8^E(*}{m^V_?}(b5k3hYo=T+$&M32+B`}81~KKZhY;2H{7O-M@vbCzuX0n zW-&HXeyr1%I3$@ns-V1~Lb@wIpkmx|8I~ob1Of7i6BTNysEwI}=!nU%q7(V_^+d*G z7G;07m(CRTJup!`cdYi93r^+LY+`M*>aMuHJm(A8_O8C#A*$!Xvddgpjx5)?_EB*q zgE8o5O>e~9IiSC@WtZpF{4Bj2J5eZ>uUzY%TgWF7wdDE!fSQIAWCP)V{;HsU3ap?4 znRsiiDbtN7i9hapO;(|Ew>Ip2TZSvK9Z^N21%J?OiA_&eP1{(Pu_=%JjKy|HOardq ze?zK^K zA%sjF64*Wufad%H<) z^|t>e*h+Z1#l=5wHexzt9HNDNXgM=-OPWKd^5p!~%SIl>Fo&7BvNpbf8{NXmH)o{r zO=aBJ;meX1^{O%q;kqdw*5k!Y7%t_30 zy{nGRVc&5qt?dBwLs+^Sfp;f`YVMSB#C>z^a9@fpZ!xb|b-JEz1LBX7ci)V@W+kvQ89KWA0T~Lj$aCcfW#nD5bt&Y_< z-q{4ZXDqVg?|0o)j1%l0^_it0WF*LCn-+)c!2y5yS7aZIN$>0LqNnkujV*YVes(v$ zY@_-!Q;!ZyJ}Bg|G-~w@or&u0RO?vlt5*9~yeoPV_UWrO2J54b4#{D(D>jF(R88u2 zo#B^@iF_%S>{iXSol8jpmsZuJ?+;epg>k=$d`?GSegAVp3n$`GVDvK${N*#L_1`44 z{w0fL{2%)0|E+qgZtjX}itZz^KJt4Y;*8uSK}Ft38+3>j|K(PxIXXR-t4VopXo#9# zt|F{LWr-?34y`$nLBVV_*UEgA6AUI65dYIbqpNq9cl&uLJ0~L}<=ESlOm?Y-S@L*d z<7vt}`)TW#f%Rp$Q}6@3=j$7Tze@_uZO@aMn<|si{?S}~maII`VTjs&?}jQ4_cut9$)PEqMukwoXobzaKx^MV z2fQwl+;LSZ$qy%Tys0oo^K=jOw$!YwCv^ei4NBVauL)tN%=wz9M{uf{IB(BxK|lT*pFkmNK_1tV`nb%jH=a0~VNq2RCKY(rG7jz!-D^k)Ec)yS%17pE#o6&eY+ z^qN(hQT$}5F(=4lgNQhlxj?nB4N6ntUY6(?+R#B?W3hY_a*)hnr4PA|vJ<6p`K3Z5Hy z{{8(|ux~NLUW=!?9Qe&WXMTAkQnLXg(g=I@(VG3{HE13OaUT|DljyWXPs2FE@?`iU z4GQlM&Q=T<4&v@Fe<+TuXiZQT3G~vZ&^POfmI1K2h6t4eD}Gk5XFGpbj1n_g*{qmD6Xy z`6Vv|lLZtLmrnv*{Q%xxtcWVj3K4M%$bdBk_a&ar{{GWyu#ljM;dII;*jP;QH z#+^o-A4np{@|Mz+LphTD0`FTyxYq#wY)*&Ls5o{0z9yg2K+K7ZN>j1>N&;r+Z`vI| zDzG1LJZ+sE?m?>x{5LJx^)g&pGEpY=fQ-4}{x=ru;}FL$inHemOg%|R*ZXPodU}Kh zFEd5#+8rGq$Y<_?k-}r5zgQ3jRV=ooHiF|@z_#D4pKVEmn5CGV(9VKCyG|sT9nc=U zEoT67R`C->KY8Wp-fEcjjFm^;Cg(ls|*ABVHq8clBE(;~K^b+S>6uj70g? z&{XQ5U&!Z$SO7zfP+y^8XBbiu*Cv-yJG|l-oe*!s5$@Lh_KpxYL2sx`B|V=dETN>5K+C+CU~a_3cI8{vbu$TNVdGf15*>D zz@f{zIlorkY>TRh7mKuAlN9A0>N>SV`X)+bEHms=mfYTMWt_AJtz_h+JMmrgH?mZt zm=lfdF`t^J*XLg7v+iS)XZROygK=CS@CvUaJo&w2W!Wb@aa?~Drtf`JV^cCMjngVZ zv&xaIBEo8EYWuML+vxCpjjY^s1-ahXJzAV6hTw%ZIy!FjI}aJ+{rE&u#>rs)vzuxz z+$5z=7W?zH2>Eb32dvgHYZtCAf!=OLY-pb4>Ae79rd68E2LkVPj-|jFeyqtBCCwiW zkB@kO_(3wFq)7qwV}bA=zD!*@UhT`geq}ITo%@O(Z5Y80nEX~;0-8kO{oB6|(4fQh z);73T!>3@{ZobPwRv*W?7m0Ml9GmJBCJd&6E?hdj9lV= z4flNfsc(J*DyPv?RCOx!MSvk(M952PJ-G|JeVxWVjN~SNS6n-_Ge3Q;TGE;EQvZg86%wZ`MB zSMQua(i*R8a75!6$QRO^(o7sGoomb+Y{OMy;m~Oa`;P9Yqo>?bJAhqXxLr7_3g_n>f#UVtxG!^F#1+y@os6x(sg z^28bsQ@8rw%Gxk-stAEPRbv^}5sLe=VMbkc@Jjimqjvmd!3E7+QnL>|(^3!R} zD-l1l7*Amu@j+PWLGHXXaFG0Ct2Q=}5YNUxEQHCAU7gA$sSC<5OGylNnQUa>>l%sM zyu}z6i&({U@x^hln**o6r2s-(C-L50tQvz|zHTqW!ir?w&V23tuYEDJVV#5pE|OJu z7^R!A$iM$YCe?8n67l*J-okwfZ+ZTkGvZ)tVPfR;|3gyFjF)8V zyXXN=!*bpyRg9#~Bg1+UDYCt0 ztp4&?t1X0q>uz;ann$OrZs{5*r`(oNvw=$7O#rD|Wuv*wIi)4b zGtq4%BX+kkagv3F9Id6~-c+1&?zny%w5j&nk9SQfo0k4LhdSU_kWGW7axkfpgR`8* z!?UTG*Zi_baA1^0eda8S|@&F z{)Rad0kiLjB|=}XFJhD(S3ssKlveFFmkN{Vl^_nb!o5M!RC=m)V&v2%e?ZoRC@h3> zJ(?pvToFd`*Zc@HFPL#=otWKwtuuQ_dT-Hr{S%pQX<6dqVJ8;f(o)4~VM_kEQkMR+ zs1SCVi~k>M`u1u2xc}>#D!V&6nOOh-E$O&SzYrjJdZpaDv1!R-QGA141WjQe2s0J~ zQ;AXG)F+K#K8_5HVqRoRM%^EduqOnS(j2)|ctA6Q^=|s_WJYU;Z%5bHp08HPL`YF2 zR)Ad1z{zh`=sDs^&V}J z%$Z$!jd7BY5AkT?j`eqMs%!Gm@T8)4w3GYEX~IwgE~`d|@T{WYHkudy(47brgHXx& zBL1yFG6!!!VOSmDxBpefy2{L_u5yTwja&HA!mYA#wg#bc-m%~8aRR|~AvMnind@zs zy>wkShe5&*un^zvSOdlVu%kHsEo>@puMQ`b1}(|)l~E{5)f7gC=E$fP(FC2=F<^|A zxeIm?{EE!3sO!Gr7e{w)Dx(uU#3WrFZ>ibmKSQ1tY?*-Nh1TDHLe+k*;{Rp!Bmd_m zb#^kh`Y*8l|9Cz2e{;RL%_lg{#^Ar+NH|3z*Zye>!alpt{z;4dFAw^^H!6ING*EFc z_yqhr8d!;%nHX9AKhFQZBGrSzfzYCi%C!(Q5*~hX>)0N`vbhZ@N|i;_972WSx*>LH z87?en(;2_`{_JHF`Sv6Wlps;dCcj+8IJ8ca6`DsOQCMb3n# z3)_w%FuJ3>fjeOOtWyq)ag|PmgQbC-s}KRHG~enBcIwqIiGW8R8jFeBNY9|YswRY5 zjGUxdGgUD26wOpwM#8a!Nuqg68*dG@VM~SbOroL_On0N6QdT9?)NeB3@0FCC?Z|E0 z6TPZj(AsPtwCw>*{eDEE}Gby>0q{*lI+g2e&(YQrsY&uGM{O~}(oM@YWmb*F zA0^rr5~UD^qmNljq$F#ARXRZ1igP`MQx4aS6*MS;Ot(1L5jF2NJ;de!NujUYg$dr# z=TEL_zTj2@>ZZN(NYCeVX2==~=aT)R30gETO{G&GM4XN<+!&W&(WcDP%oL8PyIVUC zs5AvMgh6qr-2?^unB@mXK*Dbil^y-GTC+>&N5HkzXtozVf93m~xOUHn8`HpX=$_v2 z61H;Z1qK9o;>->tb8y%#4H)765W4E>TQ1o0PFj)uTOPEvv&}%(_mG0ISmyhnQV33Z$#&yd{ zc{>8V8XK$3u8}04CmAQ#I@XvtmB*s4t8va?-IY4@CN>;)mLb_4!&P3XSw4pA_NzDb zORn!blT-aHk1%Jpi>T~oGLuh{DB)JIGZ9KOsciWs2N7mM1JWM+lna4vkDL?Q)z_Ct z`!mi0jtr+4*L&N7jk&LodVO#6?_qRGVaucqVB8*us6i3BTa^^EI0x%EREQSXV@f!lak6Wf1cNZ8>*artIJ(ADO*=<-an`3zB4d*oO*8D1K!f z*A@P1bZCNtU=p!742MrAj%&5v%Xp_dSX@4YCw%F|%Dk=u|1BOmo)HsVz)nD5USa zR~??e61sO(;PR)iaxK{M%QM_rIua9C^4ppVS$qCT9j2%?*em?`4Z;4@>I(c%M&#cH z>4}*;ej<4cKkbCAjjDsyKS8rIm90O)Jjgyxj5^venBx&7B!xLmzxW3jhj7sR(^3Fz z84EY|p1NauwXUr;FfZjdaAfh%ivyp+^!jBjJuAaKa!yCq=?T_)R!>16?{~p)FQ3LDoMyG%hL#pR!f@P%*;#90rs_y z@9}@r1BmM-SJ#DeuqCQk=J?ixDSwL*wh|G#us;dd{H}3*-Y7Tv5m=bQJMcH+_S`zVtf;!0kt*(zwJ zs+kedTm!A}cMiM!qv(c$o5K%}Yd0|nOd0iLjus&;s0Acvoi-PFrWm?+q9f^FslxGi z6ywB`QpL$rJzWDg(4)C4+!2cLE}UPCTBLa*_=c#*$b2PWrRN46$y~yST3a2$7hEH= zNjux+wna^AzQ=KEa_5#9Ph=G1{S0#hh1L3hQ`@HrVnCx{!fw_a0N5xV(iPdKZ-HOM za)LdgK}1ww*C_>V7hbQnTzjURJL`S%`6nTHcgS+dB6b_;PY1FsrdE8(2K6FN>37!62j_cBlui{jO^$dPkGHV>pXvW0EiOA zqW`YaSUBWg_v^Y5tPJfWLcLpsA8T zG)!x>pKMpt!lv3&KV!-um= zKCir6`bEL_LCFx4Z5bAFXW$g3Cq`?Q%)3q0r852XI*Der*JNuKUZ`C{cCuu8R8nkt z%pnF>R$uY8L+D!V{s^9>IC+bmt<05h**>49R*#vpM*4i0qRB2uPbg8{{s#9yC;Z18 zD7|4m<9qneQ84uX|J&f-g8a|nFKFt34@Bt{CU`v(SYbbn95Q67*)_Esl_;v291s=9 z+#2F2apZU4Tq=x+?V}CjwD(P=U~d<=mfEFuyPB`Ey82V9G#Sk8H_Ob_RnP3s?)S_3 zr%}Pb?;lt_)Nf>@zX~D~TBr;-LS<1I##8z`;0ZCvI_QbXNh8Iv)$LS=*gHr;}dgb=w5$3k2la1keIm|=7<-JD>)U%=Avl0Vj@+&vxn zt-)`vJxJr88D&!}2^{GPXc^nmRf#}nb$4MMkBA21GzB`-Or`-3lq^O^svO7Vs~FdM zv`NvzyG+0T!P8l_&8gH|pzE{N(gv_tgDU7SWeiI-iHC#0Ai%Ixn4&nt{5y3(GQs)i z&uA;~_0shP$0Wh0VooIeyC|lak__#KVJfxa7*mYmZ22@(<^W}FdKjd*U1CqSjNKW% z*z$5$=t^+;Ui=MoDW~A7;)Mj%ibX1_p4gu>RC}Z_pl`U*{_z@+HN?AF{_W z?M_X@o%w8fgFIJ$fIzBeK=v#*`mtY$HC3tqw7q^GCT!P$I%=2N4FY7j9nG8aIm$c9 zeKTxVKN!UJ{#W)zxW|Q^K!3s;(*7Gbn;e@pQBCDS(I|Y0euK#dSQ_W^)sv5pa%<^o zyu}3d?Lx`)3-n5Sy9r#`I{+t6x%I%G(iewGbvor&I^{lhu-!#}*Q3^itvY(^UWXgvthH52zLy&T+B)Pw;5>4D6>74 zO_EBS)>l!zLTVkX@NDqyN2cXTwsUVao7$HcqV2%t$YzdAC&T)dwzExa3*kt9d(}al zA~M}=%2NVNUjZiO7c>04YH)sRelXJYpWSn^aC$|Ji|E13a^-v2MB!Nc*b+=KY7MCm zqIteKfNkONq}uM;PB?vvgQvfKLPMB8u5+Am=d#>g+o&Ysb>dX9EC8q?D$pJH!MTAqa=DS5$cb+;hEvjwVfF{4;M{5U&^_+r zvZdu_rildI!*|*A$TzJ&apQWV@p{!W`=?t(o0{?9y&vM)V)ycGSlI3`;ps(vf2PUq zX745#`cmT*ra7XECC0gKkpu2eyhFEUb?;4@X7weEnLjXj_F~?OzL1U1L0|s6M+kIhmi%`n5vvDALMagi4`wMc=JV{XiO+^ z?s9i7;GgrRW{Mx)d7rj)?(;|b-`iBNPqdwtt%32se@?w4<^KU&585_kZ=`Wy^oLu9 z?DQAh5z%q;UkP48jgMFHTf#mj?#z|=w= z(q6~17Vn}P)J3M?O)x))%a5+>TFW3No~TgP;f}K$#icBh;rSS+R|}l鯊%1Et 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⁢ zv5o9#E7!G0XmdzfsS{efPNi`1b44~SZ4Z8fuX!I}#8g+(wxzQwUT#Xb2(tbY1+EUhGKoT@KEU9Ktl>_0 z%bjDJg;#*gtJZv!-Zs`?^}v5eKmnbjqlvnSzE@_SP|LG_PJ6CYU+6zY6>92%E+ z=j@TZf-iW4(%U{lnYxQA;7Q!b;^brF8n0D>)`q5>|WDDXLrqYU_tKN2>=#@~OE7grMnNh?UOz-O~6 z6%rHy{#h9K0AT+lDC7q4{hw^|q6*Ry;;L%Q@)Ga}$60_q%D)rv(CtS$CQbpq9|y1e zRSrN4;$Jyl{m5bZw`$8TGvb}(LpY{-cQ)fcyJv7l3S52TLXVDsphtv&aPuDk1OzCA z4A^QtC(!11`IsNx_HnSy?>EKpHJWT^wmS~hc^p^zIIh@9f6U@I2 zC=Mve{j2^)mS#U$e{@Q?SO6%LDsXz@SY+=cK_QMmXBIU)j!$ajc-zLx3V60EXJ!qC zi<%2x8Q24YN+&8U@CIlN zrZkcT9yh%LrlGS9`G)KdP(@9Eo-AQz@8GEFWcb7U=a0H^ZVbLmz{+&M7W(nXJ4sN8 zJLR7eeK(K8`2-}j(T7JsO`L!+CvbueT%izanm-^A1Dn{`1Nw`9P?cq;7no+XfC`K(GO9?O^5zNIt4M+M8LM0=7Gz8UA@Z0N+lg+cX)NfazRu z5D)~HA^(u%w^cz+@2@_#S|u>GpB+j4KzQ^&Wcl9f z&hG#bCA(Yk0D&t&aJE^xME^&E-&xGHhXn%}psEIj641H+Nl-}boj;)Zt*t(4wZ5DN z@GXF$bL=&pBq-#vkTkh>7hl%K5|3 z{`Vn9b$iR-SoGENp}bn4;fR3>9sA%X2@1L3aE9yTra;Wb#_`xWwLSLdfu+PAu+o3| zGVnpzPr=ch{uuoHjtw7+_!L_2;knQ!DuDl0R`|%jr+}jFzXtrHIKc323?JO{l&;VF z*L1+}JU7%QJOg|5|Tc|D8fN zJORAg=_vsy{ak|o);@)Yh8Lkcg@$FG3k@ep36BRa^>~UmnRPziS>Z=`Jb2x*Q#`%A zU*i3&Vg?TluO@X0O;r2Jl6LKLUOVhSqg1*qOt^|8*c7 zo(298@+r$k_wQNGHv{|$tW(T8L+4_`FQ{kEW5Jgg{yf7ey4ss_(SNKfz(N9lx&a;< je(UuV8hP?p&}TPdm1I$XmG#(RzlD&B2izSj9sl%y5~4qc literal 0 HcmV?d00001 diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/gradle/wrapper/gradle-wrapper.properties b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..3fa8f86 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/gradlew b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/gradlew new file mode 100755 index 0000000..1aa94a4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/gradlew.bat b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/gradlew.bat new file mode 100755 index 0000000..93e3f59 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/settings.gradle b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/settings.gradle new file mode 100644 index 0000000..4823818 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/application/store/DomainEventStorer.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/application/store/DomainEventStorer.java new file mode 100644 index 0000000..674c72e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/application/store/StoreDomainEventOnOccurred.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/application/store/StoreDomainEventOnOccurred.java new file mode 100644 index 0000000..23413ef --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEvent.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEvent.java new file mode 100644 index 0000000..c2f310e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventAggregateId.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventAggregateId.java new file mode 100644 index 0000000..beef90d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventBody.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventBody.java new file mode 100644 index 0000000..a706b7a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventId.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventId.java new file mode 100644 index 0000000..5f730dd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventName.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/AnalyticsDomainEventName.java new file mode 100644 index 0000000..d4a018e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/DomainEventsRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/analytics/main/tv/codely/analytics/domain_events/domain/DomainEventsRepository.java new file mode 100644 index 0000000..c04ac90 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/build.gradle b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/build.gradle new file mode 100644 index 0000000..7d82dc7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/resources/database/backoffice.sql b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/resources/database/backoffice.sql new file mode 100644 index 0000000..58f92fa --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/resources/database/backoffice/backoffice_courses.json b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/resources/database/backoffice/backoffice_courses.json new file mode 100644 index 0000000..7891e8b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommand.java new file mode 100644 index 0000000..5e1e74c --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandler.java new file mode 100644 index 0000000..ec43db6 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/UserAuthenticator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/application/authenticate/UserAuthenticator.java new file mode 100644 index 0000000..1b43c79 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthPassword.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthPassword.java new file mode 100644 index 0000000..331588f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthRepository.java new file mode 100644 index 0000000..37152ec --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUser.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUser.java new file mode 100644 index 0000000..af0d45e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUsername.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/AuthUsername.java new file mode 100644 index 0000000..6d1d48a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthCredentials.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthCredentials.java new file mode 100644 index 0000000..3092104 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthUsername.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/domain/InvalidAuthUsername.java new file mode 100644 index 0000000..857c10f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/infrastructure/persistence/InMemoryAuthRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/auth/infrastructure/persistence/InMemoryAuthRepository.java new file mode 100644 index 0000000..e873c4f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCourseResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCourseResponse.java new file mode 100644 index 0000000..5871ac9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCoursesResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/BackofficeCoursesResponse.java new file mode 100644 index 0000000..1225864 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java new file mode 100644 index 0000000..496b8cb --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/create/BackofficeCourseCreator.java @@ -0,0 +1,20 @@ +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) { + if (this.repository.search(id).isEmpty()) { + this.repository.save(new BackofficeCourse(id, name, duration)); + } + } +} diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/create/CreateBackofficeCourseOnCourseCreated.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/create/CreateBackofficeCourseOnCourseCreated.java new file mode 100644 index 0000000..fd06214 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/rename/BackofficeCourseRenamer.java new file mode 100644 index 0000000..e50c42f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/rename/RenameBackofficeCourseOnCourseRenamed.java new file mode 100644 index 0000000..c92e48d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/AllBackofficeCoursesSearcher.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/AllBackofficeCoursesSearcher.java new file mode 100644 index 0000000..db65ef0 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQuery.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQuery.java new file mode 100644 index 0000000..fc00a52 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_all/SearchAllBackofficeCoursesQueryHandler.java new file mode 100644 index 0000000..b500126 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/BackofficeCoursesByCriteriaSearcher.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/BackofficeCoursesByCriteriaSearcher.java new file mode 100644 index 0000000..1533079 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQuery.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQuery.java new file mode 100644 index 0000000..2671661 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/application/search_by_criteria/SearchBackofficeCoursesByCriteriaQueryHandler.java new file mode 100644 index 0000000..8bf9363 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourse.java new file mode 100644 index 0000000..05f88b1 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseNotFound.java new file mode 100644 index 0000000..f5596ff --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/domain/BackofficeCourseRepository.java new file mode 100644 index 0000000..3689352 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepository.java new file mode 100644 index 0000000..c97dd85 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepository.java new file mode 100644 index 0000000..b645f5b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepository.java new file mode 100644 index 0000000..a23fd77 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml new file mode 100644 index 0000000..a8592d0 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/courses/infrastructure/persistence/hibernate/BackofficeCourse.hbm.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeElasticsearchConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeElasticsearchConfiguration.java new file mode 100644 index 0000000..98c0f1a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeHibernateConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeHibernateConfiguration.java new file mode 100644 index 0000000..0615ff7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeMySqlEventBusConfiguration.java new file mode 100644 index 0000000..1113298 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeRabbitMqEventBusConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/main/tv/codely/backoffice/shared/infrastructure/persistence/BackofficeRabbitMqEventBusConfiguration.java new file mode 100644 index 0000000..b0fae3c --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/BackofficeContextInfrastructureTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/BackofficeContextInfrastructureTestCase.java new file mode 100644 index 0000000..efbad4e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/AuthModuleUnitTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/AuthModuleUnitTestCase.java new file mode 100644 index 0000000..4ccc749 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandlerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandHandlerShould.java new file mode 100644 index 0000000..a37020e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/application/authenticate/AuthenticateUserCommandMother.java new file mode 100644 index 0000000..42746f5 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthPasswordMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthPasswordMother.java new file mode 100644 index 0000000..8e4f0ab --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUserMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUserMother.java new file mode 100644 index 0000000..14e26c9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUsernameMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/auth/domain/AuthUsernameMother.java new file mode 100644 index 0000000..e96670c --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/ElasticsearchEnvironmentArranger.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/ElasticsearchEnvironmentArranger.java new file mode 100644 index 0000000..08df2f5 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseCriteriaMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseCriteriaMother.java new file mode 100644 index 0000000..ee83f69 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/domain/BackofficeCourseMother.java new file mode 100644 index 0000000..76a1b4b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/ElasticsearchBackofficeCourseRepositoryShould.java new file mode 100644 index 0000000..f94748b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/InMemoryCacheBackofficeCourseRepositoryShould.java new file mode 100644 index 0000000..e82dffb --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/backoffice/test/tv/codely/backoffice/courses/infrastructure/persistence/MySqlBackofficeCourseRepositoryShould.java new file mode 100644 index 0000000..8d9d5ce --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/build.gradle b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/build.gradle new file mode 100644 index 0000000..7d82dc7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/resources/database/mooc.sql b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/resources/database/mooc.sql new file mode 100644 index 0000000..6cc6def --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/CourseResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/CourseResponse.java new file mode 100644 index 0000000..22c8798 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/CoursesResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/CoursesResponse.java new file mode 100644 index 0000000..cd39f43 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/create/CourseCreator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/create/CourseCreator.java new file mode 100644 index 0000000..dbbec32 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommand.java new file mode 100644 index 0000000..5f6f783 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommandHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/create/CreateCourseCommandHandler.java new file mode 100644 index 0000000..85127f4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/find/CourseFinder.java new file mode 100644 index 0000000..b912a3e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQuery.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQuery.java new file mode 100644 index 0000000..187e5e0 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/find/FindCourseQueryHandler.java new file mode 100644 index 0000000..bc8d380 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/search_last/LastCoursesSearcher.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/search_last/LastCoursesSearcher.java new file mode 100644 index 0000000..1e0f6fa --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQuery.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQuery.java new file mode 100644 index 0000000..834a847 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryHandler.java new file mode 100644 index 0000000..91bea0f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/Course.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/Course.java new file mode 100644 index 0000000..ef44a89 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseDuration.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseDuration.java new file mode 100644 index 0000000..42c4482 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseId.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseId.java new file mode 100644 index 0000000..dd6c3c2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseName.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseName.java new file mode 100644 index 0000000..67e0a06 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseNotExist.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseNotExist.java new file mode 100644 index 0000000..d3490be --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/domain/CourseRepository.java new file mode 100644 index 0000000..4d2e696 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepository.java new file mode 100644 index 0000000..622d60a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepository.java new file mode 100644 index 0000000..b90c0cf --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml new file mode 100644 index 0000000..88809da --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses/infrastructure/persistence/hibernate/Course.hbm.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterFinder.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterFinder.java new file mode 100644 index 0000000..e0ccfe7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponse.java new file mode 100644 index 0000000..a4ee91a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQuery.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQuery.java new file mode 100644 index 0000000..7586b4b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandler.java new file mode 100644 index 0000000..1eefb82 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/CoursesCounterIncrementer.java new file mode 100644 index 0000000..2f80400 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreated.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreated.java new file mode 100644 index 0000000..f8465fb --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounter.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounter.java new file mode 100644 index 0000000..2a13583 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterId.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterId.java new file mode 100644 index 0000000..3899c15 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterNotInitialized.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterNotInitialized.java new file mode 100644 index 0000000..b20ca87 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterRepository.java new file mode 100644 index 0000000..ccbbb71 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterTotal.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/domain/CoursesCounterTotal.java new file mode 100644 index 0000000..65a299d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepository.java new file mode 100644 index 0000000..f9cb245 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/hibernate/CoursesCounter.hbm.xml b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/courses_counter/infrastructure/persistence/hibernate/CoursesCounter.hbm.xml new file mode 100644 index 0000000..8ea7482 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/NewCoursesNewsletterSender.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/NewCoursesNewsletterSender.java new file mode 100644 index 0000000..0e2ea20 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommand.java new file mode 100644 index 0000000..51f6c52 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandler.java new file mode 100644 index 0000000..2c6dad7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/Email.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/Email.java new file mode 100644 index 0000000..120feff --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/EmailId.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/EmailId.java new file mode 100644 index 0000000..dd03323 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/EmailSender.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/EmailSender.java new file mode 100644 index 0000000..b393b2d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletter.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletter.java new file mode 100644 index 0000000..0fc60cd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSent.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSent.java new file mode 100644 index 0000000..b03f209 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/infrastructure/FakeEmailSender.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/notifications/infrastructure/FakeEmailSender.java new file mode 100644 index 0000000..17501f3 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocHibernateConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocHibernateConfiguration.java new file mode 100644 index 0000000..88f915d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocMySqlEventBusConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocMySqlEventBusConfiguration.java new file mode 100644 index 0000000..dc8f346 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocRabbitMqEventBusConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/shared/infrastructure/persistence/MoocRabbitMqEventBusConfiguration.java new file mode 100644 index 0000000..df81787 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/Step.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/Step.java new file mode 100644 index 0000000..838d110 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/StepId.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/StepId.java new file mode 100644 index 0000000..7f5d07b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/StepRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/StepRepository.java new file mode 100644 index 0000000..419f53d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/StepTitle.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/StepTitle.java new file mode 100644 index 0000000..b1a14a5 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStep.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStep.java new file mode 100644 index 0000000..9d2be39 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatement.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatement.java new file mode 100644 index 0000000..bfc8a40 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStep.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStep.java new file mode 100644 index 0000000..81f6c2d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStepText.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/domain/video/VideoStepText.java new file mode 100644 index 0000000..99e36c9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepository.java new file mode 100644 index 0000000..6b4b103 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml new file mode 100644 index 0000000..31cd919 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/steps/infrastructure/persistence/hibernate/VideoStep.hbm.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/StudentResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/StudentResponse.java new file mode 100644 index 0000000..9b8bc05 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/StudentsResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/StudentsResponse.java new file mode 100644 index 0000000..9a7e035 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/search_all/AllStudentsSearcher.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/search_all/AllStudentsSearcher.java new file mode 100644 index 0000000..a079520 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQuery.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQuery.java new file mode 100644 index 0000000..618a9fa --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryHandler.java new file mode 100644 index 0000000..c9e3fa4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/domain/Student.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/domain/Student.java new file mode 100644 index 0000000..eb0353d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/domain/StudentId.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/domain/StudentId.java new file mode 100644 index 0000000..3e6735e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/domain/StudentRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/domain/StudentRepository.java new file mode 100644 index 0000000..0fbd2e6 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/infrastructure/InMemoryStudentRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/main/tv/codely/mooc/students/infrastructure/InMemoryStudentRepository.java new file mode 100644 index 0000000..aabd3ab --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/MoocContextInfrastructureTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/MoocContextInfrastructureTestCase.java new file mode 100644 index 0000000..eb574c4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/CoursesModuleInfrastructureTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/CoursesModuleInfrastructureTestCase.java new file mode 100644 index 0000000..a52cf56 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/CoursesModuleUnitTestCase.java new file mode 100644 index 0000000..eb7454d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/CourseResponseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/CourseResponseMother.java new file mode 100644 index 0000000..564e527 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/CoursesResponseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/CoursesResponseMother.java new file mode 100644 index 0000000..1067e22 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandHandlerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandHandlerShould.java new file mode 100644 index 0000000..efdd3f7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/create/CreateCourseCommandMother.java new file mode 100644 index 0000000..f4dad05 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/application/search_last/SearchLastCoursesQueryMother.java new file mode 100644 index 0000000..c589326 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseCreatedDomainEventMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseCreatedDomainEventMother.java new file mode 100644 index 0000000..e73f2d4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseDurationMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseDurationMother.java new file mode 100644 index 0000000..d9ed986 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseIdMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseIdMother.java new file mode 100644 index 0000000..76bf0d9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseMother.java new file mode 100644 index 0000000..20f225e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseNameMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/domain/CourseNameMother.java new file mode 100644 index 0000000..1ca25f8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/InMemoryCourseRepositoryShould.java new file mode 100644 index 0000000..04e3d6a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses/infrastructure/persistence/MySqlCourseRepositoryShould.java new file mode 100644 index 0000000..45b8a1b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleInfrastructureTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleInfrastructureTestCase.java new file mode 100644 index 0000000..18978bb --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleUnitTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/CoursesCounterModuleUnitTestCase.java new file mode 100644 index 0000000..8c06e7f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/application/find/CoursesCounterResponseMother.java new file mode 100644 index 0000000..4b38949 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandlerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/application/find/FindCoursesCounterQueryHandlerShould.java new file mode 100644 index 0000000..1065514 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreatedShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/application/increment/IncrementCoursesCounterOnCourseCreatedShould.java new file mode 100644 index 0000000..13d6218 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterIdMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterIdMother.java new file mode 100644 index 0000000..596a118 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterMother.java new file mode 100644 index 0000000..9e5a27f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterTotalMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/domain/CoursesCounterTotalMother.java new file mode 100644 index 0000000..b8c22cb --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/courses_counter/infrastructure/persistence/MySqlCoursesCounterRepositoryShould.java new file mode 100644 index 0000000..fabdfe8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/application/NotificationsModuleUnitTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/application/NotificationsModuleUnitTestCase.java new file mode 100644 index 0000000..6b12480 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandlerShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandHandlerShould.java new file mode 100644 index 0000000..8134c05 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/application/send_new_courses_newsletter/SendNewCoursesNewsletterCommandMother.java new file mode 100644 index 0000000..f91caff --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/domain/EmailIdMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/domain/EmailIdMother.java new file mode 100644 index 0000000..0211444 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSentMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterEmailSentMother.java new file mode 100644 index 0000000..97afd0f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/notifications/domain/NewCoursesNewsletterMother.java new file mode 100644 index 0000000..a5ff7f5 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/mysql/MySqlEventBusShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/mysql/MySqlEventBusShould.java new file mode 100644 index 0000000..17dc703 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusShould.java new file mode 100644 index 0000000..70c8de2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/TestAllWorksOnRabbitMqEventsPublished.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/shared/infrastructure/bus/event/rabbitmq/TestAllWorksOnRabbitMqEventsPublished.java new file mode 100644 index 0000000..8b1981d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/StepsModuleInfrastructureTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/StepsModuleInfrastructureTestCase.java new file mode 100644 index 0000000..cc0c90d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/StepIdMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/StepIdMother.java new file mode 100644 index 0000000..7747f37 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/StepTitleMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/StepTitleMother.java new file mode 100644 index 0000000..571bc13 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepMother.java new file mode 100644 index 0000000..46fcb75 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatementMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/challenge/ChallengeStepStatementMother.java new file mode 100644 index 0000000..8e85970 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepMother.java new file mode 100644 index 0000000..3cb9432 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepTextMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/domain/video/VideoStepTextMother.java new file mode 100644 index 0000000..7dca6fa --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepositoryShould.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/steps/infrastructure/persistence/MySqlStepRepositoryShould.java new file mode 100644 index 0000000..88c684e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/students/application/StudentResponseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/students/application/StudentResponseMother.java new file mode 100644 index 0000000..b29fb25 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/students/application/StudentsResponseMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/students/application/StudentsResponseMother.java new file mode 100644 index 0000000..89e879a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/students/application/search_all/SearchAllStudentsQueryMother.java new file mode 100644 index 0000000..93a0e28 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/students/domain/StudentIdMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/mooc/test/tv/codely/mooc/students/domain/StudentIdMother.java new file mode 100644 index 0000000..b98ca0e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/build.gradle b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/build.gradle new file mode 100644 index 0000000..7d82dc7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/build.gradle @@ -0,0 +1,2 @@ +dependencies { +} diff --git a/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/AggregateRoot.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/AggregateRoot.java new file mode 100644 index 0000000..796e327 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/DomainError.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/DomainError.java new file mode 100644 index 0000000..1d65456 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Identifier.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Identifier.java new file mode 100644 index 0000000..b25970b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/IntValueObject.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/IntValueObject.java new file mode 100644 index 0000000..4e943e5 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Logger.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Logger.java new file mode 100644 index 0000000..efeab9f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Monitoring.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Monitoring.java new file mode 100644 index 0000000..0958579 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Service.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Service.java new file mode 100644 index 0000000..d3f9566 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/StringValueObject.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/StringValueObject.java new file mode 100644 index 0000000..45525e5 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Utils.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/Utils.java new file mode 100644 index 0000000..a3a56a6 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/UuidGenerator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/UuidGenerator.java new file mode 100644 index 0000000..8348a24 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/VideoUrl.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/VideoUrl.java new file mode 100644 index 0000000..47aaccc --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/Command.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/Command.java new file mode 100644 index 0000000..da5a342 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/CommandBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/CommandBus.java new file mode 100644 index 0000000..dabddf2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandler.java new file mode 100644 index 0000000..177e09b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandlerExecutionError.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/CommandHandlerExecutionError.java new file mode 100644 index 0000000..60d0d74 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/CommandNotRegisteredError.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/command/CommandNotRegisteredError.java new file mode 100644 index 0000000..2d3af85 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/event/DomainEvent.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/event/DomainEvent.java new file mode 100644 index 0000000..901955c --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/event/DomainEventSubscriber.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/event/DomainEventSubscriber.java new file mode 100644 index 0000000..9895464 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/event/EventBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/event/EventBus.java new file mode 100644 index 0000000..cd13e0b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/Query.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/Query.java new file mode 100644 index 0000000..cdc5477 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/QueryBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/QueryBus.java new file mode 100644 index 0000000..197945f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandler.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandler.java new file mode 100644 index 0000000..4b56bd8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandlerExecutionError.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/QueryHandlerExecutionError.java new file mode 100644 index 0000000..2e5135b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/QueryNotRegisteredError.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/QueryNotRegisteredError.java new file mode 100644 index 0000000..d2d380f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/Response.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/bus/query/Response.java new file mode 100644 index 0000000..ac3eefc --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/course/CourseCreatedDomainEvent.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/course/CourseCreatedDomainEvent.java new file mode 100644 index 0000000..23181f0 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/course/CourseRenamedDomainEvent.java new file mode 100644 index 0000000..2fb4097 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/Criteria.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/Criteria.java new file mode 100644 index 0000000..aea18af --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/Filter.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/Filter.java new file mode 100644 index 0000000..b3244a6 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/FilterField.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/FilterField.java new file mode 100644 index 0000000..c5f230d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/FilterOperator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/FilterOperator.java new file mode 100644 index 0000000..8614f12 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/FilterValue.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/FilterValue.java new file mode 100644 index 0000000..28a4f48 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/Filters.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/Filters.java new file mode 100644 index 0000000..83e36ae --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/Order.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/Order.java new file mode 100644 index 0000000..f95eaf8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/OrderBy.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/OrderBy.java new file mode 100644 index 0000000..2c1450d --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/OrderType.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/domain/criteria/OrderType.java new file mode 100644 index 0000000..52deae0 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/JavaUuidGenerator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/JavaUuidGenerator.java new file mode 100644 index 0000000..21667a8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/command/CommandHandlersInformation.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/command/CommandHandlersInformation.java new file mode 100644 index 0000000..5bdd8fc --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/command/InMemoryCommandBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/command/InMemoryCommandBus.java new file mode 100644 index 0000000..d8b23c7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonDeserializer.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonDeserializer.java new file mode 100644 index 0000000..ccfa660 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonSerializer.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventJsonSerializer.java new file mode 100644 index 0000000..adbedb9 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscriberInformation.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscriberInformation.java new file mode 100644 index 0000000..e14d3d2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventSubscribersInformation.java new file mode 100644 index 0000000..88556f1 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventsInformation.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/DomainEventsInformation.java new file mode 100644 index 0000000..9498913 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlDomainEventsConsumer.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlDomainEventsConsumer.java new file mode 100644 index 0000000..263da44 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlEventBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/mysql/MySqlEventBus.java new file mode 100644 index 0000000..231ab21 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java new file mode 100644 index 0000000..d1620f3 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqDomainEventsConsumer.java @@ -0,0 +1,138 @@ +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); + + System.out.println("ACK: Consumed correctly!"); + } catch (Exception error) { + System.out.println("Error consuming"); + + 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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBus.java new file mode 100644 index 0000000..2bad6fd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqEventBusConfiguration.java new file mode 100644 index 0000000..f95e188 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqExchangeNameFormatter.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqExchangeNameFormatter.java new file mode 100644 index 0000000..f399cac --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqPublisher.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqPublisher.java new file mode 100644 index 0000000..61f7fe4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqQueueNameFormatter.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/rabbitmq/RabbitMqQueueNameFormatter.java new file mode 100644 index 0000000..13c091f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/event/spring/SpringApplicationEventBus.java new file mode 100644 index 0000000..56a6205 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/query/InMemoryQueryBus.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/query/InMemoryQueryBus.java new file mode 100644 index 0000000..8d448c6 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/query/QueryHandlersInformation.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/bus/query/QueryHandlersInformation.java new file mode 100644 index 0000000..e9d7606 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/cli/ConsoleCommand.java new file mode 100644 index 0000000..dd90a88 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/config/EnvironmentConfig.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/config/EnvironmentConfig.java new file mode 100644 index 0000000..c9d898f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/config/Parameter.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/config/Parameter.java new file mode 100644 index 0000000..ac521bb --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/config/ParameterNotExist.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/config/ParameterNotExist.java new file mode 100644 index 0000000..aa329d1 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchClient.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchClient.java new file mode 100644 index 0000000..78a458f --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchCriteriaConverter.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchCriteriaConverter.java new file mode 100644 index 0000000..99e2585 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/elasticsearch/ElasticsearchRepository.java new file mode 100644 index 0000000..c87903b --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateConfigurationFactory.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateConfigurationFactory.java new file mode 100644 index 0000000..2dbd1ba --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateCriteriaConverter.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateCriteriaConverter.java new file mode 100644 index 0000000..5679bbd --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/hibernate/HibernateRepository.java new file mode 100644 index 0000000..b6ae61e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/hibernate/JsonListType.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/hibernate/JsonListType.java new file mode 100644 index 0000000..188dd98 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/spring/ApiController.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/spring/ApiController.java new file mode 100644 index 0000000..fafc2a8 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/spring/ApiExceptionMiddleware.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/spring/ApiExceptionMiddleware.java new file mode 100644 index 0000000..ac1db97 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/ValidationResponse.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/ValidationResponse.java new file mode 100644 index 0000000..6137031 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/Validator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/Validator.java new file mode 100644 index 0000000..d08cc93 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/ValidatorNotExist.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/ValidatorNotExist.java new file mode 100644 index 0000000..c75d631 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/FieldValidator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/FieldValidator.java new file mode 100644 index 0000000..65ace91 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/NotEmptyValidator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/NotEmptyValidator.java new file mode 100644 index 0000000..106e7ff --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/RequiredValidator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/RequiredValidator.java new file mode 100644 index 0000000..862b5a2 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/StringValidator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/StringValidator.java new file mode 100644 index 0000000..f653ac7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/UuidValidator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/main/tv/codely/shared/infrastructure/validation/validators/UuidValidator.java new file mode 100644 index 0000000..c39d679 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/EmailMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/EmailMother.java new file mode 100644 index 0000000..b8d4077 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/IntegerMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/IntegerMother.java new file mode 100644 index 0000000..fe197ce --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/ListMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/ListMother.java new file mode 100644 index 0000000..5dc7622 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/MotherCreator.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/MotherCreator.java new file mode 100644 index 0000000..eef2ac4 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/RandomElementPicker.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/RandomElementPicker.java new file mode 100644 index 0000000..a69497e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/UuidMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/UuidMother.java new file mode 100644 index 0000000..1075dd7 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/VideoUrlMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/VideoUrlMother.java new file mode 100644 index 0000000..1588ec6 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/WordMother.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/domain/WordMother.java new file mode 100644 index 0000000..4bad17a --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/infrastructure/InfrastructureTestCase.java new file mode 100644 index 0000000..af93e67 --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/infrastructure/UnitTestCase.java b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/src/shared/test/tv/codely/shared/infrastructure/UnitTestCase.java new file mode 100644 index 0000000..96e007e --- /dev/null +++ b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/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/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/var/log/.gitkeep b/02-duplicated_events/3-counter_handle_duplicated_events/2-counter_incrementing_once_per_course/var/log/.gitkeep new file mode 100644 index 0000000..e69de29