From 12235acc42314abd07137706ee9012991f3b9614 Mon Sep 17 00:00:00 2001 From: Alexander Kobjolke Date: Thu, 7 Nov 2024 21:02:53 +0100 Subject: [PATCH] chore: Initial import of FLEX training material --- flex-training-flexinale/.gitignore | 56 ++ flex-training-flexinale/LICENSE | 201 ++++++ flex-training-flexinale/README.md | 14 + .../flexinale-distributed/README.md | 6 + .../SpotbugsExcludeFilter.xml | 14 + .../infrastructure/docker/Dockerfile | 8 + .../infrastructure/docker/docker_build.sh | 1 + .../infrastructure/podman/Dockerfile | 8 + .../infrastructure/podman/podman_build.bat | 1 + .../flexinale-distributed-backoffice/pom.xml | 167 +++++ ...DistributedApplicationBackoffice_).run.xml | 16 + ...office - Actuator Events Published.run.xml | 5 + ...buted Backoffice - Actuator Health.run.xml | 5 + ...ributed Backoffice - Actuator Info.run.xml | 5 + ...uted Backoffice - Actuator Metrics.run.xml | 5 + ...d Backoffice - Actuator Prometheus.run.xml | 5 + ...inaleDistributedApplicationBackoffice.java | 23 + .../backoffice/api_in/rest/ExcelHelper.java | 15 + .../api_in/rest/FilmRestController.java | 130 ++++ .../rest/KinoKinoSaalRestController.java | 167 +++++ .../api_in/rest/RestBadRequestException.java | 19 + .../rest/RestResourceNotFoundException.java | 14 + .../api_in/rest/RestResponseMessage.java | 4 + .../api_in/rest/RestUploadResult.java | 7 + .../rest/VorfuehrungRestController.java | 109 ++++ .../BootstrappingPostConstructBackoffice.java | 59 ++ .../api_out/event/FilmPublisher.java | 80 +++ .../api_out/event/KinoPublisher.java | 80 +++ .../api_out/event/VorfuehrungPublisher.java | 80 +++ .../event/mapper/Film2FilmTOMapper.java | 16 + .../KinoKinoSaal2KinoKinoSaalTOMapper.java | 43 ++ .../Vorfuehrung2VorfuehrungTOMapper.java | 20 + .../application/services/FilmService.java | 25 + .../services/FilmUploadService.java | 58 ++ .../services/KinoSaalUploadService.java | 95 +++ .../application/services/KinoService.java | 26 + .../services/KinoUploadService.java | 81 +++ .../services/VorfuehrungService.java | 26 + .../services/VorfuehrungUploadService.java | 65 ++ .../backoffice/domain/dao/FilmDao.java | 13 + .../backoffice/domain/dao/KinoDao.java | 13 + .../backoffice/domain/dao/KinoSaalDao.java | 13 + .../backoffice/domain/dao/VorfuehrungDao.java | 17 + .../backoffice/domain/model/Film.java | 95 +++ .../backoffice/domain/model/Kino.java | 145 +++++ .../backoffice/domain/model/KinoSaal.java | 118 ++++ .../backoffice/domain/model/Vorfuehrung.java | 141 +++++ .../domain/services/FilmService.java | 24 + .../domain/services/KinoService.java | 24 + .../domain/services/VorfuehrungService.java | 24 + ...officeActuatorEndpointEventsPublished.java | 34 ++ .../FlexinaleBackofficeSpringFactory.java | 137 +++++ .../persistence/FilmEntity.java | 78 +++ .../persistence/FilmJpaRepository.java | 8 + .../FilmJpaRepositoryDelegate.java | 59 ++ .../persistence/KinoEntity.java | 89 +++ .../persistence/KinoJpaRepository.java | 8 + .../KinoJpaRepositoryDelegate.java | 59 ++ .../persistence/KinoSaalEntity.java | 88 +++ .../persistence/KinoSaalJpaRepository.java | 8 + .../KinoSaalJpaRepositoryDelegate.java | 58 ++ .../persistence/VorfuehrungEntity.java | 96 +++ .../persistence/VorfuehrungJpaRepository.java | 14 + .../VorfuehrungJpaRepositoryDelegate.java | 65 ++ .../mapper/FilmEntity2FilmMapper.java | 33 + ...KinoKinoSaalEntity2KinoKinoSaalMapper.java | 86 +++ .../VorfuehrungEntity2VorfuehrungMapper.java | 52 ++ .../src/main/resources/application.properties | 70 +++ .../src/main/resources/flexinale-banner.txt | 10 + .../src/main/resources/logback.xml | 20 + .../src/main/resources/static/index.html | 12 + .../backoffice-actuator-events-published.http | 3 + .../test/curl/backoffice-actuator-health.http | 3 + .../test/curl/backoffice-actuator-info.http | 3 + .../curl/backoffice-actuator-metrics.http | 3 + .../curl/backoffice-actuator-prometheus.http | 3 + .../ApplicationPropertiesFileTest.java | 21 + ...ributedApplicationBackofficeSmokeTest.java | 20 + ...ApplicationBackofficeSpringConfigTest.java | 31 + .../application-configtest.properties | 1 + .../src/test/resources/application.properties | 39 ++ .../src/test/resources/logback-test.xml | 20 + .../.gitignore | 4 + .../SpotbugsExcludeFilter.xml | 19 + .../pom.xml | 71 +++ .../event/AllBackofficeEvents.java | 21 + .../api_contract/event/FilmCRUDEvent.java | 42 ++ .../api_contract/event/FilmCreatedEvent.java | 22 + .../api_contract/event/FilmDeletedEvent.java | 22 + .../api_contract/event/FilmUpdatedEvent.java | 22 + .../api_contract/event/KinoCRUDEvent.java | 42 ++ .../api_contract/event/KinoCreatedEvent.java | 22 + .../api_contract/event/KinoDeletedEvent.java | 22 + .../api_contract/event/KinoUpdatedEvent.java | 22 + .../event/VorfuehrungCRUDEvent.java | 42 ++ .../event/VorfuehrungCreatedEvent.java | 22 + .../event/VorfuehrungDeletedEvent.java | 22 + .../event/VorfuehrungUpdatedEvent.java | 22 + .../api_contract/event/model/FilmTO.java | 33 + .../api_contract/event/model/KinoSaalTO.java | 73 +++ .../api_contract/event/model/KinoTO.java | 61 ++ .../event/model/VorfuehrungTO.java | 80 +++ .../src/main/resources/logback.xml | 20 + .../event/EventSerializationTest.java | 176 ++++++ .../event/EventStructureTest.java | 54 ++ .../api_contract/event/ReflectionHelper.java | 25 + .../SpotbugsExcludeFilter.xml | 76 +++ .../infrastructure/docker/Dockerfile | 8 + .../infrastructure/docker/docker_build.sh | 1 + .../infrastructure/podman/Dockerfile | 8 + .../infrastructure/podman/podman_build.bat | 1 + .../pom.xml | 162 +++++ ...ributedApplicationBesucherportal_).run.xml | 16 + ...erportal - Actuator Cache Contents.run.xml | 5 + ...rportal - Actuator Events Consumed.run.xml | 5 + ...portal - Actuator Events Published.run.xml | 5 + ...d Besucherportal - Actuator Health.run.xml | 5 + ...ted Besucherportal - Actuator Info.run.xml | 5 + ... Besucherportal - Actuator Metrics.run.xml | 5 + ...sucherportal - Actuator Prometheus.run.xml | 5 + ...eDistributedApplicationBesucherportal.java | 14 + .../api_in/event/FilmSubscriber.java | 78 +++ .../api_in/event/KinoSubscriber.java | 101 ++++ .../api_in/event/KontingentSubscriber.java | 54 ++ .../api_in/event/TicketSubscriber.java | 66 ++ .../api_in/event/VorfuehrungSubscriber.java | 77 +++ .../api_in/web/FilmWebController.java | 110 ++++ .../api_in/web/IndexWebController.java | 25 + .../api_in/web/KinoWebController.java | 48 ++ .../api_in/web/TicketSorter.java | 166 +++++ .../api_in/web/TicketWebController.java | 89 +++ ...GutscheinEinloesenBeauftragtPublisher.java | 46 ++ .../services/FKKsVTInMemoryCache.java | 127 ++++ .../application/services/FilmService.java | 65 ++ ...tscheinEinloesenBeauftragtPublication.java | 10 + .../application/services/KinoService.java | 23 + .../application/services/TicketBundle.java | 65 ++ .../application/services/TicketService.java | 20 + .../services/VorfuehrungService.java | 22 + .../infrastructure/ActuatorEndpointCache.java | 43 ++ ...rPortalActuatorEndpointEventsConsumed.java | 36 ++ ...PortalActuatorEndpointEventsPublished.java | 36 ++ .../FlexinaleBesucherPortalSpringFactory.java | 106 ++++ .../src/main/resources/application.properties | 74 +++ .../src/main/resources/flexinale-banner.txt | 10 + .../src/main/resources/logback.xml | 20 + .../src/main/resources/static/background.jpeg | Bin 0 -> 32245 bytes .../src/main/resources/static/clear.html | 44 ++ .../main/resources/static/construction.gif | Bin 0 -> 6159 bytes .../src/main/resources/static/crypto.js | 92 +++ .../main/resources/static/css/app/main.css | 82 +++ .../css/vendor/jquery-ui.structure.min.css | 5 + .../static/css/vendor/jquery-ui.theme.css | 443 ++++++++++++++ .../static/css/vendor/jquery.dataTables.css | 448 ++++++++++++++ .../css/vendor/jquery.ui.timepicker.css | 57 ++ .../static/css/vendor/picnic.min.css | 2 + .../static/css/vendor/select2.min.css | 1 + .../src/main/resources/static/dragon.html | 82 +++ .../src/main/resources/static/dragon.js | 91 +++ .../src/main/resources/static/dragon.png | Bin 0 -> 20628 bytes .../src/main/resources/static/drindex.html | 85 +++ .../src/main/resources/static/faq.html | 70 +++ .../src/main/resources/static/icon-192.png | Bin 0 -> 49154 bytes .../src/main/resources/static/icons/movie.png | Bin 0 -> 10972 bytes .../resources/static/icons/movie_camera.png | Bin 0 -> 4623 bytes .../main/resources/static/icons/movies.png | Bin 0 -> 14666 bytes .../src/main/resources/static/logo.png | Bin 0 -> 73427 bytes .../src/main/resources/static/manifest.json | 15 + .../src/main/resources/static/site.js | 17 + .../src/main/resources/static/styles.css | 161 +++++ .../src/main/resources/static/sw.js | 51 ++ .../src/main/resources/templates/error.html | 59 ++ .../src/main/resources/templates/film.html | 111 ++++ .../src/main/resources/templates/filme.html | 41 ++ .../src/main/resources/templates/header.html | 38 ++ .../src/main/resources/templates/index.html | 33 + .../src/main/resources/templates/kino.html | 62 ++ .../src/main/resources/templates/kinos.html | 44 ++ .../src/main/resources/templates/tickets.html | 78 +++ ...besucherportal-actuator-cacheContents.http | 3 + ...sucherportal-actuator-events-consumed.http | 3 + ...ucherportal-actuator-events-published.http | 3 + .../curl/besucherportal-actuator-health.http | 3 + .../curl/besucherportal-actuator-info.http | 3 + .../curl/besucherportal-actuator-metrics.http | 3 + .../besucherportal-actuator-prometheus.http | 3 + .../ApplicationPropertiesFileTest.java | 21 + ...tedApplicationBesucherportalSmokeTest.java | 20 + ...icationBesucherportalSpringConfigTest.java | 31 + .../application-configtest.properties | 2 + .../src/test/resources/application.properties | 39 ++ .../src/test/resources/logback-test.xml | 20 + .../.gitignore | 4 + .../pom.xml | 70 +++ .../event/AllBesucherportalEvents.java | 14 + .../GutscheinEinloesenBeauftragtEvent.java | 53 ++ .../model/GutscheinEinloesenAuftragTO.java | 38 ++ .../src/main/resources/logback.xml | 20 + .../event/EventSerializationTest.java | 67 ++ .../event/EventStructureTest.java | 52 ++ .../api_contract/event/ReflectionHelper.java | 25 + .../SpotbugsExcludeFilter.xml | 49 ++ .../flexinale-distributed-common/pom.xml | 116 ++++ .../common/api/event/AbstractEvent.java | 105 ++++ .../flexinale/common/api/event/Event.java | 14 + .../common/api/event/EventContext.java | 65 ++ .../common/api/eventbus/EventBus.java | 20 + .../common/api/eventbus/EventBusFactory.java | 7 + .../api/eventbus/EventContextHolder.java | 10 + .../api/eventbus/EventNotification.java | 7 + .../common/api/eventbus/EventPublisher.java | 56 ++ .../common/api/eventbus/EventSubscriber.java | 67 ++ .../eventbus/EventSubscriptionAtStart.java | 19 + .../flexinale/common/application/Config.java | 10 + .../common/application/PersistMode.java | 9 + .../application/PersistedEntityAndResult.java | 12 + .../application/caching/InMemoryCache.java | 129 ++++ .../AbstractExcelDataUploadService.java | 131 ++++ .../services/ExcelDataUploadService.java | 32 + .../common/domain/model/AbstractDao.java | 19 + .../FlexinaleCommonSpringFactory.java | 61 ++ .../infrastructure/FlexinaleSpringConfig.java | 70 +++ .../eventbus/EventSerializationHelper.java | 51 ++ .../EventSubscriberReceiveDispatcher.java | 88 +++ .../eventbus/InMemorySyncEventBus.java | 144 +++++ .../eventbus/InMemorySyncEventBusFactory.java | 23 + .../eventbus/InMemorySyncEventBusSpy.java | 130 ++++ .../InMemorySyncEventBusSpyFactory.java | 17 + .../InMemorySyncEventContextHolder.java | 52 ++ .../eventbus/KafkaAsyncEventBus.java | 140 +++++ .../eventbus/KafkaAsyncEventBusFactory.java | 30 + .../eventbus/KafkaAvailabilityChecker.java | 54 ++ .../eventbus/KafkaConfiguration.java | 92 +++ .../eventbus/KafkaConsumerAdapter.java | 182 ++++++ .../eventbus/KafkaConsumerErrorHandler.java | 76 +++ .../eventbus/KafkaProducerAdapter.java | 60 ++ .../eventbus/KafkaTopicHelper.java | 57 ++ .../eventbus/NopeContextHolder.java | 32 + .../eventbus/NopeEventBusSpy.java | 64 ++ .../eventbus/NopeEventBusSpyFactory.java | 17 + .../ThreadLocalEventContextHolder.java | 28 + .../common/shared_kernel/ClazzHelper.java | 19 + .../common/shared_kernel/DateTimeHelper.java | 14 + .../DeveloperMistakeException.java | 24 + .../DoNotCheckInArchitectureTests.java | 8 + .../common/shared_kernel/EqualsByContent.java | 6 + .../shared_kernel/EventClassHelper.java | 58 ++ .../FlexinaleIllegalArgumentException.java | 24 + .../FlexinaleIllegalStateException.java | 20 + .../common/shared_kernel/Identifiable.java | 31 + .../common/shared_kernel/MapWithCounter.java | 21 + .../common/shared_kernel/Mergeable.java | 5 + .../common/shared_kernel/RawWrapper.java | 25 + .../common/shared_kernel/Selection.java | 71 +++ .../shared_kernel/SelectionMultiMap.java | 30 + .../common/shared_kernel/SynchronizedSet.java | 127 ++++ .../shared_kernel/ThreadSafeCounterMap.java | 21 + .../common/shared_kernel/TimeFormatter.java | 14 + .../common/shared_kernel/Versionable.java | 32 + .../src/main/resources/logback.xml | 20 + .../flexinale/common/api/event/EventTest.java | 78 +++ .../common/api/event/EventTestdata.java | 159 +++++ ...sAndSpyHandlingMultipleEventTypesTest.java | 282 +++++++++ ...orySyncEventBusAndSpySubscriptionTest.java | 423 +++++++++++++ .../InMemorySyncEventBusAndSpyTest.java | 571 ++++++++++++++++++ .../PublisherAndSubscriberTestdata.java | 398 ++++++++++++ .../caching/InMemoryCacheTest.java | 293 +++++++++ .../EventSerializationHelperTest.java | 167 +++++ .../InMemorySyncEventContextHolderTest.java | 105 ++++ .../common/shared_kernel/ClazzHelperTest.java | 118 ++++ .../shared_kernel/EventClassHelperTest.java | 142 +++++ .../shared_kernel/MapWithCounterTest.java | 69 +++ .../common/shared_kernel/RawWrapperTest.java | 61 ++ .../shared_kernel/SelectionMultiMapTest.java | 93 +++ .../common/shared_kernel/SelectionTest.java | 568 +++++++++++++++++ .../shared_kernel/SynchronizedSetTest.java | 233 +++++++ .../ThreadSafeCounterMapTest.java | 69 +++ .../flexinale-distributed-security/pom.xml | 120 ++++ .../ApplicationSecurityConfiguration.java | 61 ++ .../security/BenutzerDetailsService.java | 50 ++ .../security/MethodSecurityConfig.java | 8 + .../flexinale/security/UserPrincipal.java | 43 ++ .../FlexinaleSecuritySpringFactory.java | 31 + .../persistence/BenutzerDao.java | 17 + .../persistence/BenutzerEntity.java | 133 ++++ .../persistence/BenutzerJpaRepository.java | 21 + .../BenutzerJpaRepositoryDelegate.java | 56 ++ .../persistence/BenutzerUploadService.java | 60 ++ .../src/main/resources/logback.xml | 20 + .../pom.xml | 64 ++ .../api_contract/BesucherRetriever.java | 8 + .../src/main/resources/logback.xml | 20 + .../pom.xml | 157 +++++ ...uted - Component Dependencies Test.run.xml | 18 + ...rfaces And Internal Structure Test.run.xml | 18 + ...rfaces And Internal Structure Test.run.xml | 18 + ...rfaces And Internal Structure Test.run.xml | 18 + ...d - No Dependencies To Spring Test.run.xml | 18 + ...tributed - Onion Dependencies Test.run.xml | 18 + ...rfaces And Internal Structure Test.run.xml | 18 + .../architecturetests/ArchUnitHelper.java | 339 +++++++++++ .../ComponentDependenciesTest.java | 290 +++++++++ .../architecturetests/FlexinaleComponent.java | 23 + ...orrectInterfacesAndAttributeTypesTest.java | 143 +++++ ...orrectInterfacesAndAttributeTypesTest.java | 108 ++++ ...orrectInterfacesAndAttributeTypesTest.java | 217 +++++++ ...orrectInterfacesAndAttributeTypesTest.java | 184 ++++++ .../NoDependenciesToSpringTest.java | 76 +++ .../OnionDependenciesTest.java | 162 +++++ .../src/test/resources/application.properties | 39 ++ .../src/test/resources/archunit.properties | 1 + .../src/test/resources/logback-test.xml | 11 + .../pom.xml | 164 +++++ ...DistributedApplicationTestDistributed.java | 13 + ...d2EndWithDistributedKafkaEventBusTest.java | 410 +++++++++++++ .../src/test/resources/application.properties | 39 ++ .../NewDataForRestCallIntegrationTest.xlsx | Bin 0 -> 16478 bytes .../src/test/resources/logback-test.xml | 20 + .../pom.xml | 157 +++++ .../de/accso/flexinale/End2EndEventsTest.java | 216 +++++++ ...eDistributedApplicationTestIntegrated.java | 21 + ...tedApplicationTestIntegratedSmokeTest.java | 20 + ...EndWithIntegratedInMemoryEventBusTest.java | 350 +++++++++++ .../api_in/rest/ExcelHelperTest.java | 39 ++ .../rest/RestControllerRetrieveDataTest.java | 152 +++++ ...KinoKinoSaal2KinoKinoSaalTOMapperTest.java | 34 ++ ...EntityVersioningAndOptimisticLockTest.java | 397 ++++++++++++ .../persistence/FKKsVPersistenceTest.java | 124 ++++ ...KinoSaalEntity2KinoKinoSaalMapperTest.java | 31 + .../api_in/event/FilmSubscriberTest.java | 68 +++ .../api_in/event/KinoSubscriberTest.java | 71 +++ .../event/KontingentSubscriberTest.java | 125 ++++ .../event/VorfuehrungSubscriberTest.java | 83 +++ ...senBeauftragtSubscriberAndSortingTest.java | 277 +++++++++ .../BenutzerSecurityAndPersistenceTest.java | 83 +++ .../shared_code_for_test/TestDataBuilder.java | 107 ++++ .../application/services/TicketCRUDTest.java | 152 +++++ .../application/services/TicketKaufTest.java | 113 ++++ .../services/VorfuehrungUpdatedTest.java | 106 ++++ .../domain/model/KontingentTest.java | 69 +++ ...ityVersioningAndOptimisticLockingTest.java | 165 +++++ .../persistence/TicketPersistenceTest.java | 105 ++++ .../src/test/resources/application.properties | 39 ++ .../NewDataForRestCallIntegrationTest.xlsx | Bin 0 -> 16424 bytes ...UpdatedDataForRestCallIntegrationTest.xlsx | Bin 0 -> 14752 bytes .../src/test/resources/logback-test.xml | 20 + .../flexinale-distributed-testdata/pom.xml | 157 +++++ ...stributed - clean all Kafka topics.run.xml | 18 + ...ata from database and Kafka topics.run.xml | 7 + ...Distributed - delete Benutzer data.run.xml | 19 + ..., Kino, KinoSaal, Vorfuehrung data.run.xml | 19 + ...delete Ticket and Kontingente data.run.xml | 19 + ...ed - delete all data from database.run.xml | 18 + .../Distributed - load Benutzer data.run.xml | 19 + ..., Kino, KinoSaal, Vorfuehrung data.run.xml | 19 + ...ibuted - load all data to database.run.xml | 18 + ...EST upload to add and update filme.run.xml | 5 + ...EST upload to add and update kinos.run.xml | 5 + ...ad to add and update vorfuehrungen.run.xml | 5 + ...t-post-upload-to-add-and-update-filme.http | 10 + ...t-post-upload-to-add-and-update-kinos.http | 10 + ...pload-to-add-and-update-vorfuehrungen.http | 10 + ...exinaleDistributedApplicationTestdata.java | 21 + .../java/testdata/AllDatabaseCleaner.java | 119 ++++ .../test/java/testdata/AllTestDataLoader.java | 109 ++++ .../testdata/BenutzerDatabaseCleaner.java | 64 ++ .../java/testdata/BenutzerTestDataLoader.java | 84 +++ .../test/java/testdata/DatabaseConfig.java | 12 + .../java/testdata/FKKsVDatabaseCleaner.java | 49 ++ .../java/testdata/FKKsVTestDataLoader.java | 137 +++++ .../test/java/testdata/KafkaTopicCleaner.java | 26 + .../TicketsAndKontingenteDatabaseCleaner.java | 43 ++ .../src/test/resources/application.properties | 40 ++ .../src/test/resources/logback-test.xml | 21 + .../resources/testdata/TestDataBenutzer.xlsx | Bin 0 -> 10481 bytes .../TestDataFilmKinoKinoSaalVorfuehrung.xlsx | Bin 0 -> 20382 bytes .../infrastructure/docker/Dockerfile | 8 + .../infrastructure/docker/docker_build.sh | 2 + .../infrastructure/podman/Dockerfile | 8 + .../infrastructure/podman/podman_build.bat | 1 + .../flexinale-distributed-ticketing/pom.xml | 139 +++++ ...eDistributedApplicationTicketing_).run.xml | 16 + ...cketing - Actuator Events Consumed.run.xml | 5 + ...keting - Actuator Events Published.run.xml | 5 + ...ibuted Ticketing - Actuator Health.run.xml | 5 + ...tributed Ticketing - Actuator Info.run.xml | 5 + ...buted Ticketing - Actuator Metrics.run.xml | 5 + ...ed Ticketing - Actuator Prometheus.run.xml | 5 + ...xinaleDistributedApplicationTicketing.java | 23 + ...utscheinEinloesenBeauftragtSubscriber.java | 81 +++ .../api_in/event/VorfuehrungSubscriber.java | 81 +++ .../OnlineKontingentChangedPublisher.java | 55 ++ .../api_out/event/TicketPublisher.java | 71 +++ .../event/mapper/Ticket2TicketTOMapper.java | 20 + .../OnlineKontingentChangedPublication.java | 9 + .../services/TicketPublication.java | 10 + .../application/services/TicketService.java | 133 ++++ .../ticketing/domain/dao/KontingentDao.java | 15 + .../ticketing/domain/dao/TicketDao.java | 20 + .../ticketing/domain/model/Kontingent.java | 186 ++++++ ...ntingentBereitsAusgeschoepftException.java | 32 + .../ticketing/domain/model/Ticket.java | 104 ++++ .../domain/services/KontingentService.java | 23 + .../BootstrappingPostConstructTicketing.java | 49 ++ ...cketingActuatorEndpointEventsConsumed.java | 34 ++ ...ketingActuatorEndpointEventsPublished.java | 34 ++ .../FlexinaleTicketingSpringFactory.java | 100 +++ .../persistence/KontingentEntity.java | 104 ++++ .../persistence/KontingentJpaRepository.java | 15 + .../KontingentJpaRepositoryDelegate.java | 65 ++ .../persistence/TicketEntity.java | 93 +++ .../persistence/TicketJpaRepository.java | 22 + .../TicketJpaRepositoryDelegate.java | 72 +++ .../KontingentEntity2KontingentMapper.java | 41 ++ .../mapper/TicketEntity2TicketMapper.java | 44 ++ .../src/main/resources/application.properties | 72 +++ .../src/main/resources/flexinale-banner.txt | 10 + .../src/main/resources/logback.xml | 20 + .../src/main/resources/static/index.html | 12 + .../ticketing-actuator-events-consumed.http | 3 + .../ticketing-actuator-events-published.http | 3 + .../test/curl/ticketing-actuator-health.http | 3 + .../test/curl/ticketing-actuator-info.http | 3 + .../test/curl/ticketing-actuator-metrics.http | 3 + .../curl/ticketing-actuator-prometheus.http | 3 + .../ApplicationPropertiesFileTest.java | 21 + ...tributedApplicationTicketingSmokeTest.java | 20 + ...dApplicationTicketingSpringConfigTest.java | 31 + .../application-configtest.properties | 2 + .../src/test/resources/application.properties | 39 ++ .../src/test/resources/logback-test.xml | 20 + .../pom.xml | 70 +++ .../event/AllTicketingEvents.java | 16 + .../event/OnlineKontingentChangedEvent.java | 63 ++ .../event/TicketGekauftEvent.java | 57 ++ .../event/TicketUngueltigEvent.java | 57 ++ .../event/model/OnlineKontingentTO.java | 29 + .../api_contract/event/model/TicketTO.java | 40 ++ .../src/main/resources/logback.xml | 20 + .../event/EventSerializationTest.java | 97 +++ .../event/EventStructureTest.java | 52 ++ .../api_contract/event/ReflectionHelper.java | 25 + .../docker/docker-compose_up.sh | 11 + .../flexinale-distributed-backoffice.yml | 16 + .../flexinale-distributed-besucherportal.yml | 14 + .../flexinale-distributed-ticketing.yml | 15 + .../flexinale-distributed-backoffice.yml | 16 + .../flexinale-distributed-besucherportal.yml | 14 + .../flexinale-distributed-ticketing.yml | 15 + .../podman/podman-compose_up.bat | 11 + .../infrastructure/pom.xml | 18 + .../flexinale-distributed/pom.xml | 44 ++ .../flexinale-modulith-1-onion/.gitignore | 4 + .../flexinale-modulith-1-onion/README.md | 7 + .../SpotbugsExcludeFilter.xml | 13 + .../infrastructure/docker/Dockerfile | 8 + .../docker/docker-compose_up.sh | 6 + .../infrastructure/docker/docker_build.sh | 1 + .../docker/flexinale-modulith-1-onion.yml | 13 + .../infrastructure/podman/Dockerfile | 8 + .../podman/flexinale-modulith-1-onion.yml | 13 + .../podman/podman-compose_up.bat | 6 + .../infrastructure/podman/podman_build.bat | 1 + .../flexinale-modulith-1-onion/pom.xml | 118 ++++ ...rfaces And Internal Structure Test.run.xml | 18 + ...rfaces And Internal Structure Test.run.xml | 18 + ...1 - No Dependencies To Spring Test.run.xml | 18 + ...dulith 1 - Onion Dependencies Test.run.xml | 18 + ...ulith 1 - REST upload to add filme.run.xml | 5 + ...ulith 1 - REST upload to add kinos.run.xml | 5 + ...- REST upload to add vorfuehrungen.run.xml | 5 + ... 1 - delete all data from database.run.xml | 19 + ...lith 1 - load all data to database.run.xml | 19 + ...exinaleModulith1OnionApplication_).run.xml | 9 + .../FlexinaleModulith1OnionApplication.java | 21 + .../rest/AbstractExcelRestController.java | 42 ++ .../flexinale/api_in/rest/ExcelHelper.java | 15 + .../api_in/rest/FilmRestController.java | 49 ++ .../rest/KinoKinoSaalRestController.java | 79 +++ .../api_in/rest/RestBadRequestException.java | 19 + .../rest/RestResourceNotFoundException.java | 14 + .../api_in/rest/RestResponseMessage.java | 4 + .../api_in/rest/RestUploadResult.java | 7 + .../rest/VorfuehrungRestController.java | 49 ++ .../api_in/web/FilmWebController.java | 67 ++ .../api_in/web/IndexWebController.java | 23 + .../api_in/web/KinoWebController.java | 46 ++ .../flexinale/api_in/web/TicketSorter.java | 88 +++ .../api_in/web/TicketWebController.java | 95 +++ .../accso/flexinale/application/Config.java | 10 + .../UeberlappendeVorfuehrungException.java | 25 + .../AbstractExcelDataUploadService.java | 103 ++++ .../services/ExcelDataUploadService.java | 26 + .../application/services/FilmService.java | 40 ++ .../services/FilmUploadService.java | 57 ++ .../services/KinoSaalUploadService.java | 84 +++ .../application/services/KinoService.java | 25 + .../services/KinoUploadService.java | 80 +++ .../application/services/PersistMode.java | 9 + .../application/services/TicketService.java | 30 + .../services/VorfuehrungService.java | 25 + .../services/VorfuehrungUploadService.java | 62 ++ .../flexinale/domain/dao/AbstractDao.java | 19 + .../accso/flexinale/domain/dao/FilmDao.java | 12 + .../accso/flexinale/domain/dao/KinoDao.java | 12 + .../flexinale/domain/dao/KinoSaalDao.java | 12 + .../flexinale/domain/dao/KontingentDao.java | 12 + .../accso/flexinale/domain/dao/TicketDao.java | 18 + .../flexinale/domain/dao/VorfuehrungDao.java | 15 + .../flexinale/domain/model/Besucher.java | 87 +++ .../de/accso/flexinale/domain/model/Film.java | 92 +++ .../de/accso/flexinale/domain/model/Kino.java | 121 ++++ .../flexinale/domain/model/KinoSaal.java | 111 ++++ .../flexinale/domain/model/Kontingent.java | 159 +++++ ...ntingentBereitsAusgeschoepftException.java | 32 + .../accso/flexinale/domain/model/Ticket.java | 120 ++++ .../flexinale/domain/model/Vorfuehrung.java | 190 ++++++ .../domain/services/FilmService.java | 69 +++ .../domain/services/KinoService.java | 24 + .../domain/services/TicketBundle.java | 35 ++ .../domain/services/TicketService.java | 62 ++ .../domain/services/VorfuehrungService.java | 24 + .../infrastructure/FlexinaleSpringConfig.java | 70 +++ .../FlexinaleSpringFactory.java | 148 +++++ .../persistence/FilmEntity.java | 78 +++ .../persistence/FilmJpaRepository.java | 8 + .../FilmJpaRepositoryDelegate.java | 59 ++ .../persistence/KinoEntity.java | 89 +++ .../persistence/KinoJpaRepository.java | 8 + .../KinoJpaRepositoryDelegate.java | 59 ++ .../persistence/KinoSaalEntity.java | 88 +++ .../persistence/KinoSaalJpaRepository.java | 8 + .../KinoSaalJpaRepositoryDelegate.java | 58 ++ .../persistence/KontingentEntity.java | 94 +++ .../persistence/KontingentJpaRepository.java | 8 + .../KontingentJpaRepositoryDelegate.java | 59 ++ .../persistence/TicketEntity.java | 90 +++ .../persistence/TicketJpaRepository.java | 17 + .../TicketJpaRepositoryDelegate.java | 66 ++ .../persistence/VorfuehrungEntity.java | 109 ++++ .../persistence/VorfuehrungJpaRepository.java | 14 + .../VorfuehrungJpaRepositoryDelegate.java | 65 ++ .../mapper/FilmEntity2FilmMapper.java | 33 + ...KinoKinoSaalEntity2KinoKinoSaalMapper.java | 89 +++ .../KontingentEntity2KontingentMapper.java | 38 ++ .../mapper/TicketEntity2TicketMapper.java | 55 ++ .../VorfuehrungEntity2VorfuehrungMapper.java | 57 ++ .../ApplicationSecurityConfiguration.java | 59 ++ .../infrastructure/security/BenutzerDao.java | 17 + .../security/BenutzerDetailsService.java | 45 ++ .../security/BenutzerEntity.java | 125 ++++ .../security/BenutzerJpaRepository.java | 21 + .../BenutzerJpaRepositoryDelegate.java | 56 ++ .../security/MethodSecurityConfig.java | 8 + .../security/UserPrincipal.java | 42 ++ .../mapper/BenutzerEntity2BesucherMapper.java | 44 ++ .../services/BenutzerUploadService.java | 64 ++ .../shared_kernel/DateTimeHelper.java | 14 + .../DeveloperMistakeException.java | 24 + .../DoNotCheckInArchitectureTests.java | 8 + .../shared_kernel/EqualsByContent.java | 6 + .../FlexinaleIllegalArgumentException.java | 25 + .../FlexinaleIllegalStateException.java | 20 + .../flexinale/shared_kernel/Identifiable.java | 31 + .../flexinale/shared_kernel/RawWrapper.java | 25 + .../shared_kernel/TimeFormatter.java | 14 + .../flexinale/shared_kernel/Versionable.java | 32 + .../src/main/resources/application.properties | 53 ++ .../src/main/resources/flexinale-banner.txt | 10 + .../src/main/resources/logback.xml | 11 + .../src/main/resources/static/background.jpeg | Bin 0 -> 32245 bytes .../src/main/resources/static/clear.html | 44 ++ .../main/resources/static/construction.gif | Bin 0 -> 6159 bytes .../src/main/resources/static/crypto.js | 92 +++ .../main/resources/static/css/app/main.css | 89 +++ .../css/vendor/jquery-ui.structure.min.css | 5 + .../static/css/vendor/jquery-ui.theme.css | 443 ++++++++++++++ .../static/css/vendor/jquery.dataTables.css | 448 ++++++++++++++ .../css/vendor/jquery.ui.timepicker.css | 57 ++ .../static/css/vendor/picnic.min.css | 2 + .../static/css/vendor/select2.min.css | 1 + .../src/main/resources/static/dragon.html | 82 +++ .../src/main/resources/static/dragon.js | 91 +++ .../src/main/resources/static/dragon.png | Bin 0 -> 20628 bytes .../src/main/resources/static/drindex.html | 85 +++ .../src/main/resources/static/faq.html | 70 +++ .../src/main/resources/static/icon-192.png | Bin 0 -> 49154 bytes .../src/main/resources/static/icons/movie.png | Bin 0 -> 10972 bytes .../resources/static/icons/movie_camera.png | Bin 0 -> 4623 bytes .../main/resources/static/icons/movies.png | Bin 0 -> 14666 bytes .../src/main/resources/static/logo.png | Bin 0 -> 73427 bytes .../src/main/resources/static/manifest.json | 15 + .../src/main/resources/static/site.js | 17 + .../src/main/resources/static/styles.css | 161 +++++ .../src/main/resources/static/sw.js | 51 ++ .../src/main/resources/templates/error.html | 59 ++ .../src/main/resources/templates/film.html | 108 ++++ .../src/main/resources/templates/filme.html | 41 ++ .../src/main/resources/templates/header.html | 38 ++ .../src/main/resources/templates/index.html | 33 + .../src/main/resources/templates/kino.html | 62 ++ .../src/main/resources/templates/kinos.html | 44 ++ .../src/main/resources/templates/tickets.html | 75 +++ ...t-post-upload-to-add-and-update-filme.http | 10 + ...t-post-upload-to-add-and-update-kinos.http | 10 + ...pload-to-add-and-update-vorfuehrungen.http | 10 + .../architecturetests/ArchUnitHelper.java | 324 ++++++++++ ...orrectInterfacesAndAttributeTypesTest.java | 125 ++++ ...orrectInterfacesAndAttributeTypesTest.java | 106 ++++ .../NoDependenciesToSpringTest.java | 55 ++ .../OnionDependenciesTest.java | 114 ++++ .../ApplicationPropertiesFileTest.java | 19 + ...aleModulith1OnionApplicationSmokeTest.java | 18 + ...lith1OnionApplicationSpringConfigTest.java | 31 + .../api_in/rest/ExcelHelperTest.java | 39 ++ .../api_in/rest/RestControllerTest.java | 148 +++++ .../api_in/web/TicketSortingTest.java | 102 ++++ .../services/TicketServiceTest.java | 133 ++++ .../domain/model/KontingentTest.java | 69 +++ .../persistence/BasicPersistenceTest.java | 201 ++++++ ...EntityVersioningAndOptimisticLockTest.java | 571 ++++++++++++++++++ ...KinoSaalEntity2KinoKinoSaalMapperTest.java | 32 + .../BenutzerSecurityAndPersistenceTest.java | 82 +++ .../shared_code_for_test/TestDataBuilder.java | 111 ++++ .../shared_kernel/RawWrapperTest.java | 61 ++ .../test/java/testdata/DatabaseCleaner.java | 42 ++ .../test/java/testdata/TestDataLoader.java | 150 +++++ .../application-configtest.properties | 4 + .../src/test/resources/application.properties | 39 ++ .../src/test/resources/logback-test.xml | 11 + .../src/test/resources/testdata/TestData.xlsx | Bin 0 -> 22145 bytes .../.gitignore | 4 + .../flexinale-modulith-2-components/README.md | 7 + .../SpotbugsExcludeFilter.xml | 30 + .../infrastructure/docker/Dockerfile | 8 + .../docker/docker-compose_up.sh | 6 + .../infrastructure/docker/docker_build.sh | 1 + .../flexinale-modulith-2-components.yml | 13 + .../infrastructure/podman/Dockerfile | 8 + .../flexinale-modulith-2-components.yml | 13 + .../podman/podman-compose_up.bat | 6 + .../infrastructure/podman/podman_build.bat | 1 + .../flexinale-modulith-2-components/pom.xml | 120 ++++ ...th 2 - Component Dependencies Test.run.xml | 18 + ...rfaces And Internal Structure Test.run.xml | 18 + ...rfaces And Internal Structure Test.run.xml | 18 + ...2 - No Dependencies To Spring Test.run.xml | 18 + ...dulith 2 - Onion Dependencies Test.run.xml | 18 + ...ulith 2 - REST upload to add filme.run.xml | 5 + ...ulith 2 - REST upload to add kinos.run.xml | 5 + ...- REST upload to add vorfuehrungen.run.xml | 5 + ...rfaces And Internal Structure Test.run.xml | 18 + .../Modulith 2 - delete Benutzer data.run.xml | 19 + ..., Kino, KinoSaal, Vorfuehrung data.run.xml | 19 + ...delete Ticket and Kontingente data.run.xml | 19 + ... 2 - delete all data from database.run.xml | 18 + .../Modulith 2 - load Benutzer data.run.xml | 18 + ..., Kino, KinoSaal, Vorfuehrung data.run.xml | 18 + ...lith 2 - load all data to database.run.xml | 18 + ...leModulith2ComponentsApplication_).run.xml | 9 + ...exinaleModulith2ComponentsApplication.java | 21 + .../rest/AbstractExcelRestController.java | 41 ++ .../backoffice/api_in/rest/ExcelHelper.java | 15 + .../api_in/rest/FilmRestController.java | 49 ++ .../rest/KinoKinoSaalRestController.java | 79 +++ .../api_in/rest/RestBadRequestException.java | 19 + .../rest/RestResourceNotFoundException.java | 14 + .../api_in/rest/RestResponseMessage.java | 4 + .../api_in/rest/RestUploadResult.java | 7 + .../rest/VorfuehrungRestController.java | 74 +++ .../services/FilmRetrieverService.java | 37 ++ .../application/services/FilmService.java | 25 + .../services/FilmUploadService.java | 58 ++ .../services/KinoRetrieverService.java | 38 ++ .../services/KinoSaalUploadService.java | 85 +++ .../application/services/KinoService.java | 26 + .../services/KinoUploadService.java | 81 +++ .../services/VorfuehrungRetrieverService.java | 37 ++ .../services/VorfuehrungService.java | 27 + .../services/VorfuehrungUploadService.java | 65 ++ .../services/mapper/Film2FilmTOMapper.java | 16 + .../KinoKinoSaal2KinoKinoSaalTOMapper.java | 44 ++ .../Vorfuehrung2VorfuehrungTOMapper.java | 20 + .../backoffice/domain/dao/FilmDao.java | 13 + .../backoffice/domain/dao/KinoDao.java | 13 + .../backoffice/domain/dao/KinoSaalDao.java | 13 + .../backoffice/domain/dao/VorfuehrungDao.java | 16 + .../backoffice/domain/model/Film.java | 92 +++ .../backoffice/domain/model/Kino.java | 121 ++++ .../backoffice/domain/model/KinoSaal.java | 111 ++++ .../backoffice/domain/model/Vorfuehrung.java | 123 ++++ .../domain/services/FilmService.java | 24 + .../domain/services/KinoService.java | 24 + .../domain/services/VorfuehrungService.java | 24 + .../FlexinaleBackofficeSpringFactory.java | 125 ++++ .../persistence/FilmEntity.java | 78 +++ .../persistence/FilmJpaRepository.java | 8 + .../FilmJpaRepositoryDelegate.java | 59 ++ .../persistence/KinoEntity.java | 89 +++ .../persistence/KinoJpaRepository.java | 8 + .../KinoJpaRepositoryDelegate.java | 59 ++ .../persistence/KinoSaalEntity.java | 88 +++ .../persistence/KinoSaalJpaRepository.java | 8 + .../KinoSaalJpaRepositoryDelegate.java | 58 ++ .../persistence/VorfuehrungEntity.java | 96 +++ .../persistence/VorfuehrungJpaRepository.java | 14 + .../VorfuehrungJpaRepositoryDelegate.java | 65 ++ .../mapper/FilmEntity2FilmMapper.java | 34 ++ ...KinoKinoSaalEntity2KinoKinoSaalMapper.java | 86 +++ .../VorfuehrungEntity2VorfuehrungMapper.java | 52 ++ .../api_contract/FilmRetriever.java | 11 + .../api_contract/KinoRetriever.java | 11 + .../api_contract/VorfuehrungRetriever.java | 11 + .../api_contract/model/FilmTO.java | 33 + .../api_contract/model/KinoSaalTO.java | 76 +++ .../api_contract/model/KinoTO.java | 60 ++ .../api_contract/model/VorfuehrungTO.java | 68 +++ .../api_in/web/FilmWebController.java | 98 +++ .../api_in/web/IndexWebController.java | 23 + .../api_in/web/KinoWebController.java | 46 ++ .../api_in/web/TicketSorter.java | 132 ++++ .../api_in/web/TicketWebController.java | 98 +++ .../application/services/FilmService.java | 77 +++ .../application/services/KinoService.java | 24 + .../application/services/TicketBundle.java | 67 ++ .../services/VorfuehrungService.java | 22 + .../FlexinaleBesucherPortalSpringFactory.java | 36 ++ .../flexinale/common/application/Config.java | 10 + .../common/application/PersistMode.java | 9 + .../AbstractExcelDataUploadService.java | 104 ++++ .../services/ExcelDataUploadService.java | 27 + .../common/domain/dao/AbstractDao.java | 19 + .../infrastructure/FlexinaleSpringConfig.java | 70 +++ .../common/shared_kernel/DateTimeHelper.java | 14 + .../DeveloperMistakeException.java | 24 + .../DoNotCheckInArchitectureTests.java | 8 + .../common/shared_kernel/EqualsByContent.java | 6 + .../FlexinaleIllegalArgumentException.java | 25 + .../FlexinaleIllegalStateException.java | 20 + .../common/shared_kernel/Identifiable.java | 31 + .../common/shared_kernel/RawWrapper.java | 25 + .../common/shared_kernel/TimeFormatter.java | 14 + .../common/shared_kernel/Versionable.java | 31 + .../ApplicationSecurityConfiguration.java | 59 ++ .../security/BenutzerDetailsService.java | 48 ++ .../security/MethodSecurityConfig.java | 8 + .../flexinale/security/UserPrincipal.java | 43 ++ .../FlexinaleSecuritySpringFactory.java | 29 + .../persistence/BenutzerDao.java | 17 + .../persistence/BenutzerEntity.java | 125 ++++ .../persistence/BenutzerJpaRepository.java | 21 + .../BenutzerJpaRepositoryDelegate.java | 56 ++ .../services/BenutzerUploadService.java | 64 ++ .../api_contract/BesucherRetriever.java | 8 + .../services/TicketRetrieverService.java | 46 ++ .../application/services/TicketService.java | 36 ++ .../ticketing/domain/dao/KontingentDao.java | 15 + .../ticketing/domain/dao/TicketDao.java | 18 + .../ticketing/domain/model/Kontingent.java | 185 ++++++ .../ticketing/domain/model/Ticket.java | 98 +++ .../domain/services/KontingentService.java | 33 + .../domain/services/TicketService.java | 82 +++ .../FlexinaleTicketingSpringFactory.java | 62 ++ .../persistence/KontingentEntity.java | 104 ++++ .../persistence/KontingentJpaRepository.java | 15 + .../KontingentJpaRepositoryDelegate.java | 65 ++ .../persistence/TicketEntity.java | 86 +++ .../persistence/TicketJpaRepository.java | 18 + .../TicketJpaRepositoryDelegate.java | 66 ++ .../KontingentEntity2KontingentMapper.java | 41 ++ .../mapper/TicketEntity2TicketMapper.java | 43 ++ ...ntingentBereitsAusgeschoepftException.java | 32 + .../api_contract/TicketRetriever.java | 11 + .../api_contract/Ticketing.java | 10 + .../api_contract/model/TicketTO.java | 40 ++ .../src/main/resources/application.properties | 53 ++ .../src/main/resources/flexinale-banner.txt | 10 + .../src/main/resources/logback.xml | 11 + .../src/main/resources/static/background.jpeg | Bin 0 -> 32245 bytes .../src/main/resources/static/clear.html | 44 ++ .../main/resources/static/construction.gif | Bin 0 -> 6159 bytes .../src/main/resources/static/crypto.js | 92 +++ .../main/resources/static/css/app/main.css | 89 +++ .../css/vendor/jquery-ui.structure.min.css | 5 + .../static/css/vendor/jquery-ui.theme.css | 443 ++++++++++++++ .../static/css/vendor/jquery.dataTables.css | 448 ++++++++++++++ .../css/vendor/jquery.ui.timepicker.css | 57 ++ .../static/css/vendor/picnic.min.css | 2 + .../static/css/vendor/select2.min.css | 1 + .../src/main/resources/static/dragon.html | 82 +++ .../src/main/resources/static/dragon.js | 91 +++ .../src/main/resources/static/dragon.png | Bin 0 -> 20628 bytes .../src/main/resources/static/drindex.html | 85 +++ .../src/main/resources/static/faq.html | 70 +++ .../src/main/resources/static/icon-192.png | Bin 0 -> 49154 bytes .../src/main/resources/static/icons/movie.png | Bin 0 -> 10972 bytes .../resources/static/icons/movie_camera.png | Bin 0 -> 4623 bytes .../main/resources/static/icons/movies.png | Bin 0 -> 14666 bytes .../src/main/resources/static/logo.png | Bin 0 -> 73427 bytes .../src/main/resources/static/manifest.json | 15 + .../src/main/resources/static/site.js | 17 + .../src/main/resources/static/styles.css | 161 +++++ .../src/main/resources/static/sw.js | 51 ++ .../src/main/resources/templates/error.html | 59 ++ .../src/main/resources/templates/film.html | 109 ++++ .../src/main/resources/templates/filme.html | 41 ++ .../src/main/resources/templates/header.html | 38 ++ .../src/main/resources/templates/index.html | 33 + .../src/main/resources/templates/kino.html | 62 ++ .../src/main/resources/templates/kinos.html | 44 ++ .../src/main/resources/templates/tickets.html | 75 +++ ...t-post-upload-to-add-and-update-filme.http | 10 + ...t-post-upload-to-add-and-update-kinos.http | 10 + ...pload-to-add-and-update-vorfuehrungen.http | 10 + .../architecturetests/ArchUnitHelper.java | 324 ++++++++++ .../ComponentDependenciesTest.java | 239 ++++++++ .../architecturetests/FlexinaleComponent.java | 23 + ...orrectInterfacesAndAttributeTypesTest.java | 142 +++++ ...orrectInterfacesAndAttributeTypesTest.java | 108 ++++ ...orrectInterfacesAndAttributeTypesTest.java | 184 ++++++ .../NoDependenciesToSpringTest.java | 76 +++ .../OnionDependenciesTest.java | 149 +++++ .../ApplicationPropertiesFileTest.java | 19 + ...dulith2ComponentsApplicationSmokeTest.java | 18 + ...ComponentsApplicationSpringConfigTest.java | 31 + .../api_in/rest/ExcelHelperTest.java | 39 ++ .../api_in/rest/RestControllerTest.java | 147 +++++ ...KinoKinoSaal2KinoKinoSaalTOMapperTest.java | 34 ++ ...EntityVersioningAndOptimisticLockTest.java | 395 ++++++++++++ .../persistence/FKKsVPersistenceTest.java | 123 ++++ ...KinoSaalEntity2KinoKinoSaalMapperTest.java | 31 + .../api_in/web/TicketSortingTest.java | 135 +++++ .../application/services/FilmServiceTest.java | 44 ++ .../common/shared_kernel/RawWrapperTest.java | 61 ++ .../BenutzerSecurityAndPersistenceTest.java | 81 +++ .../shared_code_for_test/TestDataBuilder.java | 108 ++++ .../domain/model/KontingentTest.java | 69 +++ .../domain/services/TicketServiceTest.java | 188 ++++++ ...ityVersioningAndOptimisticLockingTest.java | 163 +++++ .../persistence/TicketPersistenceTest.java | 103 ++++ .../java/testdata/AllDatabaseCleaner.java | 51 ++ .../test/java/testdata/AllTestDataLoader.java | 82 +++ .../testdata/BenutzerDatabaseCleaner.java | 28 + .../java/testdata/BenutzerTestDataLoader.java | 58 ++ .../java/testdata/FKKsVDatabaseCleaner.java | 40 ++ .../java/testdata/FKKsVTestDataLoader.java | 163 +++++ .../TicketsAndKontingenteDatabaseCleaner.java | 34 ++ .../application-configtest.properties | 4 + .../src/test/resources/application.properties | 39 ++ .../src/test/resources/archunit.properties | 1 + .../src/test/resources/logback-test.xml | 11 + .../resources/testdata/TestDataBenutzer.xlsx | Bin 0 -> 10481 bytes .../TestDataFilmKinoKinoSaalVorfuehrung.xlsx | Bin 0 -> 20377 bytes .../flexinale-monolith/.gitignore | 4 + .../flexinale-monolith/README.md | 6 + .../SpotbugsExcludeFilter.xml | 13 + .../infrastructure/docker/Dockerfile | 8 + .../docker/docker-compose_up.sh | 6 + .../infrastructure/docker/docker_build.sh | 1 + .../docker/flexinale-monolith.yml | 13 + .../infrastructure/podman/Dockerfile | 8 + .../podman/flexinale-monolith.yml | 13 + .../podman/podman-compose_up.bat | 6 + .../infrastructure/podman/podman_build.bat | 1 + .../flexinale-monolith/pom.xml | 118 ++++ ...erfaces And Internal StructureTest.run.xml | 18 + ...onolith - REST upload to add filme.run.xml | 5 + ...onolith - REST upload to add kinos.run.xml | 5 + ...- REST upload to add vorfuehrungen.run.xml | 5 + ...th - delete all data from database.run.xml | 19 + ...nolith - load all data to database.run.xml | 19 + ...p (_FlexinaleMonolithApplication_).run.xml | 9 + .../FlexinaleMonolithApplication.java | 25 + .../flexinale/application/FilmService.java | 65 ++ .../flexinale/application/KinoService.java | 24 + ...ntingentBereitsAusgeschoepftException.java | 29 + .../flexinale/application/TicketBundle.java | 35 ++ .../flexinale/application/TicketService.java | 56 ++ .../application/VorfuehrungService.java | 24 + .../accso/flexinale/data/model/Benutzer.java | 93 +++ .../de/accso/flexinale/data/model/Film.java | 70 +++ .../de/accso/flexinale/data/model/Kino.java | 86 +++ .../accso/flexinale/data/model/KinoSaal.java | 80 +++ .../flexinale/data/model/Kontingent.java | 113 ++++ .../de/accso/flexinale/data/model/Ticket.java | 79 +++ .../flexinale/data/model/Vorfuehrung.java | 120 ++++ .../data/persistence/AbstractDao.java | 20 + .../data/persistence/BenutzerDao.java | 26 + .../flexinale/data/persistence/FilmDao.java | 13 + .../flexinale/data/persistence/KinoDao.java | 13 + .../data/persistence/KinoSaalDao.java | 13 + .../data/persistence/KontingentDao.java | 13 + .../flexinale/data/persistence/TicketDao.java | 22 + .../data/persistence/VorfuehrungDao.java | 18 + .../AbstractExcelDataUploadService.java | 103 ++++ .../data/services/BenutzerUploadService.java | 62 ++ .../data/services/ExcelDataUploadService.java | 25 + .../data/services/FilmUploadService.java | 56 ++ .../data/services/KinoSaalUploadService.java | 83 +++ .../data/services/KinoUploadService.java | 79 +++ .../flexinale/data/services/PersistMode.java | 9 + .../services/VorfuehrungUploadService.java | 55 ++ .../rest/AbstractExcelRestController.java | 42 ++ .../flexinale/rest/FilmRestController.java | 49 ++ .../rest/KinoKinoSaalRestController.java | 78 +++ .../rest/RestBadRequestException.java | 19 + .../rest/RestResourceNotFoundException.java | 14 + .../flexinale/rest/RestResponseMessage.java | 4 + .../flexinale/rest/RestUploadResult.java | 7 + .../rest/VorfuehrungRestController.java | 49 ++ .../ApplicationSecurityConfiguration.java | 59 ++ .../security/BenutzerDetailsService.java | 45 ++ .../security/MethodSecurityConfig.java | 8 + .../flexinale/security/UserPrincipal.java | 43 ++ .../DeveloperMistakeException.java | 24 + .../flexinale/shared_kernel/ExcelHelper.java | 15 + .../FlexinaleIllegalArgumentException.java | 25 + .../FlexinaleIllegalStateException.java | 20 + .../flexinale/shared_kernel/Identifiable.java | 26 + .../shared_kernel/TimeFormatter.java | 14 + .../java/de/accso/flexinale/util/Config.java | 51 ++ .../flexinale/web/FilmWebController.java | 64 ++ .../flexinale/web/IndexWebController.java | 23 + .../flexinale/web/KinoWebController.java | 46 ++ .../de/accso/flexinale/web/TicketSorter.java | 84 +++ .../flexinale/web/TicketWebController.java | 82 +++ .../src/main/resources/application.properties | 53 ++ .../src/main/resources/flexinale-banner.txt | 9 + .../src/main/resources/logback.xml | 11 + .../src/main/resources/static/background.jpeg | Bin 0 -> 32245 bytes .../src/main/resources/static/clear.html | 44 ++ .../main/resources/static/construction.gif | Bin 0 -> 6159 bytes .../src/main/resources/static/crypto.js | 92 +++ .../main/resources/static/css/app/main.css | 90 +++ .../css/vendor/jquery-ui.structure.min.css | 5 + .../static/css/vendor/jquery-ui.theme.css | 443 ++++++++++++++ .../static/css/vendor/jquery.dataTables.css | 448 ++++++++++++++ .../css/vendor/jquery.ui.timepicker.css | 57 ++ .../static/css/vendor/picnic.min.css | 2 + .../static/css/vendor/select2.min.css | 1 + .../src/main/resources/static/dragon.html | 82 +++ .../src/main/resources/static/dragon.js | 91 +++ .../src/main/resources/static/dragon.png | Bin 0 -> 20628 bytes .../src/main/resources/static/drindex.html | 85 +++ .../src/main/resources/static/faq.html | 70 +++ .../src/main/resources/static/icon-192.png | Bin 0 -> 49154 bytes .../src/main/resources/static/icons/movie.png | Bin 0 -> 10972 bytes .../resources/static/icons/movie_camera.png | Bin 0 -> 4623 bytes .../main/resources/static/icons/movies.png | Bin 0 -> 14666 bytes .../src/main/resources/static/logo.png | Bin 0 -> 73427 bytes .../src/main/resources/static/manifest.json | 15 + .../src/main/resources/static/site.js | 17 + .../src/main/resources/static/styles.css | 161 +++++ .../src/main/resources/static/sw.js | 51 ++ .../src/main/resources/templates/error.html | 59 ++ .../src/main/resources/templates/film.html | 108 ++++ .../src/main/resources/templates/filme.html | 41 ++ .../src/main/resources/templates/header.html | 38 ++ .../src/main/resources/templates/index.html | 33 + .../src/main/resources/templates/kino.html | 62 ++ .../src/main/resources/templates/kinos.html | 44 ++ .../src/main/resources/templates/tickets.html | 75 +++ ...t-post-upload-to-add-and-update-filme.http | 10 + ...t-post-upload-to-add-and-update-kinos.http | 10 + ...pload-to-add-and-update-vorfuehrungen.http | 10 + .../architecturetests/ArchUnitHelper.java | 196 ++++++ ...orrectInterfacesAndAttributeTypesTest.java | 87 +++ .../ApplicationPropertiesFileTest.java | 19 + .../FlexinaleMonolithApplicationTest.java | 16 + .../flexinale/data/model/KontingentTest.java | 65 ++ .../persistence/BasicPersistenceTest.java | 202 +++++++ .../shared_code_for_test/TestDataBuilder.java | 67 ++ .../shared_kernel/ExcelHelperTest.java | 39 ++ .../flexinale/web/TicketSortingTest.java | 99 +++ .../test/java/testdata/DatabaseCleaner.java | 38 ++ .../test/java/testdata/TestDataLoader.java | 145 +++++ .../src/test/resources/application.properties | 39 ++ .../src/test/resources/logback-test.xml | 11 + .../src/test/resources/testdata/TestData.xlsx | Bin 0 -> 22622 bytes .../docker/docker-compose_all_up.sh | 8 + .../docker/flexinale-network.yml | 5 + .../docker/kafka/docker-compose_down.sh | 7 + .../docker/kafka/docker-compose_up.sh | 7 + .../kafka/flexinale-kafka-1-zookeeper.yml | 11 + .../docker/kafka/flexinale-kafka-2-kafka.yml | 17 + .../kafka/flexinale-kafka-3-kafdrop.yml | 14 + .../infrastructure/docker/postgres/Dockerfile | 5 + .../docker/postgres/docker_build.sh | 1 + .../docker/postgres/docker_run.sh | 5 + .../postgres/flexinale-postgres-init.sql | 17 + .../docker/postgres/flexinale-postgres.yml | 10 + .../podman/flexinale-network.yml | 5 + .../kafka/flexinale-kafka-1-zookeeper.yml | 11 + .../podman/kafka/flexinale-kafka-2-kafka.yml | 17 + .../kafka/flexinale-kafka-3-kafdrop.yml | 14 + .../podman/kafka/podman-compose_down.bat | 7 + .../podman/kafka/podman-compose_up.bat | 7 + .../podman/podman-compose_all_up.bat | 8 + .../infrastructure/podman/postgres/Dockerfile | 5 + .../postgres/flexinale-postgres-init.sql | 17 + .../podman/postgres/flexinale-postgres.yml | 10 + .../podman/postgres/podman_build.bat | 1 + .../podman/postgres/podman_run.bat | 5 + .../infrastructure/pom.xml | 18 + flex-training-flexinale/pom.xml | 180 ++++++ ...ctrain_Kurz-Agenda_V20241111-13_2024.3.pdf | Bin 0 -> 70097 bytes ...erlagen_und_Übungen_V20241111-13_2024.3.pdf | Bin 0 -> 250439 bytes ...pitel-0_Einleitung_V20241111-13_2024.3.pdf | Bin 0 -> 74595 bytes ...legendes-zum-Modul_V20241111-13_2024.3.pdf | Bin 0 -> 104213 bytes ...pitel-2_Motivation_V20241111-13_2024.3.pdf | Bin 0 -> 815227 bytes ...-3_Modularisierung_V20241111-13_2024.3.pdf | Bin 0 -> 2167614 bytes ...itel-4_Integration_V20241111-13_2024.3.pdf | Bin 0 -> 1783248 bytes ...lation-und-Rollout_V20241111-13_2024.3.pdf | Bin 0 -> 331912 bytes ...hung-Fehleranalyse_V20241111-13_2024.3.pdf | Bin 0 -> 170016 bytes ...se-Study-Flexinale_V20241111-13_2024.3.pdf | Bin 0 -> 1421701 bytes ...Kapitel-8_Ausblick_V20241111-13_2024.3.pdf | Bin 0 -> 63826 bytes ...len-und-Referenzen_V20241111-13_2024.3.pdf | Bin 0 -> 1161412 bytes ...allationsanleitung_V20241111-13_2024.3.pdf | Bin 0 -> 1247024 bytes ...tectrain_Deckblatt_V20241111-13_2024.3.pdf | Bin 0 -> 61441 bytes resources/readme.txt | 55 ++ 1020 files changed, 53940 insertions(+) create mode 100644 flex-training-flexinale/.gitignore create mode 100644 flex-training-flexinale/LICENSE create mode 100644 flex-training-flexinale/README.md create mode 100644 flex-training-flexinale/flexinale-distributed/README.md create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/SpotbugsExcludeFilter.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/Dockerfile create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/docker_build.sh create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/Dockerfile create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/podman_build.bat create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed - start Backoffice app (_FlexinaleDistributedApplicationBackoffice_).run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Events Published.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Health.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Info.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Metrics.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Prometheus.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBackoffice.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/ExcelHelper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/FilmRestController.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/KinoKinoSaalRestController.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestBadRequestException.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResourceNotFoundException.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResponseMessage.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestUploadResult.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/VorfuehrungRestController.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/BootstrappingPostConstructBackoffice.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/FilmPublisher.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/KinoPublisher.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/VorfuehrungPublisher.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Film2FilmTOMapper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/KinoKinoSaal2KinoKinoSaalTOMapper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Vorfuehrung2VorfuehrungTOMapper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmUploadService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoSaalUploadService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoUploadService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungUploadService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/FilmDao.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoDao.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoSaalDao.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/VorfuehrungDao.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Film.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Kino.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/KinoSaal.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Vorfuehrung.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/FilmService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/KinoService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/VorfuehrungService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeActuatorEndpointEventsPublished.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeSpringFactory.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmEntity.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoEntity.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalEntity.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungEntity.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/FilmEntity2FilmMapper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/KinoKinoSaalEntity2KinoKinoSaalMapper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/VorfuehrungEntity2VorfuehrungMapper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/flexinale-banner.txt create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/logback.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/static/index.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-events-published.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-health.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-info.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-metrics.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-prometheus.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSmokeTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSpringConfigTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application-configtest.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/logback-test.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/.gitignore create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/SpotbugsExcludeFilter.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/AllBackofficeEvents.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCRUDEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCreatedEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmDeletedEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmUpdatedEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCRUDEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCreatedEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoDeletedEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoUpdatedEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCRUDEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCreatedEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungDeletedEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungUpdatedEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/FilmTO.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoSaalTO.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoTO.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/VorfuehrungTO.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/resources/logback.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventSerializationTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventStructureTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/ReflectionHelper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/SpotbugsExcludeFilter.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/Dockerfile create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/docker_build.sh create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/Dockerfile create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/podman_build.bat create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed - start Besucherportal app (_FlexinaleDistributedApplicationBesucherportal_).run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Cache Contents.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Consumed.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Published.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Health.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Info.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Metrics.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Prometheus.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportal.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/FilmSubscriber.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KinoSubscriber.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KontingentSubscriber.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/TicketSubscriber.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/VorfuehrungSubscriber.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/FilmWebController.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/IndexWebController.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/KinoWebController.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketSorter.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketWebController.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_out/event/GutscheinEinloesenBeauftragtPublisher.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FKKsVTInMemoryCache.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FilmService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/GutscheinEinloesenBeauftragtPublication.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/KinoService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/TicketBundle.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/TicketService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/VorfuehrungService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/ActuatorEndpointCache.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalActuatorEndpointEventsConsumed.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalActuatorEndpointEventsPublished.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalSpringFactory.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/flexinale-banner.txt create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/logback.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/background.jpeg create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/clear.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/construction.gif create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/crypto.js create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/app/main.css create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery-ui.structure.min.css create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery-ui.theme.css create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery.dataTables.css create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery.ui.timepicker.css create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/picnic.min.css create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/select2.min.css create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/dragon.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/dragon.js create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/dragon.png create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/drindex.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/faq.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icon-192.png create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icons/movie.png create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icons/movie_camera.png create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icons/movies.png create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/logo.png create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/manifest.json create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/site.js create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/styles.css create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/sw.js create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/error.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/film.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/filme.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/header.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/index.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/kino.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/kinos.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/tickets.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-cacheContents.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-events-consumed.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-events-published.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-health.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-info.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-metrics.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-prometheus.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportalSmokeTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportalSpringConfigTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/resources/application-configtest.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/resources/logback-test.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/.gitignore create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/java/de/accso/flexinale/besucherportal/api_contract/event/AllBesucherportalEvents.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/java/de/accso/flexinale/besucherportal/api_contract/event/GutscheinEinloesenBeauftragtEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/java/de/accso/flexinale/besucherportal/api_contract/event/model/GutscheinEinloesenAuftragTO.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/resources/logback.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/test/java/de/accso/flexinale/besucherportal/api_contract/event/EventSerializationTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/test/java/de/accso/flexinale/besucherportal/api_contract/event/EventStructureTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/test/java/de/accso/flexinale/besucherportal/api_contract/event/ReflectionHelper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/SpotbugsExcludeFilter.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/event/AbstractEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/event/Event.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/event/EventContext.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventBus.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventBusFactory.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventContextHolder.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventNotification.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventPublisher.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventSubscriber.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventSubscriptionAtStart.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/Config.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/PersistMode.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/PersistedEntityAndResult.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/caching/InMemoryCache.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/services/AbstractExcelDataUploadService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/services/ExcelDataUploadService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/domain/model/AbstractDao.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/FlexinaleCommonSpringFactory.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/FlexinaleSpringConfig.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/EventSerializationHelper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/EventSubscriberReceiveDispatcher.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBus.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBusFactory.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBusSpy.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBusSpyFactory.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventContextHolder.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaAsyncEventBus.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaAsyncEventBusFactory.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaAvailabilityChecker.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaConfiguration.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaConsumerAdapter.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaConsumerErrorHandler.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaProducerAdapter.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaTopicHelper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/NopeContextHolder.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/NopeEventBusSpy.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/NopeEventBusSpyFactory.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/ThreadLocalEventContextHolder.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/ClazzHelper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/DateTimeHelper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/DeveloperMistakeException.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/DoNotCheckInArchitectureTests.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/EqualsByContent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/EventClassHelper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/FlexinaleIllegalArgumentException.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/FlexinaleIllegalStateException.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Identifiable.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/MapWithCounter.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Mergeable.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/RawWrapper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Selection.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/SelectionMultiMap.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/SynchronizedSet.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/ThreadSafeCounterMap.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/TimeFormatter.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Versionable.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/resources/logback.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/event/EventTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/event/EventTestdata.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/InMemorySyncEventBusAndSpyHandlingMultipleEventTypesTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/InMemorySyncEventBusAndSpySubscriptionTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/InMemorySyncEventBusAndSpyTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/PublisherAndSubscriberTestdata.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/application/caching/InMemoryCacheTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/infrastructure/eventbus/EventSerializationHelperTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventContextHolderTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/ClazzHelperTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/EventClassHelperTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/MapWithCounterTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/RawWrapperTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/SelectionMultiMapTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/SelectionTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/SynchronizedSetTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/ThreadSafeCounterMapTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security/src/main/java/de/accso/flexinale/security/ApplicationSecurityConfiguration.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security/src/main/java/de/accso/flexinale/security/BenutzerDetailsService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security/src/main/java/de/accso/flexinale/security/MethodSecurityConfig.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security/src/main/java/de/accso/flexinale/security/UserPrincipal.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security/src/main/java/de/accso/flexinale/security/infrastructure/FlexinaleSecuritySpringFactory.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security/src/main/java/de/accso/flexinale/security/infrastructure/persistence/BenutzerDao.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security/src/main/java/de/accso/flexinale/security/infrastructure/persistence/BenutzerEntity.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security/src/main/java/de/accso/flexinale/security/infrastructure/persistence/BenutzerJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security/src/main/java/de/accso/flexinale/security/infrastructure/persistence/BenutzerJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security/src/main/java/de/accso/flexinale/security/infrastructure/persistence/BenutzerUploadService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security/src/main/resources/logback.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security_api_contract/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security_api_contract/src/main/java/de/accso/flexinale/security/api_contract/BesucherRetriever.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-security_api_contract/src/main/resources/logback.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/run-configurations/Distributed - Component Dependencies Test.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/run-configurations/Distributed - Domain Classes Use Correct Interfaces And Internal Structure Test.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/run-configurations/Distributed - Entities Use Correct Interfaces And Internal Structure Test.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/run-configurations/Distributed - Event Classes Use Correct Interfaces And Internal Structure Test.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/run-configurations/Distributed - No Dependencies To Spring Test.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/run-configurations/Distributed - Onion Dependencies Test.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/run-configurations/Distributed - TO Classes Use Correct Interfaces And Internal Structure Test.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/src/test/java/architecturetests/ArchUnitHelper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/src/test/java/architecturetests/ComponentDependenciesTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/src/test/java/architecturetests/FlexinaleComponent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/src/test/java/architecturetests/InternalStructureOfDomainClassesUsingCorrectInterfacesAndAttributeTypesTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/src/test/java/architecturetests/InternalStructureOfEntityClassesUsingCorrectInterfacesAndAttributeTypesTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/src/test/java/architecturetests/InternalStructureOfEventClassesUsingCorrectInterfacesAndAttributeTypesTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/src/test/java/architecturetests/InternalStructureOfTOClassesUsingCorrectInterfacesAndAttributeTypesTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/src/test/java/architecturetests/NoDependenciesToSpringTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/src/test/java/architecturetests/OnionDependenciesTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/src/test/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/src/test/resources/archunit.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-architecture/src/test/resources/logback-test.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-distributed/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-distributed/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationTestDistributed.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-distributed/src/test/java/de/accso/flexinale/backoffice/api_in/rest/End2EndWithDistributedKafkaEventBusTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-distributed/src/test/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-distributed/src/test/resources/de/accso/flexinale/backoffice/api_in/rest/NewDataForRestCallIntegrationTest.xlsx create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-distributed/src/test/resources/logback-test.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/End2EndEventsTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationTestIntegrated.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationTestIntegratedSmokeTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/backoffice/api_in/rest/End2EndWithIntegratedInMemoryEventBusTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/backoffice/api_in/rest/ExcelHelperTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/backoffice/api_in/rest/RestControllerRetrieveDataTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/backoffice/api_out/event/mapper/KinoKinoSaal2KinoKinoSaalTOMapperTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/backoffice/infrastructure/persistence/FKKsVEntityVersioningAndOptimisticLockTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/backoffice/infrastructure/persistence/FKKsVPersistenceTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/KinoKinoSaalEntity2KinoKinoSaalMapperTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/besucherportal/api_in/event/FilmSubscriberTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/besucherportal/api_in/event/KinoSubscriberTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/besucherportal/api_in/event/KontingentSubscriberTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/besucherportal/api_in/event/VorfuehrungSubscriberTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/besucherportal/api_in/web/GutscheinEinloesenBeauftragtSubscriberAndSortingTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/security/infrastructure/persistence/BenutzerSecurityAndPersistenceTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/shared_code_for_test/TestDataBuilder.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/ticketing/application/services/TicketCRUDTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/ticketing/application/services/TicketKaufTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/ticketing/application/services/VorfuehrungUpdatedTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/ticketing/domain/model/KontingentTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/ticketing/infrastructure/persistence/TicketEntityVersioningAndOptimisticLockingTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/java/de/accso/flexinale/ticketing/infrastructure/persistence/TicketPersistenceTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/resources/de/accso/flexinale/backoffice/api_in/rest/NewDataForRestCallIntegrationTest.xlsx create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/resources/de/accso/flexinale/backoffice/api_in/rest/UpdatedDataForRestCallIntegrationTest.xlsx create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-test-integrated/src/test/resources/logback-test.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/run-configurations/Distributed - clean all Kafka topics.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/run-configurations/Distributed - clean all data from database and Kafka topics.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/run-configurations/Distributed - delete Benutzer data.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/run-configurations/Distributed - delete Film, Kino, KinoSaal, Vorfuehrung data.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/run-configurations/Distributed - delete Ticket and Kontingente data.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/run-configurations/Distributed - delete all data from database.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/run-configurations/Distributed - load Benutzer data.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/run-configurations/Distributed - load Film, Kino, KinoSaal, Vorfuehrung data.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/run-configurations/Distributed - load all data to database.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/run-configurations/Distributed Backoffice - REST upload to add and update filme.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/run-configurations/Distributed Backoffice - REST upload to add and update kinos.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/run-configurations/Distributed Backoffice - REST upload to add and update vorfuehrungen.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/curl/rest-post-upload-to-add-and-update-filme.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/curl/rest-post-upload-to-add-and-update-kinos.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/curl/rest-post-upload-to-add-and-update-vorfuehrungen.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationTestdata.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/java/testdata/AllDatabaseCleaner.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/java/testdata/AllTestDataLoader.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/java/testdata/BenutzerDatabaseCleaner.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/java/testdata/BenutzerTestDataLoader.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/java/testdata/DatabaseConfig.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/java/testdata/FKKsVDatabaseCleaner.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/java/testdata/FKKsVTestDataLoader.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/java/testdata/KafkaTopicCleaner.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/java/testdata/TicketsAndKontingenteDatabaseCleaner.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/resources/logback-test.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/resources/testdata/TestDataBenutzer.xlsx create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-testdata/src/test/resources/testdata/TestDataFilmKinoKinoSaalVorfuehrung.xlsx create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/infrastructure/docker/Dockerfile create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/infrastructure/docker/docker_build.sh create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/infrastructure/podman/Dockerfile create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/infrastructure/podman/podman_build.bat create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/run-configurations/Distributed - start Ticketing app (_FlexinaleDistributedApplicationTicketing_).run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/run-configurations/Distributed Ticketing - Actuator Events Consumed.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/run-configurations/Distributed Ticketing - Actuator Events Published.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/run-configurations/Distributed Ticketing - Actuator Health.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/run-configurations/Distributed Ticketing - Actuator Info.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/run-configurations/Distributed Ticketing - Actuator Metrics.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/run-configurations/Distributed Ticketing - Actuator Prometheus.run.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationTicketing.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/api_in/event/GutscheinEinloesenBeauftragtSubscriber.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/api_in/event/VorfuehrungSubscriber.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/api_out/event/OnlineKontingentChangedPublisher.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/api_out/event/TicketPublisher.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/api_out/event/mapper/Ticket2TicketTOMapper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/application/services/OnlineKontingentChangedPublication.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/application/services/TicketPublication.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/application/services/TicketService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/domain/dao/KontingentDao.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/domain/dao/TicketDao.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/domain/model/Kontingent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/domain/model/KontingentBereitsAusgeschoepftException.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/domain/model/Ticket.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/domain/services/KontingentService.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/infrastructure/BootstrappingPostConstructTicketing.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/infrastructure/FlexinaleTicketingActuatorEndpointEventsConsumed.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/infrastructure/FlexinaleTicketingActuatorEndpointEventsPublished.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/infrastructure/FlexinaleTicketingSpringFactory.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/KontingentEntity.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/KontingentJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/KontingentJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/TicketEntity.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/TicketJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/TicketJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/mapper/KontingentEntity2KontingentMapper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/mapper/TicketEntity2TicketMapper.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/resources/flexinale-banner.txt create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/resources/logback.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/main/resources/static/index.html create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/test/curl/ticketing-actuator-events-consumed.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/test/curl/ticketing-actuator-events-published.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/test/curl/ticketing-actuator-health.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/test/curl/ticketing-actuator-info.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/test/curl/ticketing-actuator-metrics.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/test/curl/ticketing-actuator-prometheus.http create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationTicketingSmokeTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationTicketingSpringConfigTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/test/resources/application-configtest.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/test/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing/src/test/resources/logback-test.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing_api_contract/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing_api_contract/src/main/java/de/accso/flexinale/ticketing/api_contract/event/AllTicketingEvents.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing_api_contract/src/main/java/de/accso/flexinale/ticketing/api_contract/event/OnlineKontingentChangedEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing_api_contract/src/main/java/de/accso/flexinale/ticketing/api_contract/event/TicketGekauftEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing_api_contract/src/main/java/de/accso/flexinale/ticketing/api_contract/event/TicketUngueltigEvent.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing_api_contract/src/main/java/de/accso/flexinale/ticketing/api_contract/event/model/OnlineKontingentTO.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing_api_contract/src/main/java/de/accso/flexinale/ticketing/api_contract/event/model/TicketTO.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing_api_contract/src/main/resources/logback.xml create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing_api_contract/src/test/java/de/accso/flexinale/ticketing/api_contract/event/EventSerializationTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing_api_contract/src/test/java/de/accso/flexinale/ticketing/api_contract/event/EventStructureTest.java create mode 100644 flex-training-flexinale/flexinale-distributed/flexinale-distributed-ticketing_api_contract/src/test/java/de/accso/flexinale/ticketing/api_contract/event/ReflectionHelper.java create mode 100644 flex-training-flexinale/flexinale-distributed/infrastructure/docker/docker-compose_up.sh create mode 100644 flex-training-flexinale/flexinale-distributed/infrastructure/docker/flexinale-distributed-backoffice.yml create mode 100644 flex-training-flexinale/flexinale-distributed/infrastructure/docker/flexinale-distributed-besucherportal.yml create mode 100644 flex-training-flexinale/flexinale-distributed/infrastructure/docker/flexinale-distributed-ticketing.yml create mode 100644 flex-training-flexinale/flexinale-distributed/infrastructure/podman/flexinale-distributed-backoffice.yml create mode 100644 flex-training-flexinale/flexinale-distributed/infrastructure/podman/flexinale-distributed-besucherportal.yml create mode 100644 flex-training-flexinale/flexinale-distributed/infrastructure/podman/flexinale-distributed-ticketing.yml create mode 100644 flex-training-flexinale/flexinale-distributed/infrastructure/podman/podman-compose_up.bat create mode 100644 flex-training-flexinale/flexinale-distributed/infrastructure/pom.xml create mode 100644 flex-training-flexinale/flexinale-distributed/pom.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/.gitignore create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/README.md create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/SpotbugsExcludeFilter.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/infrastructure/docker/Dockerfile create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/infrastructure/docker/docker-compose_up.sh create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/infrastructure/docker/docker_build.sh create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/infrastructure/docker/flexinale-modulith-1-onion.yml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/infrastructure/podman/Dockerfile create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/infrastructure/podman/flexinale-modulith-1-onion.yml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/infrastructure/podman/podman-compose_up.bat create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/infrastructure/podman/podman_build.bat create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/pom.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/run-configurations/Modulith 1 - Domain Classes Use Correct Interfaces And Internal Structure Test.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/run-configurations/Modulith 1 - Entities Use Correct Interfaces And Internal Structure Test.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/run-configurations/Modulith 1 - No Dependencies To Spring Test.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/run-configurations/Modulith 1 - Onion Dependencies Test.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/run-configurations/Modulith 1 - REST upload to add filme.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/run-configurations/Modulith 1 - REST upload to add kinos.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/run-configurations/Modulith 1 - REST upload to add vorfuehrungen.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/run-configurations/Modulith 1 - delete all data from database.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/run-configurations/Modulith 1 - load all data to database.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/run-configurations/Modulith 1 - start app (_FlexinaleModulith1OnionApplication_).run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/FlexinaleModulith1OnionApplication.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/rest/AbstractExcelRestController.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/rest/ExcelHelper.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/rest/FilmRestController.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/rest/KinoKinoSaalRestController.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/rest/RestBadRequestException.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/rest/RestResourceNotFoundException.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/rest/RestResponseMessage.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/rest/RestUploadResult.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/rest/VorfuehrungRestController.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/web/FilmWebController.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/web/IndexWebController.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/web/KinoWebController.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/web/TicketSorter.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/api_in/web/TicketWebController.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/Config.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/UeberlappendeVorfuehrungException.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/services/AbstractExcelDataUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/services/ExcelDataUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/services/FilmService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/services/FilmUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/services/KinoSaalUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/services/KinoService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/services/KinoUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/services/PersistMode.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/services/TicketService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/services/VorfuehrungService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/application/services/VorfuehrungUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/dao/AbstractDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/dao/FilmDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/dao/KinoDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/dao/KinoSaalDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/dao/KontingentDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/dao/TicketDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/dao/VorfuehrungDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/model/Besucher.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/model/Film.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/model/Kino.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/model/KinoSaal.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/model/Kontingent.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/model/KontingentBereitsAusgeschoepftException.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/model/Ticket.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/model/Vorfuehrung.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/services/FilmService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/services/KinoService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/services/TicketBundle.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/services/TicketService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/domain/services/VorfuehrungService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/FlexinaleSpringConfig.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/FlexinaleSpringFactory.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/FilmEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/FilmJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/FilmJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/KinoEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/KinoJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/KinoJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/KinoSaalEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/KinoSaalJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/KinoSaalJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/KontingentEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/KontingentJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/KontingentJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/TicketEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/TicketJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/TicketJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/VorfuehrungEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/VorfuehrungJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/VorfuehrungJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/mapper/FilmEntity2FilmMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/mapper/KinoKinoSaalEntity2KinoKinoSaalMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/mapper/KontingentEntity2KontingentMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/mapper/TicketEntity2TicketMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/persistence/mapper/VorfuehrungEntity2VorfuehrungMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/security/ApplicationSecurityConfiguration.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/security/BenutzerDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/security/BenutzerDetailsService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/security/BenutzerEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/security/BenutzerJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/security/BenutzerJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/security/MethodSecurityConfig.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/security/UserPrincipal.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/security/mapper/BenutzerEntity2BesucherMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/infrastructure/security/services/BenutzerUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/shared_kernel/DateTimeHelper.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/shared_kernel/DeveloperMistakeException.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/shared_kernel/DoNotCheckInArchitectureTests.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/shared_kernel/EqualsByContent.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/shared_kernel/FlexinaleIllegalArgumentException.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/shared_kernel/FlexinaleIllegalStateException.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/shared_kernel/Identifiable.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/shared_kernel/RawWrapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/shared_kernel/TimeFormatter.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/java/de/accso/flexinale/shared_kernel/Versionable.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/flexinale-banner.txt create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/logback.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/background.jpeg create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/clear.html create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/construction.gif create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/crypto.js create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/css/app/main.css create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/css/vendor/jquery-ui.structure.min.css create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/css/vendor/jquery-ui.theme.css create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/css/vendor/jquery.dataTables.css create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/css/vendor/jquery.ui.timepicker.css create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/css/vendor/picnic.min.css create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/css/vendor/select2.min.css create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/dragon.html create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/dragon.js create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/dragon.png create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/drindex.html create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/faq.html create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/icon-192.png create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/icons/movie.png create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/icons/movie_camera.png create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/icons/movies.png create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/logo.png create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/manifest.json create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/site.js create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/styles.css create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/static/sw.js create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/templates/error.html create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/templates/film.html create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/templates/filme.html create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/templates/header.html create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/templates/index.html create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/templates/kino.html create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/templates/kinos.html create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/main/resources/templates/tickets.html create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/curl/rest-post-upload-to-add-and-update-filme.http create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/curl/rest-post-upload-to-add-and-update-kinos.http create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/curl/rest-post-upload-to-add-and-update-vorfuehrungen.http create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/architecturetests/ArchUnitHelper.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/architecturetests/InternalStructureOfDomainClassesUsingCorrectInterfacesAndAttributeTypesTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/architecturetests/InternalStructureOfEntityClassesUsingCorrectInterfacesAndAttributeTypesTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/architecturetests/NoDependenciesToSpringTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/architecturetests/OnionDependenciesTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/FlexinaleModulith1OnionApplicationSmokeTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/FlexinaleModulith1OnionApplicationSpringConfigTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/api_in/rest/ExcelHelperTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/api_in/rest/RestControllerTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/api_in/web/TicketSortingTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/application/services/TicketServiceTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/domain/model/KontingentTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/infrastructure/persistence/BasicPersistenceTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/infrastructure/persistence/EntityVersioningAndOptimisticLockTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/infrastructure/persistence/mapper/KinoKinoSaalEntity2KinoKinoSaalMapperTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/infrastructure/security/BenutzerSecurityAndPersistenceTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/shared_code_for_test/TestDataBuilder.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/de/accso/flexinale/shared_kernel/RawWrapperTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/testdata/DatabaseCleaner.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/java/testdata/TestDataLoader.java create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/resources/application-configtest.properties create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/resources/logback-test.xml create mode 100644 flex-training-flexinale/flexinale-modulith-1-onion/src/test/resources/testdata/TestData.xlsx create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/.gitignore create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/README.md create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/SpotbugsExcludeFilter.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/infrastructure/docker/Dockerfile create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/infrastructure/docker/docker-compose_up.sh create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/infrastructure/docker/docker_build.sh create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/infrastructure/docker/flexinale-modulith-2-components.yml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/infrastructure/podman/Dockerfile create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/infrastructure/podman/flexinale-modulith-2-components.yml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/infrastructure/podman/podman-compose_up.bat create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/infrastructure/podman/podman_build.bat create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/pom.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - Component Dependencies Test.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - Domain Classes Use Correct Interfaces And Internal Structure Test.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - Entities Use Correct Interfaces And Internal Structure Test.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - No Dependencies To Spring Test.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - Onion Dependencies Test.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - REST upload to add filme.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - REST upload to add kinos.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - REST upload to add vorfuehrungen.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - TO Classes Use Correct Interfaces And Internal Structure Test.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - delete Benutzer data.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - delete Film, Kino, KinoSaal, Vorfuehrung data.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - delete Ticket and Kontingente data.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - delete all data from database.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - load Benutzer data.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - load Film, Kino, KinoSaal, Vorfuehrung data.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - load all data to database.run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/run-configurations/Modulith 2 - start app (_FlexinaleModulith2ComponentsApplication_).run.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/FlexinaleModulith2ComponentsApplication.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/api_in/rest/AbstractExcelRestController.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/api_in/rest/ExcelHelper.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/api_in/rest/FilmRestController.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/api_in/rest/KinoKinoSaalRestController.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestBadRequestException.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResourceNotFoundException.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResponseMessage.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestUploadResult.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/api_in/rest/VorfuehrungRestController.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/FilmRetrieverService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/FilmService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/FilmUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/KinoRetrieverService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/KinoSaalUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/KinoService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/KinoUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungRetrieverService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/mapper/Film2FilmTOMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/mapper/KinoKinoSaal2KinoKinoSaalTOMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/application/services/mapper/Vorfuehrung2VorfuehrungTOMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/domain/dao/FilmDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoSaalDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/domain/dao/VorfuehrungDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/domain/model/Film.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/domain/model/Kino.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/domain/model/KinoSaal.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/domain/model/Vorfuehrung.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/domain/services/FilmService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/domain/services/KinoService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/domain/services/VorfuehrungService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeSpringFactory.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/FilmEntity2FilmMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/KinoKinoSaalEntity2KinoKinoSaalMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/VorfuehrungEntity2VorfuehrungMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice_api_contract/api_contract/FilmRetriever.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice_api_contract/api_contract/KinoRetriever.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice_api_contract/api_contract/VorfuehrungRetriever.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice_api_contract/api_contract/model/FilmTO.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice_api_contract/api_contract/model/KinoSaalTO.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice_api_contract/api_contract/model/KinoTO.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/backoffice_api_contract/api_contract/model/VorfuehrungTO.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/besucherportal/api_in/web/FilmWebController.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/besucherportal/api_in/web/IndexWebController.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/besucherportal/api_in/web/KinoWebController.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketSorter.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketWebController.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/besucherportal/application/services/FilmService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/besucherportal/application/services/KinoService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/besucherportal/application/services/TicketBundle.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/besucherportal/application/services/VorfuehrungService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalSpringFactory.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/application/Config.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/application/PersistMode.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/application/services/AbstractExcelDataUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/application/services/ExcelDataUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/domain/dao/AbstractDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/infrastructure/FlexinaleSpringConfig.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/shared_kernel/DateTimeHelper.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/shared_kernel/DeveloperMistakeException.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/shared_kernel/DoNotCheckInArchitectureTests.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/shared_kernel/EqualsByContent.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/shared_kernel/FlexinaleIllegalArgumentException.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/shared_kernel/FlexinaleIllegalStateException.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/shared_kernel/Identifiable.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/shared_kernel/RawWrapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/shared_kernel/TimeFormatter.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/common/shared_kernel/Versionable.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/security/ApplicationSecurityConfiguration.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/security/BenutzerDetailsService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/security/MethodSecurityConfig.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/security/UserPrincipal.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/security/infrastructure/FlexinaleSecuritySpringFactory.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/security/infrastructure/persistence/BenutzerDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/security/infrastructure/persistence/BenutzerEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/security/infrastructure/persistence/BenutzerJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/security/infrastructure/persistence/BenutzerJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/security/infrastructure/services/BenutzerUploadService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/security_api_contract/api_contract/BesucherRetriever.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/application/services/TicketRetrieverService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/application/services/TicketService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/domain/dao/KontingentDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/domain/dao/TicketDao.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/domain/model/Kontingent.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/domain/model/Ticket.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/domain/services/KontingentService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/domain/services/TicketService.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/infrastructure/FlexinaleTicketingSpringFactory.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/KontingentEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/KontingentJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/KontingentJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/TicketEntity.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/TicketJpaRepository.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/TicketJpaRepositoryDelegate.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/mapper/KontingentEntity2KontingentMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing/infrastructure/persistence/mapper/TicketEntity2TicketMapper.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing_api_contract/api_contract/KontingentBereitsAusgeschoepftException.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing_api_contract/api_contract/TicketRetriever.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing_api_contract/api_contract/Ticketing.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/java/de/accso/flexinale/ticketing_api_contract/api_contract/model/TicketTO.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/flexinale-banner.txt create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/logback.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/background.jpeg create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/clear.html create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/construction.gif create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/crypto.js create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/css/app/main.css create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/css/vendor/jquery-ui.structure.min.css create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/css/vendor/jquery-ui.theme.css create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/css/vendor/jquery.dataTables.css create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/css/vendor/jquery.ui.timepicker.css create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/css/vendor/picnic.min.css create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/css/vendor/select2.min.css create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/dragon.html create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/dragon.js create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/dragon.png create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/drindex.html create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/faq.html create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/icon-192.png create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/icons/movie.png create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/icons/movie_camera.png create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/icons/movies.png create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/logo.png create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/manifest.json create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/site.js create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/styles.css create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/static/sw.js create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/templates/error.html create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/templates/film.html create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/templates/filme.html create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/templates/header.html create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/templates/index.html create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/templates/kino.html create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/templates/kinos.html create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/main/resources/templates/tickets.html create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/curl/rest-post-upload-to-add-and-update-filme.http create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/curl/rest-post-upload-to-add-and-update-kinos.http create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/curl/rest-post-upload-to-add-and-update-vorfuehrungen.http create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/architecturetests/ArchUnitHelper.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/architecturetests/ComponentDependenciesTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/architecturetests/FlexinaleComponent.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/architecturetests/InternalStructureOfDomainClassesUsingCorrectInterfacesAndAttributeTypesTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/architecturetests/InternalStructureOfEntityClassesUsingCorrectInterfacesAndAttributeTypesTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/architecturetests/InternalStructureOfTOClassesUsingCorrectInterfacesAndAttributeTypesTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/architecturetests/NoDependenciesToSpringTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/architecturetests/OnionDependenciesTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/FlexinaleModulith2ComponentsApplicationSmokeTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/FlexinaleModulith2ComponentsApplicationSpringConfigTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/backoffice/api_in/rest/ExcelHelperTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/backoffice/api_in/rest/RestControllerTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/backoffice/application/services/mapper/KinoKinoSaal2KinoKinoSaalTOMapperTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/backoffice/infrastructure/persistence/FKKsVEntityVersioningAndOptimisticLockTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/backoffice/infrastructure/persistence/FKKsVPersistenceTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/KinoKinoSaalEntity2KinoKinoSaalMapperTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/besucherportal/api_in/web/TicketSortingTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/besucherportal/application/services/FilmServiceTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/common/shared_kernel/RawWrapperTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/security/infrastructure/persistence/BenutzerSecurityAndPersistenceTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/shared_code_for_test/TestDataBuilder.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/ticketing/domain/model/KontingentTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/ticketing/domain/services/TicketServiceTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/ticketing/infrastructure/persistence/TicketEntityVersioningAndOptimisticLockingTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/de/accso/flexinale/ticketing/infrastructure/persistence/TicketPersistenceTest.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/testdata/AllDatabaseCleaner.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/testdata/AllTestDataLoader.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/testdata/BenutzerDatabaseCleaner.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/testdata/BenutzerTestDataLoader.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/testdata/FKKsVDatabaseCleaner.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/testdata/FKKsVTestDataLoader.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/java/testdata/TicketsAndKontingenteDatabaseCleaner.java create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/resources/application-configtest.properties create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/resources/archunit.properties create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/resources/logback-test.xml create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/resources/testdata/TestDataBenutzer.xlsx create mode 100644 flex-training-flexinale/flexinale-modulith-2-components/src/test/resources/testdata/TestDataFilmKinoKinoSaalVorfuehrung.xlsx create mode 100644 flex-training-flexinale/flexinale-monolith/.gitignore create mode 100644 flex-training-flexinale/flexinale-monolith/README.md create mode 100644 flex-training-flexinale/flexinale-monolith/SpotbugsExcludeFilter.xml create mode 100644 flex-training-flexinale/flexinale-monolith/infrastructure/docker/Dockerfile create mode 100644 flex-training-flexinale/flexinale-monolith/infrastructure/docker/docker-compose_up.sh create mode 100644 flex-training-flexinale/flexinale-monolith/infrastructure/docker/docker_build.sh create mode 100644 flex-training-flexinale/flexinale-monolith/infrastructure/docker/flexinale-monolith.yml create mode 100644 flex-training-flexinale/flexinale-monolith/infrastructure/podman/Dockerfile create mode 100644 flex-training-flexinale/flexinale-monolith/infrastructure/podman/flexinale-monolith.yml create mode 100644 flex-training-flexinale/flexinale-monolith/infrastructure/podman/podman-compose_up.bat create mode 100644 flex-training-flexinale/flexinale-monolith/infrastructure/podman/podman_build.bat create mode 100644 flex-training-flexinale/flexinale-monolith/pom.xml create mode 100644 flex-training-flexinale/flexinale-monolith/run-configurations/Monolith - Entities Use Correct Interfaces And Internal StructureTest.run.xml create mode 100644 flex-training-flexinale/flexinale-monolith/run-configurations/Monolith - REST upload to add filme.run.xml create mode 100644 flex-training-flexinale/flexinale-monolith/run-configurations/Monolith - REST upload to add kinos.run.xml create mode 100644 flex-training-flexinale/flexinale-monolith/run-configurations/Monolith - REST upload to add vorfuehrungen.run.xml create mode 100644 flex-training-flexinale/flexinale-monolith/run-configurations/Monolith - delete all data from database.run.xml create mode 100644 flex-training-flexinale/flexinale-monolith/run-configurations/Monolith - load all data to database.run.xml create mode 100644 flex-training-flexinale/flexinale-monolith/run-configurations/Monolith - start app (_FlexinaleMonolithApplication_).run.xml create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/FlexinaleMonolithApplication.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/application/FilmService.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/application/KinoService.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/application/KontingentBereitsAusgeschoepftException.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/application/TicketBundle.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/application/TicketService.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/application/VorfuehrungService.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/model/Benutzer.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/model/Film.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/model/Kino.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/model/KinoSaal.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/model/Kontingent.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/model/Ticket.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/model/Vorfuehrung.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/persistence/AbstractDao.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/persistence/BenutzerDao.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/persistence/FilmDao.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/persistence/KinoDao.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/persistence/KinoSaalDao.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/persistence/KontingentDao.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/persistence/TicketDao.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/persistence/VorfuehrungDao.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/services/AbstractExcelDataUploadService.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/services/BenutzerUploadService.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/services/ExcelDataUploadService.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/services/FilmUploadService.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/services/KinoSaalUploadService.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/services/KinoUploadService.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/services/PersistMode.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/data/services/VorfuehrungUploadService.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/rest/AbstractExcelRestController.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/rest/FilmRestController.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/rest/KinoKinoSaalRestController.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/rest/RestBadRequestException.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/rest/RestResourceNotFoundException.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/rest/RestResponseMessage.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/rest/RestUploadResult.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/rest/VorfuehrungRestController.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/security/ApplicationSecurityConfiguration.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/security/BenutzerDetailsService.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/security/MethodSecurityConfig.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/security/UserPrincipal.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/shared_kernel/DeveloperMistakeException.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/shared_kernel/ExcelHelper.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/shared_kernel/FlexinaleIllegalArgumentException.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/shared_kernel/FlexinaleIllegalStateException.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/shared_kernel/Identifiable.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/shared_kernel/TimeFormatter.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/util/Config.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/web/FilmWebController.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/web/IndexWebController.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/web/KinoWebController.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/web/TicketSorter.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/java/de/accso/flexinale/web/TicketWebController.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/flexinale-banner.txt create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/logback.xml create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/background.jpeg create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/clear.html create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/construction.gif create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/crypto.js create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/css/app/main.css create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/css/vendor/jquery-ui.structure.min.css create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/css/vendor/jquery-ui.theme.css create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/css/vendor/jquery.dataTables.css create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/css/vendor/jquery.ui.timepicker.css create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/css/vendor/picnic.min.css create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/css/vendor/select2.min.css create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/dragon.html create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/dragon.js create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/dragon.png create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/drindex.html create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/faq.html create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/icon-192.png create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/icons/movie.png create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/icons/movie_camera.png create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/icons/movies.png create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/logo.png create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/manifest.json create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/site.js create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/styles.css create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/static/sw.js create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/templates/error.html create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/templates/film.html create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/templates/filme.html create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/templates/header.html create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/templates/index.html create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/templates/kino.html create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/templates/kinos.html create mode 100644 flex-training-flexinale/flexinale-monolith/src/main/resources/templates/tickets.html create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/curl/rest-post-upload-to-add-and-update-filme.http create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/curl/rest-post-upload-to-add-and-update-kinos.http create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/curl/rest-post-upload-to-add-and-update-vorfuehrungen.http create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/java/architecturetests/ArchUnitHelper.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/java/architecturetests/InternalStructureOfEntityClassesUsingCorrectInterfacesAndAttributeTypesTest.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/java/de/accso/flexinale/FlexinaleMonolithApplicationTest.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/java/de/accso/flexinale/data/model/KontingentTest.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/java/de/accso/flexinale/data/persistence/BasicPersistenceTest.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/java/de/accso/flexinale/shared_code_for_test/TestDataBuilder.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/java/de/accso/flexinale/shared_kernel/ExcelHelperTest.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/java/de/accso/flexinale/web/TicketSortingTest.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/java/testdata/DatabaseCleaner.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/java/testdata/TestDataLoader.java create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/resources/application.properties create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/resources/logback-test.xml create mode 100644 flex-training-flexinale/flexinale-monolith/src/test/resources/testdata/TestData.xlsx create mode 100644 flex-training-flexinale/infrastructure/docker/docker-compose_all_up.sh create mode 100644 flex-training-flexinale/infrastructure/docker/flexinale-network.yml create mode 100644 flex-training-flexinale/infrastructure/docker/kafka/docker-compose_down.sh create mode 100644 flex-training-flexinale/infrastructure/docker/kafka/docker-compose_up.sh create mode 100644 flex-training-flexinale/infrastructure/docker/kafka/flexinale-kafka-1-zookeeper.yml create mode 100644 flex-training-flexinale/infrastructure/docker/kafka/flexinale-kafka-2-kafka.yml create mode 100644 flex-training-flexinale/infrastructure/docker/kafka/flexinale-kafka-3-kafdrop.yml create mode 100644 flex-training-flexinale/infrastructure/docker/postgres/Dockerfile create mode 100644 flex-training-flexinale/infrastructure/docker/postgres/docker_build.sh create mode 100644 flex-training-flexinale/infrastructure/docker/postgres/docker_run.sh create mode 100644 flex-training-flexinale/infrastructure/docker/postgres/flexinale-postgres-init.sql create mode 100644 flex-training-flexinale/infrastructure/docker/postgres/flexinale-postgres.yml create mode 100644 flex-training-flexinale/infrastructure/podman/flexinale-network.yml create mode 100644 flex-training-flexinale/infrastructure/podman/kafka/flexinale-kafka-1-zookeeper.yml create mode 100644 flex-training-flexinale/infrastructure/podman/kafka/flexinale-kafka-2-kafka.yml create mode 100644 flex-training-flexinale/infrastructure/podman/kafka/flexinale-kafka-3-kafdrop.yml create mode 100644 flex-training-flexinale/infrastructure/podman/kafka/podman-compose_down.bat create mode 100644 flex-training-flexinale/infrastructure/podman/kafka/podman-compose_up.bat create mode 100644 flex-training-flexinale/infrastructure/podman/podman-compose_all_up.bat create mode 100644 flex-training-flexinale/infrastructure/podman/postgres/Dockerfile create mode 100644 flex-training-flexinale/infrastructure/podman/postgres/flexinale-postgres-init.sql create mode 100644 flex-training-flexinale/infrastructure/podman/postgres/flexinale-postgres.yml create mode 100644 flex-training-flexinale/infrastructure/podman/postgres/podman_build.bat create mode 100644 flex-training-flexinale/infrastructure/podman/postgres/podman_run.bat create mode 100644 flex-training-flexinale/infrastructure/pom.xml create mode 100644 flex-training-flexinale/pom.xml create mode 100644 resources/FLEX Agenda/FLEX_Albion-tectrain_Kurz-Agenda_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX Schulungsunterlagen + Übungen/FLEX_Albion-tectrain_Schulungsunterlagen_und_Übungen_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX Schulungsunterlagen Handout/FLEX_Albion-tectrain_Schulungsunterlagen_Kapitel-0_Einleitung_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX Schulungsunterlagen Handout/FLEX_Albion-tectrain_Schulungsunterlagen_Kapitel-1_Grundlegendes-zum-Modul_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX Schulungsunterlagen Handout/FLEX_Albion-tectrain_Schulungsunterlagen_Kapitel-2_Motivation_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX Schulungsunterlagen Handout/FLEX_Albion-tectrain_Schulungsunterlagen_Kapitel-3_Modularisierung_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX Schulungsunterlagen Handout/FLEX_Albion-tectrain_Schulungsunterlagen_Kapitel-4_Integration_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX Schulungsunterlagen Handout/FLEX_Albion-tectrain_Schulungsunterlagen_Kapitel-5_Installation-und-Rollout_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX Schulungsunterlagen Handout/FLEX_Albion-tectrain_Schulungsunterlagen_Kapitel-6_Betrieb-Ueberwachung-Fehleranalyse_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX Schulungsunterlagen Handout/FLEX_Albion-tectrain_Schulungsunterlagen_Kapitel-7_Case-Study-Flexinale_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX Schulungsunterlagen Handout/FLEX_Albion-tectrain_Schulungsunterlagen_Kapitel-8_Ausblick_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX Schulungsunterlagen Handout/FLEX_Albion-tectrain_Schulungsunterlagen_Kapitel-9_Quellen-und-Referenzen_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX Sourcen/FLEX_Albion-tectrain_Schulungsunterlagen_Installationsanleitung_V20241111-13_2024.3.pdf create mode 100644 resources/FLEX_Albion-tectrain_Deckblatt_V20241111-13_2024.3.pdf create mode 100644 resources/readme.txt diff --git a/flex-training-flexinale/.gitignore b/flex-training-flexinale/.gitignore new file mode 100644 index 0000000..65e9f4e --- /dev/null +++ b/flex-training-flexinale/.gitignore @@ -0,0 +1,56 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### IntelliJ +.idea/ +out/ +*.iml +*.ipr +*.iws +target/ + +~*xlsx + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + diff --git a/flex-training-flexinale/LICENSE b/flex-training-flexinale/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/flex-training-flexinale/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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 + + http://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. diff --git a/flex-training-flexinale/README.md b/flex-training-flexinale/README.md new file mode 100644 index 0000000..64dc0c6 --- /dev/null +++ b/flex-training-flexinale/README.md @@ -0,0 +1,14 @@ +# flex-training-flexinale +iSAQB Advanced Level FLEX training: This code contains our case study for the "flexinale" example. +This is the main repo containing blueprint and solution. + +## Architecture as a monolithic system +This part is in the folder flexinale-monolith + +## Architecture as a modulithic system +Part 1 is in the folder flexinale-modulith-1-onion +Part 2 is in the folder flexinale-modulith-1-components + +## Architecture as a distributed, service-oriented, event-based system +This part is in the folder flexinale-distributed + diff --git a/flex-training-flexinale/flexinale-distributed/README.md b/flex-training-flexinale/flexinale-distributed/README.md new file mode 100644 index 0000000..ae9ef11 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/README.md @@ -0,0 +1,6 @@ +# flex-training-filmfestival +iSAQB Advanced Level FLEX training: This code contains our case study code for the "filmfestival" example. +This is the main repo containing blueprint and solution. + +## Architecture as a distributed, service-oriented, event-based system +This part is in the folder flexinale-distributed diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/SpotbugsExcludeFilter.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/SpotbugsExcludeFilter.xml new file mode 100644 index 0000000..ecf3aeb --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/SpotbugsExcludeFilter.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/Dockerfile b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/Dockerfile new file mode 100644 index 0000000..39721bb --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/Dockerfile @@ -0,0 +1,8 @@ +FROM amazoncorretto:21.0.1-alpine3.18 + +WORKDIR /app +COPY ../../target/flexinale-distributed-backoffice-2024.3.0-spring-boot-fat-jar.jar /app + +EXPOSE 8081 + +CMD ["java", "-jar", "flexinale-distributed-backoffice-2024.3.0-spring-boot-fat-jar.jar"] \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/docker_build.sh b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/docker_build.sh new file mode 100644 index 0000000..6ce54f5 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/docker/docker_build.sh @@ -0,0 +1 @@ +docker build -t de.accso/flexinale-distributed-backoffice:2024.3.0 -f Dockerfile ../../ \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/Dockerfile b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/Dockerfile new file mode 100644 index 0000000..39721bb --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/Dockerfile @@ -0,0 +1,8 @@ +FROM amazoncorretto:21.0.1-alpine3.18 + +WORKDIR /app +COPY ../../target/flexinale-distributed-backoffice-2024.3.0-spring-boot-fat-jar.jar /app + +EXPOSE 8081 + +CMD ["java", "-jar", "flexinale-distributed-backoffice-2024.3.0-spring-boot-fat-jar.jar"] \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/podman_build.bat b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/podman_build.bat new file mode 100644 index 0000000..3466b89 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/infrastructure/podman/podman_build.bat @@ -0,0 +1 @@ +podman build -t de.accso/flexinale-distributed-backoffice:2024.3.0 -f Dockerfile ../../ \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/pom.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/pom.xml new file mode 100644 index 0000000..c522a9c --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/pom.xml @@ -0,0 +1,167 @@ + + + 4.0.0 + + + de.accso + flexinale-distributed + 2024.3.0 + ../pom.xml + + + flexinale-distributed-backoffice + 2024.3.0 + Flexinale Distributed Backoffice + Flexinale - FLEX case-study "film festival", distributed services, backoffice + + jar + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + ${micrometer.version} + + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + org.postgresql + postgresql + ${org-postgres.version} + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + org.apache.poi + poi + ${apache-poi.version} + + + org.apache.poi + poi-ooxml + ${apache-poi.version} + + + + de.accso + flexinale-distributed-common + 2024.3.0 + + + de.accso + flexinale-distributed-common + 2024.3.0 + test-jar + test + + + de.accso + flexinale-distributed-security + runtime + 2024.3.0 + + + de.accso + flexinale-distributed-security_api_contract + 2024.3.0 + + + de.accso + flexinale-distributed-backoffice_api_contract + 2024.3.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + spring-boot-fat-jar + + + + + build-info + + + + Distributed + Backoffice + + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs-maven-plugin.version} + + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs-maven-plugin.version} + + + SpotbugsExcludeFilter.xml + + + + + com.github.spotbugs + spotbugs + ${spotbugs.version} + + + + + verify + + check + + + + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed - start Backoffice app (_FlexinaleDistributedApplicationBackoffice_).run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed - start Backoffice app (_FlexinaleDistributedApplicationBackoffice_).run.xml new file mode 100644 index 0000000..b19d135 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed - start Backoffice app (_FlexinaleDistributedApplicationBackoffice_).run.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Events Published.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Events Published.run.xml new file mode 100644 index 0000000..42efdf7 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Events Published.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Health.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Health.run.xml new file mode 100644 index 0000000..b2f3088 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Health.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Info.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Info.run.xml new file mode 100644 index 0000000..8198fa7 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Info.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Metrics.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Metrics.run.xml new file mode 100644 index 0000000..8e365df --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Metrics.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Prometheus.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Prometheus.run.xml new file mode 100644 index 0000000..05de5cd --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/run-configurations/Distributed Backoffice - Actuator Prometheus.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBackoffice.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBackoffice.java new file mode 100644 index 0000000..ad5a2ef --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBackoffice.java @@ -0,0 +1,23 @@ +package de.accso.flexinale; + +import jakarta.persistence.EntityManagerFactory; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Profile; +import org.springframework.orm.jpa.JpaTransactionManager; +import org.springframework.transaction.PlatformTransactionManager; + +@SpringBootApplication +@Profile({"!test-integrated &!test-distributed & !testdata"}) +public class FlexinaleDistributedApplicationBackoffice { + + public static void main(String[] args) { + SpringApplication.run(FlexinaleDistributedApplicationBackoffice.class, args); + } + + @Bean + public PlatformTransactionManager transactionManager(final EntityManagerFactory entityManagerFactory) { + return new JpaTransactionManager(entityManagerFactory); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/ExcelHelper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/ExcelHelper.java new file mode 100644 index 0000000..2b773d8 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/ExcelHelper.java @@ -0,0 +1,15 @@ +package de.accso.flexinale.backoffice.api_in.rest; + +import org.apache.poi.poifs.filesystem.FileMagic; + +import java.io.IOException; +import java.io.InputStream; + +final class ExcelHelper { + private ExcelHelper() { + } + + static boolean isExcelFile(final InputStream stream) throws IOException { + return (FileMagic.valueOf(stream) == FileMagic.OOXML); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/FilmRestController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/FilmRestController.java new file mode 100644 index 0000000..ea9525f --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/FilmRestController.java @@ -0,0 +1,130 @@ +package de.accso.flexinale.backoffice.api_in.rest; + +import de.accso.flexinale.backoffice.api_out.event.FilmPublisher; +import de.accso.flexinale.backoffice.api_out.event.VorfuehrungPublisher; +import de.accso.flexinale.backoffice.application.services.FilmUploadService; +import de.accso.flexinale.backoffice.domain.model.Film; +import de.accso.flexinale.backoffice.domain.model.Vorfuehrung; +import de.accso.flexinale.backoffice.application.services.FilmService; +import de.accso.flexinale.backoffice.application.services.VorfuehrungService; +import de.accso.flexinale.common.application.PersistMode; +import de.accso.flexinale.common.application.PersistedEntityAndResult; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.ADDED; +import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.UPDATED; + +@RestController +@Profile({ "!testdata-besucherportal & !testdata-ticketing" }) +public class FilmRestController { + private static final Logger LOGGER = LoggerFactory.getLogger(FilmRestController.class); + + @Autowired + private FilmService filmService; + + @Autowired + private FilmUploadService filmUploadService; + + @Autowired + private FilmPublisher filmPublisher; + + @Autowired + private VorfuehrungService vorfuehrungService; + + @Autowired + private VorfuehrungPublisher vorfuehrungPublisher; + + @GetMapping(value = "/rest/filme", produces = "application/json") + @PreAuthorize("hasRole('ROLE_BESUCHER')") + public List filme() { + return filmService.filme(); + } + + @GetMapping(value = "/rest/film/{id}", produces = "application/json") + @PreAuthorize("hasRole('ROLE_BESUCHER')") + public Film film(@PathVariable final String id) { + Identifiable.Id filmId = Identifiable.Id.of(id); + Film film = filmService.film(filmId); + if (film == null) { + throw new RestResourceNotFoundException("no Film %s found.".formatted(id)); + } + + return film; + } + + // ----------------------------------------------------------------------------------------------------------- + + ResponseEntity loadAndPersistAndPublishNewFilme( + final BufferedInputStream stream, final String originalFilename) throws IOException + { + // load and persist + Collection> addedAndUpdatedFilms = + filmUploadService.loadDataFromExcelSheetAndPersistAndReturn(stream, PersistMode.UPDATE); + + List addedFilms = addedAndUpdatedFilms.stream() + .filter(filmWithResult -> filmWithResult.result().equals(ADDED)) + .map(PersistedEntityAndResult::entity) + .toList(); + List updatedFilms = addedAndUpdatedFilms.stream() + .filter(filmWithResult -> filmWithResult.result().equals(UPDATED)) + .map(PersistedEntityAndResult::entity) + .toList(); + + publishFilmsAndVorfuehrungen(addedFilms, updatedFilms); + + String message = "Uploaded the Excel file %s adding %d new Film entities and updating %d existing Film entities." + .formatted(originalFilename, addedFilms.size(), updatedFilms.size()); + + RestUploadResult restUploadResult = new RestUploadResult(HttpStatus.OK, message); + return ResponseEntity.status(restUploadResult.status()) + .body(new RestResponseMessage(restUploadResult.message())); + } + + private void publishFilmsAndVorfuehrungen(final List addedFilms, final List updatedFilms) { + filmPublisher.publishNewFilms(addedFilms); + filmPublisher.publishUpdatedFilms(updatedFilms); + + List vorfuehrungen = vorfuehrungService.vorfuehrungen(); + for (Film updatedFilm: updatedFilms) { + List vorfuehrungenToPublishAsUpdated = vorfuehrungen.stream() + .filter(vorfuehrung -> vorfuehrung.film.id.equals(updatedFilm.id)) + .toList(); + vorfuehrungPublisher.publishUpdatedVorfuehrungen(vorfuehrungenToPublishAsUpdated); + } + } + + @PostMapping("/rest/filme") + @PreAuthorize("hasRole('ROLE_ADMIN')") + public ResponseEntity uploadFilme(@RequestParam("file") final MultipartFile file) { + try (BufferedInputStream stream = new BufferedInputStream(file.getInputStream())) { + if (!ExcelHelper.isExcelFile(stream)) { + String message = "Please upload a valid Excel file!"; + throw new FlexinaleIllegalStateException(message); + } + + return loadAndPersistAndPublishNewFilme(stream, file.getOriginalFilename()); + } + catch (Exception ex) { + String message = "Could not upload the Excel file: %s: %s".formatted(file.getOriginalFilename(), ex.getMessage()); + RestBadRequestException exception = new RestBadRequestException(message, ex); + LOGGER.error(message, exception); + + throw exception; + } + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/KinoKinoSaalRestController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/KinoKinoSaalRestController.java new file mode 100644 index 0000000..f3c0ddd --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/KinoKinoSaalRestController.java @@ -0,0 +1,167 @@ +package de.accso.flexinale.backoffice.api_in.rest; + +import de.accso.flexinale.backoffice.api_out.event.KinoPublisher; +import de.accso.flexinale.backoffice.api_out.event.VorfuehrungPublisher; +import de.accso.flexinale.backoffice.application.services.KinoUploadService; +import de.accso.flexinale.backoffice.application.services.KinoSaalUploadService; +import de.accso.flexinale.backoffice.domain.model.Kino; +import de.accso.flexinale.backoffice.domain.model.KinoSaal; +import de.accso.flexinale.backoffice.domain.model.Vorfuehrung; +import de.accso.flexinale.backoffice.application.services.KinoService; +import de.accso.flexinale.backoffice.application.services.VorfuehrungService; +import de.accso.flexinale.common.application.PersistMode; +import de.accso.flexinale.common.application.PersistedEntityAndResult; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.ADDED; +import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.UPDATED; + +@RestController +@Profile({ "!testdata-besucherportal & !testdata-ticketing" }) +public class KinoKinoSaalRestController { + private static final Logger LOGGER = LoggerFactory.getLogger(KinoKinoSaalRestController.class); + + @Autowired + private KinoService kinoService; + + @Autowired + private KinoUploadService kinoUploadService; + + @Autowired + private KinoSaalUploadService kinoSaalUploadService; + + @Autowired + private KinoPublisher kinoPublisher; + + @Autowired + private VorfuehrungService vorfuehrungService; + + @Autowired + private VorfuehrungPublisher vorfuehrungPublisher; + + @GetMapping(value = "/rest/kinos", produces = "application/json") + @PreAuthorize("hasRole('ROLE_BESUCHER')") + public List kinos() { + return kinoService.kinos(); + } + + @GetMapping(value = "/rest/kino/{id}", produces = "application/json") + @PreAuthorize("hasRole('ROLE_BESUCHER')") + public Kino kino(@PathVariable final String id) { + Identifiable.Id kinoId = Identifiable.Id.of(id); + Kino kino = kinoService.kino(kinoId); + if (kino == null) { + throw new RestResourceNotFoundException("no Kino %s found.".formatted(id)); + } + + return kino; + } + + // ----------------------------------------------------------------------------------------------------------- + + @PostMapping("/rest/kinos") + @PreAuthorize("hasRole('ROLE_ADMIN')") + public ResponseEntity uploadKinosKinoSaele(@RequestParam("file") final MultipartFile file) { + String originalFilename = file.getOriginalFilename(); + + try { + Collection alleKinoSaele; + + // we need to open the stream twice, separately for KinoSaele ... + try (BufferedInputStream stream = new BufferedInputStream(file.getInputStream())) { + if (!ExcelHelper.isExcelFile(stream)) { + String message = "Please upload a valid Excel file!"; + throw new FlexinaleIllegalStateException(message); + } + alleKinoSaele = loadKinoSaele(stream); + } + + // ... and for Kino + try (BufferedInputStream stream = new BufferedInputStream(file.getInputStream())) { + return loadAndPersistAndPublishNewKinosKinoSaele(stream, originalFilename, alleKinoSaele); + } + } + catch (Exception ex) { + String message = "Could not upload the Excel file: %s: %s".formatted(file.getOriginalFilename(), ex.getMessage()); + RestBadRequestException exception = new RestBadRequestException(message, ex); + LOGGER.error(message, exception); + + throw exception; + } + } + + Collection loadKinoSaele(final BufferedInputStream stream) throws IOException { + return kinoSaalUploadService.loadDataFromExcelSheet(stream); + } + + ResponseEntity loadAndPersistAndPublishNewKinosKinoSaele( + final BufferedInputStream stream, final String originalFilename, + final Collection alleKinoSaele) throws IOException { + // and then load and persist all Kinos (including their KinoSaele) + Map> alleKinosUndIhreSaele = + kinoSaalUploadService.mapKinoSaeleToKino(alleKinoSaele); + + List addedKinos; + List updatedKinos; + try { + kinoUploadService.beforeLoad(alleKinosUndIhreSaele); // fix state of Kino-KinoSaal in map + // load and persist + Collection> addedAndUpdatedKinos = + kinoUploadService.loadDataFromExcelSheetAndPersistAndReturn(stream, PersistMode.UPDATE); + + addedKinos = addedAndUpdatedKinos.stream() + .filter(kinoWithResult -> kinoWithResult.result().equals(ADDED)) + .map(PersistedEntityAndResult::entity) + .toList(); + updatedKinos = addedAndUpdatedKinos.stream() + .filter(kinoWithResult -> kinoWithResult.result().equals(UPDATED)) + .map(PersistedEntityAndResult::entity) + .toList(); + + publishKinosAndVorfuehrungen(addedKinos, updatedKinos); + } + finally { + kinoUploadService.afterLoad(); // clear map + } + + String message = "Uploaded the Excel file %s adding %d new Kinos entities and updating %d existing Kinos entities." + .formatted(originalFilename, addedKinos.size(), updatedKinos.size()); + + RestUploadResult restUploadResult = new RestUploadResult(HttpStatus.OK, message); + return ResponseEntity.status(restUploadResult.status()) + .body(new RestResponseMessage(restUploadResult.message())); + } + + private void publishKinosAndVorfuehrungen(final List addedKinos, final List updatedKinos) { + kinoPublisher.publishNewKinos(addedKinos); + kinoPublisher.publishUpdatedKinos(updatedKinos); + + List vorfuehrungen = vorfuehrungService.vorfuehrungen(); + List vorfuehrungenToPublishAsUpdated = new ArrayList<>(); + for (Kino updatedKino: updatedKinos) { + for (KinoSaal updatedKinoSaal: updatedKino.kinoSaele) { + vorfuehrungen.stream() + .filter(vorfuehrung -> vorfuehrung.kinoSaal.id.equals(updatedKinoSaal.id)) + .forEach(vorfuehrungenToPublishAsUpdated::add); + } + } + vorfuehrungPublisher.publishUpdatedVorfuehrungen(vorfuehrungenToPublishAsUpdated); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestBadRequestException.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestBadRequestException.java new file mode 100644 index 0000000..266d42f --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestBadRequestException.java @@ -0,0 +1,19 @@ +package de.accso.flexinale.backoffice.api_in.rest; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.BAD_REQUEST) +@SuppressWarnings("unused") +public class RestBadRequestException extends RuntimeException { + + // see https://www.springboottutorial.com/spring-boot-exception-handling-for-rest-services + + public RestBadRequestException(final String message) { + super(message); + } + + public RestBadRequestException(final String message, final Exception ex) { + super(message, ex); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResourceNotFoundException.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResourceNotFoundException.java new file mode 100644 index 0000000..12efd83 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResourceNotFoundException.java @@ -0,0 +1,14 @@ +package de.accso.flexinale.backoffice.api_in.rest; + +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ResponseStatus; + +@ResponseStatus(HttpStatus.NOT_FOUND) +public class RestResourceNotFoundException extends RuntimeException { + + // see https://www.springboottutorial.com/spring-boot-exception-handling-for-rest-services + + public RestResourceNotFoundException(final String message) { + super(message); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResponseMessage.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResponseMessage.java new file mode 100644 index 0000000..63e271e --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestResponseMessage.java @@ -0,0 +1,4 @@ +package de.accso.flexinale.backoffice.api_in.rest; + +record RestResponseMessage(String message) { +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestUploadResult.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestUploadResult.java new file mode 100644 index 0000000..d56cee6 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/RestUploadResult.java @@ -0,0 +1,7 @@ +package de.accso.flexinale.backoffice.api_in.rest; + +import org.springframework.http.HttpStatus; + +@SuppressWarnings("SameParameterValue") +record RestUploadResult(HttpStatus status, String message) { +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/VorfuehrungRestController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/VorfuehrungRestController.java new file mode 100644 index 0000000..befcdd6 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_in/rest/VorfuehrungRestController.java @@ -0,0 +1,109 @@ +package de.accso.flexinale.backoffice.api_in.rest; + +import de.accso.flexinale.backoffice.api_out.event.VorfuehrungPublisher; +import de.accso.flexinale.backoffice.application.services.VorfuehrungUploadService; +import de.accso.flexinale.backoffice.domain.model.Vorfuehrung; +import de.accso.flexinale.backoffice.application.services.VorfuehrungService; +import de.accso.flexinale.common.application.PersistMode; +import de.accso.flexinale.common.application.PersistedEntityAndResult; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.util.Collection; +import java.util.List; + +import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.ADDED; +import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.UPDATED; + +@RestController +@Profile({ "!testdata-besucherportal & !testdata-ticketing" }) +public class VorfuehrungRestController { + private static final Logger LOGGER = LoggerFactory.getLogger(VorfuehrungRestController.class); + + @Autowired + private VorfuehrungService vorfuehrungService; + + @Autowired + private VorfuehrungUploadService vorfuehrungUploadService; + + @Autowired + private VorfuehrungPublisher vorfuehrungPublisher; + + @GetMapping(value = "/rest/vorfuehrungen", produces = "application/json") + @PreAuthorize("hasRole('ROLE_BESUCHER')") + public List vorfuehrungen() { + return vorfuehrungService.vorfuehrungen(); + } + + @GetMapping(value = "/rest/vorfuehrungen/{id}", produces = "application/json") + @PreAuthorize("hasRole('ROLE_BESUCHER')") + public Vorfuehrung vorfuehrung(@PathVariable final String id) { + Identifiable.Id vorfuehrungId = Identifiable.Id.of(id); + Vorfuehrung vorfuehrung = vorfuehrungService.vorfuehrung(vorfuehrungId); + if (vorfuehrung == null) { + throw new RestResourceNotFoundException("no Vorfuehrung %s found.".formatted(id)); + } + + return vorfuehrung; + } + + // ----------------------------------------------------------------------------------------------------------- + + @PostMapping("/rest/vorfuehrungen") + @PreAuthorize("hasRole('ROLE_ADMIN')") + public ResponseEntity uploadVorfuehrungen(@RequestParam("file") final MultipartFile file) { + try (BufferedInputStream stream = new BufferedInputStream(file.getInputStream())) { + if (!ExcelHelper.isExcelFile(stream)) { + String message = "Please upload a valid Excel file!"; + throw new FlexinaleIllegalStateException(message); + } + + return loadAndPersistAndPublishNewVorfuehrungen(stream, file.getOriginalFilename()); + } + catch (Exception ex) { + String message = "Could not upload the Excel file: %s: %s".formatted(file.getOriginalFilename(), ex.getMessage()); + RestBadRequestException exception = new RestBadRequestException(message, ex); + LOGGER.error(message, exception); + + throw exception; + } + } + + ResponseEntity loadAndPersistAndPublishNewVorfuehrungen( + final BufferedInputStream stream, final String originalFilename) throws IOException + { + // load and persist + Collection> addedAndUpdatedVorfuehrungen = + vorfuehrungUploadService.loadDataFromExcelSheetAndPersistAndReturn(stream, PersistMode.UPDATE); + + // publish as events + List addedVorfuehrungen = addedAndUpdatedVorfuehrungen.stream() + .filter(vorfuehrungWithResult -> vorfuehrungWithResult.result().equals(ADDED)) + .map(PersistedEntityAndResult::entity) + .toList(); + vorfuehrungPublisher.publishNewVorfuehrungen(addedVorfuehrungen); + List updatedVorfuehrungen = addedAndUpdatedVorfuehrungen.stream() + .filter(vorfuehrungWithResult -> vorfuehrungWithResult.result().equals(UPDATED)) + .map(PersistedEntityAndResult::entity) + .toList(); + vorfuehrungPublisher.publishUpdatedVorfuehrungen(updatedVorfuehrungen); + + String message = "Uploaded the Excel file %s adding %d new Vorfuehrungen entities and updating %d existing Vorfuehrungen entities." + .formatted(originalFilename, addedVorfuehrungen.size(), updatedVorfuehrungen.size()); + + RestUploadResult restUploadResult = new RestUploadResult(HttpStatus.OK, message); + return ResponseEntity.status(restUploadResult.status()) + .body(new RestResponseMessage(restUploadResult.message())); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/BootstrappingPostConstructBackoffice.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/BootstrappingPostConstructBackoffice.java new file mode 100644 index 0000000..961552e --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/BootstrappingPostConstructBackoffice.java @@ -0,0 +1,59 @@ +package de.accso.flexinale.backoffice.api_out.event; + +import de.accso.flexinale.backoffice.application.services.FilmService; +import de.accso.flexinale.backoffice.application.services.KinoService; +import de.accso.flexinale.backoffice.application.services.VorfuehrungService; +import jakarta.annotation.PostConstruct; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile({"!test-integrated & !testdata"}) +public class BootstrappingPostConstructBackoffice { + private static final Logger LOGGER = LoggerFactory.getLogger(BootstrappingPostConstructBackoffice.class); + + @Autowired + FilmService filmService; + + @Autowired + FilmPublisher filmPublisher; + + @Autowired + KinoService kinoService; + + @Autowired + KinoPublisher kinoPublisher; + + @Autowired + VorfuehrungService vorfuehrungService; + + @Autowired + VorfuehrungPublisher vorfuehrungPublisher; + + // Alternatively annotate method with @EventListener(ApplicationReadyEvent.class) + @PostConstruct + public void postConstruct() { + LOGGER.info("Publishing existing data for FKKsV as *Created events"); + + loadAllFilmeFromDatabaseAndPublish(); + loadAllKinosFromDatabaseAndPublish(); + loadAllVorfuehrungenFromDatabaseAndPublish(); + + LOGGER.info("Publishing existing data for FKKsV as *Created events ... done"); + } + + private void loadAllVorfuehrungenFromDatabaseAndPublish() { + filmPublisher.publishNewFilms( filmService.filme() ); + } + + private void loadAllKinosFromDatabaseAndPublish() { + kinoPublisher.publishNewKinos( kinoService.kinos() ); + } + + private void loadAllFilmeFromDatabaseAndPublish() { + vorfuehrungPublisher.publishNewVorfuehrungen( vorfuehrungService.vorfuehrungen() ); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/FilmPublisher.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/FilmPublisher.java new file mode 100644 index 0000000..cba0f7e --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/FilmPublisher.java @@ -0,0 +1,80 @@ +package de.accso.flexinale.backoffice.api_out.event; + +import de.accso.flexinale.backoffice.api_contract.event.FilmCRUDEvent; +import de.accso.flexinale.backoffice.api_contract.event.FilmCreatedEvent; +import de.accso.flexinale.backoffice.api_contract.event.FilmDeletedEvent; +import de.accso.flexinale.backoffice.api_contract.event.FilmUpdatedEvent; +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.backoffice.api_out.event.mapper.Film2FilmTOMapper; +import de.accso.flexinale.backoffice.domain.model.Film; +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import de.accso.flexinale.common.api.eventbus.EventNotification; +import de.accso.flexinale.common.api.eventbus.EventPublisher.EventPublisher3; + +import java.util.Collection; + +@SuppressWarnings("unused") +public class FilmPublisher implements EventPublisher3 { + private final EventBus createBus; + private final EventBus updateBus; + private final EventBus deleteBus; + + private final EventNotification notification; + + public FilmPublisher(final EventBusFactory eventBusFactory, + final EventNotification notification) { + this.createBus = eventBusFactory.createOrGetEventBusFor(FilmCreatedEvent.class); + this.updateBus = eventBusFactory.createOrGetEventBusFor(FilmUpdatedEvent.class); + this.deleteBus = eventBusFactory.createOrGetEventBusFor(FilmDeletedEvent.class); + + this.notification = notification; + } + + public void publishNewFilms(final Collection films) { + publishCRUDEvents(films, FilmCRUDEvent.CRUD.CREATE); + } + + public void publishUpdatedFilms(final Collection films) { + publishCRUDEvents(films, FilmCRUDEvent.CRUD.UPDATE); + } + + // currently we do not publish Deletions events + public void publishDeletedFilms(final Collection films) { + publishCRUDEvents(films, FilmCRUDEvent.CRUD.DELETE); + } + + private void publishCRUDEvents(final Collection changedFilms, final FilmCRUDEvent.CRUD mode) { + for (Film film: changedFilms) { + FilmTO filmTO = Film2FilmTOMapper.map(film); + switch (mode) { + case CREATE -> this.post (FilmCreatedEvent.class, new FilmCreatedEvent(filmTO)); + case UPDATE -> this.post2(FilmUpdatedEvent.class, new FilmUpdatedEvent(filmTO)); + case DELETE -> this.post3(FilmDeletedEvent.class, new FilmDeletedEvent(filmTO)); + } + } + } + + @Override + public String getName() { + return FilmPublisher.class.getName(); + } + + @Override + public void post(final Class eventType, final FilmCreatedEvent event) { + this.createBus.publish(eventType, event); + this.notification.notify(event); + } + + @Override + public void post2(final Class eventType, final FilmUpdatedEvent event) { + this.updateBus.publish(eventType, event); + this.notification.notify(event); + } + + @Override + public void post3(final Class eventType, final FilmDeletedEvent event) { + this.deleteBus.publish(eventType, event); + this.notification.notify(event); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/KinoPublisher.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/KinoPublisher.java new file mode 100644 index 0000000..240ca48 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/KinoPublisher.java @@ -0,0 +1,80 @@ +package de.accso.flexinale.backoffice.api_out.event; + +import de.accso.flexinale.backoffice.api_contract.event.KinoCRUDEvent; +import de.accso.flexinale.backoffice.api_contract.event.KinoCreatedEvent; +import de.accso.flexinale.backoffice.api_contract.event.KinoDeletedEvent; +import de.accso.flexinale.backoffice.api_contract.event.KinoUpdatedEvent; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.backoffice.api_out.event.mapper.KinoKinoSaal2KinoKinoSaalTOMapper; +import de.accso.flexinale.backoffice.domain.model.Kino; +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import de.accso.flexinale.common.api.eventbus.EventNotification; +import de.accso.flexinale.common.api.eventbus.EventPublisher.EventPublisher3; + +import java.util.Collection; + +@SuppressWarnings("unused") +public class KinoPublisher implements EventPublisher3 { + private final EventBus createBus; + private final EventBus updateBus; + private final EventBus deleteBus; + + private final EventNotification notification; + + public KinoPublisher(final EventBusFactory eventBusFactory, + final EventNotification notification) { + this.createBus = eventBusFactory.createOrGetEventBusFor(KinoCreatedEvent.class); + this.updateBus = eventBusFactory.createOrGetEventBusFor(KinoUpdatedEvent.class); + this.deleteBus = eventBusFactory.createOrGetEventBusFor(KinoDeletedEvent.class); + + this.notification = notification; + } + + public void publishNewKinos(final Collection kinos) { + publishCRUDEvents(kinos, KinoCRUDEvent.CRUD.CREATE); + } + + public void publishUpdatedKinos(final Collection kinos) { + publishCRUDEvents(kinos, KinoCRUDEvent.CRUD.UPDATE); + } + + // currently we do not publish Deletions events + public void publishDeletedKinos(final Collection kinos) { + publishCRUDEvents(kinos, KinoCRUDEvent.CRUD.DELETE); + } + + private void publishCRUDEvents(final Collection changedKinos, final KinoCRUDEvent.CRUD mode) { + for (Kino kino: changedKinos) { + KinoTO kinoTO = KinoKinoSaal2KinoKinoSaalTOMapper.mapKino(kino); + switch (mode) { + case CREATE -> this.post (KinoCreatedEvent.class, new KinoCreatedEvent(kinoTO)); + case UPDATE -> this.post2(KinoUpdatedEvent.class, new KinoUpdatedEvent(kinoTO)); + case DELETE -> this.post3(KinoDeletedEvent.class, new KinoDeletedEvent(kinoTO)); + } + } + } + + @Override + public String getName() { + return KinoPublisher.class.getName(); + } + + @Override + public void post(final Class eventType, final KinoCreatedEvent event) { + this.createBus.publish(eventType, event); + this.notification.notify(event); + } + + @Override + public void post2(final Class eventType, final KinoUpdatedEvent event) { + this.updateBus.publish(eventType, event); + this.notification.notify(event); + } + + @Override + public void post3(final Class eventType, final KinoDeletedEvent event) { + this.deleteBus.publish(eventType, event); + this.notification.notify(event); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/VorfuehrungPublisher.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/VorfuehrungPublisher.java new file mode 100644 index 0000000..cbdb6e5 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/VorfuehrungPublisher.java @@ -0,0 +1,80 @@ +package de.accso.flexinale.backoffice.api_out.event; + +import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungCRUDEvent; +import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungCreatedEvent; +import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungDeletedEvent; +import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungUpdatedEvent; +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; +import de.accso.flexinale.backoffice.api_out.event.mapper.Vorfuehrung2VorfuehrungTOMapper; +import de.accso.flexinale.backoffice.domain.model.Vorfuehrung; +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import de.accso.flexinale.common.api.eventbus.EventNotification; +import de.accso.flexinale.common.api.eventbus.EventPublisher.EventPublisher3; + +import java.util.Collection; + +@SuppressWarnings("unused") +public class VorfuehrungPublisher implements EventPublisher3 { + private final EventBus createBus; + private final EventBus updateBus; + private final EventBus deleteBus; + + private final EventNotification notification; + + public VorfuehrungPublisher(final EventBusFactory eventBusFactory, + final EventNotification notification) { + this.createBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungCreatedEvent.class); + this.updateBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungUpdatedEvent.class); + this.deleteBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungDeletedEvent.class); + + this.notification = notification; + } + + public void publishNewVorfuehrungen(final Collection vorfuehrungen) { + publishCRUDEvents(vorfuehrungen, VorfuehrungCRUDEvent.CRUD.CREATE); + } + + public void publishUpdatedVorfuehrungen(final Collection vorfuehrungen) { + publishCRUDEvents(vorfuehrungen, VorfuehrungCRUDEvent.CRUD.UPDATE); + } + + // currently we do not publish Deletions events + public void publishDeletedVorfuehrungen(final Collection vorfuehrungen) { + publishCRUDEvents(vorfuehrungen, VorfuehrungCRUDEvent.CRUD.DELETE); + } + + private void publishCRUDEvents(final Collection changedVorfuehrungen, final VorfuehrungCRUDEvent.CRUD mode) { + for (Vorfuehrung vorfuehrung: changedVorfuehrungen) { + VorfuehrungTO vorfuehrungTO = Vorfuehrung2VorfuehrungTOMapper.map(vorfuehrung); + switch (mode) { + case CREATE -> this.post (VorfuehrungCreatedEvent.class, new VorfuehrungCreatedEvent(vorfuehrungTO)); + case UPDATE -> this.post2(VorfuehrungUpdatedEvent.class, new VorfuehrungUpdatedEvent(vorfuehrungTO)); + case DELETE -> this.post3(VorfuehrungDeletedEvent.class, new VorfuehrungDeletedEvent(vorfuehrungTO)); + } + } + } + + @Override + public String getName() { + return VorfuehrungPublisher.class.getName(); + } + + @Override + public void post(final Class eventType, final VorfuehrungCreatedEvent event) { + this.createBus.publish(eventType, event); + this.notification.notify(event); + } + + @Override + public void post2(final Class eventType, final VorfuehrungUpdatedEvent event) { + this.updateBus.publish(eventType, event); + this.notification.notify(event); + } + + @Override + public void post3(final Class eventType, final VorfuehrungDeletedEvent event) { + this.deleteBus.publish(eventType, event); + this.notification.notify(event); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Film2FilmTOMapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Film2FilmTOMapper.java new file mode 100644 index 0000000..6132d37 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Film2FilmTOMapper.java @@ -0,0 +1,16 @@ +package de.accso.flexinale.backoffice.api_out.event.mapper; + +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.backoffice.domain.model.Film; + +import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull; + +public final class Film2FilmTOMapper { + + public static FilmTO map(final Film film) { + return new FilmTO(film.id(), film.version, + new FilmTO.Titel(getRawOrNull(film.titel)), + new FilmTO.ImdbUrl(getRawOrNull(film.imdbUrl)), + new FilmTO.DauerInMinuten(getRawOrNull(film.dauerInMinuten))); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/KinoKinoSaal2KinoKinoSaalTOMapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/KinoKinoSaal2KinoKinoSaalTOMapper.java new file mode 100644 index 0000000..bfff4bd --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/KinoKinoSaal2KinoKinoSaalTOMapper.java @@ -0,0 +1,43 @@ +package de.accso.flexinale.backoffice.api_out.event.mapper; + +import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.backoffice.domain.model.Kino; +import de.accso.flexinale.backoffice.domain.model.KinoSaal; + +import java.util.HashSet; + +import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull; + +public final class KinoKinoSaal2KinoKinoSaalTOMapper { + + public static KinoTO mapKino(final Kino kino) { + final KinoTO mappedKino = new KinoTO(kino.id(), kino.version(), + new KinoTO.Name(getRawOrNull(kino.name)), + new KinoTO.Adresse(getRawOrNull(kino.adresse)), + new KinoTO.EmailAdresse(getRawOrNull(kino.emailAdresse)), + new HashSet<>()); + + kino.kinoSaele.stream() + .map(kinoSaal -> mapKinoSaal(kinoSaal, mappedKino)) + .forEach(kinoSaal -> mappedKino.kinoSaele().add(kinoSaal)); + + return mappedKino; + } + + static KinoSaalTO mapKinoSaal(final KinoSaal kinoSaal) { + Kino kino = kinoSaal.kino; + KinoTO mappedKino = mapKino(kino); + return new KinoSaalTO(kinoSaal.id(), kino.version, + new KinoSaalTO.Name(getRawOrNull(kinoSaal.name)), + new KinoSaalTO.AnzahlPlaetze(getRawOrNull(kinoSaal.anzahlPlaetze)), + mappedKino); + } + + private static KinoSaalTO mapKinoSaal(final KinoSaal kinoSaal, final KinoTO kino) { + return new KinoSaalTO(kinoSaal.id(), kinoSaal.version, + new KinoSaalTO.Name(getRawOrNull(kinoSaal.name)), + new KinoSaalTO.AnzahlPlaetze(getRawOrNull(kinoSaal.anzahlPlaetze)), + kino); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Vorfuehrung2VorfuehrungTOMapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Vorfuehrung2VorfuehrungTOMapper.java new file mode 100644 index 0000000..8fa2606 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/api_out/event/mapper/Vorfuehrung2VorfuehrungTOMapper.java @@ -0,0 +1,20 @@ +package de.accso.flexinale.backoffice.api_out.event.mapper; + +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO; +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; +import de.accso.flexinale.backoffice.domain.model.Vorfuehrung; + +import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull; + +public final class Vorfuehrung2VorfuehrungTOMapper { + + public static VorfuehrungTO map(final Vorfuehrung vorfuehrung) { + FilmTO filmTO = Film2FilmTOMapper.map(vorfuehrung.film); + KinoSaalTO kinoSaalTO = KinoKinoSaal2KinoKinoSaalTOMapper.mapKinoSaal(vorfuehrung.kinoSaal); + + return new VorfuehrungTO(vorfuehrung.id(), vorfuehrung.version(), + new VorfuehrungTO.Zeit(getRawOrNull(vorfuehrung.zeit)), + filmTO, kinoSaalTO, kinoSaalTO.kino().id()); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmService.java new file mode 100644 index 0000000..59919cc --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmService.java @@ -0,0 +1,25 @@ +package de.accso.flexinale.backoffice.application.services; + +import de.accso.flexinale.backoffice.domain.model.Film; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import jakarta.transaction.Transactional; + +import java.util.List; + +@Transactional +public class FilmService { + + private final de.accso.flexinale.backoffice.domain.services.FilmService filmService; + + public FilmService(de.accso.flexinale.backoffice.domain.services.FilmService filmService) { + this.filmService = filmService; + } + + public List filme() { + return filmService.filme(); + } + + public Film film(Identifiable.Id filmId) { + return filmService.film(filmId); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmUploadService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmUploadService.java new file mode 100644 index 0000000..fb0d94d --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/FilmUploadService.java @@ -0,0 +1,58 @@ +package de.accso.flexinale.backoffice.application.services; + +import de.accso.flexinale.backoffice.domain.dao.FilmDao; +import de.accso.flexinale.backoffice.domain.model.Film; +import de.accso.flexinale.common.domain.model.AbstractDao; +import de.accso.flexinale.common.application.services.AbstractExcelDataUploadService; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalArgumentException; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import jakarta.transaction.Transactional; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@Transactional +public class FilmUploadService extends AbstractExcelDataUploadService { + private static final Logger LOGGER = LoggerFactory.getLogger(FilmUploadService.class); + + private final FilmDao filmDao; + + public FilmUploadService(final FilmDao filmDao) { + this.filmDao = filmDao; + } + + @Override + public AbstractDao getDao() { + return filmDao; + } + + @Override + public String getNameOfExcelDataType() { + return Film.class.getSimpleName(); + } + + @Override + public Film createDataFromExcelRow(final Row excelRow) { + String filmId = excelRow.getCell(0).getStringCellValue(); + + Cell cellDauer = excelRow.getCell(3); + int dauerInMinuten = + switch(cellDauer.getCellType()) { + case NUMERIC -> (int) cellDauer.getNumericCellValue(); + case STRING -> Integer.parseInt(cellDauer.getStringCellValue()); + default -> throw new FlexinaleIllegalArgumentException("Dauer is not a number"); + }; + + Film film = new Film( + Identifiable.Id.of(filmId), + new Film.Titel(excelRow.getCell(1).getStringCellValue()), + new Film.ImdbUrl(excelRow.getCell(2).getStringCellValue()), + new Film.DauerInMinuten(dauerInMinuten) + ); + + LOGGER.debug("New Film created " + film); + + return film; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoSaalUploadService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoSaalUploadService.java new file mode 100644 index 0000000..df3b4d7 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoSaalUploadService.java @@ -0,0 +1,95 @@ +package de.accso.flexinale.backoffice.application.services; + +import de.accso.flexinale.backoffice.domain.dao.KinoSaalDao; +import de.accso.flexinale.backoffice.domain.model.Kino; +import de.accso.flexinale.backoffice.domain.model.KinoSaal; +import de.accso.flexinale.common.application.PersistMode; +import de.accso.flexinale.common.application.PersistedEntityAndResult; +import de.accso.flexinale.common.domain.model.AbstractDao; +import de.accso.flexinale.common.application.services.AbstractExcelDataUploadService; +import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalArgumentException; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import jakarta.transaction.Transactional; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.InputStream; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +@Transactional +public class KinoSaalUploadService extends AbstractExcelDataUploadService { + private static final Logger LOGGER = LoggerFactory.getLogger(KinoSaalUploadService.class); + + private final KinoSaalDao kinoSaalDao; + + public KinoSaalUploadService(final KinoSaalDao kinoSaalDao) { + this.kinoSaalDao = kinoSaalDao; + } + + @Override + public AbstractDao getDao() { + return kinoSaalDao; + } + + @Override + public String getNameOfExcelDataType() { + return KinoSaal.class.getSimpleName(); + } + + @Override + public Collection> loadDataFromExcelSheetAndPersistAndReturn( + final InputStream stream, final PersistMode mode) { + throw new DeveloperMistakeException("method must not be called on KinoSaalUploadService"); + } + + @Override + public KinoSaal createDataFromExcelRow(final Row excelRow) { + String kinoSaalId = excelRow.getCell(0).getStringCellValue(); + + String kinoId = excelRow.getCell(3).getStringCellValue(); + + Cell cellAnzahlPlaetze = excelRow.getCell(2); + int anzahlPlaetze = + switch(cellAnzahlPlaetze.getCellType()) { + case NUMERIC -> (int) cellAnzahlPlaetze.getNumericCellValue(); + case STRING -> Integer.parseInt(cellAnzahlPlaetze.getStringCellValue()); + default -> throw new FlexinaleIllegalArgumentException("Anzahl Plaetze is not a number"); + }; + + KinoSaal kinoSaal = new KinoSaal( + Identifiable.Id.of(kinoSaalId), + new KinoSaal.Name(excelRow.getCell(1).getStringCellValue()), + new KinoSaal.AnzahlPlaetze(anzahlPlaetze), + new Kino(Identifiable.Id.of(kinoId)) + ); + + LOGGER.debug("New KinoSaal created: " + kinoSaal); + + return kinoSaal; + } + + public Map> mapKinoSaeleToKino(final Collection kinoSaele) { + Map> saeleUndKinos = new HashMap<>(); + for (KinoSaal saal : kinoSaele) { + Identifiable.Id key = saal.kino.id(); + + Collection kinoSaeleAlreadyMapped = saeleUndKinos.get(key); + if (kinoSaeleAlreadyMapped == null) { + HashSet saele = new HashSet<>(); + saele.add(saal); + saeleUndKinos.put(key, saele); + } + else { + kinoSaeleAlreadyMapped.add(saal); + saeleUndKinos.put(key, kinoSaeleAlreadyMapped); + } + } + return saeleUndKinos; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoService.java new file mode 100644 index 0000000..313deed --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoService.java @@ -0,0 +1,26 @@ +package de.accso.flexinale.backoffice.application.services; + +import de.accso.flexinale.backoffice.domain.model.Kino; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import jakarta.transaction.Transactional; + +import java.util.List; + +@Transactional +public class KinoService { + + private final de.accso.flexinale.backoffice.domain.services.KinoService kinoService; + + public KinoService(de.accso.flexinale.backoffice.domain.services.KinoService kinoService) { + this.kinoService = kinoService; + } + + + public Kino kino(Identifiable.Id kinoId) { + return kinoService.kino(kinoId); + } + + public List kinos() { + return kinoService.kinos(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoUploadService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoUploadService.java new file mode 100644 index 0000000..98afa69 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/KinoUploadService.java @@ -0,0 +1,81 @@ +package de.accso.flexinale.backoffice.application.services; + +import de.accso.flexinale.backoffice.domain.dao.KinoDao; +import de.accso.flexinale.backoffice.domain.model.Kino; +import de.accso.flexinale.backoffice.domain.model.KinoSaal; +import de.accso.flexinale.common.domain.model.AbstractDao; +import de.accso.flexinale.common.application.services.AbstractExcelDataUploadService; +import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import jakarta.transaction.Transactional; +import org.apache.poi.ss.usermodel.Row; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +@Transactional +public class KinoUploadService extends AbstractExcelDataUploadService { + private static final Logger LOGGER = LoggerFactory.getLogger(KinoUploadService.class); + + private final KinoDao kinoDao; + + private final Map> alleKinosUndIhreSaele = new HashMap<>(); + + public KinoUploadService(KinoDao kinoDao) { + this.kinoDao = kinoDao; + } + + @Override + public AbstractDao getDao() { + return kinoDao; + } + + @Override + public String getNameOfExcelDataType() { + return Kino.class.getSimpleName(); + } + + @Override + public void beforeLoad(Object... o) { + if ((o.length != 1) && (!(o[0] instanceof Map))) { + throw new DeveloperMistakeException("wrong type: expecting a " + + "Map> of Kino and their KinoSaele"); + } + + @SuppressWarnings("unchecked") + Map> newKinoAndKinoSaele = (Map>) o[0]; + this.alleKinosUndIhreSaele.putAll(newKinoAndKinoSaele); + } + + @Override + public void afterLoad(Object... o) { + alleKinosUndIhreSaele.clear(); + } + + @Override + public Kino createDataFromExcelRow(final Row excelRow) { + String kinoId = excelRow.getCell(0).getStringCellValue(); + + Collection saeleImKino = alleKinosUndIhreSaele.get(Identifiable.Id.of(kinoId)); + if (saeleImKino == null || saeleImKino.isEmpty()) { + throw new FlexinaleIllegalStateException("no Saele for Kino %s found".formatted(kinoId)); + } + + Kino kino = new Kino( + Identifiable.Id.of(kinoId), + new Kino.Name(excelRow.getCell(1).getStringCellValue()), + new Kino.Adresse(excelRow.getCell(2).getStringCellValue()), + new Kino.EmailAdresse(excelRow.getCell(3).getStringCellValue()), + Set.copyOf(saeleImKino) + ); + + LOGGER.debug("New Kino created: " + kino); + + return kino; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungService.java new file mode 100644 index 0000000..e4b0560 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungService.java @@ -0,0 +1,26 @@ +package de.accso.flexinale.backoffice.application.services; + +import de.accso.flexinale.backoffice.domain.model.Vorfuehrung; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import jakarta.transaction.Transactional; + +import java.util.List; + +@Transactional +public class VorfuehrungService { + + private final de.accso.flexinale.backoffice.domain.services.VorfuehrungService vorfuehrungService; + + public VorfuehrungService(de.accso.flexinale.backoffice.domain.services.VorfuehrungService vorfuehrungService) { + this.vorfuehrungService = vorfuehrungService; + } + + + public List vorfuehrungen() { + return vorfuehrungService.vorfuehrungen(); + } + + public Vorfuehrung vorfuehrung(Identifiable.Id vorfuehrungId) { + return vorfuehrungService.vorfuehrung(vorfuehrungId); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungUploadService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungUploadService.java new file mode 100644 index 0000000..eb954f3 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/application/services/VorfuehrungUploadService.java @@ -0,0 +1,65 @@ +package de.accso.flexinale.backoffice.application.services; + +import de.accso.flexinale.backoffice.domain.dao.FilmDao; +import de.accso.flexinale.backoffice.domain.dao.KinoSaalDao; +import de.accso.flexinale.backoffice.domain.dao.VorfuehrungDao; +import de.accso.flexinale.backoffice.domain.model.Film; +import de.accso.flexinale.backoffice.domain.model.KinoSaal; +import de.accso.flexinale.backoffice.domain.model.Vorfuehrung; +import de.accso.flexinale.common.domain.model.AbstractDao; +import de.accso.flexinale.common.application.services.AbstractExcelDataUploadService; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import jakarta.transaction.Transactional; +import org.apache.poi.ss.usermodel.Row; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.time.LocalDateTime; + +@Transactional +public class VorfuehrungUploadService extends AbstractExcelDataUploadService { + private static final Logger LOGGER = LoggerFactory.getLogger(VorfuehrungUploadService.class); + + private final FilmDao filmDao; + private final KinoSaalDao kinoSaalDao; + + private final VorfuehrungDao vorfuehrungDao; + + public VorfuehrungUploadService(final VorfuehrungDao vorfuehrungDao, + final FilmDao filmDao, + final KinoSaalDao kinoSaalDao) { + this.filmDao = filmDao; + this.kinoSaalDao = kinoSaalDao; + this.vorfuehrungDao = vorfuehrungDao; + } + + @Override + public AbstractDao getDao() { + return vorfuehrungDao; + } + + @Override + public String getNameOfExcelDataType() { + return Vorfuehrung.class.getSimpleName(); + } + + @Override + public Vorfuehrung createDataFromExcelRow(final Row excelRow) { + String vorfuehrungId = excelRow.getCell(0).getStringCellValue(); + + // expects date/time in format like for example 2023-02-20T17:45 + LocalDateTime zeit = LocalDateTime.parse(excelRow.getCell(1).getStringCellValue()); + + String filmId = excelRow.getCell(2).getStringCellValue(); + Film film = filmDao.findById(Identifiable.Id.of(filmId)).orElseThrow(IllegalStateException::new); + + String kinoSaalId = excelRow.getCell(3).getStringCellValue(); + KinoSaal kinoSaal = kinoSaalDao.findById(Identifiable.Id.of(kinoSaalId)).orElseThrow(IllegalStateException::new); + + Vorfuehrung vorfuehrung = new Vorfuehrung(Identifiable.Id.of(vorfuehrungId), new Vorfuehrung.Zeit(zeit), film, kinoSaal); + + LOGGER.debug("New Vorfuehrung created: " + vorfuehrung); + + return vorfuehrung; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/FilmDao.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/FilmDao.java new file mode 100644 index 0000000..21762c6 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/FilmDao.java @@ -0,0 +1,13 @@ +package de.accso.flexinale.backoffice.domain.dao; + +import de.accso.flexinale.backoffice.domain.model.Film; +import de.accso.flexinale.common.domain.model.AbstractDao; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.Optional; + +public interface FilmDao extends AbstractDao { + + @Override + Optional findById(final Identifiable.Id id); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoDao.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoDao.java new file mode 100644 index 0000000..59a61a3 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoDao.java @@ -0,0 +1,13 @@ +package de.accso.flexinale.backoffice.domain.dao; + +import de.accso.flexinale.backoffice.domain.model.Kino; +import de.accso.flexinale.common.domain.model.AbstractDao; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.Optional; + +public interface KinoDao extends AbstractDao { + + @Override + Optional findById(final Identifiable.Id id); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoSaalDao.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoSaalDao.java new file mode 100644 index 0000000..a9cdb73 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/KinoSaalDao.java @@ -0,0 +1,13 @@ +package de.accso.flexinale.backoffice.domain.dao; + +import de.accso.flexinale.backoffice.domain.model.KinoSaal; +import de.accso.flexinale.common.domain.model.AbstractDao; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.Optional; + +public interface KinoSaalDao extends AbstractDao { + + @Override + Optional findById(final Identifiable.Id id); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/VorfuehrungDao.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/VorfuehrungDao.java new file mode 100644 index 0000000..eaa9e26 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/dao/VorfuehrungDao.java @@ -0,0 +1,17 @@ +package de.accso.flexinale.backoffice.domain.dao; + +import de.accso.flexinale.backoffice.domain.model.Vorfuehrung; +import de.accso.flexinale.common.domain.model.AbstractDao; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("unused") +public interface VorfuehrungDao extends AbstractDao { + + @Override + Optional findById(final Identifiable.Id id); + + List findByFilmId(final Identifiable.Id filmId); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Film.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Film.java new file mode 100644 index 0000000..1b15336 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Film.java @@ -0,0 +1,95 @@ +package de.accso.flexinale.backoffice.domain.model; + +import de.accso.flexinale.common.shared_kernel.*; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +public class Film implements Identifiable, Versionable, Mergeable, EqualsByContent { + public record Titel(String raw) implements RawWrapper {} + public record ImdbUrl(String raw) implements RawWrapper {} + public record DauerInMinuten(Integer raw) implements RawWrapper {} + + public final Id id; + public final Version version; + + public Titel titel; + public ImdbUrl imdbUrl; + public DauerInMinuten dauerInMinuten; + + public Film(final Id id) { + this(id, Versionable.unknownVersion()); + } + + public Film(final Id id, final Version version) { + this.id = id; + this.version = version; + } + + public Film(final Id id, + final Titel titel, final ImdbUrl imdbUrl, final DauerInMinuten dauerInMinuten) { + this(id, Versionable.unknownVersion(), titel, imdbUrl, dauerInMinuten); + } + + public Film(final Id id, final Version version, + final Titel titel, final ImdbUrl imdbUrl, final DauerInMinuten dauerInMinuten) { + this.id = id; + this.version = version; + this.titel = titel; + this.imdbUrl = imdbUrl; + this.dauerInMinuten = dauerInMinuten; + } + + @Override + public Id id() { + return id; + } + + @Override + public Version version() { + return version; + } + + @Override + public Film merge(final Film newData) { + return new Film(this.id, this.version, + newData.titel, newData.imdbUrl, newData.dauerInMinuten); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + } + + @Override + public boolean equals(final Object o) { + if (!equalsByContent(o)) return false; + + if (this == o) {return true;} + if (o == null || getClass() != o.getClass()) {return false;} + final Film that = (Film) o; + return new EqualsBuilder().append(version, that.version).isEquals(); + } + + @Override + public boolean equalsByContent(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + final Film that = (Film) o; + + return new EqualsBuilder() + .append(id, that.id) + .append(titel, that.titel).append(imdbUrl, that.imdbUrl) + .append(dauerInMinuten, that.dauerInMinuten).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(id).append(version) + .append(titel).append(imdbUrl).append(dauerInMinuten).toHashCode(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Kino.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Kino.java new file mode 100644 index 0000000..aeadf51 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Kino.java @@ -0,0 +1,145 @@ +package de.accso.flexinale.backoffice.domain.model; + +import de.accso.flexinale.common.shared_kernel.*; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.*; +import java.util.stream.Collectors; + +public class Kino implements Identifiable, Versionable, Mergeable, EqualsByContent { + public record Name(String raw) implements RawWrapper {} + public record Adresse(String raw) implements RawWrapper {} + public record EmailAdresse(String raw) implements RawWrapper {} + + public final Id id; + public final Version version; + + public Name name; + public Adresse adresse; + public EmailAdresse emailAdresse; + @DoNotCheckInArchitectureTests + public Set kinoSaele = new HashSet<>(); + + public Kino(final Id id) { + this(id, Versionable.unknownVersion()); + } + + public Kino(final Id id, final Version version) { + this.id = id; + this.version = version; + } + + public Kino(final Id id, + final Name name, final Adresse adresse, + final EmailAdresse emailAdresse, final Set kinoSaele) { + this(id, Versionable.unknownVersion(), name, adresse, emailAdresse, kinoSaele); + } + + public Kino(final Id id, final Version version, + final Name name, final Adresse adresse, + final EmailAdresse emailAdresse, final Set kinoSaele) { + this.id = id; + this.version = version; + this.name = name; + this.adresse = adresse; + this.emailAdresse = emailAdresse; + this.kinoSaele = kinoSaele; + } + + @Override + public Id id() { + return id; + } + + @Override + public Version version() { + return version; + } + + @Override + public Kino merge(final Kino newData) { + Map mapIdToOldKinoSaal = new HashMap<>(); + this.kinoSaele.forEach(oldKinoSaal -> { + mapIdToOldKinoSaal.put(oldKinoSaal.id(), oldKinoSaal); + }); + + Set mergedKinoSaele = newData.kinoSaele.stream().map(newKinoSaal -> { + KinoSaal oldKinoSaal = mapIdToOldKinoSaal.get(newKinoSaal.id); + if (oldKinoSaal != null) { + return oldKinoSaal.merge(newKinoSaal); + } + else { + return newKinoSaal; + } + }) + .collect(Collectors.toSet()); + + Kino mergedKino = new Kino(this.id, this.version, + newData.name, newData.adresse, newData.emailAdresse, mergedKinoSaele); + mergedKinoSaele.forEach(kinoSaal -> kinoSaal.kino = mergedKino); + + return mergedKino; + } + + public void addSaele(Collection saele) { + kinoSaele.addAll(saele); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + } + + @Override + public boolean equals(final Object o) { + if (!equalsByContent(o)) return false; + + if (this == o) {return true;} + if (o == null || getClass() != o.getClass()) {return false;} + Kino that = (Kino) o; + return new EqualsBuilder().append(version, that.version).isEquals(); + } + + @SuppressWarnings("ConstantValue") + @Override + public boolean equalsByContent(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + Kino that = (Kino) o; + + boolean result = new EqualsBuilder() + .append(id, that.id) + .append(name, that.name) + .append(adresse, that.adresse).append(emailAdresse, that.emailAdresse) + .isEquals(); + if (!result) return false; + + if (kinoSaele == null && that.kinoSaele == null) return true; + if (kinoSaele == null && that.kinoSaele != null) return false; + if (kinoSaele != null && that.kinoSaele == null) return false; + + List thisKSList = kinoSaele.stream().toList(); + List thatKSList = that.kinoSaele.stream().toList(); + if (thisKSList.size() != thatKSList.size()) return false; + for (int counter = 0; counter < thisKSList.size(); counter++) { + if (!thisKSList.get(counter).equalsByContent(thatKSList.get(counter))) return false; + } + + return true; + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(id).append(version) + .append(name).append(adresse).append(emailAdresse) + .append(kinoSaele.stream().toList()) // need to use list, as Set does not support deep equals + .toHashCode(); + } + +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/KinoSaal.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/KinoSaal.java new file mode 100644 index 0000000..52bf17f --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/KinoSaal.java @@ -0,0 +1,118 @@ +package de.accso.flexinale.backoffice.domain.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import de.accso.flexinale.common.shared_kernel.*; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public class KinoSaal implements Identifiable, Versionable, Mergeable, EqualsByContent { + public record Name(String raw) implements RawWrapper {} + public record AnzahlPlaetze(Integer raw) implements RawWrapper {} + + public final Id id; + public final Version version; + + public Name name; + public AnzahlPlaetze anzahlPlaetze = new KinoSaal.AnzahlPlaetze(0); + + @JsonIgnore + @DoNotCheckInArchitectureTests + public Kino kino; + + public KinoSaal(final Id id) { + this(id, Versionable.unknownVersion()); + } + + public KinoSaal(final Id id, final Version version) { + this.id = id; + this.version = version; + } + + public KinoSaal(final Id id, + final Name name, final AnzahlPlaetze anzahlPlaetze, final Kino kino) { + this(id, Versionable.unknownVersion(), name, anzahlPlaetze, kino); + } + + public KinoSaal(final Id id, final Version version, + final Name name, final AnzahlPlaetze anzahlPlaetze, final Kino kino) { + this.id = id; + this.version = version; + this.name = name; + this.anzahlPlaetze = anzahlPlaetze; + this.kino = kino; + } + + @Override + public Id id() { + return id; + } + + @Override + public Version version() { + return version; + } + + @Override + public KinoSaal merge(final KinoSaal newData) { + return new KinoSaal(this.id, + newData.name, newData.anzahlPlaetze, + /* needs to be corrected outside */ null); + } + + @Override + public String toString() { + return "KinoSaal{" + + "id='" + id + '\'' + + ", version=" + version + + ", name='" + name + '\'' + + ", anzahlPlaetze=" + anzahlPlaetze + + (kino != null ? ", kino.id=" + kino.id : ", kino=null") + + '}'; + } + + @Override + public boolean equals(final Object o) { + if (!equalsByContent(o)) return false; + + if (this == o) {return true;} + if (o == null || getClass() != o.getClass()) {return false;} + KinoSaal that = (KinoSaal) o; + return new EqualsBuilder().append(version, that.version).isEquals(); + } + + @SuppressWarnings("ConstantValue") + @Override + public boolean equalsByContent(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + KinoSaal that = (KinoSaal) o; + + boolean result = new EqualsBuilder() + .append(id, that.id) + .append(name, that.name) + .append(anzahlPlaetze, that.anzahlPlaetze). + isEquals(); + if (!result) return false; + + if (kino == null && that.kino == null) return true; + if (kino == null && that.kino != null) return false; + if (kino != null) { + result = kino.id.equals(that.kino.id); // do not check kino but only its id (otherwise Stackoverflow error) + } + + return result; + } + + @Override + public int hashCode() { + HashCodeBuilder builder = new HashCodeBuilder(17, 37) + .append(id).append(version) + .append(name).append(anzahlPlaetze); + if (kino != null) { + builder.append(kino.id); // do not use kino but only its id (otherwise Stackoverflow error) + } + return builder.toHashCode(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Vorfuehrung.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Vorfuehrung.java new file mode 100644 index 0000000..dad4745 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/model/Vorfuehrung.java @@ -0,0 +1,141 @@ +package de.accso.flexinale.backoffice.domain.model; + +import de.accso.flexinale.common.shared_kernel.*; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.time.LocalDateTime; +import java.util.Optional; + +public final class Vorfuehrung implements Identifiable, Versionable, Mergeable, EqualsByContent { // TODO is only final as otherwise Spotbugs complains, get rid of Exception in constructor! + public record Zeit(LocalDateTime raw) implements RawWrapper { + public Zeit(LocalDateTime raw) { + this.raw = raw.withNano(0); // precision is second + } + } + + public final Id id; + public final Version version; + + public final Zeit zeit; + + @DoNotCheckInArchitectureTests + public final Film film; + @DoNotCheckInArchitectureTests + public final KinoSaal kinoSaal; + + public Vorfuehrung(final Id id, + final Zeit zeit, + final Film film, + final KinoSaal kinoSaal) { + this(id, Versionable.unknownVersion(), zeit, film, kinoSaal); + } + + public Vorfuehrung(final Id id, final Version version, + final Zeit zeit, + final Film film, + final KinoSaal kinoSaal) { + this.id = id; + this.version = version; + this.zeit = zeit; + this.film = film; + this.kinoSaal = kinoSaal; + + //TODO this validation should not be done here. If class is not final, Spotbugs complains. For DB entities use "nullable=false" and "optional=false" for DB checks. In domain classes check with jakarta.annotation NonNull? Also: Why is KinoSaal obligatory but not Film? + if (kinoSaal == null) { + throw new FlexinaleIllegalArgumentException("KinoSaal of Vorfuehrung " + this + + " must not be null"); + } + //TODO this validation should not be done here but in KinoSaal itself + if (kinoSaal.anzahlPlaetze == null) { + throw new FlexinaleIllegalArgumentException("KinoSaal's Anzahl Plaetze of Vorfuehrung " + this + + " must not be null"); + } + } + + @Override + public Id id() { + return id; + } + + @Override + public Version version() { + return version; + } + + @Override + public Vorfuehrung merge(final Vorfuehrung newData) { + // merging Vorfuehrung has some business flaws - like when its film or its kinoSaal is changed, + // then all sold tickets might get obsolete - we ignore this here but care for that in Ticketing + Film mergedFilm = this.film.merge(newData.film); + + Kino mergedKino = this.kinoSaal.kino.merge(newData.kinoSaal.kino); + Optional mergedKinoSaal = mergedKino.kinoSaele.stream().filter( + kinoSaal -> kinoSaal.id.equals(this.kinoSaal.id)).findFirst(); + if (mergedKinoSaal.isEmpty()) { + throw new DeveloperMistakeException("merged KinoSaal from Vorfuehrung could not be retrieved"); + } + + return new Vorfuehrung(this.id, this.version, + newData.zeit, mergedFilm, mergedKinoSaal.get()); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + } + + @Override + public boolean equals(final Object o) { + if (!equalsByContent(o)) return false; + + if (this == o) {return true;} + if (o == null || getClass() != o.getClass()) {return false;} + Vorfuehrung that = (Vorfuehrung) o; + return new EqualsBuilder().append(version, that.version).isEquals(); + } + + @SuppressWarnings({"UnusedAssignment", "DataFlowIssue"}) + @Override + public boolean equalsByContent(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + Vorfuehrung that = (Vorfuehrung) o; + + boolean result = new EqualsBuilder() + .append(id, that.id) + .append(zeit, that.zeit) + .isEquals(); + if (!result) return false; + + if (film == null && that.film == null) result = true; + if (film == null && that.film != null) return false; + if (film != null) { + result = film.equalsByContent(that.film); + if (!result) return false; + } + + if (kinoSaal == null && that.kinoSaal == null) result = true; + if (kinoSaal == null && that.kinoSaal != null) return false; + if (kinoSaal != null) { + result = kinoSaal.equalsByContent(that.kinoSaal); + return result; + } + + return true; + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(id).append(version) + .append(zeit) + .append(film) + .append(kinoSaal) + .toHashCode(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/FilmService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/FilmService.java new file mode 100644 index 0000000..a6bf116 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/FilmService.java @@ -0,0 +1,24 @@ +package de.accso.flexinale.backoffice.domain.services; + +import de.accso.flexinale.backoffice.domain.dao.FilmDao; +import de.accso.flexinale.backoffice.domain.model.Film; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.List; + +public class FilmService { + + private final FilmDao filmDao; + + public FilmService(final FilmDao filmDao) { + this.filmDao = filmDao; + } + + public List filme() { + return filmDao.findAll(); + } + + public Film film(final Identifiable.Id id) { + return filmDao.findById(id).orElse(null); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/KinoService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/KinoService.java new file mode 100644 index 0000000..4b0a46b --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/KinoService.java @@ -0,0 +1,24 @@ +package de.accso.flexinale.backoffice.domain.services; + +import de.accso.flexinale.backoffice.domain.dao.KinoDao; +import de.accso.flexinale.backoffice.domain.model.Kino; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.List; + +public class KinoService { + + private final KinoDao kinoDao; + + public KinoService(final KinoDao kinoDao) { + this.kinoDao = kinoDao; + } + + public List kinos() { + return kinoDao.findAll(); + } + + public Kino kino(final Identifiable.Id id) { + return kinoDao.findById(id).orElse(null); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/VorfuehrungService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/VorfuehrungService.java new file mode 100644 index 0000000..90e9ded --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/domain/services/VorfuehrungService.java @@ -0,0 +1,24 @@ +package de.accso.flexinale.backoffice.domain.services; + +import de.accso.flexinale.backoffice.domain.dao.VorfuehrungDao; +import de.accso.flexinale.backoffice.domain.model.Vorfuehrung; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.List; + +public class VorfuehrungService { + + private final VorfuehrungDao vorfuehrungDao; + + public VorfuehrungService(final VorfuehrungDao vorfuehrungDao) { + this.vorfuehrungDao = vorfuehrungDao; + } + + public List vorfuehrungen() { + return vorfuehrungDao.findAll(); + } + + public Vorfuehrung vorfuehrung(final Identifiable.Id id) { + return vorfuehrungDao.findById(id).orElse(null); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeActuatorEndpointEventsPublished.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeActuatorEndpointEventsPublished.java new file mode 100644 index 0000000..cea991f --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeActuatorEndpointEventsPublished.java @@ -0,0 +1,34 @@ +package de.accso.flexinale.backoffice.infrastructure; + +import de.accso.flexinale.common.api.eventbus.EventNotification; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +@Component +@Endpoint(id = "backofficeEventsPublished") +public class FlexinaleBackofficeActuatorEndpointEventsPublished implements EventNotification { + private final Queue eventsPublished = new ConcurrentLinkedQueue<>(); // event list is ordered by production time + + @ReadOperation + public List eventsPublished() { + return eventsPublished.stream().toList(); + } + + @ReadOperation + public List eventsPublishedFilteredBy(@Selector Identifiable.Id correlationId) { + return eventsPublished.stream().filter(event -> event.correlationId().equals(correlationId)).toList(); + } + + @Override + public void notify(final Event event) {// might want to use a generic subscriber + eventsPublished.add(event); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeSpringFactory.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeSpringFactory.java new file mode 100644 index 0000000..7c21e87 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/FlexinaleBackofficeSpringFactory.java @@ -0,0 +1,137 @@ +package de.accso.flexinale.backoffice.infrastructure; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import de.accso.flexinale.backoffice.api_out.event.FilmPublisher; +import de.accso.flexinale.backoffice.api_out.event.KinoPublisher; +import de.accso.flexinale.backoffice.api_out.event.VorfuehrungPublisher; +import de.accso.flexinale.backoffice.application.services.FilmService; +import de.accso.flexinale.backoffice.application.services.KinoService; +import de.accso.flexinale.backoffice.application.services.VorfuehrungService; +import de.accso.flexinale.backoffice.application.services.FilmUploadService; +import de.accso.flexinale.backoffice.application.services.KinoUploadService; +import de.accso.flexinale.backoffice.application.services.KinoSaalUploadService; +import de.accso.flexinale.backoffice.application.services.VorfuehrungUploadService; +import de.accso.flexinale.backoffice.domain.dao.FilmDao; +import de.accso.flexinale.backoffice.domain.dao.KinoDao; +import de.accso.flexinale.backoffice.domain.dao.KinoSaalDao; +import de.accso.flexinale.backoffice.domain.dao.VorfuehrungDao; +import de.accso.flexinale.backoffice.infrastructure.persistence.*; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@Profile({ "!testdata-besucherportal & !testdata-ticketing" }) +@EnableJpaRepositories({"de.accso.flexinale.backoffice.infrastructure.persistence"}) +@EnableTransactionManagement +@EntityScan(basePackages={"de.accso.flexinale.backoffice.infrastructure.persistence"}) +public class FlexinaleBackofficeSpringFactory { + + @Bean + public FilmService createApplicationFilmService(final de.accso.flexinale.backoffice.domain.services.FilmService filmService) { + return new FilmService(filmService); + } + + @Bean + public de.accso.flexinale.backoffice.domain.services.FilmService createBackofficeFilmService(final FilmDao filmDao) { + return new de.accso.flexinale.backoffice.domain.services.FilmService(filmDao); + } + + @Bean + public FilmUploadService createBackofficeFilmUploadService(final FilmDao filmDao) { + return new FilmUploadService(filmDao); + } + + @Bean + public FilmDao createBackofficeFilmDao(final FilmJpaRepository filmJpaRepository) { + return new FilmJpaRepositoryDelegate(filmJpaRepository); + } + + @Bean + public FilmPublisher createBackofficeFilmPublisher(final EventBusFactory eventBusFactory, + final FlexinaleBackofficeActuatorEndpointEventsPublished endpointEventsPublished) { + return new FilmPublisher(eventBusFactory, endpointEventsPublished); + } + + // ------------------------------------------------------------------------------------------------ + + @Bean + public KinoService createApplicationKinoService(final de.accso.flexinale.backoffice.domain.services.KinoService kinoService) { + return new KinoService(kinoService); + } + + @Bean + public de.accso.flexinale.backoffice.domain.services.KinoService createBackofficeKinoService(final KinoDao KinoDao) { + return new de.accso.flexinale.backoffice.domain.services.KinoService(KinoDao); + } + + @Bean + public KinoUploadService createBackofficeKinoUploadService(final KinoDao kinoDao) { + return new KinoUploadService(kinoDao); + } + + @Bean + public KinoSaalUploadService createBackofficeKinoSaalUploadService(final KinoSaalDao kinoSaalDao) { + return new KinoSaalUploadService(kinoSaalDao); + } + + @Bean + public KinoDao createBackofficeKinoDao(final KinoJpaRepository kinoJpaRepository) { + return new KinoJpaRepositoryDelegate(kinoJpaRepository); + } + + @Bean + public KinoSaalDao createBackofficeKinoSaalDao(final KinoSaalJpaRepository kinoSaalJpaRepository) { + return new KinoSaalJpaRepositoryDelegate(kinoSaalJpaRepository); + } + + @Bean + public KinoPublisher createBackofficeKinoPublisher(final EventBusFactory eventBusFactory, + final FlexinaleBackofficeActuatorEndpointEventsPublished endpointEventsPublished) { + return new KinoPublisher(eventBusFactory, endpointEventsPublished); + } + + // ------------------------------------------------------------------------------------------------ + + @Bean + public VorfuehrungService createApplicationVorfuehrungService(final de.accso.flexinale.backoffice.domain.services.VorfuehrungService vorfuehrungService) { + return new VorfuehrungService(vorfuehrungService); + } + + @Bean + public de.accso.flexinale.backoffice.domain.services.VorfuehrungService createBackofficeVorfuehrungService(final VorfuehrungDao vorfuehrungDao) { + return new de.accso.flexinale.backoffice.domain.services.VorfuehrungService(vorfuehrungDao); + } + + @Bean + public VorfuehrungUploadService createBackofficeVorfuehrungUploadService(final VorfuehrungDao vorfuehrungDao, + final FilmDao filmDao, + final KinoSaalDao kinoSaalDao) { + return new VorfuehrungUploadService(vorfuehrungDao, filmDao, kinoSaalDao); + } + + @Bean + public VorfuehrungDao createBackofficeVorfuehrungDao(final VorfuehrungJpaRepository vorfuehrungJpaRepository) { + return new VorfuehrungJpaRepositoryDelegate(vorfuehrungJpaRepository); + } + + @Bean + public VorfuehrungPublisher createBackofficeVorfuehrungPublisher(final EventBusFactory eventBusFactory, + final FlexinaleBackofficeActuatorEndpointEventsPublished endpointEventsPublished) { + return new VorfuehrungPublisher(eventBusFactory, endpointEventsPublished); + } + + // ------------------------------------------------------------------------------------------------ + + // Actuator Event serialization + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonBackofficeCustomizer() { + return builder -> builder.visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmEntity.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmEntity.java new file mode 100644 index 0000000..ea33dd6 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmEntity.java @@ -0,0 +1,78 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence; + +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.Versionable; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.io.Serializable; + +@jakarta.persistence.Entity(name = "Film") +public class FilmEntity implements Identifiable, Versionable, Serializable { + @jakarta.persistence.Id + public String id; // primary key + + @jakarta.persistence.Column + public String titel; + + @jakarta.persistence.Column + public String imdbUrl; + + @jakarta.persistence.Column + public Integer dauerInMinuten; + + @jakarta.persistence.Version + private Integer version = 0; + + protected FilmEntity() { + } + + public FilmEntity(final String id, final Integer version, + final String titel, final String imdbUrl, final Integer dauerInMinuten) { + this.id = id; + this.version = version; + this.titel = titel; + this.imdbUrl = imdbUrl; + this.dauerInMinuten = dauerInMinuten; + } + + @Override + public Id id() { + return Identifiable.Id.of(id); + } + + @Override + public Version version() { + return Versionable.Version.of(version); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + FilmEntity that = (FilmEntity) o; + + return new EqualsBuilder() + .append(id, that.id).append(version, that.version) + .append(titel, that.titel) + .append(imdbUrl, that.imdbUrl) + .append(dauerInMinuten, that.dauerInMinuten) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(id).append(version) + .append(titel).append(imdbUrl).append(dauerInMinuten).toHashCode(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepository.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepository.java new file mode 100644 index 0000000..e10323f --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepository.java @@ -0,0 +1,8 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface FilmJpaRepository extends JpaRepository { +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepositoryDelegate.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepositoryDelegate.java new file mode 100644 index 0000000..74481e8 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/FilmJpaRepositoryDelegate.java @@ -0,0 +1,59 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence; + +import de.accso.flexinale.backoffice.domain.dao.FilmDao; +import de.accso.flexinale.backoffice.domain.model.Film; +import de.accso.flexinale.backoffice.infrastructure.persistence.mapper.FilmEntity2FilmMapper; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@SuppressWarnings("unused") +public class FilmJpaRepositoryDelegate implements FilmDao { + + private final FilmJpaRepository filmJpaRepository; + + public FilmJpaRepositoryDelegate(final FilmJpaRepository filmJpaRepository) { + this.filmJpaRepository = filmJpaRepository; + } + + @Override + public List findAll() { + List filmEntities = filmJpaRepository.findAll(); + + return filmEntities.stream() + .map(FilmEntity2FilmMapper::map) + .collect(Collectors.toCollection(ArrayList::new)); + } + + @Override + public Optional findById(final Identifiable.Id id) { + Optional filmEntity = filmJpaRepository.findById(id.id()); + return FilmEntity2FilmMapper.map(filmEntity); + } + + @Override + public Film save(final Film film) { + FilmEntity filmEntity = FilmEntity2FilmMapper.map(film); + FilmEntity savedEntity = filmJpaRepository.save(filmEntity); + return FilmEntity2FilmMapper.map(savedEntity); + } + + @Override + public void delete(final Film film) { + FilmEntity filmEntity = FilmEntity2FilmMapper.map(film); + filmJpaRepository.delete(filmEntity); + } + + @Override + public void deleteById(final Identifiable.Id id) { + filmJpaRepository.deleteById(id.id()); + } + + @Override + public void deleteAll() { + filmJpaRepository.deleteAll(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoEntity.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoEntity.java new file mode 100644 index 0000000..29cccbf --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoEntity.java @@ -0,0 +1,89 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence; + +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.Versionable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.FetchType; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.Set; + +@jakarta.persistence.Entity(name = "Kino") +public class KinoEntity implements Identifiable, Versionable, Serializable { + @jakarta.persistence.Id + public String id; // primary key + + @jakarta.persistence.Column + public String name; + + @jakarta.persistence.Column + public String adresse; + + @jakarta.persistence.Column + public String emailAdresse; + + @jakarta.persistence.Version + private Integer version = 0; + + @jakarta.persistence.OneToMany(mappedBy = "kino", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + public Set kinoSaele = new HashSet<>(); + + protected KinoEntity() { + } + + public KinoEntity(final String id, final Integer version, + final String name, final String adresse, final String emailAdresse) { + this.id = id; + this.version = version; + this.name = name; + this.adresse = adresse; + this.emailAdresse = emailAdresse; + } + + @Override + public Id id() { + return Identifiable.Id.of(id); + } + + @Override + public Version version() { + return Versionable.Version.of(version); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + KinoEntity that = (KinoEntity) o; + + return new EqualsBuilder() + .append(id, that.id).append(version, that.version) + .append(name, that.name) + .append(adresse, that.adresse) + .append(emailAdresse, that.emailAdresse) + .append(kinoSaele.stream().toList(), that.kinoSaele.stream().toList()) // need to use list, as Set does not support deep equals + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(id).append(version) + .append(name).append(adresse) + .append(emailAdresse) + .append(kinoSaele.stream().toList()) // need to use list, as Set does not support deep equals + .toHashCode(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepository.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepository.java new file mode 100644 index 0000000..9838433 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepository.java @@ -0,0 +1,8 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface KinoJpaRepository extends JpaRepository { +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepositoryDelegate.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepositoryDelegate.java new file mode 100644 index 0000000..40c268d --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoJpaRepositoryDelegate.java @@ -0,0 +1,59 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence; + +import de.accso.flexinale.backoffice.domain.dao.KinoDao; +import de.accso.flexinale.backoffice.domain.model.Kino; +import de.accso.flexinale.backoffice.infrastructure.persistence.mapper.KinoKinoSaalEntity2KinoKinoSaalMapper; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@SuppressWarnings("unused") +public class KinoJpaRepositoryDelegate implements KinoDao { + + private final KinoJpaRepository kinoJpaRepository; + + public KinoJpaRepositoryDelegate(final KinoJpaRepository KinoJpaRepository) { + this.kinoJpaRepository = KinoJpaRepository; + } + + @Override + public List findAll() { + List kinoEntities = kinoJpaRepository.findAll(); + + return kinoEntities.stream() + .map(KinoKinoSaalEntity2KinoKinoSaalMapper::mapKino) + .collect(Collectors.toCollection(ArrayList::new)); + } + + @Override + public Optional findById(final Identifiable.Id id) { + Optional kinoEntity = kinoJpaRepository.findById(id.id()); + return KinoKinoSaalEntity2KinoKinoSaalMapper.mapKino(kinoEntity); + } + + @Override + public Kino save(final Kino kino) { + KinoEntity kinoEntity = KinoKinoSaalEntity2KinoKinoSaalMapper.mapKino(kino); + KinoEntity savedEntity = kinoJpaRepository.save(kinoEntity); + return KinoKinoSaalEntity2KinoKinoSaalMapper.mapKino(savedEntity); + } + + @Override + public void delete(final Kino kino) { + KinoEntity kinoEntity = KinoKinoSaalEntity2KinoKinoSaalMapper.mapKino(kino); + kinoJpaRepository.delete(kinoEntity); + } + + @Override + public void deleteById(final Identifiable.Id id) { + kinoJpaRepository.deleteById(id.id()); + } + + @Override + public void deleteAll() { + kinoJpaRepository.deleteAll(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalEntity.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalEntity.java new file mode 100644 index 0000000..f58de25 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalEntity.java @@ -0,0 +1,88 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence; + +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.Versionable; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +import java.io.Serializable; + +@jakarta.persistence.Entity(name = "KinoSaal") +public class KinoSaalEntity implements Identifiable, Versionable, Serializable { + @jakarta.persistence.Id + public String id; // primary key + + @jakarta.persistence.Column + public String name; + + @jakarta.persistence.Column + public Integer anzahlPlaetze = 0; + + @jakarta.persistence.ManyToOne + @jakarta.persistence.JoinColumn(name = "kino", nullable = false) + public KinoEntity kino; + + @jakarta.persistence.Version + private Integer version = 0; + + protected KinoSaalEntity() { + } + + public KinoSaalEntity(final String id, final Integer version, + final String name, final Integer anzahlPlaetze, final KinoEntity kino) { + this.id = id; + this.version = version; + this.name = name; + this.anzahlPlaetze = anzahlPlaetze; + this.kino = kino; + } + + @Override + public Id id() { + return Identifiable.Id.of(id); + } + + @Override + public Version version() { + return Versionable.Version.of(version); + } + + @Override + public String toString() { + return "KinoSaalEntity{" + + "id='" + id + '\'' + + ", name='" + name + '\'' + + ", anzahlPlaetze=" + anzahlPlaetze + + (kino != null ? ", kino.id=" + kino.id : ", kino=null") + + ", version=" + version + + '}'; + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + KinoSaalEntity that = (KinoSaalEntity) o; + + EqualsBuilder builder = new EqualsBuilder() + .append(id, that.id).append(version, that.version) + .append(name, that.name).append(anzahlPlaetze, that.anzahlPlaetze); + if (kino != null) { + builder.append(kino.id, that.kino.id); // do not check kino but only its id (otherwise Stackoverflow error) + } + return builder.isEquals(); + } + + @Override + public int hashCode() { + HashCodeBuilder builder = new HashCodeBuilder(17, 37) + .append(id).append(version) + .append(name).append(anzahlPlaetze); + if (kino != null) { + builder.append(kino.id); // do not check kino but only its id (otherwise Stackoverflow error) + } + return builder.toHashCode(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepository.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepository.java new file mode 100644 index 0000000..41a0eb3 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepository.java @@ -0,0 +1,8 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface KinoSaalJpaRepository extends JpaRepository { +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepositoryDelegate.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepositoryDelegate.java new file mode 100644 index 0000000..c612d65 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/KinoSaalJpaRepositoryDelegate.java @@ -0,0 +1,58 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence; + +import de.accso.flexinale.backoffice.domain.dao.KinoSaalDao; +import de.accso.flexinale.backoffice.domain.model.KinoSaal; +import de.accso.flexinale.backoffice.infrastructure.persistence.mapper.KinoKinoSaalEntity2KinoKinoSaalMapper; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@SuppressWarnings("unused") +public class KinoSaalJpaRepositoryDelegate implements KinoSaalDao { + + private final KinoSaalJpaRepository kinoSaalJpaRepository; + + public KinoSaalJpaRepositoryDelegate(final KinoSaalJpaRepository kinoSaalJpaRepository) { + this.kinoSaalJpaRepository = kinoSaalJpaRepository; + } + + @Override + public List findAll() { + List kinoSaalEntities = kinoSaalJpaRepository.findAll(); + + return kinoSaalEntities.stream() + .map(KinoKinoSaalEntity2KinoKinoSaalMapper::mapKinoSaal) + .collect(Collectors.toList()); + } + + @Override + public Optional findById(final Identifiable.Id id) { + Optional kinoSaalEntity = kinoSaalJpaRepository.findById(id.id()); + return KinoKinoSaalEntity2KinoKinoSaalMapper.mapToKinoSaal(kinoSaalEntity); + } + + @Override + public KinoSaal save(final KinoSaal kinoSaal) { + KinoSaalEntity kinoSaalEntity = KinoKinoSaalEntity2KinoKinoSaalMapper.mapKinoSaal(kinoSaal); + KinoSaalEntity savedEntity = kinoSaalJpaRepository.save(kinoSaalEntity); + return KinoKinoSaalEntity2KinoKinoSaalMapper.mapKinoSaal(savedEntity); + } + + @Override + public void delete(final KinoSaal kinoSaal) { + KinoSaalEntity kinoSaalEntity = KinoKinoSaalEntity2KinoKinoSaalMapper.mapKinoSaal(kinoSaal); + kinoSaalJpaRepository.delete(kinoSaalEntity); + } + + @Override + public void deleteById(final Identifiable.Id id) { + kinoSaalJpaRepository.deleteById(id.id()); + } + + @Override + public void deleteAll() { + kinoSaalJpaRepository.deleteAll(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungEntity.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungEntity.java new file mode 100644 index 0000000..48fdb6e --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungEntity.java @@ -0,0 +1,96 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence; + +import de.accso.flexinale.common.shared_kernel.DateTimeHelper; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalArgumentException; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.Versionable; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@jakarta.persistence.Entity(name = "Vorfuehrung") +public final class VorfuehrungEntity implements Identifiable, Versionable, Serializable { // TODO is only final as otherwise Spotbugs complains, get rid of Exception in constructor! + @jakarta.persistence.Id + public String id; // primary key + + @jakarta.persistence.Column + public Long zeit; // save EPOCH seconds instead of Date/Time (Timezone is UTC) + + @jakarta.persistence.ManyToOne + public FilmEntity film; + + @jakarta.persistence.ManyToOne + public KinoSaalEntity kinoSaal; + + @jakarta.persistence.Version + private Integer version = 0; + + protected VorfuehrungEntity() { + } + + public VorfuehrungEntity(final String id, final Integer version, + final LocalDateTime zeit, + final FilmEntity film, + final KinoSaalEntity kinoSaal) { + this.id = id; + this.version = version; + this.zeit = DateTimeHelper.toEpochSeconds(zeit); + this.film = film; + this.kinoSaal = kinoSaal; + + //TODO this validation should not be done here. If class is not final, Spotbugs complains. For DB entities use "nullable=false" and "optional=false" for DB checks. In domain classes check with jakarta.annotation NonNull? Also: Why is KinoSaal obligatory but not Film? + if (kinoSaal == null) { + throw new FlexinaleIllegalArgumentException("KinoSaal of Vorfuehrung " + this + + " must not be null"); + } + //TODO this validation should not be done here but in KinoSaal itself + if (kinoSaal.anzahlPlaetze == null) { + throw new FlexinaleIllegalArgumentException("KinoSaal's Anzahl Plaetze of Vorfuehrung " + this + + " must not be null"); + } + } + + @Override + public Id id() { + return Identifiable.Id.of(id); + } + + @Override + public Version version() { + return Versionable.Version.of(version); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + VorfuehrungEntity that = (VorfuehrungEntity) o; + + return new EqualsBuilder() + .append(id, that.id).append(version, that.version) + .append(zeit, that.zeit) + .append(film, that.film).append(kinoSaal, that.kinoSaal) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(id).append(version) + .append(zeit).append(film).append(kinoSaal) + .toHashCode(); + } +} + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepository.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepository.java new file mode 100644 index 0000000..46e6164 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepository.java @@ -0,0 +1,14 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface VorfuehrungJpaRepository extends JpaRepository { + + @Query("SELECT v FROM Vorfuehrung v WHERE v.film.id = :filmId ORDER BY v.zeit") + List findByFilmId(final String filmId); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepositoryDelegate.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepositoryDelegate.java new file mode 100644 index 0000000..4e5e1c3 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/VorfuehrungJpaRepositoryDelegate.java @@ -0,0 +1,65 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence; + +import de.accso.flexinale.backoffice.domain.dao.VorfuehrungDao; +import de.accso.flexinale.backoffice.domain.model.Vorfuehrung; +import de.accso.flexinale.backoffice.infrastructure.persistence.mapper.VorfuehrungEntity2VorfuehrungMapper; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@SuppressWarnings("unused") +public class VorfuehrungJpaRepositoryDelegate implements VorfuehrungDao { + + private final VorfuehrungJpaRepository vorfuehrungJpaRepository; + + public VorfuehrungJpaRepositoryDelegate(final VorfuehrungJpaRepository vorfuehrungJpaRepository) { + this.vorfuehrungJpaRepository = vorfuehrungJpaRepository; + } + + @Override + public List findAll() { + List vorfuehrungEntities = vorfuehrungJpaRepository.findAll(); + + return vorfuehrungEntities.stream() + .map(VorfuehrungEntity2VorfuehrungMapper::map) + .collect(Collectors.toCollection(ArrayList::new)); + } + + @Override + public Optional findById(final Identifiable.Id id) { + Optional vorfuehrungEntity = vorfuehrungJpaRepository.findById(id.id()); + return VorfuehrungEntity2VorfuehrungMapper.map(vorfuehrungEntity); + } + + @Override + public List findByFilmId(final Identifiable.Id filmId) { + List vorfuehrungEntities = vorfuehrungJpaRepository.findByFilmId(filmId.id()); + return VorfuehrungEntity2VorfuehrungMapper.map(vorfuehrungEntities); + } + + @Override + public Vorfuehrung save(final Vorfuehrung vorfuehrung) { + VorfuehrungEntity vorfuehrungEntity = VorfuehrungEntity2VorfuehrungMapper.map(vorfuehrung); + VorfuehrungEntity savedEntity = vorfuehrungJpaRepository.save(vorfuehrungEntity); + return VorfuehrungEntity2VorfuehrungMapper.map(savedEntity); + } + + @Override + public void delete(final Vorfuehrung vorfuehrung) { + VorfuehrungEntity vorfuehrungEntity = VorfuehrungEntity2VorfuehrungMapper.map(vorfuehrung); + vorfuehrungJpaRepository.delete(vorfuehrungEntity); + } + + @Override + public void deleteById(final Identifiable.Id id) { + vorfuehrungJpaRepository.deleteById(id.id()); + } + + @Override + public void deleteAll() { + vorfuehrungJpaRepository.deleteAll(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/FilmEntity2FilmMapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/FilmEntity2FilmMapper.java new file mode 100644 index 0000000..6b0343d --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/FilmEntity2FilmMapper.java @@ -0,0 +1,33 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence.mapper; + +import de.accso.flexinale.backoffice.domain.model.Film; +import de.accso.flexinale.backoffice.infrastructure.persistence.FilmEntity; + +import java.util.Optional; + +import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull; + +public final class FilmEntity2FilmMapper { + + public static Film map(final FilmEntity filmEntity) { + return new Film(filmEntity.id(), filmEntity.version(), + new Film.Titel(filmEntity.titel), new Film.ImdbUrl(filmEntity.imdbUrl), + new Film.DauerInMinuten(filmEntity.dauerInMinuten)); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public static Optional map(final Optional optionalFilmEntity) { + if (optionalFilmEntity.isEmpty()) { + return Optional.empty(); + } + else { + FilmEntity filmEntity = optionalFilmEntity.get(); + return Optional.of(map(filmEntity)); + } + } + + public static FilmEntity map(final Film film) { + return new FilmEntity(film.id().id(), film.version().version(), + getRawOrNull(film.titel), getRawOrNull(film.imdbUrl), getRawOrNull(film.dauerInMinuten)); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/KinoKinoSaalEntity2KinoKinoSaalMapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/KinoKinoSaalEntity2KinoKinoSaalMapper.java new file mode 100644 index 0000000..2adee13 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/KinoKinoSaalEntity2KinoKinoSaalMapper.java @@ -0,0 +1,86 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence.mapper; + +import de.accso.flexinale.backoffice.domain.model.Kino; +import de.accso.flexinale.backoffice.domain.model.KinoSaal; +import de.accso.flexinale.backoffice.infrastructure.persistence.KinoEntity; +import de.accso.flexinale.backoffice.infrastructure.persistence.KinoSaalEntity; + +import java.util.HashSet; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull; + +public final class KinoKinoSaalEntity2KinoKinoSaalMapper { + + public static Kino mapKino(final KinoEntity kinoEntity) { + final Kino kino = new Kino(kinoEntity.id(), kinoEntity.version()); + Set mappedKinoSaele = kinoEntity.kinoSaele.stream() + .map(kinoSaalEntity -> mapKinoSaal(kinoSaalEntity, kino)) + .collect(Collectors.toCollection(HashSet::new)); + + kino.name = new Kino.Name(kinoEntity.name); + kino.adresse = new Kino.Adresse(kinoEntity.adresse); + kino.emailAdresse = new Kino.EmailAdresse(kinoEntity.emailAdresse); + kino.kinoSaele = mappedKinoSaele; + + return kino; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public static Optional mapKino(final Optional optionalKinoEntity) { + if (optionalKinoEntity.isEmpty()) { + return Optional.empty(); + } + else { + KinoEntity kinoEntity = optionalKinoEntity.get(); + return Optional.of(mapKino(kinoEntity)); + } + } + + public static KinoEntity mapKino(final Kino kino) { + final KinoEntity kinoEntity = new KinoEntity(kino.id().id(), kino.version().version(), + getRawOrNull(kino.name), getRawOrNull(kino.adresse), getRawOrNull(kino.emailAdresse)); + + kinoEntity.kinoSaele = kino.kinoSaele.stream() + .map(kinoSaal -> mapKinoSaal(kinoSaal, kinoEntity)) + .collect(Collectors.toCollection(HashSet::new)); + + return kinoEntity; + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public static Optional mapToKinoSaal(final Optional kinoSaalEntity) { + return kinoSaalEntity.map(KinoKinoSaalEntity2KinoKinoSaalMapper::mapKinoSaal); + } + + public static KinoSaal mapKinoSaal(KinoSaalEntity kinoSaalEntity) { + KinoEntity kinoEntity = kinoSaalEntity.kino; + Kino mappedKino = mapKino(kinoEntity); + return new KinoSaal(kinoSaalEntity.id(), kinoEntity.version(), + new KinoSaal.Name(kinoSaalEntity.name), new KinoSaal.AnzahlPlaetze(kinoSaalEntity.anzahlPlaetze), mappedKino); + } + + private static KinoSaal mapKinoSaal(final KinoSaalEntity kinoSaalEntity, Kino kino) { + return new KinoSaal(kinoSaalEntity.id(), kinoSaalEntity.version(), + new KinoSaal.Name(kinoSaalEntity.name), new KinoSaal.AnzahlPlaetze(kinoSaalEntity.anzahlPlaetze), kino); + } + + @SuppressWarnings({"OptionalUsedAsFieldOrParameterType", "unused"}) + public static Optional mapFromKinoSaal(final Optional kinoSaal) { + return kinoSaal.map(KinoKinoSaalEntity2KinoKinoSaalMapper::mapKinoSaal); + } + + public static KinoSaalEntity mapKinoSaal(final KinoSaal kinoSaal) { + Kino kino = kinoSaal.kino; + KinoEntity mappedKino = mapKino(kino); + return new KinoSaalEntity(kinoSaal.id().id(), kinoSaal.version().version(), + getRawOrNull(kinoSaal.name), getRawOrNull(kinoSaal.anzahlPlaetze), mappedKino); + } + + private static KinoSaalEntity mapKinoSaal(final KinoSaal kinoSaal, final KinoEntity kinoEntity) { + return new KinoSaalEntity(kinoSaal.id().id(), kinoEntity.version().version(), + getRawOrNull(kinoSaal.name), getRawOrNull(kinoSaal.anzahlPlaetze), kinoEntity); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/VorfuehrungEntity2VorfuehrungMapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/VorfuehrungEntity2VorfuehrungMapper.java new file mode 100644 index 0000000..a63695d --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/java/de/accso/flexinale/backoffice/infrastructure/persistence/mapper/VorfuehrungEntity2VorfuehrungMapper.java @@ -0,0 +1,52 @@ +package de.accso.flexinale.backoffice.infrastructure.persistence.mapper; + +import de.accso.flexinale.backoffice.domain.model.Film; +import de.accso.flexinale.backoffice.domain.model.KinoSaal; +import de.accso.flexinale.backoffice.domain.model.Vorfuehrung; +import de.accso.flexinale.backoffice.infrastructure.persistence.FilmEntity; +import de.accso.flexinale.backoffice.infrastructure.persistence.KinoSaalEntity; +import de.accso.flexinale.backoffice.infrastructure.persistence.VorfuehrungEntity; +import de.accso.flexinale.common.shared_kernel.DateTimeHelper; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull; + +public class VorfuehrungEntity2VorfuehrungMapper { + + public static Vorfuehrung map(final VorfuehrungEntity vorfuehrungEntity) { + Film mappedFilm = FilmEntity2FilmMapper.map(vorfuehrungEntity.film); + KinoSaal mappedKinoSaal = KinoKinoSaalEntity2KinoKinoSaalMapper.mapKinoSaal(vorfuehrungEntity.kinoSaal); + + return new Vorfuehrung(vorfuehrungEntity.id(), vorfuehrungEntity.version(), + new Vorfuehrung.Zeit(DateTimeHelper.fromEpochSeconds(vorfuehrungEntity.zeit)), + mappedFilm, mappedKinoSaal); + } + + @SuppressWarnings("OptionalUsedAsFieldOrParameterType") + public static Optional map(final Optional optionalVorfuehrungEntity) { + if (optionalVorfuehrungEntity.isEmpty()) { + return Optional.empty(); + } + else { + VorfuehrungEntity vorfuehrungEntity = optionalVorfuehrungEntity.get(); + return Optional.of(map(vorfuehrungEntity)); + } + } + + public static VorfuehrungEntity map(final Vorfuehrung vorfuehrung) { + FilmEntity mappedFilm = FilmEntity2FilmMapper.map(vorfuehrung.film); + KinoSaalEntity mappedKinoSaal = KinoKinoSaalEntity2KinoKinoSaalMapper.mapKinoSaal(vorfuehrung.kinoSaal); + + return new VorfuehrungEntity(vorfuehrung.id().id(), vorfuehrung.version().version(), + getRawOrNull(vorfuehrung.zeit), mappedFilm, mappedKinoSaal); + } + + public static List map(final List vorfuehrungEntities) { + return vorfuehrungEntities.stream().map(VorfuehrungEntity2VorfuehrungMapper::map) + .collect(Collectors.toCollection(ArrayList::new)); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/application.properties b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/application.properties new file mode 100644 index 0000000..5cc44ae --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/application.properties @@ -0,0 +1,70 @@ +application.title=FLEXinale as Distributed Services, Backoffice +application.version=@pom.version@ @maven.build.timestamp@ +spring.banner.location=classpath:/flexinale-banner.txt + +######################################################################### +# For endpoint /version +######################################################################### +build.version=@pom.version@ +build.date=@maven.build.timestamp@ + +######################################################################### +# Persistence +######################################################################### +spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/distributed-backoffice +spring.datasource.name=flexinale +spring.datasource.username=flexinale +spring.datasource.password=flexinale +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.database=postgresql +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.generate-ddl=true +# uses schema if existing (or creates anew) +# in a real production environment one should use 'validate' which does just validation but doesn't change anything +spring.jpa.hibernate.ddl-auto=update +# Pros and Cons: See https://www.baeldung.com/spring-open-session-in-view, +# https://stackoverflow.com/questions/30549489/what-is-this-spring-jpa-open-in-view-true-property-in-spring-boot +spring.jpa.open-in-view=false +# spring.jpa.show-sql=true + +######################################################################### +# Web +######################################################################### +server.error.path=/error +server.error.include-stacktrace=always +server.error.include-exception=true +server.error.include-message=always +server.error.whitelabel.enabled=false +server.port=8081 + +######################################################################### +# Security +######################################################################### +security.enable-csrf=true + +######################################################################### +# Kafka +######################################################################### +spring.kafka.consumer.group-id=flexinale-distributed-backoffice + +# Spring Kafka Consumer +spring.kafka.consumer.bootstrap-servers=localhost:29092 +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer + +# Spring Kafka Producer +spring.kafka.producer.bootstrap-servers=localhost:29092 +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer + +######################################################################### +# Metrics endpoints, micrometer/prometheus/grafana +######################################################################### +# enable and expose +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always +spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false + +######################################################################### +# flexinale properties +######################################################################### diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/flexinale-banner.txt b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/flexinale-banner.txt new file mode 100644 index 0000000..cac6e52 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/flexinale-banner.txt @@ -0,0 +1,10 @@ +------------------------------------------------------------------------- +,------. ,--. ,------. ,--. ,--. ,--. ,--. +| .---' | | | .---' \ `.' / `--' ,--,--, ,--,--. | | ,---. +| `--, | | | `--, .' \ ,--. | \ ' ,-. | | | | .-. : +| |` | '--. | `---. / .'. \ | | | || | \ '-' | | | \ --. +`--' `-----' `------' '--' '--' `--' `--''--' `--`--' `--' `----' +${application.title} on port ${server.port} +(version ${application.version}) +Powered by Spring Boot ${spring-boot.version} +------------------------------------------------------------------------- diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/logback.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/logback.xml new file mode 100644 index 0000000..87e8669 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/logback.xml @@ -0,0 +1,20 @@ + + + + %d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/static/index.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/static/index.html new file mode 100644 index 0000000..31ea037 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/main/resources/static/index.html @@ -0,0 +1,12 @@ + + + + Flexinale Distributed - Backoffice + + + + +Flexinale Distributed - Backoffice + + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-events-published.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-events-published.http new file mode 100644 index 0000000..61a6207 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-events-published.http @@ -0,0 +1,3 @@ +# curl -v -X GET -u admin:admin1 http://localhost:8081/actuator/backofficeEventsPublished +GET http://localhost:8081/actuator/backofficeEventsPublished +Authorization: Basic admin admin1 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-health.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-health.http new file mode 100644 index 0000000..af9cf0d --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-health.http @@ -0,0 +1,3 @@ +# curl -v -X GET -u admin:admin1 http://localhost:8081/actuator/health +GET http://localhost:8081/actuator/health +Authorization: Basic admin admin1 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-info.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-info.http new file mode 100644 index 0000000..aa1b43a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-info.http @@ -0,0 +1,3 @@ +# curl -v -X GET -u admin:admin1 http://localhost:8081/actuator/info +GET http://localhost:8081/actuator/info +Authorization: Basic admin admin1 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-metrics.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-metrics.http new file mode 100644 index 0000000..5498552 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-metrics.http @@ -0,0 +1,3 @@ +# curl -v -X GET -u admin:admin1 http://localhost:8081/actuator/metrics +GET http://localhost:8081/actuator/metrics +Authorization: Basic admin admin1 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-prometheus.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-prometheus.http new file mode 100644 index 0000000..73fbf35 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/curl/backoffice-actuator-prometheus.http @@ -0,0 +1,3 @@ +# curl -v -X GET -u admin:admin1 http://localhost:8081/actuator/prometheus +GET http://localhost:8081/actuator/prometheus +Authorization: Basic admin admin1 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java new file mode 100644 index 0000000..cd1709b --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java @@ -0,0 +1,21 @@ +package de.accso.flexinale; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = {FlexinaleDistributedApplicationBackoffice.class}) +@ActiveProfiles("smoketest") +public class ApplicationPropertiesFileTest { + + @Value("${application.version}") + private String propertyApplicationVersion; + + @Test + public void testReadsTestPropertiesFile() { + assertThat(propertyApplicationVersion).isEqualTo("test"); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSmokeTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSmokeTest.java new file mode 100644 index 0000000..f35dc4a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSmokeTest.java @@ -0,0 +1,20 @@ +package de.accso.flexinale; + +import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(classes = {FlexinaleDistributedApplicationBackoffice.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) +@ActiveProfiles("smoketest") +@DoNotCheckInArchitectureTests +class FlexinaleDistributedApplicationBackofficeSmokeTest { + + @Test + @SuppressWarnings("EmptyMethod") + void testLoadInitialApplicationContext() { + // nope + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSpringConfigTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSpringConfigTest.java new file mode 100644 index 0000000..d268b31 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBackofficeSpringConfigTest.java @@ -0,0 +1,31 @@ +package de.accso.flexinale; + +import de.accso.flexinale.common.application.Config; +import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = {FlexinaleDistributedApplicationBackoffice.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) +@ActiveProfiles("configtest") +@DoNotCheckInArchitectureTests +class FlexinaleDistributedApplicationBackofficeSpringConfigTest { + + @Autowired + Config config; + + @Test + void testLoadSpringConfig() { + // arrange, act + // nope, is loaded auto-magically by Spring, see application-configtest.properties + + // assert + assertThat(config.getQuoteOnline()).isEqualTo(33); // default + assertThat(config.getMinZeitZwischenVorfuehrungenInMinuten()).isEqualTo(30); // default + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application-configtest.properties b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application-configtest.properties new file mode 100644 index 0000000..1bb8bf6 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application-configtest.properties @@ -0,0 +1 @@ +# empty diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application.properties b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application.properties new file mode 100644 index 0000000..5c60591 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/application.properties @@ -0,0 +1,39 @@ +application.title=FLEXinale as Distributed Services, Backoffice +application.version=test +spring.banner.location=classpath:/flexinale-banner.txt + +######################################################################### +# For endpoint /version +######################################################################### +build.version=@pom.version@ +build.date=@maven.build.timestamp@ + +######################################################################### +# Persistence +######################################################################### +spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/distributed-backoffice +spring.datasource.name=flexinale +spring.datasource.username=flexinale +spring.datasource.password=flexinale +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.database=postgresql +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.generate-ddl=true +# spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=create + +######################################################################### +# Web +######################################################################### +spring.thymeleaf.prefix=classpath:/templates/ +spring.thymeleaf.suffix=.html +server.error.path=/error +server.port=9091 + +######################################################################### +# flexinale properties +######################################################################### +# Quote for online kontingent in percent +de.accso.flexinale.kontingent.quote.online=33 +# time in minutes +de.accso.flexinale.vorfuehrung.min-zeit-zwischen-vorfuehrungen-in-minuten=30 \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/logback-test.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/logback-test.xml new file mode 100644 index 0000000..ac74e21 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice/src/test/resources/logback-test.xml @@ -0,0 +1,20 @@ + + + + %d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/.gitignore b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/.gitignore new file mode 100644 index 0000000..b8de55e --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/.gitignore @@ -0,0 +1,4 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/SpotbugsExcludeFilter.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/SpotbugsExcludeFilter.xml new file mode 100644 index 0000000..029f096 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/SpotbugsExcludeFilter.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/pom.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/pom.xml new file mode 100644 index 0000000..0b58a16 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + + de.accso + flexinale-distributed + 2024.3.0 + ../pom.xml + + + flexinale-distributed-backoffice_api_contract + 2024.3.0 + Flexinale Distributed Backoffice API Contract + Flexinale - FLEX case-study "film festival", distributed services, backoffice_api-contract + + jar + + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + de.accso + flexinale-distributed-common + 2024.3.0 + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs-maven-plugin.version} + + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs-maven-plugin.version} + + + SpotbugsExcludeFilter.xml + + + + + com.github.spotbugs + spotbugs + ${spotbugs.version} + + + + + verify + + check + + + + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/AllBackofficeEvents.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/AllBackofficeEvents.java new file mode 100644 index 0000000..18ae24c --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/AllBackofficeEvents.java @@ -0,0 +1,21 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import de.accso.flexinale.common.api.event.Event; + +import java.util.Set; + +public final class AllBackofficeEvents { + + public static final Set> eventTypes = + Set.of( + FilmCreatedEvent.class, + FilmUpdatedEvent.class, + FilmDeletedEvent.class, + KinoCreatedEvent.class, + KinoUpdatedEvent.class, + KinoDeletedEvent.class, + VorfuehrungCreatedEvent.class, + VorfuehrungUpdatedEvent.class, + VorfuehrungDeletedEvent.class + ); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCRUDEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCRUDEvent.java new file mode 100644 index 0000000..2c8224b --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCRUDEvent.java @@ -0,0 +1,42 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.common.api.event.AbstractEvent; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public abstract sealed class FilmCRUDEvent extends AbstractEvent implements Event + permits FilmCreatedEvent, FilmUpdatedEvent, FilmDeletedEvent { + + public enum CRUD { CREATE, UPDATE, DELETE } + + @DoNotCheckInArchitectureTests + public final FilmTO film; + + protected FilmCRUDEvent(final FilmTO film) { + this.film = film; + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + final FilmCRUDEvent that = (FilmCRUDEvent) o; + + return new EqualsBuilder().appendSuper(super.equals(o)) + .append(version(), that.version()) + .append(film, that.film).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .appendSuper(super.hashCode()).append(film) + .append(version()) + .toHashCode(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCreatedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCreatedEvent.java new file mode 100644 index 0000000..64e050f --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmCreatedEvent.java @@ -0,0 +1,22 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import com.fasterxml.jackson.annotation.JsonGetter; +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.common.shared_kernel.Versionable; + +@SuppressWarnings({"unused", "CanBeFinal"}) +public non-sealed class FilmCreatedEvent extends FilmCRUDEvent { + private static Version version = Versionable.initialVersion().inc(); + + private FilmCreatedEvent() { super(null); } // needed for (de)serialization via Jackson + + public FilmCreatedEvent(final FilmTO film) { + super(film); + } + + @Override + @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized + public Version version() { + return version; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmDeletedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmDeletedEvent.java new file mode 100644 index 0000000..e8ce85a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmDeletedEvent.java @@ -0,0 +1,22 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import com.fasterxml.jackson.annotation.JsonGetter; +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.common.shared_kernel.Versionable; + +@SuppressWarnings({"unused", "CanBeFinal"}) +public non-sealed class FilmDeletedEvent extends FilmCRUDEvent { + private static Version version = Versionable.initialVersion().inc(); + + private FilmDeletedEvent() { super(null); } // needed for (de)serialization via Jackson + + public FilmDeletedEvent(final FilmTO film) { + super(film); + } + + @Override + @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized + public Version version() { + return version; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmUpdatedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmUpdatedEvent.java new file mode 100644 index 0000000..de74c2f --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/FilmUpdatedEvent.java @@ -0,0 +1,22 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import com.fasterxml.jackson.annotation.JsonGetter; +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.common.shared_kernel.Versionable; + +@SuppressWarnings({"unused", "CanBeFinal"}) +public non-sealed class FilmUpdatedEvent extends FilmCRUDEvent { + private static Version version = Versionable.initialVersion().inc(); + + private FilmUpdatedEvent() { super(null); } // needed for (de)serialization via Jackson + + public FilmUpdatedEvent(final FilmTO film) { + super(film); + } + + @Override + @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized + public Version version() { + return version; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCRUDEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCRUDEvent.java new file mode 100644 index 0000000..a84192a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCRUDEvent.java @@ -0,0 +1,42 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.common.api.event.AbstractEvent; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public abstract sealed class KinoCRUDEvent extends AbstractEvent implements Event + permits KinoCreatedEvent, KinoUpdatedEvent, KinoDeletedEvent { + + public enum CRUD { CREATE, UPDATE, DELETE } + + @DoNotCheckInArchitectureTests + public final KinoTO kino; + + protected KinoCRUDEvent(final KinoTO kino) { + this.kino = kino; + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + final KinoCRUDEvent that = (KinoCRUDEvent) o; + + return new EqualsBuilder().appendSuper(super.equals(o)) + .append(version(), that.version()) + .append(kino, that.kino).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .appendSuper(super.hashCode()).append(kino) + .append(version()) + .toHashCode(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCreatedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCreatedEvent.java new file mode 100644 index 0000000..7768abd --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoCreatedEvent.java @@ -0,0 +1,22 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import com.fasterxml.jackson.annotation.JsonGetter; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.common.shared_kernel.Versionable; + +@SuppressWarnings({"unused", "CanBeFinal"}) +public non-sealed class KinoCreatedEvent extends KinoCRUDEvent { + private static Version version = Versionable.initialVersion().inc(); + + private KinoCreatedEvent() { super(null); } // needed for (de)serialization via Jackson + + public KinoCreatedEvent(final KinoTO kino) { + super(kino); + } + + @Override + @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized + public Version version() { + return version; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoDeletedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoDeletedEvent.java new file mode 100644 index 0000000..81a1ab7 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoDeletedEvent.java @@ -0,0 +1,22 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import com.fasterxml.jackson.annotation.JsonGetter; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.common.shared_kernel.Versionable; + +@SuppressWarnings({"unused", "CanBeFinal"}) +public non-sealed class KinoDeletedEvent extends KinoCRUDEvent { + private static Version version = Versionable.initialVersion().inc(); + + private KinoDeletedEvent() { super(null); } // needed for (de)serialization via Jackson + + public KinoDeletedEvent(final KinoTO kino) { + super(kino); + } + + @Override + @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized + public Version version() { + return version; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoUpdatedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoUpdatedEvent.java new file mode 100644 index 0000000..6c579ba --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/KinoUpdatedEvent.java @@ -0,0 +1,22 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import com.fasterxml.jackson.annotation.JsonGetter; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.common.shared_kernel.Versionable; + +@SuppressWarnings({"unused", "CanBeFinal"}) +public non-sealed class KinoUpdatedEvent extends KinoCRUDEvent { + private static Version version = Versionable.initialVersion().inc(); + + private KinoUpdatedEvent() { super(null); } // needed for (de)serialization via Jackson + + public KinoUpdatedEvent(final KinoTO kino) { + super(kino); + } + + @Override + @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized + public Version version() { + return version; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCRUDEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCRUDEvent.java new file mode 100644 index 0000000..6b57e15 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCRUDEvent.java @@ -0,0 +1,42 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; +import de.accso.flexinale.common.api.event.AbstractEvent; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public abstract sealed class VorfuehrungCRUDEvent extends AbstractEvent implements Event + permits VorfuehrungCreatedEvent, VorfuehrungUpdatedEvent, VorfuehrungDeletedEvent { + + public enum CRUD { CREATE, UPDATE, DELETE } + + @DoNotCheckInArchitectureTests + public final VorfuehrungTO vorfuehrung; + + protected VorfuehrungCRUDEvent(final VorfuehrungTO vorfuehrung) { + this.vorfuehrung = vorfuehrung; + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + final VorfuehrungCRUDEvent that = (VorfuehrungCRUDEvent) o; + + return new EqualsBuilder().appendSuper(super.equals(o)) + .append(version(), that.version()) + .append(vorfuehrung, that.vorfuehrung).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .appendSuper(super.hashCode()).append(vorfuehrung) + .append(version()) + .toHashCode(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCreatedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCreatedEvent.java new file mode 100644 index 0000000..7c29121 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungCreatedEvent.java @@ -0,0 +1,22 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import com.fasterxml.jackson.annotation.JsonGetter; +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; +import de.accso.flexinale.common.shared_kernel.Versionable; + +@SuppressWarnings({"unused", "CanBeFinal"}) +public non-sealed class VorfuehrungCreatedEvent extends VorfuehrungCRUDEvent { + private static Version version = Versionable.initialVersion().inc(); + + private VorfuehrungCreatedEvent() { super(null); } // needed for (de)serialization via Jackson + + public VorfuehrungCreatedEvent(final VorfuehrungTO vorfuehrung) { + super(vorfuehrung); + } + + @Override + @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized + public Version version() { + return version; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungDeletedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungDeletedEvent.java new file mode 100644 index 0000000..3ca39ca --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungDeletedEvent.java @@ -0,0 +1,22 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import com.fasterxml.jackson.annotation.JsonGetter; +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; +import de.accso.flexinale.common.shared_kernel.Versionable; + +@SuppressWarnings({"unused", "CanBeFinal"}) +public non-sealed class VorfuehrungDeletedEvent extends VorfuehrungCRUDEvent { + private static Version version = Versionable.initialVersion().inc(); + + private VorfuehrungDeletedEvent() { super(null); } // needed for (de)serialization via Jackson + + public VorfuehrungDeletedEvent(final VorfuehrungTO vorfuehrung) { + super(vorfuehrung); + } + + @Override + @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized + public Version version() { + return version; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungUpdatedEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungUpdatedEvent.java new file mode 100644 index 0000000..99880ac --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/VorfuehrungUpdatedEvent.java @@ -0,0 +1,22 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import com.fasterxml.jackson.annotation.JsonGetter; +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; +import de.accso.flexinale.common.shared_kernel.Versionable; + +@SuppressWarnings({"unused", "CanBeFinal"}) +public non-sealed class VorfuehrungUpdatedEvent extends VorfuehrungCRUDEvent { + private static Version version = Versionable.initialVersion().inc(); + + private VorfuehrungUpdatedEvent() { super(null); } // needed for (de)serialization via Jackson + + public VorfuehrungUpdatedEvent(final VorfuehrungTO vorfuehrung) { + super(vorfuehrung); + } + + @Override + @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized + public Version version() { + return version; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/FilmTO.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/FilmTO.java new file mode 100644 index 0000000..12a573b --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/FilmTO.java @@ -0,0 +1,33 @@ +package de.accso.flexinale.backoffice.api_contract.event.model; + +import de.accso.flexinale.common.shared_kernel.EqualsByContent; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.RawWrapper; +import de.accso.flexinale.common.shared_kernel.Versionable; +import org.apache.commons.lang3.builder.EqualsBuilder; + +import java.io.Serializable; + +@SuppressWarnings("unused") +public record FilmTO(Id id, Version version, + Titel titel, ImdbUrl imdbUrl, DauerInMinuten dauerInMinuten) + implements Identifiable, Versionable, EqualsByContent, Serializable +{ + public record Titel(String raw) implements RawWrapper {} + public record ImdbUrl(String raw) implements RawWrapper {} + public record DauerInMinuten(Integer raw) implements RawWrapper {} + + @Override + public boolean equalsByContent(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + final FilmTO that = (FilmTO) o; + + return new EqualsBuilder() + .append(id, that.id) + .append(titel, that.titel).append(imdbUrl, that.imdbUrl) + .append(dauerInMinuten, that.dauerInMinuten).isEquals(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoSaalTO.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoSaalTO.java new file mode 100644 index 0000000..24c40a0 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoSaalTO.java @@ -0,0 +1,73 @@ +package de.accso.flexinale.backoffice.api_contract.event.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import de.accso.flexinale.common.shared_kernel.*; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +import java.io.Serializable; + +public record KinoSaalTO(Id id, Version version, + Name name, AnzahlPlaetze anzahlPlaetze, + @JsonIgnore @DoNotCheckInArchitectureTests KinoTO kino) + implements Identifiable, Versionable, EqualsByContent, Serializable +{ + public record Name(String raw) implements RawWrapper {} + public record AnzahlPlaetze(Integer raw) implements RawWrapper {} + + @Override + public String toString() { + return "KinoSaalTO{" + + "id='" + id + '\'' + + ", version=" + version + + ", name='" + name + '\'' + + ", anzahlPlaetze=" + anzahlPlaetze + + (kino != null ? ", kino.id=" + kino.id() : ", kino=null") + + '}'; + } + + @Override + public boolean equals(final Object o) { + if (!equalsByContent(o)) return false; + + if (this == o) {return true;} + if (o == null || getClass() != o.getClass()) {return false;} + KinoSaalTO that = (KinoSaalTO) o; + return new EqualsBuilder().append(version, that.version).isEquals(); + } + + @Override + public int hashCode() { + HashCodeBuilder builder = new HashCodeBuilder(17, 37).append(id).append(version) + .append(name).append(anzahlPlaetze); + if (kino != null) { + builder.append(kino.id()); // do not use kino but only its id (otherwise Stackoverflow error) + } + return builder.toHashCode(); + } + + @SuppressWarnings("ConstantValue") + @Override + public boolean equalsByContent(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + KinoSaalTO that = (KinoSaalTO) o; + + boolean result = new EqualsBuilder() + .append(id, that.id) + .append(name, that.name) + .append(anzahlPlaetze, that.anzahlPlaetze). + isEquals(); + if (!result) return false; + + if (kino == null && that.kino == null) return true; + if (kino == null && that.kino != null) return false; + if (kino != null) { + result = kino.id().equals(that.kino.id()); // do not check kino but only its id (otherwise Stackoverflow error) + } + + return result; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoTO.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoTO.java new file mode 100644 index 0000000..5cca8c0 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/KinoTO.java @@ -0,0 +1,61 @@ +package de.accso.flexinale.backoffice.api_contract.event.model; + +import de.accso.flexinale.common.shared_kernel.*; +import org.apache.commons.lang3.builder.EqualsBuilder; + +import java.io.Serializable; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public record KinoTO(Id id, Version version, + Name name, Adresse adresse, EmailAdresse emailAdresse, + @DoNotCheckInArchitectureTests Set kinoSaele) + implements Identifiable, Versionable, EqualsByContent, Serializable +{ + public record Name(String raw) implements RawWrapper {} + public record Adresse(String raw) implements RawWrapper {} + public record EmailAdresse(String raw) implements RawWrapper {} + + public KinoTO(final Id id, final Version version) { + this(id, version, new KinoTO.Name(""), new KinoTO.Adresse(""), new KinoTO.EmailAdresse(""), + new HashSet<>()); // cannot use Set.of() as this would be an ImmutableCollections.EMPTY_SET + } + + public KinoTO(final Id id, + final Name name, final Adresse adresse, + final EmailAdresse emailAdresse, final Set kinoSaele) { + this(id, Versionable.unknownVersion(), name, adresse, emailAdresse, kinoSaele); + } + + @SuppressWarnings({"ConstantValue", "unused"}) + @Override + public boolean equalsByContent(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + KinoTO that = (KinoTO) o; + + boolean result = new EqualsBuilder() + .append(id, that.id) + .append(name, that.name) + .append(adresse, that.adresse).append(emailAdresse, that.emailAdresse) + .isEquals(); + if (!result) return false; + + if (kinoSaele == null && that.kinoSaele == null) return true; + if (kinoSaele == null && that.kinoSaele != null) return false; + if (kinoSaele != null && that.kinoSaele == null) return false; + + List thisKSList = kinoSaele.stream().toList(); + List thatKSList = that.kinoSaele.stream().toList(); + if (thisKSList.size() != thatKSList.size()) return false; + for (int counter = 0; counter < thisKSList.size(); counter++) { + if (!thisKSList.get(counter).equalsByContent(thatKSList.get(counter))) return false; + } + + return true; + } + +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/VorfuehrungTO.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/VorfuehrungTO.java new file mode 100644 index 0000000..a995be2 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/java/de/accso/flexinale/backoffice/api_contract/event/model/VorfuehrungTO.java @@ -0,0 +1,80 @@ +package de.accso.flexinale.backoffice.api_contract.event.model; + +import de.accso.flexinale.common.shared_kernel.*; +import org.apache.commons.lang3.builder.EqualsBuilder; + +import java.io.Serializable; +import java.time.LocalDateTime; + +public record VorfuehrungTO(Id id, Version version, + Zeit zeit, + @DoNotCheckInArchitectureTests FilmTO film, + @DoNotCheckInArchitectureTests KinoSaalTO kinoSaal, + Id kinoId) + implements Identifiable, Versionable, EqualsByContent, Serializable +{ + public record Zeit(LocalDateTime raw) implements RawWrapper { + public Zeit(LocalDateTime raw) { + this.raw = raw.withNano(0); // precision is second + } + } + + public VorfuehrungTO(final Id id, final Version version, + final Zeit zeit, + final FilmTO film, + final KinoSaalTO kinoSaal, + final Id kinoId) { + this.id = id; + this.version = version; + this.zeit = zeit; + this.film = film; + this.kinoSaal = kinoSaal; + + // VorfuehrungTO also gets kinoId, which is actually redundant (as a KinoSaalTO already contains the Kino). + // But during serialization the Kino is set to null! Therefore, we add the extra kinoId here! + this.kinoId = kinoId; + + //TODO this validation should not be done here. If class is not final, Spotbugs complains. For DB entities use "nullable=false" and "optional=false" for DB checks. In domain classes check with jakarta.annotation NonNull? Also: Why is KinoSaal obligatory but not Film? + if (kinoSaal == null) { + throw new FlexinaleIllegalArgumentException("KinoSaal of Vorfuehrung " + this + + " must not be null"); + } + //TODO this validation should not be done here but in KinoSaal itself + if (kinoSaal.anzahlPlaetze() == null) { + throw new FlexinaleIllegalArgumentException("KinoSaal's Anzahl Plaetze of Vorfuehrung " + this + + " must not be null"); + } + //TODO Why is this here (check not done in any other Vorfuehrung* class)? + if (kinoId == null) { + throw new FlexinaleIllegalArgumentException("Kino Id of Vorfuehrung " + this + + " must not be null"); + } + } + + @SuppressWarnings({"UnusedAssignment", "DataFlowIssue", "unused"}) + @Override + public boolean equalsByContent(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + VorfuehrungTO that = (VorfuehrungTO) o; + + boolean result = new EqualsBuilder() + .append(id, that.id) + .append(film, that.film) + .append(zeit, that.zeit) + .append(kinoId, that.kinoId) + .isEquals(); + if (!result) return false; + + if (kinoSaal == null && that.kinoSaal == null) result = true; + if (kinoSaal == null && that.kinoSaal != null) return false; + if (kinoSaal != null) { + result = kinoSaal.equalsByContent(that.kinoSaal); + return result; + } + + return true; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/resources/logback.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/resources/logback.xml new file mode 100644 index 0000000..87e8669 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/main/resources/logback.xml @@ -0,0 +1,20 @@ + + + + %d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventSerializationTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventSerializationTest.java new file mode 100644 index 0000000..23430b1 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventSerializationTest.java @@ -0,0 +1,176 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import com.fasterxml.jackson.core.JsonProcessingException; +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.infrastructure.eventbus.EventSerializationHelper; +import static de.accso.flexinale.backoffice.api_contract.event.ReflectionHelper.setField; +import de.accso.flexinale.common.shared_kernel.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.Set; +import java.util.stream.Stream; + +import static de.accso.flexinale.common.shared_kernel.Identifiable.Id.uuidString; +import static org.assertj.core.api.Assertions.assertThat; + +class EventSerializationTest { + private static Stream allFilmEventTypes() { + return Set.of(FilmCreatedEvent.class, FilmUpdatedEvent.class, FilmDeletedEvent.class).stream().map(Arguments::of); + } + + private static Stream allKinoEventTypes() { + return Set.of(KinoCreatedEvent.class, KinoUpdatedEvent.class, KinoDeletedEvent.class).stream().map(Arguments::of); + } + + private static Stream allVorfuehrungEventTypes() { + return Set.of(VorfuehrungCreatedEvent.class, VorfuehrungUpdatedEvent.class, VorfuehrungDeletedEvent.class).stream().map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("allFilmEventTypes") + void testSerializeFilmEventClass2JsonString(final Class filmEventType) + throws JsonProcessingException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { + // arrange + Constructor constructor = filmEventType.getDeclaredConstructor(); + constructor.setAccessible(true); + Event event = constructor.newInstance(); + + FilmTO filmTO = createFilmTO(); + setField(filmEventType, event, "film", filmTO, true); + + // act - serialize to JSON + String jsonString = EventSerializationHelper.serializeEvent2JsonString(event); + + // assert + assertThat(jsonString).contains(getExpectedJsonStringForVersion(event.version())); + assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("titel", filmTO.titel())); + assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("imdbUrl", filmTO.imdbUrl())); + assertThat(jsonString).contains(getExpectedJsonStringForRawTypeInteger("dauerInMinuten", filmTO.dauerInMinuten())); + + // act - deserialize back from JSON + E deserializedEvent = EventSerializationHelper.deserializeJsonString2Event(jsonString, filmEventType); + + // assert + assertThat(deserializedEvent).isInstanceOf(filmEventType); + assertThat(deserializedEvent).isInstanceOf(FilmCRUDEvent.class); + FilmCRUDEvent castedDeserializedEvent = (FilmCRUDEvent) deserializedEvent; + assertThat(castedDeserializedEvent.version()).isEqualTo(event.version()); + assertThat(castedDeserializedEvent.film).isEqualTo(filmTO); + } + + @ParameterizedTest + @MethodSource("allKinoEventTypes") + void testSerializeKinoEventClass2JsonString(final Class kinoEventType) + throws JsonProcessingException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { + // arrange + Constructor constructor = kinoEventType.getDeclaredConstructor(); + constructor.setAccessible(true); + Event event = constructor.newInstance(); + + KinoTO kinoTO = createKinoTO(); + setField(kinoEventType, event, "kino", kinoTO, true); + + // act + String jsonString = EventSerializationHelper.serializeEvent2JsonString(event); + + // assert + assertThat(jsonString).contains(getExpectedJsonStringForVersion(event.version())); + assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("name", kinoTO.name())); + assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("adresse", kinoTO.adresse())); + assertThat(jsonString).contains(getExpectedJsonStringForRawTypeString("emailAdresse", kinoTO.emailAdresse())); + + // act - deserialize back from JSON + E deserializedEvent = EventSerializationHelper.deserializeJsonString2Event(jsonString, kinoEventType); + + // assert + assertThat(deserializedEvent).isInstanceOf(kinoEventType); + assertThat(deserializedEvent).isInstanceOf(KinoCRUDEvent.class); + KinoCRUDEvent castedDeserializedEvent = (KinoCRUDEvent) deserializedEvent; + assertThat(castedDeserializedEvent.version()).isEqualTo(event.version()); +//TODO bidirectional relation K<->KS, assertion commented out, as does not work as the de/serialization is not working correctly for bidirectional relation between Kino and KinoSaal + // assertThat(castedDeserializedEvent.kino).isEqualTo(kinoTO); + } + + @ParameterizedTest + @MethodSource("allVorfuehrungEventTypes") + void testSerializeVorfuehrungEventClass2JsonString(final Class vorfuehrungEventType) + throws JsonProcessingException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { + // arrange + Constructor constructor = vorfuehrungEventType.getDeclaredConstructor(); + constructor.setAccessible(true); + Event event = constructor.newInstance(); + + VorfuehrungTO vorfuehrungTO = createVorfuehrungTO(); + setField(vorfuehrungEventType, event, "vorfuehrung", vorfuehrungTO, true); + + // act + String jsonString = EventSerializationHelper.serializeEvent2JsonString(event); + + // assert + assertThat(jsonString).contains(getExpectedJsonStringForVersion(event.version())); + assertThat(jsonString).contains(getExpectedJsonStringForRawTypeLocalDateTime("zeit", vorfuehrungTO.zeit())); + + // act - deserialize back from JSON + E deserializedEvent = EventSerializationHelper.deserializeJsonString2Event(jsonString, vorfuehrungEventType); + + // assert + assertThat(deserializedEvent).isInstanceOf(vorfuehrungEventType); + assertThat(deserializedEvent).isInstanceOf(VorfuehrungCRUDEvent.class); + VorfuehrungCRUDEvent castedDeserializedEvent = (VorfuehrungCRUDEvent) deserializedEvent; + assertThat(castedDeserializedEvent.version()).isEqualTo(event.version()); + assertThat(castedDeserializedEvent.vorfuehrung).isEqualTo(vorfuehrungTO); + } + + private static FilmTO createFilmTO() { + FilmTO.Titel titel = new FilmTO.Titel(uuidString()); + FilmTO.ImdbUrl imdbUrl = new FilmTO.ImdbUrl(uuidString()); + FilmTO.DauerInMinuten dauerInMinuten = new FilmTO.DauerInMinuten(42); + return new FilmTO(Identifiable.Id.of(), Versionable.unknownVersion(), titel, imdbUrl, dauerInMinuten); + } + + private static KinoTO createKinoTO() { + KinoTO.Name name = new KinoTO.Name(uuidString()); + KinoTO.Adresse adresse = new KinoTO.Adresse(uuidString()); + KinoTO.EmailAdresse emailAdresse = new KinoTO.EmailAdresse(uuidString()); + KinoTO kinoTO = new KinoTO(Identifiable.Id.of(), Versionable.unknownVersion(), name, adresse, emailAdresse, new HashSet<>()); + kinoTO.kinoSaele().add(createKinoSaalTO(kinoTO)); + return kinoTO; + } + + private static KinoSaalTO createKinoSaalTO(KinoTO kinoTO) { + KinoSaalTO.Name name = new KinoSaalTO.Name(uuidString()); + KinoSaalTO.AnzahlPlaetze adresse = new KinoSaalTO.AnzahlPlaetze(42); + return new KinoSaalTO(Identifiable.Id.of(), Versionable.unknownVersion(), name, adresse, kinoTO); + } + + private static VorfuehrungTO createVorfuehrungTO() { + VorfuehrungTO.Zeit zeit = new VorfuehrungTO.Zeit(LocalDateTime.now().withSecond(0)); // intentionally as ISO format changes then! + KinoSaalTO kinoSaalTO = createKinoSaalTO(null); + return new VorfuehrungTO(Identifiable.Id.of(), Versionable.unknownVersion(), zeit, + createFilmTO(), kinoSaalTO, Identifiable.Id.of()); + } + + private static String getExpectedJsonStringForVersion(Versionable.Version field) { + return String.format("\"%s\":{\"version\":%d", "version", field.version()); + } + private static String getExpectedJsonStringForRawTypeString(String fieldName, RawWrapper field) { + return String.format("\"%s\":{\"raw\":\"%s\"", fieldName, field.raw()); + } + private static String getExpectedJsonStringForRawTypeInteger(String fieldName, RawWrapper field) { + return String.format("\"%s\":{\"raw\":%d", fieldName, field.raw()); + } + private static String getExpectedJsonStringForRawTypeLocalDateTime(String fieldName, RawWrapper field) { + return String.format("\"%s\":{\"raw\":\"%s\"", fieldName, field.raw().format(DateTimeFormatter.ISO_DATE_TIME)); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventStructureTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventStructureTest.java new file mode 100644 index 0000000..52c0d0e --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/EventStructureTest.java @@ -0,0 +1,54 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import com.fasterxml.jackson.core.JsonProcessingException; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.infrastructure.eventbus.EventSerializationHelper; +import de.accso.flexinale.common.shared_kernel.EventClassHelper; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.Versionable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.time.LocalDateTime; +import java.util.stream.Stream; + +import static de.accso.flexinale.backoffice.api_contract.event.ReflectionHelper.setField; +import static org.assertj.core.api.Assertions.*; + +class EventStructureTest { + private static Stream allEventTypes() { + return AllBackofficeEvents.eventTypes.stream().map(Arguments::of); + } + + // check for all Event subclasses if no exception is thrown for (de)serialization and reflection access + // a) default constructor is needed + // b) field 'version' is needed + + @ParameterizedTest + @MethodSource("allEventTypes") + void testCheckEventStructure(final Class eventType) + throws InstantiationException, IllegalAccessException, JsonProcessingException, NoSuchMethodException, InvocationTargetException { + // arrange + Constructor constructor = eventType.getDeclaredConstructor(); + constructor.setAccessible(true); + Event event = constructor.newInstance(); + + // set id, timestamp and version explicitely, so that (de)serialization is somewhat more realistic + setField(eventType, event, "id", Identifiable.Id.of(), false); + setField(eventType, event, "timestamp", LocalDateTime.now(), false); + + // act - check serialization and deserialization + String jsonString = EventSerializationHelper.serializeEvent2JsonString(event); + EventSerializationHelper.deserializeJsonString2Event(jsonString, eventType); + + // act - check instantiation and version attribute + assertThatCode(() -> { + Versionable.Version eventClazzVersion = EventClassHelper.getEventClazzVersion(eventType); + assertThat(eventClazzVersion).isEqualTo(event.version()); + }) + .doesNotThrowAnyException(); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/ReflectionHelper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/ReflectionHelper.java new file mode 100644 index 0000000..9ae774b --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-backoffice_api_contract/src/test/java/de/accso/flexinale/backoffice/api_contract/event/ReflectionHelper.java @@ -0,0 +1,25 @@ +package de.accso.flexinale.backoffice.api_contract.event; + +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalArgumentException; + +import java.lang.reflect.Field; + +public final class ReflectionHelper { + public static void setField(final Class eventType, final Event event, final String fieldName, final Object fieldValue, + boolean includeDerivedFieldsFromSuperClasses) throws IllegalAccessException { + if (eventType.equals(Object.class)) { + throw new FlexinaleIllegalArgumentException("event hierarchy does not have a field " + fieldName); + } + try { + Field field = (includeDerivedFieldsFromSuperClasses) + ? eventType.getField(fieldName) + : eventType.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(event, fieldValue); + } + catch (NoSuchFieldException nosfex) { + setField(eventType.getSuperclass(), event, fieldName, fieldValue, includeDerivedFieldsFromSuperClasses); + } + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/SpotbugsExcludeFilter.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/SpotbugsExcludeFilter.xml new file mode 100644 index 0000000..a307e71 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/SpotbugsExcludeFilter.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/Dockerfile b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/Dockerfile new file mode 100644 index 0000000..5c6bb98 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/Dockerfile @@ -0,0 +1,8 @@ +FROM amazoncorretto:21.0.1-alpine3.18 + +WORKDIR /app +COPY ../../target/flexinale-distributed-besucherportal-2024.3.0-spring-boot-fat-jar.jar /app + +EXPOSE 8080 + +CMD ["java", "-jar", "flexinale-distributed-besucherportal-2024.3.0-spring-boot-fat-jar.jar"] \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/docker_build.sh b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/docker_build.sh new file mode 100644 index 0000000..9b89feb --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/docker/docker_build.sh @@ -0,0 +1 @@ +docker build -t de.accso/flexinale-distributed-besucherportal:2024.3.0 -f Dockerfile ../../ \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/Dockerfile b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/Dockerfile new file mode 100644 index 0000000..5c6bb98 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/Dockerfile @@ -0,0 +1,8 @@ +FROM amazoncorretto:21.0.1-alpine3.18 + +WORKDIR /app +COPY ../../target/flexinale-distributed-besucherportal-2024.3.0-spring-boot-fat-jar.jar /app + +EXPOSE 8080 + +CMD ["java", "-jar", "flexinale-distributed-besucherportal-2024.3.0-spring-boot-fat-jar.jar"] \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/podman_build.bat b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/podman_build.bat new file mode 100644 index 0000000..8df13e6 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/infrastructure/podman/podman_build.bat @@ -0,0 +1 @@ +podman build -t de.accso/flexinale-distributed-besucherportal:2024.3.0 -f Dockerfile ../../ \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/pom.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/pom.xml new file mode 100644 index 0000000..ebc8ede --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/pom.xml @@ -0,0 +1,162 @@ + + + 4.0.0 + + + de.accso + flexinale-distributed + 2024.3.0 + ../pom.xml + + + flexinale-distributed-besucherportal + 2024.3.0 + Flexinale Distributed Besucherportal + Flexinale - FLEX case-study "film festival", distributed services, besucherportal + + jar + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity6 + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-actuator + + + io.micrometer + micrometer-registry-prometheus + ${micrometer.version} + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + + de.accso + flexinale-distributed-common + 2024.3.0 + + + de.accso + flexinale-distributed-common + 2024.3.0 + test-jar + test + + + de.accso + flexinale-distributed-security + runtime + 2024.3.0 + + + de.accso + flexinale-distributed-security_api_contract + 2024.3.0 + + + de.accso + flexinale-distributed-besucherportal_api_contract + 2024.3.0 + + + de.accso + flexinale-distributed-backoffice_api_contract + 2024.3.0 + + + de.accso + flexinale-distributed-ticketing_api_contract + 2024.3.0 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + spring-boot-fat-jar + + + + + build-info + + + + Distributed + Besucherportal + + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs-maven-plugin.version} + + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs-maven-plugin.version} + + + SpotbugsExcludeFilter.xml + + + + + com.github.spotbugs + spotbugs + ${spotbugs.version} + + + + + verify + + check + + + + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed - start Besucherportal app (_FlexinaleDistributedApplicationBesucherportal_).run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed - start Besucherportal app (_FlexinaleDistributedApplicationBesucherportal_).run.xml new file mode 100644 index 0000000..5fdf369 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed - start Besucherportal app (_FlexinaleDistributedApplicationBesucherportal_).run.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Cache Contents.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Cache Contents.run.xml new file mode 100644 index 0000000..ed0ce44 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Cache Contents.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Consumed.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Consumed.run.xml new file mode 100644 index 0000000..64c9ff6 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Consumed.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Published.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Published.run.xml new file mode 100644 index 0000000..8812ecd --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Events Published.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Health.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Health.run.xml new file mode 100644 index 0000000..3baa27c --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Health.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Info.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Info.run.xml new file mode 100644 index 0000000..3cc0f9b --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Info.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Metrics.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Metrics.run.xml new file mode 100644 index 0000000..578d5fa --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Metrics.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Prometheus.run.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Prometheus.run.xml new file mode 100644 index 0000000..a2eb186 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/run-configurations/Distributed Besucherportal - Actuator Prometheus.run.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportal.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportal.java new file mode 100644 index 0000000..689c940 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportal.java @@ -0,0 +1,14 @@ +package de.accso.flexinale; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Profile; + +@SpringBootApplication +@Profile({"!test-integrated &!test-distributed & !testdata"}) +public class FlexinaleDistributedApplicationBesucherportal { + + public static void main(String[] args) { + SpringApplication.run(FlexinaleDistributedApplicationBesucherportal.class, args); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/FilmSubscriber.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/FilmSubscriber.java new file mode 100644 index 0000000..f2edeef --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/FilmSubscriber.java @@ -0,0 +1,78 @@ +package de.accso.flexinale.besucherportal.api_in.event; + +import de.accso.flexinale.backoffice.api_contract.event.FilmCreatedEvent; +import de.accso.flexinale.backoffice.api_contract.event.FilmDeletedEvent; +import de.accso.flexinale.backoffice.api_contract.event.FilmUpdatedEvent; +import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache; +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import de.accso.flexinale.common.api.eventbus.EventNotification; +import de.accso.flexinale.common.api.eventbus.EventSubscriber; +import de.accso.flexinale.common.api.eventbus.EventSubscriber.EventSubscriber3; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING; + +@SuppressWarnings("unused") +public class FilmSubscriber implements EventSubscriber3 { + private static final Logger LOGGER = LoggerFactory.getLogger(FilmSubscriber.class); + + private final FKKsVTInMemoryCache cache; + + private final EventNotification notification; + + @SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"}) + public FilmSubscriber(final EventBusFactory eventBusFactory, + final FKKsVTInMemoryCache cache, + final EventNotification notification) { + EventBus createBus = eventBusFactory.createOrGetEventBusFor(FilmCreatedEvent.class); + EventBus updateBus = eventBusFactory.createOrGetEventBusFor(FilmUpdatedEvent.class); + EventBus deleteBus = eventBusFactory.createOrGetEventBusFor(FilmDeletedEvent.class); + + createBus.subscribe(FilmCreatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING); + updateBus.subscribe(FilmUpdatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING); + deleteBus.subscribe(FilmDeletedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING); + + this.cache = cache; + this.notification = notification; + } + + @Override + public String getName() { + return FilmSubscriber.class.getName(); + } + + @Override + public String getGroupName() { + return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties + } + + @Override + public void receive(final FilmCreatedEvent event) { + LOGGER.debug("received new event " + event); + + cache.addOrUpdate(event.film); + this.notification.notify(event); + } + + @Override + public void receive2(final FilmUpdatedEvent event) { + LOGGER.debug("received new event " + event); + + cache.addOrUpdate(event.film); + this.notification.notify(event); + } + + @Override + public void receive3(final FilmDeletedEvent event) { + LOGGER.debug("received new event " + event); + + cache.delete(event.film); + this.notification.notify(event); + } + + public FKKsVTInMemoryCache getCache() { + return cache; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KinoSubscriber.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KinoSubscriber.java new file mode 100644 index 0000000..c60fde5 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KinoSubscriber.java @@ -0,0 +1,101 @@ +package de.accso.flexinale.besucherportal.api_in.event; + +import de.accso.flexinale.backoffice.api_contract.event.KinoCreatedEvent; +import de.accso.flexinale.backoffice.api_contract.event.KinoDeletedEvent; +import de.accso.flexinale.backoffice.api_contract.event.KinoUpdatedEvent; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache; +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import de.accso.flexinale.common.api.eventbus.EventNotification; +import de.accso.flexinale.common.api.eventbus.EventSubscriber; +import de.accso.flexinale.common.api.eventbus.EventSubscriber.EventSubscriber3; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.HashSet; + +import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING; + +@SuppressWarnings("unused") +public class KinoSubscriber implements EventSubscriber3 { + private static final Logger LOGGER = LoggerFactory.getLogger(KinoSubscriber.class); + + private final FKKsVTInMemoryCache cache; + + private final EventNotification notification; + + @SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"}) + public KinoSubscriber(final EventBusFactory eventBusFactory, + final FKKsVTInMemoryCache cache, + final EventNotification notification) { + EventBus createBus = eventBusFactory.createOrGetEventBusFor(KinoCreatedEvent.class); + EventBus updateBus = eventBusFactory.createOrGetEventBusFor(KinoUpdatedEvent.class); + EventBus deleteBus = eventBusFactory.createOrGetEventBusFor(KinoDeletedEvent.class); + + createBus.subscribe(KinoCreatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING); + updateBus.subscribe(KinoUpdatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING); + deleteBus.subscribe(KinoDeletedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING); + + this.cache = cache; + this.notification = notification; + } + + @Override + public String getName() { + return KinoSubscriber.class.getName(); + } + + @Override + public String getGroupName() { + return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties + } + + @Override + public void receive(final KinoCreatedEvent event) { + LOGGER.debug("received new event " + event); + + KinoTO fixedKino = fixKinoSaalTO2KinoTORelation(event.kino); + cache.addOrUpdate(fixedKino); + this.notification.notify(event); + } + + @Override + public void receive2(final KinoUpdatedEvent event) { + LOGGER.debug("received new event " + event); + + KinoTO fixedKino = fixKinoSaalTO2KinoTORelation(event.kino); + cache.addOrUpdate(fixedKino); + this.notification.notify(event); + } + + @Override + public void receive3(final KinoDeletedEvent event) { + LOGGER.debug("received new event " + event); + + cache.delete(event.kino); + this.notification.notify(event); + } + + // need to fix in all KinoSaalTO the relation to the correct KinoTO (as it is null after Json deserialization) + private KinoTO fixKinoSaalTO2KinoTORelation(final KinoTO kinoTOFromEvent) { + if (kinoTOFromEvent == null) return null; + + final KinoTO newKinoTO = new KinoTO(kinoTOFromEvent.id(), kinoTOFromEvent.version(), kinoTOFromEvent.name(), + kinoTOFromEvent.adresse(), kinoTOFromEvent.emailAdresse(), new HashSet<>()); + kinoTOFromEvent.kinoSaele() + .stream() + .map(kinoSaalTOFromEvent -> + new KinoSaalTO(kinoSaalTOFromEvent.id(), kinoSaalTOFromEvent.version(), + kinoSaalTOFromEvent.name(), kinoSaalTOFromEvent.anzahlPlaetze(), newKinoTO)) + .forEach(kinoSaalTOFromEvent -> + newKinoTO.kinoSaele().add(kinoSaalTOFromEvent)); + + return newKinoTO; + } + + public FKKsVTInMemoryCache getCache() { + return cache; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KontingentSubscriber.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KontingentSubscriber.java new file mode 100644 index 0000000..5f383a0 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/KontingentSubscriber.java @@ -0,0 +1,54 @@ +package de.accso.flexinale.besucherportal.api_in.event; + +import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache; +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import de.accso.flexinale.common.api.eventbus.EventNotification; +import de.accso.flexinale.common.api.eventbus.EventSubscriber; +import de.accso.flexinale.ticketing.api_contract.event.OnlineKontingentChangedEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING; +import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull; + +@SuppressWarnings("unused") +public class KontingentSubscriber implements EventSubscriber { + private static final Logger LOGGER = LoggerFactory.getLogger(KontingentSubscriber.class); + + private final FKKsVTInMemoryCache cache; + + private final EventNotification notification; + + public KontingentSubscriber(final EventBusFactory eventBusFactory, + final FKKsVTInMemoryCache cache, + final EventNotification notification) { + EventBus kontingentBus = eventBusFactory.createOrGetEventBusFor(OnlineKontingentChangedEvent.class); + kontingentBus.subscribe(OnlineKontingentChangedEvent.class, this, START_READING_FROM_BEGINNING); + + this.cache = cache; + this.notification = notification; + } + + @Override + public String getName() { + return KontingentSubscriber.class.getName(); + } + + @Override + public String getGroupName() { + return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties + } + + @Override + public void receive(final OnlineKontingentChangedEvent event) { + LOGGER.debug("received new event " + event); + + cache.addOrUpdate(event.onlineKontingent.vorfuehrungId(), getRawOrNull(event.onlineKontingent.neuesOnlineRestKontingent())); + this.notification.notify(event); + } + + public FKKsVTInMemoryCache getCache() { + return cache; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/TicketSubscriber.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/TicketSubscriber.java new file mode 100644 index 0000000..c690bb9 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/TicketSubscriber.java @@ -0,0 +1,66 @@ +package de.accso.flexinale.besucherportal.api_in.event; + +import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache; +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import de.accso.flexinale.common.api.eventbus.EventNotification; +import de.accso.flexinale.common.api.eventbus.EventSubscriber; +import de.accso.flexinale.ticketing.api_contract.event.TicketGekauftEvent; +import de.accso.flexinale.ticketing.api_contract.event.TicketUngueltigEvent; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING; + +@SuppressWarnings("unused") +public class TicketSubscriber implements EventSubscriber.EventSubscriber2 { + private static final Logger LOGGER = LoggerFactory.getLogger(TicketSubscriber.class); + + private final FKKsVTInMemoryCache cache; + + private final EventNotification notification; + + @SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"}) + public TicketSubscriber(final EventBusFactory eventBusFactory, + final FKKsVTInMemoryCache cache, + final EventNotification notification) { + EventBus createBus = eventBusFactory.createOrGetEventBusFor(TicketGekauftEvent.class); + EventBus ungueltigBus = eventBusFactory.createOrGetEventBusFor(TicketUngueltigEvent.class); + + createBus.subscribe(TicketGekauftEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING); + ungueltigBus.subscribe(TicketUngueltigEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING); + + this.cache = cache; + this.notification = notification; + } + + @Override + public String getName() { + return TicketSubscriber.class.getName(); + } + + @Override + public String getGroupName() { + return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties + } + + @Override + public void receive(final TicketGekauftEvent event) { + LOGGER.debug("received new event " + event); + + cache.addOrUpdate(event.ticket); + this.notification.notify(event); + } + + @Override + public void receive2(final TicketUngueltigEvent event) { + LOGGER.debug("received new event " + event); + + cache.addOrUpdate(event.ticket); + this.notification.notify(event); + } + + public FKKsVTInMemoryCache getCache() { + return cache; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/VorfuehrungSubscriber.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/VorfuehrungSubscriber.java new file mode 100644 index 0000000..f60bfcf --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/event/VorfuehrungSubscriber.java @@ -0,0 +1,77 @@ +package de.accso.flexinale.besucherportal.api_in.event; + +import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungCreatedEvent; +import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungDeletedEvent; +import de.accso.flexinale.backoffice.api_contract.event.VorfuehrungUpdatedEvent; +import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache; +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import de.accso.flexinale.common.api.eventbus.EventNotification; +import de.accso.flexinale.common.api.eventbus.EventSubscriber; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_BEGINNING; + +@SuppressWarnings("unused") +public class VorfuehrungSubscriber implements EventSubscriber.EventSubscriber3 { + private static final Logger LOGGER = LoggerFactory.getLogger(VorfuehrungSubscriber.class); + + private final FKKsVTInMemoryCache cache; + + private final EventNotification notification; + + @SuppressWarnings({"RedundantCast", "unchecked", "rawtypes"}) + public VorfuehrungSubscriber(final EventBusFactory eventBusFactory, + final FKKsVTInMemoryCache cache, + final EventNotification notification) { + EventBus createBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungCreatedEvent.class); + EventBus updateBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungUpdatedEvent.class); + EventBus deleteBus = eventBusFactory.createOrGetEventBusFor(VorfuehrungDeletedEvent.class); + + createBus.subscribe(VorfuehrungCreatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING); + updateBus.subscribe(VorfuehrungUpdatedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING); + deleteBus.subscribe(VorfuehrungDeletedEvent.class, (EventSubscriber) this, START_READING_FROM_BEGINNING); + + this.cache = cache; + this.notification = notification; + } + + @Override + public String getName() { + return VorfuehrungSubscriber.class.getName(); + } + + @Override + public String getGroupName() { + return "flexinale-distributed-besucherportal"; //TODO make configurable, use "spring.kafka.consumer.group-id" from application.properties + } + + @Override + public void receive(final VorfuehrungCreatedEvent event) { + LOGGER.debug("received new event " + event); + + cache.addOrUpdate(event.vorfuehrung); + this.notification.notify(event); + } + + @Override + public void receive2(final VorfuehrungUpdatedEvent event) { + LOGGER.debug("received new event " + event); + + cache.addOrUpdate(event.vorfuehrung); + this.notification.notify(event); + } + + @Override + public void receive3(final VorfuehrungDeletedEvent event) { + LOGGER.debug("received new event " + event); + + cache.delete(event.vorfuehrung); + this.notification.notify(event); + } + + public FKKsVTInMemoryCache getCache() { + return cache; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/FilmWebController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/FilmWebController.java new file mode 100644 index 0000000..fdcd854 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/FilmWebController.java @@ -0,0 +1,110 @@ +package de.accso.flexinale.besucherportal.api_in.web; + +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; +import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache; +import de.accso.flexinale.besucherportal.application.services.FilmService; +import de.accso.flexinale.besucherportal.application.services.KinoService; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.RawWrapper; +import de.accso.flexinale.security.api_contract.BesucherRetriever; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.server.ResponseStatusException; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull; + +@Controller +@Profile({ "!testdata-backoffice & !testdata-ticketing" }) +public class FilmWebController { + + private record VorfuehrungMitRestkontingentTO(Identifiable.Id vorfuehrungId, FilmTO film, Zeit zeit, + KinoSaalTO kinoSaal, KinoTO kino, RestKontingent restkontingentOnline) { + public record Zeit(LocalDateTime raw) implements RawWrapper { + public Zeit(LocalDateTime raw) { + this.raw = raw.withNano(0); // precision is second + } + } + + public record RestKontingent(Integer raw) implements RawWrapper {} + } + + @Autowired + private FilmService filmService; + + @Autowired + private KinoService kinoService; + + @Autowired + private BesucherRetriever besucherRetriever; + + @Autowired + private FKKsVTInMemoryCache cache; + + @GetMapping(value="/filme") + @PreAuthorize("hasRole('ROLE_BESUCHER')") + @SuppressWarnings("SameReturnValue") + public String filme(final Model model) { + List filme = filmService.filme(); + model.addAttribute("filme", filme); + return "filme"; + } + + @GetMapping(value="/film/{id}") + @PreAuthorize("hasRole('ROLE_BESUCHER')") + @SuppressWarnings("SameReturnValue") + public String film(@PathVariable("id") final String id, final Model model) { + Identifiable.Id filmId = Identifiable.Id.of(id); + + FilmTO film = filmService.film(filmId); + if (film == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "film %s not available".formatted(id)); + } + else { + model.addAttribute("film", film); + List vorfuehrungenOfFilm = filmService.vorfuehrungenFuer(filmId); + + List vorfuehrungenMitRestkontingent = vorfuehrungenOfFilm + .stream() + .map(v -> getRestkontingentOnlineAndMap(v, film)) + .collect(Collectors.toList()); + + model.addAttribute("vorfuehrungenMitRestkontingent", vorfuehrungenMitRestkontingent); + + if (!vorfuehrungenOfFilm.isEmpty()) { + Identifiable.Id idOfLoggedInBesucher = besucherRetriever.getIdOfLoggedInBesucher(); + List vorfuehrungenMitUeberlapp = + filmService.vorfuehrungenMitUeberlapp(vorfuehrungenOfFilm, idOfLoggedInBesucher); + model.addAttribute("vorfuehrungenMitUeberlapp", vorfuehrungenMitUeberlapp); + List vorfuehrungenMitTicket = + filmService.vorfuehrungenFuerDieDerBenutzerEinTicketHat(vorfuehrungenOfFilm, idOfLoggedInBesucher); + model.addAttribute("vorfuehrungenMitTicket", vorfuehrungenMitTicket); + } + } + + return "film"; + } + + private VorfuehrungMitRestkontingentTO getRestkontingentOnlineAndMap(final VorfuehrungTO vorfuehrung, + final FilmTO film) { + VorfuehrungMitRestkontingentTO.RestKontingent restKontingentOnline = + new VorfuehrungMitRestkontingentTO.RestKontingent(cache.getRestkontingentOnline(vorfuehrung.id())); + KinoTO kino = kinoService.kino(vorfuehrung.kinoId()); + + return new VorfuehrungMitRestkontingentTO(vorfuehrung.id(), film, + new VorfuehrungMitRestkontingentTO.Zeit(getRawOrNull(vorfuehrung.zeit())), + vorfuehrung.kinoSaal(), kino, restKontingentOnline); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/IndexWebController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/IndexWebController.java new file mode 100644 index 0000000..43f2b73 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/IndexWebController.java @@ -0,0 +1,25 @@ +package de.accso.flexinale.besucherportal.api_in.web; + +import de.accso.flexinale.common.application.Config; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +@Profile({ "!testdata-backoffice & !testdata-ticketing" }) +public class IndexWebController { + @Autowired + private Config config; + + @GetMapping(value="/") + @SuppressWarnings("SameReturnValue") + public String index(final Model model) { + model.addAttribute("applicationTitle", config.getApplicationTitle()); + model.addAttribute("buildVersion", config.getBuildVersion()); + model.addAttribute("buildDate", config.getBuildDate()); + + return "index"; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/KinoWebController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/KinoWebController.java new file mode 100644 index 0000000..6e005d4 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/KinoWebController.java @@ -0,0 +1,48 @@ +package de.accso.flexinale.besucherportal.api_in.web; + +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.besucherportal.application.services.KinoService; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.server.ResponseStatusException; + +import java.util.List; + +@Controller +@Profile({ "!testdata-backoffice & !testdata-ticketing" }) +public class KinoWebController { + + @Autowired + private KinoService kinoService; + + @GetMapping(value="/kinos") + @PreAuthorize("hasRole('ROLE_BESUCHER')") + @SuppressWarnings("SameReturnValue") + public String kinos(final Model model) { + List kinos = kinoService.kinos(); + model.addAttribute("kinos", kinos); + return "kinos"; + } + + @GetMapping(value="/kino/{id}") + @PreAuthorize("hasRole('ROLE_BESUCHER')") + @SuppressWarnings("SameReturnValue") + public String kino(@PathVariable("id") final String id, final Model model) { + Identifiable.Id kinoId = Identifiable.Id.of(id); + KinoTO kino = kinoService.kino(kinoId); + if (kino == null) { + throw new ResponseStatusException(HttpStatus.NOT_FOUND, "kino %s not available".formatted(id)); + } + else { + model.addAttribute("kino", kino); + return "kino"; + } + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketSorter.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketSorter.java new file mode 100644 index 0000000..0659b86 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketSorter.java @@ -0,0 +1,166 @@ +package de.accso.flexinale.besucherportal.api_in.web; + +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; +import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException; +import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.Serializable; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.*; + +import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull; + +class TicketSorter { + + private static final Logger LOGGER = LoggerFactory.getLogger(TicketSorter.class); + + /** + * return a list of list of tickets, sorted by time (inner list) and date (outer list). + * each list contains the tickets for a day + */ + public static List> sortAndMapTicketsPerDay(final List allTickets, + final FKKsVTInMemoryCache cache) { + if (allTickets == null || allTickets.isEmpty()) { + return new ArrayList<>(); + } + + Map ticket2Vorfuehrung = createTicketToVorfuehrungMap(allTickets, cache); + + // 0) sort tickets by time (not done in database SQL query as before) + allTickets.sort(new TicketTOByZeitSortingComparator(ticket2Vorfuehrung)); + + // 1) tickets by day + List> ticketsProTag = new ArrayList<>(); + + LocalDate previousDate = LocalDate.MIN; // far past :-) + for (TicketTO ticket : allTickets) { + LocalDateTime zeit = getRawOrNull(ticket2Vorfuehrung.get(ticket).zeit()); + LocalDate currentDate = zeit.toLocalDate(); + + if (currentDate.equals(previousDate)) { + ticketsProTag.getLast().add(ticket); + } + else { + List tickets = new ArrayList<>(); + tickets.add(ticket); + ticketsProTag.add(tickets); + } + previousDate = currentDate; + } + + // 2) inner structure: Vorfuehrung and tickets + List> vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag = new ArrayList<>(); + + for (List ticketsForOneDay : ticketsProTag) { + List vorfuehrungenUndAnzahlTicketsFuerEinenTag = new ArrayList<>(); + int anzahlGueltigeTicketsFuerVorfuehrung = 0; + int anzahlUngueltigeTicketsFuerVorfuehrung = 0; + VorfuehrungTO previousVorfuehrung = ticket2Vorfuehrung.get(ticketsForOneDay.getFirst()); + + for (TicketTO ticket : ticketsForOneDay) { + // inside each day, Vorfuehrungen are ordered by zeit. + // There is never more than one Vorfuehrung at a zeit for a user. + // So tickets for the same Vorfuehrung are all in a row - that's why the following works. + VorfuehrungTO currentVorfuehrung = ticket2Vorfuehrung.get(ticket); + + if (currentVorfuehrung.equals(previousVorfuehrung)) { + Boolean ticketGueltig = getRawOrNull(ticket.gueltig()); + + if (ticketGueltig) { + anzahlGueltigeTicketsFuerVorfuehrung++; + } + else { + anzahlUngueltigeTicketsFuerVorfuehrung++; + } + } + else { + Boolean ticketGueltig = getRawOrNull(ticket.gueltig()); + + // add the information for previous Vorfuehrung + VorfuehrungMitAnzahlTicketsTO to = mapVorfuehrungToTO( + anzahlGueltigeTicketsFuerVorfuehrung, anzahlUngueltigeTicketsFuerVorfuehrung, + previousVorfuehrung, cache); + vorfuehrungenUndAnzahlTicketsFuerEinenTag.add(to); + + // ... we also have a new ticket for the next Vorfuehrung + if (ticketGueltig) { + anzahlGueltigeTicketsFuerVorfuehrung = 1; + anzahlUngueltigeTicketsFuerVorfuehrung = 0; + } else { + anzahlGueltigeTicketsFuerVorfuehrung = 0; + anzahlUngueltigeTicketsFuerVorfuehrung = 1; + } + } + + previousVorfuehrung = currentVorfuehrung; + } + + // Also add the last Vorfuehrung an anzahlTickets when day is over + VorfuehrungMitAnzahlTicketsTO to = mapVorfuehrungToTO( + anzahlGueltigeTicketsFuerVorfuehrung, anzahlUngueltigeTicketsFuerVorfuehrung, previousVorfuehrung, cache); + vorfuehrungenUndAnzahlTicketsFuerEinenTag.add(to); + vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag.add(vorfuehrungenUndAnzahlTicketsFuerEinenTag); + } + return vorfuehrungUndAnzahlTicketsFuerVorfuehrungProTag; + } + + private static VorfuehrungMitAnzahlTicketsTO mapVorfuehrungToTO(final int anzahlGueltigeTicketsFuerVorfuehrung, + final int anzahlUngueltigeTicketsFuerVorfuehrung, + final VorfuehrungTO vorfuehrung, + final FKKsVTInMemoryCache cache) { + FilmTO film = vorfuehrung.film(); + KinoTO kino = cache.kino(vorfuehrung.kinoId()); + if (film == null) { + throw new FlexinaleIllegalStateException("no Film for Vorfuehrung %s found".formatted(vorfuehrung.id())); + } + else { + return new VorfuehrungMitAnzahlTicketsTO( + new VorfuehrungMitAnzahlTicketsTO.Zeit(getRawOrNull(vorfuehrung.zeit())), + film, + vorfuehrung.kinoSaal(), + kino, + new VorfuehrungMitAnzahlTicketsTO.AnzahlTickets(anzahlGueltigeTicketsFuerVorfuehrung), + new VorfuehrungMitAnzahlTicketsTO.AnzahlTickets(anzahlUngueltigeTicketsFuerVorfuehrung) + ); + } + } + + private static Map createTicketToVorfuehrungMap(final List allTickets, + final FKKsVTInMemoryCache cache) { + Map ticketToVorfuehrung = new HashMap<>(); + for (TicketTO ticket : allTickets) { + VorfuehrungTO vorfuehrung = cache.vorfuehrung(ticket.vorfuehrungId()); + if (vorfuehrung == null) { + LOGGER.error("Vorfuehrung %s of ticket %s cannot be found.".formatted(ticket.vorfuehrungId(), ticket.id())); + } + else { + ticketToVorfuehrung.put(ticket, vorfuehrung); + } + } + return ticketToVorfuehrung; + } +} + +class TicketTOByZeitSortingComparator implements Comparator, Serializable { + + private final Map ticket2Vorfuehrung; + + TicketTOByZeitSortingComparator(final Map ticket2Vorfuehrung) { + this.ticket2Vorfuehrung = ticket2Vorfuehrung; + } + + @Override + public int compare(final TicketTO t1, final TicketTO t2) { + VorfuehrungTO v1 = ticket2Vorfuehrung.get(t1); + VorfuehrungTO v2 = ticket2Vorfuehrung.get(t2); + LocalDateTime v1Zeit = getRawOrNull(v1.zeit()); + LocalDateTime v2Zeit = getRawOrNull(v2.zeit()); + return v1Zeit.compareTo(v2Zeit); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketWebController.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketWebController.java new file mode 100644 index 0000000..d1dbf00 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_in/web/TicketWebController.java @@ -0,0 +1,89 @@ +package de.accso.flexinale.besucherportal.api_in.web; + +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoSaalTO; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache; +import de.accso.flexinale.besucherportal.application.services.TicketService; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.RawWrapper; +import de.accso.flexinale.security.api_contract.BesucherRetriever; +import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import java.time.LocalDateTime; +import java.util.List; + +@Controller +@Profile({ "!testdata-backoffice & !testdata-ticketing" }) +public class TicketWebController { + + private static final Logger LOGGER = LoggerFactory.getLogger(TicketWebController.class); + + @Autowired + private TicketService ticketService; + + @Autowired + private FKKsVTInMemoryCache cache; + + @Autowired + private BesucherRetriever besucherRetriever; + + @GetMapping(value="/tickets") + @PreAuthorize("hasRole('ROLE_BESUCHER')") + public String listTickets(final Model model) { + Identifiable.Id besucherId = besucherRetriever.getIdOfLoggedInBesucher(); + + List allTickets = cache.ticketsByBesucher(besucherId); + + List> ticketsByDay + = TicketSorter.sortAndMapTicketsPerDay(allTickets, cache); + model.addAttribute("ticketsByDay", ticketsByDay); + + int totalNumberOfTickets = cache.gesamtZahlDerTicketsFuer(besucherId); + model.addAttribute("totalNumberOfTickets", totalNumberOfTickets); + + return ("tickets"); + } + + @PostMapping("/vorfuehrung/loeseGutscheineOnlineEin") + @PreAuthorize("hasRole('ROLE_BESUCHER')") + public String loeseGutscheineOnlineEin( + @RequestParam("vorfuehrungId") final String vId, + @RequestParam("filmId") final String fId, + @RequestParam("anzahl") final int anzahl, + RedirectAttributes redirAttrs) { + Identifiable.Id vorfuehrungId = Identifiable.Id.of(vId); + Identifiable.Id filmId = Identifiable.Id.of(fId); + Identifiable.Id besucherId = besucherRetriever.getIdOfLoggedInBesucher(); + + ticketService.loeseGutscheineOnlineFuerVorfuehrungEin(vorfuehrungId, filmId, besucherId, anzahl); + + redirAttrs.addFlashAttribute("success", + "Kauf von %d Ticket(s) beauftragt".formatted(anzahl)); + return "redirect:/tickets"; + } +} + +// class used to "flatten" Vorfuehrung and number of Tickets for Vorfuehrung for Clients +record VorfuehrungMitAnzahlTicketsTO(Zeit zeit, FilmTO film, + KinoSaalTO kinoSaal, KinoTO kino, + AnzahlTickets anzahlGueltigeTickets, AnzahlTickets anzahlUngueltigeTickets) { + public record Zeit(LocalDateTime raw) implements RawWrapper { + public Zeit(LocalDateTime raw) { + this.raw = raw.withNano(0); // precision is second + } + } + + public record AnzahlTickets(Integer raw) implements RawWrapper {} +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_out/event/GutscheinEinloesenBeauftragtPublisher.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_out/event/GutscheinEinloesenBeauftragtPublisher.java new file mode 100644 index 0000000..7ee5bed --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/api_out/event/GutscheinEinloesenBeauftragtPublisher.java @@ -0,0 +1,46 @@ +package de.accso.flexinale.besucherportal.api_out.event; + +import de.accso.flexinale.besucherportal.api_contract.event.GutscheinEinloesenBeauftragtEvent; +import de.accso.flexinale.besucherportal.api_contract.event.model.GutscheinEinloesenAuftragTO; +import de.accso.flexinale.besucherportal.application.services.GutscheinEinloesenBeauftragtPublication; +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import de.accso.flexinale.common.api.eventbus.EventNotification; +import de.accso.flexinale.common.api.eventbus.EventPublisher; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.Versionable; + +public class GutscheinEinloesenBeauftragtPublisher implements EventPublisher, GutscheinEinloesenBeauftragtPublication { + private final EventBus gutscheinEinloesenBeauftragtEventBus; + private final EventNotification notification; + + public GutscheinEinloesenBeauftragtPublisher(final EventBusFactory eventBusFactory, + final EventNotification notification) { + this.gutscheinEinloesenBeauftragtEventBus = eventBusFactory.createOrGetEventBusFor(GutscheinEinloesenBeauftragtEvent.class); + this.notification = notification; + } + + @Override + public void publishGutscheinEinloesenBeauftragtEvent(final Identifiable.Id vorfuehrungId, + final Identifiable.Id filmId, + final Identifiable.Id besucherId, + final int anzahlGutscheine) { + GutscheinEinloesenAuftragTO gutschein = new GutscheinEinloesenAuftragTO(Identifiable.Id.of(), Versionable.initialVersion(), + filmId, vorfuehrungId, besucherId, + new GutscheinEinloesenAuftragTO.AnzahlTickets(anzahlGutscheine)); + + post(GutscheinEinloesenBeauftragtEvent.class, + new GutscheinEinloesenBeauftragtEvent(gutschein)); + } + + @Override + public String getName() { + return GutscheinEinloesenBeauftragtPublisher.class.getName(); + } + + @Override + public void post(Class eventType, GutscheinEinloesenBeauftragtEvent event) { + this.gutscheinEinloesenBeauftragtEventBus.publish(eventType, event); + this.notification.notify(event); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FKKsVTInMemoryCache.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FKKsVTInMemoryCache.java new file mode 100644 index 0000000..fdbc573 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FKKsVTInMemoryCache.java @@ -0,0 +1,127 @@ +package de.accso.flexinale.besucherportal.application.services; + +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; +import de.accso.flexinale.common.application.caching.InMemoryCache; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO; + +import java.util.List; +import java.util.stream.Collectors; + +import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull; + +public final class FKKsVTInMemoryCache { + + // caches are currently never cleared - might want to use Cache implementation with automatic time-to-live + private final InMemoryCache filmCache = new InMemoryCache<>(); + private final InMemoryCache kinoCache = new InMemoryCache<>(); + private final InMemoryCache vorfuehrungCache = new InMemoryCache<>(); + private final InMemoryCache ticketCache = new InMemoryCache<>(); + + private final InMemoryCache vorfuehrung2RestKontingentOnlineCache = new InMemoryCache<>(); + + public void addOrUpdate(final FilmTO film) { + filmCache.put(film.id(), film); + } + + public void delete(final FilmTO film) { + filmCache.remove(film.id()); + } + + public FilmTO film(final Identifiable.Id id) { + return filmCache.get(id); + } + + public List filme() { + return filmCache.values().stream().toList(); + } + + // ------------------------------------------------------------------------------------------------------------- + + public void addOrUpdate(final KinoTO kino) { + kinoCache.put(kino.id(), kino); + } + + public void delete(final KinoTO kino) { + kinoCache.remove(kino.id()); + } + + + public KinoTO kino(final Identifiable.Id id) { + return kinoCache.get(id); + } + + public List kinos() { + return kinoCache.values().stream().toList(); + } + + // ------------------------------------------------------------------------------------------------------------- + + public void addOrUpdate(final VorfuehrungTO vorfuehrung) { + vorfuehrungCache.put(vorfuehrung.id(), vorfuehrung); + } + + public void delete(final VorfuehrungTO vorfuehrung) { + vorfuehrungCache.remove(vorfuehrung.id()); + } + + public List vorfuehrungen() { + return vorfuehrungCache.values().stream().toList(); + } + + public VorfuehrungTO vorfuehrung(final Identifiable.Id id) { + return vorfuehrungCache.get(id); + } + + public List vorfuehrungenByFilmId(final Identifiable.Id filmId) { + return vorfuehrungCache.values() + .stream() + .filter(v -> v.film().id().equals(filmId)) + .toList(); + } + + // ------------------------------------------------------------------------------------------------------------- + + public void addOrUpdate(final TicketTO ticket) { + ticketCache.put(ticket.id(), ticket); + } + + public List tickets() { + return ticketCache.values().stream().toList(); + } + + public List ticketsByBesucher(final Identifiable.Id besucherId) { + return ticketCache.values() + .stream() + .filter(t -> t.besucherId().equals(besucherId)) + .collect(Collectors.toList()); // don't use .toList() here as we need to sort the list later + } + + public List gueltigeTicketsByBesucher(final Identifiable.Id besucherId) { + return ticketsByBesucher(besucherId) + .stream() + .filter((t -> getRawOrNull(t.gueltig()))) + .collect(Collectors.toList()); + } + + public int gesamtZahlDerTicketsFuer(final Identifiable.Id besucherId) { + return (int) ticketCache.values() + .stream() + .filter(t -> t.besucherId().equals(besucherId)) + .filter((t -> getRawOrNull(t.gueltig()))) + .count(); + } + + // ------------------------------------------------------------------------------------------------------------- + + public void addOrUpdate(final Identifiable.Id vorfuehrungId, final Integer onlineRestKontingent) { + vorfuehrung2RestKontingentOnlineCache.put(vorfuehrungId, onlineRestKontingent); + } + + public Integer getRestkontingentOnline(final Identifiable.Id vorfuehrungId) { + Integer restKontingentFromCache = vorfuehrung2RestKontingentOnlineCache.get(vorfuehrungId); + return (restKontingentFromCache != null) ? restKontingentFromCache : 0; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FilmService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FilmService.java new file mode 100644 index 0000000..c976102 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/FilmService.java @@ -0,0 +1,65 @@ +package de.accso.flexinale.besucherportal.application.services; + +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.ArrayList; +import java.util.List; + +public class FilmService { + + private final FKKsVTInMemoryCache cache; + + private final VorfuehrungService vorfuehrungService; + + private final long minZeitZwischenVorfuehrungenInMinuten; + + public FilmService(final FKKsVTInMemoryCache cache, + final VorfuehrungService vorfuehrungService, + final long minZeitZwischenVorfuehrungenInMinuten) { + this.cache = cache; + this.vorfuehrungService = vorfuehrungService; + this.minZeitZwischenVorfuehrungenInMinuten = minZeitZwischenVorfuehrungenInMinuten; + } + + public List filme() { + return cache.filme(); + } + + public FilmTO film(final Identifiable.Id id) { + return cache.film(id); + } + + public List vorfuehrungenFuer(final Identifiable.Id filmId) { + return cache.vorfuehrungenByFilmId(filmId); + } + + public List vorfuehrungenMitUeberlapp(final List vorfuehrungen, + final Identifiable.Id besucherId) { + List vorfuehrungenMitUeberlapp = new ArrayList<>(); + + TicketBundle ticketBundle = new TicketBundle(besucherId, cache, vorfuehrungService, minZeitZwischenVorfuehrungenInMinuten); + + for (VorfuehrungTO vorfuehrung : vorfuehrungen) { + if (ticketBundle.mindestensEinGueltigesTicketImTicketBundleUeberlapptMit(vorfuehrung)) { + vorfuehrungenMitUeberlapp.add(vorfuehrung.id().id()); + } + } + return vorfuehrungenMitUeberlapp; + } + + public List vorfuehrungenFuerDieDerBenutzerEinTicketHat(final List vorfuehrungen, + final Identifiable.Id besucherId) { + List vorfuehrungenMitTicket = new ArrayList<>(); + + TicketBundle ticketBundle = new TicketBundle(besucherId, cache, vorfuehrungService, minZeitZwischenVorfuehrungenInMinuten); + + for (VorfuehrungTO vorfuehrung : vorfuehrungen) { + if (ticketBundle.hatSchonGueltigesTicketFuer(vorfuehrung)) { + vorfuehrungenMitTicket.add(vorfuehrung.id().id()); + } + } + return vorfuehrungenMitTicket; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/GutscheinEinloesenBeauftragtPublication.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/GutscheinEinloesenBeauftragtPublication.java new file mode 100644 index 0000000..baa7282 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/GutscheinEinloesenBeauftragtPublication.java @@ -0,0 +1,10 @@ +package de.accso.flexinale.besucherportal.application.services; + +import de.accso.flexinale.common.shared_kernel.Identifiable; + +public interface GutscheinEinloesenBeauftragtPublication { + void publishGutscheinEinloesenBeauftragtEvent(final Identifiable.Id vorfuehrungId, + final Identifiable.Id filmId, + final Identifiable.Id besucherId, + final int anzahlGutscheine); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/KinoService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/KinoService.java new file mode 100644 index 0000000..9ac82da --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/KinoService.java @@ -0,0 +1,23 @@ +package de.accso.flexinale.besucherportal.application.services; + +import de.accso.flexinale.backoffice.api_contract.event.model.KinoTO; +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.List; + +public class KinoService { + + private final FKKsVTInMemoryCache cache; + + public KinoService(final FKKsVTInMemoryCache cache) { + this.cache = cache; + } + + public List kinos() { + return cache.kinos(); + } + + public KinoTO kino(final Identifiable.Id id) { + return cache.kino(id); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/TicketBundle.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/TicketBundle.java new file mode 100644 index 0000000..7cebc9e --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/TicketBundle.java @@ -0,0 +1,65 @@ +package de.accso.flexinale.besucherportal.application.services; + +import de.accso.flexinale.backoffice.api_contract.event.model.FilmTO; +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.ticketing.api_contract.event.model.TicketTO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull; + +public class TicketBundle { + private static final Logger LOGGER = LoggerFactory.getLogger(TicketBundle.class); + + private final FKKsVTInMemoryCache cache; + + private final VorfuehrungService vorfuehrungService; + + private final long minZeitZwischenVorfuehrungenInMinuten; + + private final List gueltigeTicketsOfBesucher; + + public TicketBundle(final Identifiable.Id besucherId, final FKKsVTInMemoryCache cache, + final VorfuehrungService vorfuehrungService, + final long minZeitZwischenVorfuehrungenInMinuten) { + this.cache = cache; + this.vorfuehrungService = vorfuehrungService; + this.minZeitZwischenVorfuehrungenInMinuten = minZeitZwischenVorfuehrungenInMinuten; + + this.gueltigeTicketsOfBesucher = cache.gueltigeTicketsByBesucher(besucherId); + } + + public boolean mindestensEinGueltigesTicketImTicketBundleUeberlapptMit(final VorfuehrungTO vorfuehrung) { + FilmTO filmOfVorfuehrung = vorfuehrung.film(); + + return gueltigeTicketsOfBesucher.stream().anyMatch(ticket -> { + VorfuehrungTO vorfuehrungOfTicket = cache.vorfuehrung(ticket.vorfuehrungId()); + if (vorfuehrungOfTicket == null) + return false; + else { + FilmTO filmOfVorfuehrungOfTicket = vorfuehrungOfTicket.film(); + if (filmOfVorfuehrungOfTicket == null) { + String message = "Film for Vorfuehrung %s could not be found".formatted(vorfuehrungOfTicket.id()); + LOGGER.error(message); + throw new FlexinaleIllegalStateException(message); + } + + return vorfuehrungService.vorfuehrungUeberlapptMit(vorfuehrung, vorfuehrungOfTicket, + getRawOrNull(filmOfVorfuehrung.dauerInMinuten()), + getRawOrNull(filmOfVorfuehrungOfTicket.dauerInMinuten()), + minZeitZwischenVorfuehrungenInMinuten); + } + }); + } + + public boolean hatSchonGueltigesTicketFuer(final VorfuehrungTO vorfuehrung) { + return gueltigeTicketsOfBesucher.stream() + .map(TicketTO::vorfuehrungId) + .toList() + .contains(vorfuehrung.id()); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/TicketService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/TicketService.java new file mode 100644 index 0000000..b9b3bb4 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/TicketService.java @@ -0,0 +1,20 @@ +package de.accso.flexinale.besucherportal.application.services; + +import de.accso.flexinale.common.shared_kernel.Identifiable; + +public class TicketService { + + final GutscheinEinloesenBeauftragtPublication gutscheinEinloesenBeauftragtPublisher; + + public TicketService(final GutscheinEinloesenBeauftragtPublication gutscheinEinloesenBeauftragtPublisher) { + this.gutscheinEinloesenBeauftragtPublisher = gutscheinEinloesenBeauftragtPublisher; + } + + public void loeseGutscheineOnlineFuerVorfuehrungEin(final Identifiable.Id vorfuehrungId, + final Identifiable.Id filmId, + final Identifiable.Id besucherId, + final int anzahlGutscheine) { + gutscheinEinloesenBeauftragtPublisher + .publishGutscheinEinloesenBeauftragtEvent(vorfuehrungId, filmId, besucherId, anzahlGutscheine); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/VorfuehrungService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/VorfuehrungService.java new file mode 100644 index 0000000..058a87a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/application/services/VorfuehrungService.java @@ -0,0 +1,22 @@ +package de.accso.flexinale.besucherportal.application.services; + +import de.accso.flexinale.backoffice.api_contract.event.model.VorfuehrungTO; + +import java.time.LocalDateTime; + +import static de.accso.flexinale.common.shared_kernel.RawWrapper.getRawOrNull; + +public final class VorfuehrungService { + + public boolean vorfuehrungUeberlapptMit(final VorfuehrungTO one, final VorfuehrungTO other, + final Integer oneDauerInMinuten, final Integer otherDauerInMinuten, + final long puffer) { + LocalDateTime begin = getRawOrNull(one.zeit()); + LocalDateTime beginOther = getRawOrNull(other.zeit()); + LocalDateTime end = begin.plusMinutes(oneDauerInMinuten); + LocalDateTime endOther = beginOther.plusMinutes(otherDauerInMinuten); + + return ((begin.isBefore(endOther.plusMinutes(puffer))) + && (end.isAfter(beginOther.minusMinutes(puffer)))); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/ActuatorEndpointCache.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/ActuatorEndpointCache.java new file mode 100644 index 0000000..8d80299 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/ActuatorEndpointCache.java @@ -0,0 +1,43 @@ +package de.accso.flexinale.besucherportal.infrastructure; + +import de.accso.flexinale.besucherportal.application.services.FKKsVTInMemoryCache; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@Profile({ "!testdata-backoffice & !testdata-ticketing" }) +@Endpoint(id = "besucherportalCacheContents") +public class ActuatorEndpointCache { + + private final FKKsVTInMemoryCache cache; + + public ActuatorEndpointCache(final FKKsVTInMemoryCache cache) { + this.cache = cache; + } + + @ReadOperation + public List cacheContents() { + return List.of( + cache.filme(), + cache.kinos(), + cache.vorfuehrungen(), + cache.tickets() + ); + } + + @ReadOperation + public List cacheContentsFilteredBy(@Selector String contentType) { + return switch (contentType) { + case "filme" -> cache.filme(); + case "kinos" -> cache.kinos(); + case "vorfuehrungen" -> cache.vorfuehrungen(); + case "tickets" -> cache.tickets(); + default -> throw new UnsupportedOperationException("type %s not supported".formatted(contentType)); + }; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalActuatorEndpointEventsConsumed.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalActuatorEndpointEventsConsumed.java new file mode 100644 index 0000000..03deb59 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalActuatorEndpointEventsConsumed.java @@ -0,0 +1,36 @@ +package de.accso.flexinale.besucherportal.infrastructure; + +import de.accso.flexinale.common.api.eventbus.EventNotification; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +@Component +@Profile({ "!testdata-backoffice & !testdata-ticketing" }) +@Endpoint(id = "besucherportalEventsConsumed") +public class FlexinaleBesucherPortalActuatorEndpointEventsConsumed implements EventNotification { + private final Queue eventsConsumed = new ConcurrentLinkedQueue<>(); // event list is ordered by consumption time + + @ReadOperation + public List eventsConsumed() { + return eventsConsumed.stream().toList(); + } + + @ReadOperation + public List eventsConsumedFilteredBy(@Selector Identifiable.Id correlationId) { + return eventsConsumed.stream().filter(event -> event.correlationId().equals(correlationId)).toList(); + } + + @Override + public void notify(final Event event) {// might want to use a generic subscriber + eventsConsumed.add(event); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalActuatorEndpointEventsPublished.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalActuatorEndpointEventsPublished.java new file mode 100644 index 0000000..720ce5b --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalActuatorEndpointEventsPublished.java @@ -0,0 +1,36 @@ +package de.accso.flexinale.besucherportal.infrastructure; + +import de.accso.flexinale.common.api.eventbus.EventNotification; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; + +@Component +@Profile({ "!testdata-backoffice & !testdata-ticketing" }) +@Endpoint(id = "besucherportalEventsPublished") +public class FlexinaleBesucherPortalActuatorEndpointEventsPublished implements EventNotification { + private final Queue eventsPublished = new ConcurrentLinkedQueue<>(); // event list is ordered by production time + + @ReadOperation + public List eventsPublished() { + return eventsPublished.stream().toList(); + } + + @ReadOperation + public List eventsPublishedFilteredBy(@Selector Identifiable.Id correlationId) { + return eventsPublished.stream().filter(event -> event.correlationId().equals(correlationId)).toList(); + } + + @Override + public void notify(final Event event) {// might want to use a generic subscriber + eventsPublished.add(event); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalSpringFactory.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalSpringFactory.java new file mode 100644 index 0000000..baa61bb --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/java/de/accso/flexinale/besucherportal/infrastructure/FlexinaleBesucherPortalSpringFactory.java @@ -0,0 +1,106 @@ +package de.accso.flexinale.besucherportal.infrastructure; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import de.accso.flexinale.besucherportal.api_in.event.*; +import de.accso.flexinale.besucherportal.api_out.event.GutscheinEinloesenBeauftragtPublisher; +import de.accso.flexinale.besucherportal.application.services.*; +import de.accso.flexinale.common.application.Config; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Configuration +@Profile({ "!testdata-backoffice & !testdata-ticketing" }) +public class FlexinaleBesucherPortalSpringFactory { + + // Backoffice + + // Film + @Bean + public FilmService createBesucherPortalFilmService(final FKKsVTInMemoryCache cache, + final VorfuehrungService vorfuehrungService, + final Config config) { + return new FilmService(cache, vorfuehrungService, config.getMinZeitZwischenVorfuehrungenInMinuten()); + } + + @Bean + public FilmSubscriber createBesucherPortalFilmSubscriber(final EventBusFactory eventBusFactory, + final FKKsVTInMemoryCache cache, + final FlexinaleBesucherPortalActuatorEndpointEventsConsumed endpointEventsConsumed) { + return new FilmSubscriber(eventBusFactory, cache, endpointEventsConsumed); + } + + // Kino + @Bean + public KinoService createBesucherPortalBesucherPortalKinoService(final FKKsVTInMemoryCache cache) { + return new KinoService(cache); + } + + @Bean + public KinoSubscriber createBesucherPortalKinoSubscriber(final EventBusFactory eventBusFactory, + final FKKsVTInMemoryCache cache, + final FlexinaleBesucherPortalActuatorEndpointEventsConsumed endpointEventsConsumed) { + return new KinoSubscriber(eventBusFactory, cache, endpointEventsConsumed); + } + + // Vorfuehrung + @Bean + public VorfuehrungService createBesucherPortalVorfuehrungService() { + return new VorfuehrungService(); + } + + @Bean + public VorfuehrungSubscriber createBesucherPortalVorfuehrungSubscriber(final EventBusFactory eventBusFactory, + final FKKsVTInMemoryCache cache, + final FlexinaleBesucherPortalActuatorEndpointEventsConsumed endpointEventsConsumed) { + return new VorfuehrungSubscriber(eventBusFactory, cache, endpointEventsConsumed); + } + + // Ticket + @Bean + public TicketSubscriber createBesucherPortalTicketGekauftSubscriber(final EventBusFactory eventBusFactory, + final FKKsVTInMemoryCache cache, + final FlexinaleBesucherPortalActuatorEndpointEventsConsumed endpointEventsConsumed) { + return new TicketSubscriber(eventBusFactory, cache, endpointEventsConsumed); + } + + @Bean + public TicketService createBesucherPortalTicketService( + final GutscheinEinloesenBeauftragtPublication gutscheinEinloesenBeauftragtPublisher) { + return new TicketService(gutscheinEinloesenBeauftragtPublisher); + } + + @Bean + public GutscheinEinloesenBeauftragtPublication createBesucherPortalGutscheinEinloesenBeauftragtPublisher( + final EventBusFactory eventBusFactory, + final FlexinaleBesucherPortalActuatorEndpointEventsPublished endpointEventsPublished) { + return new GutscheinEinloesenBeauftragtPublisher(eventBusFactory, endpointEventsPublished); + } + + // Kontingent + @Bean + public KontingentSubscriber createBesucherPortalKontingentSubscriber(final EventBusFactory eventBusFactory, + final FKKsVTInMemoryCache cache, + final FlexinaleBesucherPortalActuatorEndpointEventsConsumed endpointEventsConsumed) { + return new KontingentSubscriber(eventBusFactory, cache, endpointEventsConsumed); + } + + // ------------------------------------------------------------------------------------------------ + + // Cache + @Bean + public FKKsVTInMemoryCache createBesucherPortalFKKsVTInMemoryCache() { + return new FKKsVTInMemoryCache(); + } + + // ------------------------------------------------------------------------------------------------ + + // Actuator Event serialization + @Bean + public Jackson2ObjectMapperBuilderCustomizer jsonBesucherPortalCustomizer() { + return builder -> builder.visibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/application.properties b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/application.properties new file mode 100644 index 0000000..cf8b9a4 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/application.properties @@ -0,0 +1,74 @@ +application.title=FLEXinale as Distributed Services, Besucherportal +application.version=@pom.version@ @maven.build.timestamp@ +spring.banner.location=classpath:/flexinale-banner.txt + +######################################################################### +# For endpoint /version +######################################################################### +build.version=@pom.version@ +build.date=@maven.build.timestamp@ + +######################################################################### +# Persistence +######################################################################### +spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/distributed-besucherportal +spring.datasource.name=flexinale +spring.datasource.username=flexinale +spring.datasource.password=flexinale +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.database=postgresql +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.generate-ddl=true +# uses schema if existing (or creates anew) +# in a real production environment one should use 'validate' which does just validation but doesn't change anything +spring.jpa.hibernate.ddl-auto=update +# Pros and Cons: See https://www.baeldung.com/spring-open-session-in-view, +# https://stackoverflow.com/questions/30549489/what-is-this-spring-jpa-open-in-view-true-property-in-spring-boot +spring.jpa.open-in-view=false +# spring.jpa.show-sql=true + +######################################################################### +# Web +######################################################################### +spring.thymeleaf.prefix=classpath:/templates/ +spring.thymeleaf.suffix=.html +server.error.path=/error +server.error.include-stacktrace=always +server.error.include-exception=true +server.error.include-message=always +server.error.whitelabel.enabled=false +server.port=8080 + +######################################################################### +# Security +######################################################################### +security.enable-csrf=true + +######################################################################### +# Kafka +######################################################################### +spring.kafka.consumer.group-id=flexinale-distributed-besucherportal + +# Spring Kafka Consumer +spring.kafka.consumer.bootstrap-servers=localhost:29092 +spring.kafka.consumer.key-deserializer=org.apache.kafka.common.serialization.StringDeserializer +spring.kafka.consumer.value-deserializer=org.apache.kafka.common.serialization.StringDeserializer + +# Spring Kafka Producer +spring.kafka.producer.bootstrap-servers=localhost:29092 +spring.kafka.producer.key-serializer=org.apache.kafka.common.serialization.StringSerializer +spring.kafka.producer.value-serializer=org.apache.kafka.common.serialization.StringSerializer + +######################################################################### +# Metrics endpoints, micrometer/prometheus/grafana +######################################################################### +# enable and expose +management.endpoints.web.exposure.include=* +management.endpoint.health.show-details=always +spring.jackson.serialization.FAIL_ON_EMPTY_BEANS=false + +######################################################################### +# flexinale properties +######################################################################### +# time in minutes +de.accso.flexinale.vorfuehrung.min-zeit-zwischen-vorfuehrungen-in-minuten=30 \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/flexinale-banner.txt b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/flexinale-banner.txt new file mode 100644 index 0000000..cac6e52 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/flexinale-banner.txt @@ -0,0 +1,10 @@ +------------------------------------------------------------------------- +,------. ,--. ,------. ,--. ,--. ,--. ,--. +| .---' | | | .---' \ `.' / `--' ,--,--, ,--,--. | | ,---. +| `--, | | | `--, .' \ ,--. | \ ' ,-. | | | | .-. : +| |` | '--. | `---. / .'. \ | | | || | \ '-' | | | \ --. +`--' `-----' `------' '--' '--' `--' `--''--' `--`--' `--' `----' +${application.title} on port ${server.port} +(version ${application.version}) +Powered by Spring Boot ${spring-boot.version} +------------------------------------------------------------------------- diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/logback.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/logback.xml new file mode 100644 index 0000000..87e8669 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/logback.xml @@ -0,0 +1,20 @@ + + + + %d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/background.jpeg b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/background.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..058d4637ce6d184f92c0f2c6f19bbd82cf918466 GIT binary patch literal 32245 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 + + + + + Clear Cache + + + + + + + + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/construction.gif b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/construction.gif new file mode 100644 index 0000000000000000000000000000000000000000..3689979e19fa6732fc8badd018a59c1b04108382 GIT binary patch literal 6159 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 output[i] = c.charCodeAt(0)); + return output; +} + +function stringToBase64Url(s) { + const base64 = window.btoa(s); + return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/\=+$/, ''); +} + +function JSONToBase64Url(data) { + const s = JSON.stringify(data); + return stringToBase64Url(s); +} + +function uint8ArrayToBase64Url(array) { + const s = String.fromCodePoint(...array); + return stringToBase64Url(s); +} + +/** + * @param {string} publicKey public key in URL-safe base64 + * @param {string} privateKey private key in URL-safe base64 + * @param {string} endpoint from PushSubscription + * @param {string} sender either "mailto:" or a web address + * @return {!Promise} the Authorization header + */ +function prepareAuthorization(publicKey, privateKey, endpoint, sender) { + const origin = new URL(endpoint).origin; + const defaultExpiration = Math.floor(Date.now() / 1000) + 43200; // 12 hours in future + + const header = { + typ: 'JWT', + alg: 'ES256' + }; + const jwtPayload = { + aud: origin, + exp: defaultExpiration, + sub: sender, + }; + + // unsignedToken is the URL-safe base64 encoded JSON header and body joined by a dot. + const unsignedToken = JSONToBase64Url(header) + '.' + JSONToBase64Url(jwtPayload); + + // Sign unsignedToken using ES256 (SHA-256 over ECDSA). This requires the private key. + const publicKeyArray = urlB64ToUint8Array(publicKey); + const key = { + kty: 'EC', + crv: 'P-256', + x: uint8ArrayToBase64Url(publicKeyArray.subarray(1, 33)), + y: uint8ArrayToBase64Url(publicKeyArray.subarray(33, 65)), + d: privateKey, + }; + + // Perform the signing. importKey returns a Promise, so wait for it to finish. + const args = {name: 'ECDSA', namedCurve: 'P-256'}; + return crypto.subtle.importKey('jwk', key, args, true, ['sign']) + .then(key => { + return crypto.subtle.sign({ + name: 'ECDSA', + hash: { + name: 'SHA-256', + }, + }, key, (new TextEncoder('utf-8')).encode(unsignedToken)); + }) + .then(buffer => new Uint8Array(buffer)) + .then(signature => { + return 'WebPush ' + unsignedToken + '.' + uint8ArrayToBase64Url(signature); + }); +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/app/main.css b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/app/main.css new file mode 100644 index 0000000..bfc6802 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/app/main.css @@ -0,0 +1,82 @@ +.header { + margin: 15px; +} +.nav.black { + background-color: #000000; +} + +.FLEXnav.distributed { + background-color: darkseagreen; +} +.container { + margin: 15px; + font-size: 85%; +} +.FLEXmain { + margin: 15px; + font-size: 500%; + font-weight: bold; +} +.FLEXmainSmall { + margin: 15px; + font-size: 200%; + font-weight: bold; + color:green; +} +.FLEXerror { + margin: 15px; + font-size: 500%; + font-weight: bold; + color: red; +} +.brand.link { + color: #ffffff; + font-weight: normal; + font-size: large; +} +.brand.day { + color: #ff0000; + font-weight: normal; + font-size: large; +} +.FLEXfooter { + margin: 25px; + font-size: small; +} +.FLEXlist { + width: 100%; + table-layout: fixed; +} + +.FLEXdetails { + width: 100%; + table-layout: fixed; +} +.FLEXid { + width: 12%; + min-width: 10px; +} +.FLEXstring { + width: 22%; + min-width: 32px; +} +.FLEXstring.red { + color: red; +} +.FLEXlink { + width: 10%; + min-width: 20px; +} +.FLEXbutton { + min-width: 102px; +} +.FLEXbutton.red { + background-color: #ff4136; +} +.FLEXbutton.green { + background-color: #2ecc40; +} +.FLEXbutton.lightblue { + background-color: #5897fb; +} + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery-ui.structure.min.css b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery-ui.structure.min.css new file mode 100644 index 0000000..9ebbb9d --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery-ui.structure.min.css @@ -0,0 +1,5 @@ +/*! jQuery UI - v1.12.1 - 2018-11-04 +* http://jqueryui.com +* Copyright jQuery Foundation and other contributors; Licensed MIT */ + +.ui-draggable-handle{-ms-touch-action:none;touch-action:none}.ui-helper-hidden{display:none}.ui-helper-hidden-accessible{border:0;clip:rect(0 0 0 0);height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px}.ui-helper-reset{margin:0;padding:0;border:0;outline:0;line-height:1.3;text-decoration:none;font-size:100%;list-style:none}.ui-helper-clearfix:before,.ui-helper-clearfix:after{content:"";display:table;border-collapse:collapse}.ui-helper-clearfix:after{clear:both}.ui-helper-zfix{width:100%;height:100%;top:0;left:0;position:absolute;opacity:0;filter:Alpha(Opacity=0)}.ui-front{z-index:100}.ui-state-disabled{cursor:default!important;pointer-events:none}.ui-icon{display:inline-block;vertical-align:middle;margin-top:-.25em;position:relative;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat}.ui-widget-icon-block{left:50%;margin-left:-8px;display:block}.ui-widget-overlay{position:fixed;top:0;left:0;width:100%;height:100%}.ui-resizable{position:relative}.ui-resizable-handle{position:absolute;font-size:0.1px;display:block;-ms-touch-action:none;touch-action:none}.ui-resizable-disabled .ui-resizable-handle,.ui-resizable-autohide .ui-resizable-handle{display:none}.ui-resizable-n{cursor:n-resize;height:7px;width:100%;top:-5px;left:0}.ui-resizable-s{cursor:s-resize;height:7px;width:100%;bottom:-5px;left:0}.ui-resizable-e{cursor:e-resize;width:7px;right:-5px;top:0;height:100%}.ui-resizable-w{cursor:w-resize;width:7px;left:-5px;top:0;height:100%}.ui-resizable-se{cursor:se-resize;width:12px;height:12px;right:1px;bottom:1px}.ui-resizable-sw{cursor:sw-resize;width:9px;height:9px;left:-5px;bottom:-5px}.ui-resizable-nw{cursor:nw-resize;width:9px;height:9px;left:-5px;top:-5px}.ui-resizable-ne{cursor:ne-resize;width:9px;height:9px;right:-5px;top:-5px}.ui-selectable{-ms-touch-action:none;touch-action:none}.ui-selectable-helper{position:absolute;z-index:100;border:1px dotted black}.ui-sortable-handle{-ms-touch-action:none;touch-action:none}.ui-accordion .ui-accordion-header{display:block;cursor:pointer;position:relative;margin:2px 0 0 0;padding:.5em .5em .5em .7em;font-size:100%}.ui-accordion .ui-accordion-content{padding:1em 2.2em;border-top:0;overflow:auto}.ui-autocomplete{position:absolute;top:0;left:0;cursor:default}.ui-menu{list-style:none;padding:0;margin:0;display:block;outline:0}.ui-menu .ui-menu{position:absolute}.ui-menu .ui-menu-item{margin:0;cursor:pointer;list-style-image:url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7")}.ui-menu .ui-menu-item-wrapper{position:relative;padding:3px 1em 3px .4em}.ui-menu .ui-menu-divider{margin:5px 0;height:0;font-size:0;line-height:0;border-width:1px 0 0 0}.ui-menu .ui-state-focus,.ui-menu .ui-state-active{margin:-1px}.ui-menu-icons{position:relative}.ui-menu-icons .ui-menu-item-wrapper{padding-left:2em}.ui-menu .ui-icon{position:absolute;top:0;bottom:0;left:.2em;margin:auto 0}.ui-menu .ui-menu-icon{left:auto;right:0}.ui-button{padding:.4em 1em;display:inline-block;position:relative;line-height:normal;margin-right:.1em;cursor:pointer;vertical-align:middle;text-align:center;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;overflow:visible}.ui-button,.ui-button:link,.ui-button:visited,.ui-button:hover,.ui-button:active{text-decoration:none}.ui-button-icon-only{width:2em;box-sizing:border-box;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-button-icon-only{text-indent:0}.ui-button-icon-only .ui-icon{position:absolute;top:50%;left:50%;margin-top:-8px;margin-left:-8px}.ui-button.ui-icon-notext .ui-icon{padding:0;width:2.1em;height:2.1em;text-indent:-9999px;white-space:nowrap}input.ui-button.ui-icon-notext .ui-icon{width:auto;height:auto;text-indent:0;white-space:normal;padding:.4em 1em}input.ui-button::-moz-focus-inner,button.ui-button::-moz-focus-inner{border:0;padding:0}.ui-controlgroup{vertical-align:middle;display:inline-block}.ui-controlgroup > .ui-controlgroup-item{float:left;margin-left:0;margin-right:0}.ui-controlgroup > .ui-controlgroup-item:focus,.ui-controlgroup > .ui-controlgroup-item.ui-visual-focus{z-index:9999}.ui-controlgroup-vertical > .ui-controlgroup-item{display:block;float:none;width:100%;margin-top:0;margin-bottom:0;text-align:left}.ui-controlgroup-vertical .ui-controlgroup-item{box-sizing:border-box}.ui-controlgroup .ui-controlgroup-label{padding:.4em 1em}.ui-controlgroup .ui-controlgroup-label span{font-size:80%}.ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item{border-left:none}.ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item{border-top:none}.ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content{border-right:none}.ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content{border-bottom:none}.ui-controlgroup-vertical .ui-spinner-input{width:75%;width:calc( 100% - 2.4em )}.ui-controlgroup-vertical .ui-spinner .ui-spinner-up{border-top-style:solid}.ui-checkboxradio-label .ui-icon-background{box-shadow:inset 1px 1px 1px #ccc;border-radius:.12em;border:none}.ui-checkboxradio-radio-label .ui-icon-background{width:16px;height:16px;border-radius:1em;overflow:visible;border:none}.ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon,.ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon{background-image:none;width:8px;height:8px;border-width:4px;border-style:solid}.ui-checkboxradio-disabled{pointer-events:none}.ui-datepicker{width:17em;padding:.2em .2em 0;display:none}.ui-datepicker .ui-datepicker-header{position:relative;padding:.2em 0}.ui-datepicker .ui-datepicker-prev,.ui-datepicker .ui-datepicker-next{position:absolute;top:2px;width:1.8em;height:1.8em}.ui-datepicker .ui-datepicker-prev-hover,.ui-datepicker .ui-datepicker-next-hover{top:1px}.ui-datepicker .ui-datepicker-prev{left:2px}.ui-datepicker .ui-datepicker-next{right:2px}.ui-datepicker .ui-datepicker-prev-hover{left:1px}.ui-datepicker .ui-datepicker-next-hover{right:1px}.ui-datepicker .ui-datepicker-prev span,.ui-datepicker .ui-datepicker-next span{display:block;position:absolute;left:50%;margin-left:-8px;top:50%;margin-top:-8px}.ui-datepicker .ui-datepicker-title{margin:0 2.3em;line-height:1.8em;text-align:center}.ui-datepicker .ui-datepicker-title select{font-size:1em;margin:1px 0}.ui-datepicker select.ui-datepicker-month,.ui-datepicker select.ui-datepicker-year{width:45%}.ui-datepicker table{width:100%;font-size:.9em;border-collapse:collapse;margin:0 0 .4em}.ui-datepicker th{padding:.7em .3em;text-align:center;font-weight:bold;border:0}.ui-datepicker td{border:0;padding:1px}.ui-datepicker td span,.ui-datepicker td a{display:block;padding:.2em;text-align:right;text-decoration:none}.ui-datepicker .ui-datepicker-buttonpane{background-image:none;margin:.7em 0 0 0;padding:0 .2em;border-left:0;border-right:0;border-bottom:0}.ui-datepicker .ui-datepicker-buttonpane button{float:right;margin:.5em .2em .4em;cursor:pointer;padding:.2em .6em .3em .6em;width:auto;overflow:visible}.ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current{float:left}.ui-datepicker.ui-datepicker-multi{width:auto}.ui-datepicker-multi .ui-datepicker-group{float:left}.ui-datepicker-multi .ui-datepicker-group table{width:95%;margin:0 auto .4em}.ui-datepicker-multi-2 .ui-datepicker-group{width:50%}.ui-datepicker-multi-3 .ui-datepicker-group{width:33.3%}.ui-datepicker-multi-4 .ui-datepicker-group{width:25%}.ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header{border-left-width:0}.ui-datepicker-multi .ui-datepicker-buttonpane{clear:left}.ui-datepicker-row-break{clear:both;width:100%;font-size:0}.ui-datepicker-rtl{direction:rtl}.ui-datepicker-rtl .ui-datepicker-prev{right:2px;left:auto}.ui-datepicker-rtl .ui-datepicker-next{left:2px;right:auto}.ui-datepicker-rtl .ui-datepicker-prev:hover{right:1px;left:auto}.ui-datepicker-rtl .ui-datepicker-next:hover{left:1px;right:auto}.ui-datepicker-rtl .ui-datepicker-buttonpane{clear:right}.ui-datepicker-rtl .ui-datepicker-buttonpane button{float:left}.ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current,.ui-datepicker-rtl .ui-datepicker-group{float:right}.ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header,.ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header{border-right-width:0;border-left-width:1px}.ui-datepicker .ui-icon{display:block;text-indent:-99999px;overflow:hidden;background-repeat:no-repeat;left:.5em;top:.3em}.ui-dialog{position:absolute;top:0;left:0;padding:.2em;outline:0}.ui-dialog .ui-dialog-titlebar{padding:.4em 1em;position:relative}.ui-dialog .ui-dialog-title{float:left;margin:.1em 0;white-space:nowrap;width:90%;overflow:hidden;text-overflow:ellipsis}.ui-dialog .ui-dialog-titlebar-close{position:absolute;right:.3em;top:50%;width:20px;margin:-10px 0 0 0;padding:1px;height:20px}.ui-dialog .ui-dialog-content{position:relative;border:0;padding:.5em 1em;background:none;overflow:auto}.ui-dialog .ui-dialog-buttonpane{text-align:left;border-width:1px 0 0 0;background-image:none;margin-top:.5em;padding:.3em 1em .5em .4em}.ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset{float:right}.ui-dialog .ui-dialog-buttonpane button{margin:.5em .4em .5em 0;cursor:pointer}.ui-dialog .ui-resizable-n{height:2px;top:0}.ui-dialog .ui-resizable-e{width:2px;right:0}.ui-dialog .ui-resizable-s{height:2px;bottom:0}.ui-dialog .ui-resizable-w{width:2px;left:0}.ui-dialog .ui-resizable-se,.ui-dialog .ui-resizable-sw,.ui-dialog .ui-resizable-ne,.ui-dialog .ui-resizable-nw{width:7px;height:7px}.ui-dialog .ui-resizable-se{right:0;bottom:0}.ui-dialog .ui-resizable-sw{left:0;bottom:0}.ui-dialog .ui-resizable-ne{right:0;top:0}.ui-dialog .ui-resizable-nw{left:0;top:0}.ui-draggable .ui-dialog-titlebar{cursor:move}.ui-progressbar{height:2em;text-align:left;overflow:hidden}.ui-progressbar .ui-progressbar-value{margin:-1px;height:100%}.ui-progressbar .ui-progressbar-overlay{background:url("data:image/gif;base64,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");height:100%;filter:alpha(opacity=25);opacity:0.25}.ui-progressbar-indeterminate .ui-progressbar-value{background-image:none}.ui-selectmenu-menu{padding:0;margin:0;position:absolute;top:0;left:0;display:none}.ui-selectmenu-menu .ui-menu{overflow:auto;overflow-x:hidden;padding-bottom:1px}.ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup{font-size:1em;font-weight:bold;line-height:1.5;padding:2px 0.4em;margin:0.5em 0 0 0;height:auto;border:0}.ui-selectmenu-open{display:block}.ui-selectmenu-text{display:block;margin-right:20px;overflow:hidden;text-overflow:ellipsis}.ui-selectmenu-button.ui-button{text-align:left;white-space:nowrap;width:14em}.ui-selectmenu-icon.ui-icon{float:right;margin-top:0}.ui-slider{position:relative;text-align:left}.ui-slider .ui-slider-handle{position:absolute;z-index:2;width:1.2em;height:1.2em;cursor:default;-ms-touch-action:none;touch-action:none}.ui-slider .ui-slider-range{position:absolute;z-index:1;font-size:.7em;display:block;border:0;background-position:0 0}.ui-slider.ui-state-disabled .ui-slider-handle,.ui-slider.ui-state-disabled .ui-slider-range{filter:inherit}.ui-slider-horizontal{height:.8em}.ui-slider-horizontal .ui-slider-handle{top:-.3em;margin-left:-.6em}.ui-slider-horizontal .ui-slider-range{top:0;height:100%}.ui-slider-horizontal .ui-slider-range-min{left:0}.ui-slider-horizontal .ui-slider-range-max{right:0}.ui-slider-vertical{width:.8em;height:100px}.ui-slider-vertical .ui-slider-handle{left:-.3em;margin-left:0;margin-bottom:-.6em}.ui-slider-vertical .ui-slider-range{left:0;width:100%}.ui-slider-vertical .ui-slider-range-min{bottom:0}.ui-slider-vertical .ui-slider-range-max{top:0}.ui-spinner{position:relative;display:inline-block;overflow:hidden;padding:0;vertical-align:middle}.ui-spinner-input{border:none;background:none;color:inherit;padding:.222em 0;margin:.2em 0;vertical-align:middle;margin-left:.4em;margin-right:2em}.ui-spinner-button{width:1.6em;height:50%;font-size:.5em;padding:0;margin:0;text-align:center;position:absolute;cursor:default;display:block;overflow:hidden;right:0}.ui-spinner a.ui-spinner-button{border-top-style:none;border-bottom-style:none;border-right-style:none}.ui-spinner-up{top:0}.ui-spinner-down{bottom:0}.ui-tabs{position:relative;padding:.2em}.ui-tabs .ui-tabs-nav{margin:0;padding:.2em .2em 0}.ui-tabs .ui-tabs-nav li{list-style:none;float:left;position:relative;top:0;margin:1px .2em 0 0;border-bottom-width:0;padding:0;white-space:nowrap}.ui-tabs .ui-tabs-nav .ui-tabs-anchor{float:left;padding:.5em 1em;text-decoration:none}.ui-tabs .ui-tabs-nav li.ui-tabs-active{margin-bottom:-1px;padding-bottom:1px}.ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor,.ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor{cursor:text}.ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor{cursor:pointer}.ui-tabs .ui-tabs-panel{display:block;border-width:0;padding:1em 1.4em;background:none}.ui-tooltip{padding:8px;position:absolute;z-index:9999;max-width:300px}body .ui-tooltip{border-width:2px} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery-ui.theme.css b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery-ui.theme.css new file mode 100644 index 0000000..14901c7 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery-ui.theme.css @@ -0,0 +1,443 @@ +/*! + * jQuery UI CSS Framework 1.12.1 + * http://jqueryui.com + * + * Copyright jQuery Foundation and other contributors + * Released under the MIT license. + * http://jquery.org/license + * + * http://api.jqueryui.com/category/theming/ + * + * To view and modify this theme, visit http://jqueryui.com/themeroller/?scope=&folderName=base&cornerRadiusShadow=8px&offsetLeftShadow=0px&offsetTopShadow=0px&thicknessShadow=5px&opacityShadow=30&bgImgOpacityShadow=0&bgTextureShadow=flat&bgColorShadow=666666&opacityOverlay=30&bgImgOpacityOverlay=0&bgTextureOverlay=flat&bgColorOverlay=aaaaaa&iconColorError=cc0000&fcError=5f3f3f&borderColorError=f1a899&bgTextureError=flat&bgColorError=fddfdf&iconColorHighlight=777620&fcHighlight=777620&borderColorHighlight=dad55e&bgTextureHighlight=flat&bgColorHighlight=fffa90&iconColorActive=ffffff&fcActive=ffffff&borderColorActive=003eff&bgTextureActive=flat&bgColorActive=007fff&iconColorHover=555555&fcHover=2b2b2b&borderColorHover=cccccc&bgTextureHover=flat&bgColorHover=ededed&iconColorDefault=777777&fcDefault=454545&borderColorDefault=c5c5c5&bgTextureDefault=flat&bgColorDefault=f6f6f6&iconColorContent=444444&fcContent=333333&borderColorContent=dddddd&bgTextureContent=flat&bgColorContent=ffffff&iconColorHeader=444444&fcHeader=333333&borderColorHeader=dddddd&bgTextureHeader=flat&bgColorHeader=e9e9e9&cornerRadius=3px&fwDefault=normal&fsDefault=1em&ffDefault=Arial%2CHelvetica%2Csans-serif + */ + + +/* Component containers +----------------------------------*/ +.ui-widget { + font-family: Arial,Helvetica,sans-serif; + font-size: 1em; +} +.ui-widget .ui-widget { + font-size: 1em; +} +.ui-widget input, +.ui-widget select, +.ui-widget textarea, +.ui-widget button { + font-family: Arial,Helvetica,sans-serif; + font-size: 1em; +} +.ui-widget.ui-widget-content { + border: 1px solid #c5c5c5; +} +.ui-widget-content { + border: 1px solid #dddddd; + background: #ffffff; + color: #333333; +} +.ui-widget-content a { + color: #333333; +} +.ui-widget-header { + border: 1px solid #dddddd; + background: #e9e9e9; + color: #333333; + font-weight: bold; +} +.ui-widget-header a { + color: #333333; +} + +/* Interaction states +----------------------------------*/ +.ui-state-default, +.ui-widget-content .ui-state-default, +.ui-widget-header .ui-state-default, +.ui-button, + +/* We use html here because we need a greater specificity to make sure disabled +works properly when clicked or hovered */ +html .ui-button.ui-state-disabled:hover, +html .ui-button.ui-state-disabled:active { + border: 1px solid #c5c5c5; + background: #f6f6f6; + font-weight: normal; + color: #454545; +} +.ui-state-default a, +.ui-state-default a:link, +.ui-state-default a:visited, +a.ui-button, +a:link.ui-button, +a:visited.ui-button, +.ui-button { + color: #454545; + text-decoration: none; +} +.ui-state-hover, +.ui-widget-content .ui-state-hover, +.ui-widget-header .ui-state-hover, +.ui-state-focus, +.ui-widget-content .ui-state-focus, +.ui-widget-header .ui-state-focus, +.ui-button:hover, +.ui-button:focus { + border: 1px solid #cccccc; + background: #ededed; + font-weight: normal; + color: #2b2b2b; +} +.ui-state-hover a, +.ui-state-hover a:hover, +.ui-state-hover a:link, +.ui-state-hover a:visited, +.ui-state-focus a, +.ui-state-focus a:hover, +.ui-state-focus a:link, +.ui-state-focus a:visited, +a.ui-button:hover, +a.ui-button:focus { + color: #2b2b2b; + text-decoration: none; +} + +.ui-visual-focus { + box-shadow: 0 0 3px 1px rgb(94, 158, 214); +} +.ui-state-active, +.ui-widget-content .ui-state-active, +.ui-widget-header .ui-state-active, +a.ui-button:active, +.ui-button:active, +.ui-button.ui-state-active:hover { + border: 1px solid #003eff; + background: #007fff; + font-weight: normal; + color: #ffffff; +} +.ui-icon-background, +.ui-state-active .ui-icon-background { + border: #003eff; + background-color: #ffffff; +} +.ui-state-active a, +.ui-state-active a:link, +.ui-state-active a:visited { + color: #ffffff; + text-decoration: none; +} + +/* Interaction Cues +----------------------------------*/ +.ui-state-highlight, +.ui-widget-content .ui-state-highlight, +.ui-widget-header .ui-state-highlight { + border: 1px solid #dad55e; + background: #fffa90; + color: #777620; +} +.ui-state-checked { + border: 1px solid #dad55e; + background: #fffa90; +} +.ui-state-highlight a, +.ui-widget-content .ui-state-highlight a, +.ui-widget-header .ui-state-highlight a { + color: #777620; +} +.ui-state-error, +.ui-widget-content .ui-state-error, +.ui-widget-header .ui-state-error { + border: 1px solid #f1a899; + background: #fddfdf; + color: #5f3f3f; +} +.ui-state-error a, +.ui-widget-content .ui-state-error a, +.ui-widget-header .ui-state-error a { + color: #5f3f3f; +} +.ui-state-error-text, +.ui-widget-content .ui-state-error-text, +.ui-widget-header .ui-state-error-text { + color: #5f3f3f; +} +.ui-priority-primary, +.ui-widget-content .ui-priority-primary, +.ui-widget-header .ui-priority-primary { + font-weight: bold; +} +.ui-priority-secondary, +.ui-widget-content .ui-priority-secondary, +.ui-widget-header .ui-priority-secondary { + opacity: .7; + filter:Alpha(Opacity=70); /* support: IE8 */ + font-weight: normal; +} +.ui-state-disabled, +.ui-widget-content .ui-state-disabled, +.ui-widget-header .ui-state-disabled { + opacity: .35; + filter:Alpha(Opacity=35); /* support: IE8 */ + background-image: none; +} +.ui-state-disabled .ui-icon { + filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ +} + +/* Icons +----------------------------------*/ + +/* states and images */ +.ui-icon { + width: 16px; + height: 16px; +} +.ui-icon, +.ui-widget-content .ui-icon { + background-image: url("../../images/ui-icons_444444_256x240.png"); +} +.ui-widget-header .ui-icon { + background-image: url("../../images/ui-icons_444444_256x240.png"); +} +.ui-state-hover .ui-icon, +.ui-state-focus .ui-icon, +.ui-button:hover .ui-icon, +.ui-button:focus .ui-icon { + background-image: url("../../images/ui-icons_555555_256x240.png"); +} +.ui-state-active .ui-icon, +.ui-button:active .ui-icon { + background-image: url("../../images/ui-icons_ffffff_256x240.png"); +} +.ui-state-highlight .ui-icon, +.ui-button .ui-state-highlight.ui-icon { + background-image: url("../../images/ui-icons_777620_256x240.png"); +} +.ui-state-error .ui-icon, +.ui-state-error-text .ui-icon { + background-image: url("../../images/ui-icons_cc0000_256x240.png"); +} +.ui-button .ui-icon { + background-image: url("../../images/ui-icons_777777_256x240.png"); +} + +/* positioning */ +.ui-icon-blank { background-position: 16px 16px; } +.ui-icon-caret-1-n { background-position: 0 0; } +.ui-icon-caret-1-ne { background-position: -16px 0; } +.ui-icon-caret-1-e { background-position: -32px 0; } +.ui-icon-caret-1-se { background-position: -48px 0; } +.ui-icon-caret-1-s { background-position: -65px 0; } +.ui-icon-caret-1-sw { background-position: -80px 0; } +.ui-icon-caret-1-w { background-position: -96px 0; } +.ui-icon-caret-1-nw { background-position: -112px 0; } +.ui-icon-caret-2-n-s { background-position: -128px 0; } +.ui-icon-caret-2-e-w { background-position: -144px 0; } +.ui-icon-triangle-1-n { background-position: 0 -16px; } +.ui-icon-triangle-1-ne { background-position: -16px -16px; } +.ui-icon-triangle-1-e { background-position: -32px -16px; } +.ui-icon-triangle-1-se { background-position: -48px -16px; } +.ui-icon-triangle-1-s { background-position: -65px -16px; } +.ui-icon-triangle-1-sw { background-position: -80px -16px; } +.ui-icon-triangle-1-w { background-position: -96px -16px; } +.ui-icon-triangle-1-nw { background-position: -112px -16px; } +.ui-icon-triangle-2-n-s { background-position: -128px -16px; } +.ui-icon-triangle-2-e-w { background-position: -144px -16px; } +.ui-icon-arrow-1-n { background-position: 0 -32px; } +.ui-icon-arrow-1-ne { background-position: -16px -32px; } +.ui-icon-arrow-1-e { background-position: -32px -32px; } +.ui-icon-arrow-1-se { background-position: -48px -32px; } +.ui-icon-arrow-1-s { background-position: -65px -32px; } +.ui-icon-arrow-1-sw { background-position: -80px -32px; } +.ui-icon-arrow-1-w { background-position: -96px -32px; } +.ui-icon-arrow-1-nw { background-position: -112px -32px; } +.ui-icon-arrow-2-n-s { background-position: -128px -32px; } +.ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } +.ui-icon-arrow-2-e-w { background-position: -160px -32px; } +.ui-icon-arrow-2-se-nw { background-position: -176px -32px; } +.ui-icon-arrowstop-1-n { background-position: -192px -32px; } +.ui-icon-arrowstop-1-e { background-position: -208px -32px; } +.ui-icon-arrowstop-1-s { background-position: -224px -32px; } +.ui-icon-arrowstop-1-w { background-position: -240px -32px; } +.ui-icon-arrowthick-1-n { background-position: 1px -48px; } +.ui-icon-arrowthick-1-ne { background-position: -16px -48px; } +.ui-icon-arrowthick-1-e { background-position: -32px -48px; } +.ui-icon-arrowthick-1-se { background-position: -48px -48px; } +.ui-icon-arrowthick-1-s { background-position: -64px -48px; } +.ui-icon-arrowthick-1-sw { background-position: -80px -48px; } +.ui-icon-arrowthick-1-w { background-position: -96px -48px; } +.ui-icon-arrowthick-1-nw { background-position: -112px -48px; } +.ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } +.ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } +.ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } +.ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } +.ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } +.ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } +.ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } +.ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } +.ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } +.ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } +.ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } +.ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } +.ui-icon-arrowreturn-1-w { background-position: -64px -64px; } +.ui-icon-arrowreturn-1-n { background-position: -80px -64px; } +.ui-icon-arrowreturn-1-e { background-position: -96px -64px; } +.ui-icon-arrowreturn-1-s { background-position: -112px -64px; } +.ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } +.ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } +.ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } +.ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } +.ui-icon-arrow-4 { background-position: 0 -80px; } +.ui-icon-arrow-4-diag { background-position: -16px -80px; } +.ui-icon-extlink { background-position: -32px -80px; } +.ui-icon-newwin { background-position: -48px -80px; } +.ui-icon-refresh { background-position: -64px -80px; } +.ui-icon-shuffle { background-position: -80px -80px; } +.ui-icon-transfer-e-w { background-position: -96px -80px; } +.ui-icon-transferthick-e-w { background-position: -112px -80px; } +.ui-icon-folder-collapsed { background-position: 0 -96px; } +.ui-icon-folder-open { background-position: -16px -96px; } +.ui-icon-document { background-position: -32px -96px; } +.ui-icon-document-b { background-position: -48px -96px; } +.ui-icon-note { background-position: -64px -96px; } +.ui-icon-mail-closed { background-position: -80px -96px; } +.ui-icon-mail-open { background-position: -96px -96px; } +.ui-icon-suitcase { background-position: -112px -96px; } +.ui-icon-comment { background-position: -128px -96px; } +.ui-icon-person { background-position: -144px -96px; } +.ui-icon-print { background-position: -160px -96px; } +.ui-icon-trash { background-position: -176px -96px; } +.ui-icon-locked { background-position: -192px -96px; } +.ui-icon-unlocked { background-position: -208px -96px; } +.ui-icon-bookmark { background-position: -224px -96px; } +.ui-icon-tag { background-position: -240px -96px; } +.ui-icon-home { background-position: 0 -112px; } +.ui-icon-flag { background-position: -16px -112px; } +.ui-icon-calendar { background-position: -32px -112px; } +.ui-icon-cart { background-position: -48px -112px; } +.ui-icon-pencil { background-position: -64px -112px; } +.ui-icon-clock { background-position: -80px -112px; } +.ui-icon-disk { background-position: -96px -112px; } +.ui-icon-calculator { background-position: -112px -112px; } +.ui-icon-zoomin { background-position: -128px -112px; } +.ui-icon-zoomout { background-position: -144px -112px; } +.ui-icon-search { background-position: -160px -112px; } +.ui-icon-wrench { background-position: -176px -112px; } +.ui-icon-gear { background-position: -192px -112px; } +.ui-icon-heart { background-position: -208px -112px; } +.ui-icon-star { background-position: -224px -112px; } +.ui-icon-link { background-position: -240px -112px; } +.ui-icon-cancel { background-position: 0 -128px; } +.ui-icon-plus { background-position: -16px -128px; } +.ui-icon-plusthick { background-position: -32px -128px; } +.ui-icon-minus { background-position: -48px -128px; } +.ui-icon-minusthick { background-position: -64px -128px; } +.ui-icon-close { background-position: -80px -128px; } +.ui-icon-closethick { background-position: -96px -128px; } +.ui-icon-key { background-position: -112px -128px; } +.ui-icon-lightbulb { background-position: -128px -128px; } +.ui-icon-scissors { background-position: -144px -128px; } +.ui-icon-clipboard { background-position: -160px -128px; } +.ui-icon-copy { background-position: -176px -128px; } +.ui-icon-contact { background-position: -192px -128px; } +.ui-icon-image { background-position: -208px -128px; } +.ui-icon-video { background-position: -224px -128px; } +.ui-icon-script { background-position: -240px -128px; } +.ui-icon-alert { background-position: 0 -144px; } +.ui-icon-info { background-position: -16px -144px; } +.ui-icon-notice { background-position: -32px -144px; } +.ui-icon-help { background-position: -48px -144px; } +.ui-icon-check { background-position: -64px -144px; } +.ui-icon-bullet { background-position: -80px -144px; } +.ui-icon-radio-on { background-position: -96px -144px; } +.ui-icon-radio-off { background-position: -112px -144px; } +.ui-icon-pin-w { background-position: -128px -144px; } +.ui-icon-pin-s { background-position: -144px -144px; } +.ui-icon-play { background-position: 0 -160px; } +.ui-icon-pause { background-position: -16px -160px; } +.ui-icon-seek-next { background-position: -32px -160px; } +.ui-icon-seek-prev { background-position: -48px -160px; } +.ui-icon-seek-end { background-position: -64px -160px; } +.ui-icon-seek-start { background-position: -80px -160px; } +/* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ +.ui-icon-seek-first { background-position: -80px -160px; } +.ui-icon-stop { background-position: -96px -160px; } +.ui-icon-eject { background-position: -112px -160px; } +.ui-icon-volume-off { background-position: -128px -160px; } +.ui-icon-volume-on { background-position: -144px -160px; } +.ui-icon-power { background-position: 0 -176px; } +.ui-icon-signal-diag { background-position: -16px -176px; } +.ui-icon-signal { background-position: -32px -176px; } +.ui-icon-battery-0 { background-position: -48px -176px; } +.ui-icon-battery-1 { background-position: -64px -176px; } +.ui-icon-battery-2 { background-position: -80px -176px; } +.ui-icon-battery-3 { background-position: -96px -176px; } +.ui-icon-circle-plus { background-position: 0 -192px; } +.ui-icon-circle-minus { background-position: -16px -192px; } +.ui-icon-circle-close { background-position: -32px -192px; } +.ui-icon-circle-triangle-e { background-position: -48px -192px; } +.ui-icon-circle-triangle-s { background-position: -64px -192px; } +.ui-icon-circle-triangle-w { background-position: -80px -192px; } +.ui-icon-circle-triangle-n { background-position: -96px -192px; } +.ui-icon-circle-arrow-e { background-position: -112px -192px; } +.ui-icon-circle-arrow-s { background-position: -128px -192px; } +.ui-icon-circle-arrow-w { background-position: -144px -192px; } +.ui-icon-circle-arrow-n { background-position: -160px -192px; } +.ui-icon-circle-zoomin { background-position: -176px -192px; } +.ui-icon-circle-zoomout { background-position: -192px -192px; } +.ui-icon-circle-check { background-position: -208px -192px; } +.ui-icon-circlesmall-plus { background-position: 0 -208px; } +.ui-icon-circlesmall-minus { background-position: -16px -208px; } +.ui-icon-circlesmall-close { background-position: -32px -208px; } +.ui-icon-squaresmall-plus { background-position: -48px -208px; } +.ui-icon-squaresmall-minus { background-position: -64px -208px; } +.ui-icon-squaresmall-close { background-position: -80px -208px; } +.ui-icon-grip-dotted-vertical { background-position: 0 -224px; } +.ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } +.ui-icon-grip-solid-vertical { background-position: -32px -224px; } +.ui-icon-grip-solid-horizontal { background-position: -48px -224px; } +.ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } +.ui-icon-grip-diagonal-se { background-position: -80px -224px; } + + +/* Misc visuals +----------------------------------*/ + +/* Corner radius */ +.ui-corner-all, +.ui-corner-top, +.ui-corner-left, +.ui-corner-tl { + border-top-left-radius: 3px; +} +.ui-corner-all, +.ui-corner-top, +.ui-corner-right, +.ui-corner-tr { + border-top-right-radius: 3px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-left, +.ui-corner-bl { + border-bottom-left-radius: 3px; +} +.ui-corner-all, +.ui-corner-bottom, +.ui-corner-right, +.ui-corner-br { + border-bottom-right-radius: 3px; +} + +/* Overlays */ +.ui-widget-overlay { + background: #aaaaaa; + opacity: .3; + filter: Alpha(Opacity=30); /* support: IE8 */ +} +.ui-widget-shadow { + -webkit-box-shadow: 0px 0px 5px #666666; + box-shadow: 0px 0px 5px #666666; +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery.dataTables.css b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery.dataTables.css new file mode 100644 index 0000000..163c0e9 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery.dataTables.css @@ -0,0 +1,448 @@ +/* + * Table styles + */ +table.dataTable { + width: 100%; + margin: 0 auto; + clear: both; + border-collapse: separate; + border-spacing: 0; + /* + * Header and footer styles + */ + /* + * Body styles + */ +} +table.dataTable thead th, +table.dataTable tfoot th { + font-weight: bold; +} +table.dataTable thead th, +table.dataTable thead td { + padding: 10px 18px; + border-bottom: 1px solid #111; +} +table.dataTable thead th:active, +table.dataTable thead td:active { + outline: none; +} +table.dataTable tfoot th, +table.dataTable tfoot td { + padding: 10px 18px 6px 18px; + border-top: 1px solid #111; +} +table.dataTable thead .sorting, +table.dataTable thead .sorting_asc, +table.dataTable thead .sorting_desc, +table.dataTable thead .sorting_asc_disabled, +table.dataTable thead .sorting_desc_disabled { + cursor: pointer; + *cursor: hand; + background-repeat: no-repeat; + background-position: center right; +} +table.dataTable thead .sorting { + background-image: url("../../images/sort_both.png"); +} +table.dataTable thead .sorting_asc { + background-image: url("../../images/sort_asc.png"); +} +table.dataTable thead .sorting_desc { + background-image: url("../../images/sort_desc.png"); +} +table.dataTable thead .sorting_asc_disabled { + background-image: url("../../images/sort_asc_disabled.png"); +} +table.dataTable thead .sorting_desc_disabled { + background-image: url("../../images/sort_desc_disabled.png"); +} +table.dataTable tbody tr { + background-color: #ffffff; +} +table.dataTable tbody tr.selected { + background-color: #B0BED9; +} +table.dataTable tbody th, +table.dataTable tbody td { + padding: 8px 10px; +} +table.dataTable.row-border tbody th, table.dataTable.row-border tbody td, table.dataTable.display tbody th, table.dataTable.display tbody td { + border-top: 1px solid #ddd; +} +table.dataTable.row-border tbody tr:first-child th, +table.dataTable.row-border tbody tr:first-child td, table.dataTable.display tbody tr:first-child th, +table.dataTable.display tbody tr:first-child td { + border-top: none; +} +table.dataTable.cell-border tbody th, table.dataTable.cell-border tbody td { + border-top: 1px solid #ddd; + border-right: 1px solid #ddd; +} +table.dataTable.cell-border tbody tr th:first-child, +table.dataTable.cell-border tbody tr td:first-child { + border-left: 1px solid #ddd; +} +table.dataTable.cell-border tbody tr:first-child th, +table.dataTable.cell-border tbody tr:first-child td { + border-top: none; +} +table.dataTable.stripe tbody tr.odd, table.dataTable.display tbody tr.odd { + background-color: #f9f9f9; +} +table.dataTable.stripe tbody tr.odd.selected, table.dataTable.display tbody tr.odd.selected { + background-color: #acbad4; +} +table.dataTable.hover tbody tr:hover, table.dataTable.display tbody tr:hover { + background-color: #f6f6f6; +} +table.dataTable.hover tbody tr:hover.selected, table.dataTable.display tbody tr:hover.selected { + background-color: #aab7d1; +} +table.dataTable.order-column tbody tr > .sorting_1, +table.dataTable.order-column tbody tr > .sorting_2, +table.dataTable.order-column tbody tr > .sorting_3, table.dataTable.display tbody tr > .sorting_1, +table.dataTable.display tbody tr > .sorting_2, +table.dataTable.display tbody tr > .sorting_3 { + background-color: #fafafa; +} +table.dataTable.order-column tbody tr.selected > .sorting_1, +table.dataTable.order-column tbody tr.selected > .sorting_2, +table.dataTable.order-column tbody tr.selected > .sorting_3, table.dataTable.display tbody tr.selected > .sorting_1, +table.dataTable.display tbody tr.selected > .sorting_2, +table.dataTable.display tbody tr.selected > .sorting_3 { + background-color: #acbad5; +} +table.dataTable.display tbody tr.odd > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd > .sorting_1 { + background-color: #f1f1f1; +} +table.dataTable.display tbody tr.odd > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd > .sorting_2 { + background-color: #f3f3f3; +} +table.dataTable.display tbody tr.odd > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd > .sorting_3 { + background-color: whitesmoke; +} +table.dataTable.display tbody tr.odd.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_1 { + background-color: #a6b4cd; +} +table.dataTable.display tbody tr.odd.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_2 { + background-color: #a8b5cf; +} +table.dataTable.display tbody tr.odd.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.odd.selected > .sorting_3 { + background-color: #a9b7d1; +} +table.dataTable.display tbody tr.even > .sorting_1, table.dataTable.order-column.stripe tbody tr.even > .sorting_1 { + background-color: #fafafa; +} +table.dataTable.display tbody tr.even > .sorting_2, table.dataTable.order-column.stripe tbody tr.even > .sorting_2 { + background-color: #fcfcfc; +} +table.dataTable.display tbody tr.even > .sorting_3, table.dataTable.order-column.stripe tbody tr.even > .sorting_3 { + background-color: #fefefe; +} +table.dataTable.display tbody tr.even.selected > .sorting_1, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_1 { + background-color: #acbad5; +} +table.dataTable.display tbody tr.even.selected > .sorting_2, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_2 { + background-color: #aebcd6; +} +table.dataTable.display tbody tr.even.selected > .sorting_3, table.dataTable.order-column.stripe tbody tr.even.selected > .sorting_3 { + background-color: #afbdd8; +} +table.dataTable.display tbody tr:hover > .sorting_1, table.dataTable.order-column.hover tbody tr:hover > .sorting_1 { + background-color: #eaeaea; +} +table.dataTable.display tbody tr:hover > .sorting_2, table.dataTable.order-column.hover tbody tr:hover > .sorting_2 { + background-color: #ececec; +} +table.dataTable.display tbody tr:hover > .sorting_3, table.dataTable.order-column.hover tbody tr:hover > .sorting_3 { + background-color: #efefef; +} +table.dataTable.display tbody tr:hover.selected > .sorting_1, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_1 { + background-color: #a2aec7; +} +table.dataTable.display tbody tr:hover.selected > .sorting_2, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_2 { + background-color: #a3b0c9; +} +table.dataTable.display tbody tr:hover.selected > .sorting_3, table.dataTable.order-column.hover tbody tr:hover.selected > .sorting_3 { + background-color: #a5b2cb; +} +table.dataTable.no-footer { + border-bottom: 1px solid #111; +} +table.dataTable.nowrap th, table.dataTable.nowrap td { + white-space: nowrap; +} +table.dataTable.compact thead th, +table.dataTable.compact thead td { + padding: 4px 17px 4px 4px; +} +table.dataTable.compact tfoot th, +table.dataTable.compact tfoot td { + padding: 4px; +} +table.dataTable.compact tbody th, +table.dataTable.compact tbody td { + padding: 4px; +} +table.dataTable th.dt-left, +table.dataTable td.dt-left { + text-align: left; +} +table.dataTable th.dt-center, +table.dataTable td.dt-center, +table.dataTable td.dataTables_empty { + text-align: center; +} +table.dataTable th.dt-right, +table.dataTable td.dt-right { + text-align: right; +} +table.dataTable th.dt-justify, +table.dataTable td.dt-justify { + text-align: justify; +} +table.dataTable th.dt-nowrap, +table.dataTable td.dt-nowrap { + white-space: nowrap; +} +table.dataTable thead th.dt-head-left, +table.dataTable thead td.dt-head-left, +table.dataTable tfoot th.dt-head-left, +table.dataTable tfoot td.dt-head-left { + text-align: left; +} +table.dataTable thead th.dt-head-center, +table.dataTable thead td.dt-head-center, +table.dataTable tfoot th.dt-head-center, +table.dataTable tfoot td.dt-head-center { + text-align: center; +} +table.dataTable thead th.dt-head-right, +table.dataTable thead td.dt-head-right, +table.dataTable tfoot th.dt-head-right, +table.dataTable tfoot td.dt-head-right { + text-align: right; +} +table.dataTable thead th.dt-head-justify, +table.dataTable thead td.dt-head-justify, +table.dataTable tfoot th.dt-head-justify, +table.dataTable tfoot td.dt-head-justify { + text-align: justify; +} +table.dataTable thead th.dt-head-nowrap, +table.dataTable thead td.dt-head-nowrap, +table.dataTable tfoot th.dt-head-nowrap, +table.dataTable tfoot td.dt-head-nowrap { + white-space: nowrap; +} +table.dataTable tbody th.dt-body-left, +table.dataTable tbody td.dt-body-left { + text-align: left; +} +table.dataTable tbody th.dt-body-center, +table.dataTable tbody td.dt-body-center { + text-align: center; +} +table.dataTable tbody th.dt-body-right, +table.dataTable tbody td.dt-body-right { + text-align: right; +} +table.dataTable tbody th.dt-body-justify, +table.dataTable tbody td.dt-body-justify { + text-align: justify; +} +table.dataTable tbody th.dt-body-nowrap, +table.dataTable tbody td.dt-body-nowrap { + white-space: nowrap; +} + +table.dataTable, +table.dataTable th, +table.dataTable td { + box-sizing: content-box; +} + +/* + * Control feature layout + */ +.dataTables_wrapper { + position: relative; + clear: both; + *zoom: 1; + zoom: 1; +} +.dataTables_wrapper .dataTables_length { + float: left; +} +.dataTables_wrapper .dataTables_filter { + float: right; + text-align: right; +} +.dataTables_wrapper .dataTables_filter input { + margin-left: 0.5em; +} +.dataTables_wrapper .dataTables_info { + clear: both; + float: left; + padding-top: 0.755em; +} +.dataTables_wrapper .dataTables_paginate { + float: right; + text-align: right; + padding-top: 0.25em; +} +.dataTables_wrapper .dataTables_paginate .paginate_button { + box-sizing: border-box; + display: inline-block; + min-width: 1.5em; + padding: 0.5em 1em; + margin-left: 2px; + text-align: center; + text-decoration: none !important; + cursor: pointer; + *cursor: hand; + color: #333 !important; + border: 1px solid transparent; + border-radius: 2px; +} +.dataTables_wrapper .dataTables_paginate .paginate_button.current, .dataTables_wrapper .dataTables_paginate .paginate_button.current:hover { + color: #333 !important; + border: 1px solid #979797; + background-color: white; + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, white), color-stop(100%, #dcdcdc)); + /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, white 0%, #dcdcdc 100%); + /* Chrome10+,Safari5.1+ */ + background: -moz-linear-gradient(top, white 0%, #dcdcdc 100%); + /* FF3.6+ */ + background: -ms-linear-gradient(top, white 0%, #dcdcdc 100%); + /* IE10+ */ + background: -o-linear-gradient(top, white 0%, #dcdcdc 100%); + /* Opera 11.10+ */ + background: linear-gradient(to bottom, white 0%, #dcdcdc 100%); + /* W3C */ +} +.dataTables_wrapper .dataTables_paginate .paginate_button.disabled, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:hover, .dataTables_wrapper .dataTables_paginate .paginate_button.disabled:active { + cursor: default; + color: #666 !important; + border: 1px solid transparent; + background: transparent; + box-shadow: none; +} +.dataTables_wrapper .dataTables_paginate .paginate_button:hover { + color: white !important; + border: 1px solid #111; + background-color: #585858; + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #585858), color-stop(100%, #111)); + /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #585858 0%, #111 100%); + /* Chrome10+,Safari5.1+ */ + background: -moz-linear-gradient(top, #585858 0%, #111 100%); + /* FF3.6+ */ + background: -ms-linear-gradient(top, #585858 0%, #111 100%); + /* IE10+ */ + background: -o-linear-gradient(top, #585858 0%, #111 100%); + /* Opera 11.10+ */ + background: linear-gradient(to bottom, #585858 0%, #111 100%); + /* W3C */ +} +.dataTables_wrapper .dataTables_paginate .paginate_button:active { + outline: none; + background-color: #2b2b2b; + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #2b2b2b), color-stop(100%, #0c0c0c)); + /* Chrome,Safari4+ */ + background: -webkit-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); + /* Chrome10+,Safari5.1+ */ + background: -moz-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); + /* FF3.6+ */ + background: -ms-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); + /* IE10+ */ + background: -o-linear-gradient(top, #2b2b2b 0%, #0c0c0c 100%); + /* Opera 11.10+ */ + background: linear-gradient(to bottom, #2b2b2b 0%, #0c0c0c 100%); + /* W3C */ + box-shadow: inset 0 0 3px #111; +} +.dataTables_wrapper .dataTables_paginate .ellipsis { + padding: 0 1em; +} +.dataTables_wrapper .dataTables_processing { + position: absolute; + top: 50%; + left: 50%; + width: 100%; + height: 40px; + margin-left: -50%; + margin-top: -25px; + padding-top: 20px; + text-align: center; + font-size: 1.2em; + background-color: white; + background: -webkit-gradient(linear, left top, right top, color-stop(0%, rgba(255, 255, 255, 0)), color-stop(25%, rgba(255, 255, 255, 0.9)), color-stop(75%, rgba(255, 255, 255, 0.9)), color-stop(100%, rgba(255, 255, 255, 0))); + background: -webkit-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); + background: -moz-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); + background: -ms-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); + background: -o-linear-gradient(left, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); + background: linear-gradient(to right, rgba(255, 255, 255, 0) 0%, rgba(255, 255, 255, 0.9) 25%, rgba(255, 255, 255, 0.9) 75%, rgba(255, 255, 255, 0) 100%); +} +.dataTables_wrapper .dataTables_length, +.dataTables_wrapper .dataTables_filter, +.dataTables_wrapper .dataTables_info, +.dataTables_wrapper .dataTables_processing, +.dataTables_wrapper .dataTables_paginate { + color: #333; +} +.dataTables_wrapper .dataTables_scroll { + clear: both; +} +.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody { + *margin-top: -1px; + -webkit-overflow-scrolling: touch; +} +.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td { + vertical-align: middle; +} +.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > th > div.dataTables_sizing, +.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > thead > tr > td > div.dataTables_sizing, .dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > th > div.dataTables_sizing, +.dataTables_wrapper .dataTables_scroll div.dataTables_scrollBody > table > tbody > tr > td > div.dataTables_sizing { + height: 0; + overflow: hidden; + margin: 0 !important; + padding: 0 !important; +} +.dataTables_wrapper.no-footer .dataTables_scrollBody { + border-bottom: 1px solid #111; +} +.dataTables_wrapper.no-footer div.dataTables_scrollHead table.dataTable, +.dataTables_wrapper.no-footer div.dataTables_scrollBody > table { + border-bottom: none; +} +.dataTables_wrapper:after { + visibility: hidden; + display: block; + content: ""; + clear: both; + height: 0; +} + +@media screen and (max-width: 767px) { + .dataTables_wrapper .dataTables_info, + .dataTables_wrapper .dataTables_paginate { + float: none; + text-align: center; + } + .dataTables_wrapper .dataTables_paginate { + margin-top: 0.5em; + } +} +@media screen and (max-width: 640px) { + .dataTables_wrapper .dataTables_length, + .dataTables_wrapper .dataTables_filter { + float: none; + text-align: center; + } + .dataTables_wrapper .dataTables_filter { + margin-top: 0.5em; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery.ui.timepicker.css b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery.ui.timepicker.css new file mode 100644 index 0000000..7eb8bda --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/jquery.ui.timepicker.css @@ -0,0 +1,57 @@ +/* + * Timepicker stylesheet + * Highly inspired from datepicker + * FG - Nov 2010 - Web3R + * + * version 0.0.3 : Fixed some settings, more dynamic + * version 0.0.4 : Removed width:100% on tables + * version 0.1.1 : set width 0 on tables to fix an ie6 bug + */ + +.ui-timepicker-inline { display: inline; } + +#ui-timepicker-div { padding: 0.2em; } +.ui-timepicker-table { display: inline-table; width: 0; } +.ui-timepicker-table table { margin:0.15em 0 0 0; border-collapse: collapse; } + +.ui-timepicker-hours, .ui-timepicker-minutes { padding: 0.2em; } + +.ui-timepicker-table .ui-timepicker-title { line-height: 1.8em; text-align: center; } +.ui-timepicker-table td { padding: 0.1em; width: 2.2em; } +.ui-timepicker-table th.periods { padding: 0.1em; width: 2.2em; } + +/* span for disabled cells */ +.ui-timepicker-table td span { + display:block; + padding:0.2em 0.3em 0.2em 0.5em; + width: 1.2em; + + text-align:right; + text-decoration:none; +} +/* anchors for clickable cells */ +.ui-timepicker-table td a { + display:block; + padding:0.2em 0.3em 0.2em 0.5em; + /*width: 1.2em;*/ + cursor: pointer; + text-align:right; + text-decoration:none; +} + + +/* buttons and button pane styling */ +.ui-timepicker .ui-timepicker-buttonpane { + background-image: none; margin: .7em 0 0 0; padding:0 .2em; border-left: 0; border-right: 0; border-bottom: 0; +} +.ui-timepicker .ui-timepicker-buttonpane button { margin: .5em .2em .4em; cursor: pointer; padding: .2em .6em .3em .6em; width:auto; overflow:visible; } +/* The close button */ +.ui-timepicker .ui-timepicker-close { float: right } + +/* the now button */ +.ui-timepicker .ui-timepicker-now { float: left; } + +/* the deselect button */ +.ui-timepicker .ui-timepicker-deselect { float: left; } + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/picnic.min.css b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/picnic.min.css new file mode 100644 index 0000000..808ef1a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/picnic.min.css @@ -0,0 +1,2 @@ +/* Picnic CSS v6.5.0 http://picnicss.com/ */ +html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:0;padding:0}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}*{box-sizing:inherit}html,body{font-family:Arial, Helvetica, sans-serif;box-sizing:border-box;height:100%}body{color:#111;font-size:1.1em;line-height:1.5;background:#fff}main{display:block}h1,h2,h3,h4,h5,h6{margin:0;padding:.6em 0}li{margin:0 0 .3em}a{color:#0074d9;text-decoration:none;box-shadow:none;transition:all 0.3s}code{padding:.3em .6em;font-size:.8em;background:#f5f5f5}pre{text-align:left;padding:.3em .6em;background:#f5f5f5;border-radius:.2em}pre code{padding:0}blockquote{padding:0 0 0 1em;margin:0 0 0 .1em;box-shadow:inset 5px 0 rgba(17,17,17,0.3)}label{cursor:pointer}[class^="icon-"]:before,[class*=" icon-"]:before{margin:0 .6em 0 0}i[class^="icon-"]:before,i[class*=" icon-"]:before{margin:0}.label,[data-tooltip]:after,button,.button,[type=submit],.dropimage{display:inline-block;text-align:center;letter-spacing:inherit;margin:0;padding:.3em .9em;vertical-align:middle;background:#0074d9;color:#fff;border:0;border-radius:.2em;width:auto;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.success.label,.success[data-tooltip]:after,button.success,.success.button,.success[type=submit],.success.dropimage{background:#2ecc40}.warning.label,.warning[data-tooltip]:after,button.warning,.warning.button,.warning[type=submit],.warning.dropimage{background:#ff851b}.error.label,.error[data-tooltip]:after,button.error,.error.button,.error[type=submit],.error.dropimage{background:#ff4136}.pseudo.label,.pseudo[data-tooltip]:after,button.pseudo,.pseudo.button,.pseudo[type=submit],.pseudo.dropimage{background-color:transparent;color:inherit}.label,[data-tooltip]:after{font-size:.6em;padding:.4em .6em;margin-left:1em;line-height:1}button,.button,[type=submit],.dropimage{margin:.3em 0;cursor:pointer;transition:all 0.3s;border-radius:.2em;height:auto;vertical-align:baseline;box-shadow:0 0 transparent inset}button:hover,.button:hover,[type=submit]:hover,.dropimage:hover,button:focus,.button:focus,[type=submit]:focus,.dropimage:focus{box-shadow:inset 0 0 0 99em rgba(255,255,255,0.2);border:0}button.pseudo:hover,.pseudo.button:hover,.pseudo[type=submit]:hover,.pseudo.dropimage:hover,button.pseudo:focus,.pseudo.button:focus,.pseudo[type=submit]:focus,.pseudo.dropimage:focus{box-shadow:inset 0 0 0 99em rgba(17,17,17,0.1)}button.active,.active.button,.active[type=submit],.active.dropimage,button:active,.button:active,[type=submit]:active,.dropimage:active,button.pseudo:active,.pseudo.button:active,.pseudo[type=submit]:active,.pseudo.dropimage:active{box-shadow:inset 0 0 0 99em rgba(17,17,17,0.2)}button[disabled],[disabled].button,[disabled][type=submit],[disabled].dropimage{cursor:default;box-shadow:none;background:#bbb}:checked+.toggle,:checked+.toggle:hover{box-shadow:inset 0 0 0 99em rgba(17,17,17,0.2)}[type]+.toggle{padding:.3em .9em;margin-right:0}[type]+.toggle:after,[type]+.toggle:before{display:none}input,textarea,.select select{line-height:1.5;margin:0;height:2.1em;padding:.3em .6em;border:1px solid #ccc;background-color:#fff;border-radius:.2em;transition:all 0.3s;width:100%}input:focus,textarea:focus,.select select:focus{border:1px solid #0074d9;outline:0}textarea{height:auto}[type=file],[type=color]{cursor:pointer}[type=file]{height:auto}select{background:#fff url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyIiBoZWlnaHQ9IjMiPjxwYXRoIGQ9Im0gMCwxIDEsMiAxLC0yIHoiLz48L3N2Zz4=) no-repeat scroll 95% center/10px 15px;background-position:calc(100% - 15px) center;border:1px solid #ccc;border-radius:.2em;cursor:pointer;width:100%;height:2.2em;box-sizing:border-box;padding:.3em .45em;transition:all .3s;-moz-appearance:none;-webkit-appearance:none;appearance:none}select::-ms-expand{display:none}select:focus,select:active{border:1px solid #0074d9;transition:outline 0s}select:-moz-focusring{color:transparent;text-shadow:0 0 0 #111}select option{font-size:inherit;padding:.3em .45em}[type=radio],[type=checkbox]{opacity:0;width:0;position:absolute;display:inline-block}[type=radio]+.checkable:hover:before,[type=checkbox]+.checkable:hover:before,[type=radio]:focus+.checkable:before,[type=checkbox]:focus+.checkable:before{border:1px solid #0074d9}[type=radio]+.checkable,[type=checkbox]+.checkable{position:relative;cursor:pointer;padding-left:1.5em;margin-right:.6em}[type=radio]+.checkable:before,[type=checkbox]+.checkable:before,[type=radio]+.checkable:after,[type=checkbox]+.checkable:after{content:'';position:absolute;display:inline-block;left:0;top:50%;transform:translateY(-50%);font-size:1em;line-height:1em;color:transparent;font-family:sans;text-align:center;box-sizing:border-box;width:1em;height:1em;border-radius:50%;transition:all 0.3s}[type=radio]+.checkable:before,[type=checkbox]+.checkable:before{border:1px solid #aaa}[type=radio]:checked+.checkable:after,[type=checkbox]:checked+.checkable:after{background:#555;transform:scale(0.5) translateY(-100%)}[type=checkbox]+.checkable:before{border-radius:.2em}[type=checkbox]+.checkable:after{content:"✔";background:none;transform:scale(2) translateY(-25%);visibility:hidden;opacity:0}[type=checkbox]:checked+.checkable:after{color:#111;background:none;transform:translateY(-50%);transition:all 0.3s;visibility:visible;opacity:1}table{text-align:left}td,th{padding:.3em 2.4em .3em .6em}th{text-align:left;font-weight:900;color:#fff;background-color:#0074d9}.success th{background-color:#2ecc40}.warning th{background-color:#ff851b}.error th{background-color:#ff4136}.dull th{background-color:#aaa}tr:nth-child(even){background:rgba(0,0,0,0.05)}.flex{display:-ms-flexbox;display:flex;margin-left:-0.6em;width:calc(100% + .6em);flex-wrap:wrap;transition:all .3s ease}.flex>*{box-sizing:border-box;flex:1 1 auto;padding-left:.6em;padding-bottom:.6em}.flex[class*="one"]>*,.flex[class*="two"]>*,.flex[class*="three"]>*,.flex[class*="four"]>*,.flex[class*="five"]>*,.flex[class*="six"]>*,.flex[class*="seven"]>*,.flex[class*="eight"]>*,.flex[class*="nine"]>*,.flex[class*="ten"]>*,.flex[class*="eleven"]>*,.flex[class*="twelve"]>*{flex-grow:0}.flex.grow>*{flex-grow:1}.center{justify-content:center}.one>*{width:100%}.two>*{width:50%}.three>*{width:33.33333%}.four>*{width:25%}.five>*{width:20%}.six>*{width:16.66666%}.seven>*{width:14.28571%}.eight>*{width:12.5%}.nine>*{width:11.11111%}.ten>*{width:10%}.eleven>*{width:9.09091%}.twelve>*{width:8.33333%}@media all and (min-width: 500px){.one-500>*{width:100%}.two-500>*{width:50%}.three-500>*{width:33.33333%}.four-500>*{width:25%}.five-500>*{width:20%}.six-500>*{width:16.66666%}.seven-500>*{width:14.28571%}.eight-500>*{width:12.5%}.nine-500>*{width:11.11111%}.ten-500>*{width:10%}.eleven-500>*{width:9.09091%}.twelve-500>*{width:8.33333%}}@media all and (min-width: 600px){.one-600>*{width:100%}.two-600>*{width:50%}.three-600>*{width:33.33333%}.four-600>*{width:25%}.five-600>*{width:20%}.six-600>*{width:16.66666%}.seven-600>*{width:14.28571%}.eight-600>*{width:12.5%}.nine-600>*{width:11.11111%}.ten-600>*{width:10%}.eleven-600>*{width:9.09091%}.twelve-600>*{width:8.33333%}}@media all and (min-width: 700px){.one-700>*{width:100%}.two-700>*{width:50%}.three-700>*{width:33.33333%}.four-700>*{width:25%}.five-700>*{width:20%}.six-700>*{width:16.66666%}.seven-700>*{width:14.28571%}.eight-700>*{width:12.5%}.nine-700>*{width:11.11111%}.ten-700>*{width:10%}.eleven-700>*{width:9.09091%}.twelve-700>*{width:8.33333%}}@media all and (min-width: 800px){.one-800>*{width:100%}.two-800>*{width:50%}.three-800>*{width:33.33333%}.four-800>*{width:25%}.five-800>*{width:20%}.six-800>*{width:16.66666%}.seven-800>*{width:14.28571%}.eight-800>*{width:12.5%}.nine-800>*{width:11.11111%}.ten-800>*{width:10%}.eleven-800>*{width:9.09091%}.twelve-800>*{width:8.33333%}}@media all and (min-width: 900px){.one-900>*{width:100%}.two-900>*{width:50%}.three-900>*{width:33.33333%}.four-900>*{width:25%}.five-900>*{width:20%}.six-900>*{width:16.66666%}.seven-900>*{width:14.28571%}.eight-900>*{width:12.5%}.nine-900>*{width:11.11111%}.ten-900>*{width:10%}.eleven-900>*{width:9.09091%}.twelve-900>*{width:8.33333%}}@media all and (min-width: 1000px){.one-1000>*{width:100%}.two-1000>*{width:50%}.three-1000>*{width:33.33333%}.four-1000>*{width:25%}.five-1000>*{width:20%}.six-1000>*{width:16.66666%}.seven-1000>*{width:14.28571%}.eight-1000>*{width:12.5%}.nine-1000>*{width:11.11111%}.ten-1000>*{width:10%}.eleven-1000>*{width:9.09091%}.twelve-1000>*{width:8.33333%}}@media all and (min-width: 1100px){.one-1100>*{width:100%}.two-1100>*{width:50%}.three-1100>*{width:33.33333%}.four-1100>*{width:25%}.five-1100>*{width:20%}.six-1100>*{width:16.66666%}.seven-1100>*{width:14.28571%}.eight-1100>*{width:12.5%}.nine-1100>*{width:11.11111%}.ten-1100>*{width:10%}.eleven-1100>*{width:9.09091%}.twelve-1100>*{width:8.33333%}}@media all and (min-width: 1200px){.one-1200>*{width:100%}.two-1200>*{width:50%}.three-1200>*{width:33.33333%}.four-1200>*{width:25%}.five-1200>*{width:20%}.six-1200>*{width:16.66666%}.seven-1200>*{width:14.28571%}.eight-1200>*{width:12.5%}.nine-1200>*{width:11.11111%}.ten-1200>*{width:10%}.eleven-1200>*{width:9.09091%}.twelve-1200>*{width:8.33333%}}@media all and (min-width: 1300px){.one-1300>*{width:100%}.two-1300>*{width:50%}.three-1300>*{width:33.33333%}.four-1300>*{width:25%}.five-1300>*{width:20%}.six-1300>*{width:16.66666%}.seven-1300>*{width:14.28571%}.eight-1300>*{width:12.5%}.nine-1300>*{width:11.11111%}.ten-1300>*{width:10%}.eleven-1300>*{width:9.09091%}.twelve-1300>*{width:8.33333%}}@media all and (min-width: 1400px){.one-1400>*{width:100%}.two-1400>*{width:50%}.three-1400>*{width:33.33333%}.four-1400>*{width:25%}.five-1400>*{width:20%}.six-1400>*{width:16.66666%}.seven-1400>*{width:14.28571%}.eight-1400>*{width:12.5%}.nine-1400>*{width:11.11111%}.ten-1400>*{width:10%}.eleven-1400>*{width:9.09091%}.twelve-1400>*{width:8.33333%}}@media all and (min-width: 1500px){.one-1500>*{width:100%}.two-1500>*{width:50%}.three-1500>*{width:33.33333%}.four-1500>*{width:25%}.five-1500>*{width:20%}.six-1500>*{width:16.66666%}.seven-1500>*{width:14.28571%}.eight-1500>*{width:12.5%}.nine-1500>*{width:11.11111%}.ten-1500>*{width:10%}.eleven-1500>*{width:9.09091%}.twelve-1500>*{width:8.33333%}}@media all and (min-width: 1600px){.one-1600>*{width:100%}.two-1600>*{width:50%}.three-1600>*{width:33.33333%}.four-1600>*{width:25%}.five-1600>*{width:20%}.six-1600>*{width:16.66666%}.seven-1600>*{width:14.28571%}.eight-1600>*{width:12.5%}.nine-1600>*{width:11.11111%}.ten-1600>*{width:10%}.eleven-1600>*{width:9.09091%}.twelve-1600>*{width:8.33333%}}@media all and (min-width: 1700px){.one-1700>*{width:100%}.two-1700>*{width:50%}.three-1700>*{width:33.33333%}.four-1700>*{width:25%}.five-1700>*{width:20%}.six-1700>*{width:16.66666%}.seven-1700>*{width:14.28571%}.eight-1700>*{width:12.5%}.nine-1700>*{width:11.11111%}.ten-1700>*{width:10%}.eleven-1700>*{width:9.09091%}.twelve-1700>*{width:8.33333%}}@media all and (min-width: 1800px){.one-1800>*{width:100%}.two-1800>*{width:50%}.three-1800>*{width:33.33333%}.four-1800>*{width:25%}.five-1800>*{width:20%}.six-1800>*{width:16.66666%}.seven-1800>*{width:14.28571%}.eight-1800>*{width:12.5%}.nine-1800>*{width:11.11111%}.ten-1800>*{width:10%}.eleven-1800>*{width:9.09091%}.twelve-1800>*{width:8.33333%}}@media all and (min-width: 1900px){.one-1900>*{width:100%}.two-1900>*{width:50%}.three-1900>*{width:33.33333%}.four-1900>*{width:25%}.five-1900>*{width:20%}.six-1900>*{width:16.66666%}.seven-1900>*{width:14.28571%}.eight-1900>*{width:12.5%}.nine-1900>*{width:11.11111%}.ten-1900>*{width:10%}.eleven-1900>*{width:9.09091%}.twelve-1900>*{width:8.33333%}}@media all and (min-width: 2000px){.one-2000>*{width:100%}.two-2000>*{width:50%}.three-2000>*{width:33.33333%}.four-2000>*{width:25%}.five-2000>*{width:20%}.six-2000>*{width:16.66666%}.seven-2000>*{width:14.28571%}.eight-2000>*{width:12.5%}.nine-2000>*{width:11.11111%}.ten-2000>*{width:10%}.eleven-2000>*{width:9.09091%}.twelve-2000>*{width:8.33333%}}.full{width:100%}.half{width:50%}.third{width:33.33333%}.two-third{width:66.66666%}.fourth{width:25%}.three-fourth{width:75%}.fifth{width:20%}.two-fifth{width:40%}.three-fifth{width:60%}.four-fifth{width:80%}.sixth{width:16.66666%}.none{display:none}@media all and (min-width: 500px){.full-500{width:100%;display:block}.half-500{width:50%;display:block}.third-500{width:33.33333%;display:block}.two-third-500{width:66.66666%;display:block}.fourth-500{width:25%;display:block}.three-fourth-500{width:75%;display:block}.fifth-500{width:20%;display:block}.two-fifth-500{width:40%;display:block}.three-fifth-500{width:60%;display:block}.four-fifth-500{width:80%;display:block}.sixth-500{width:16.66666%;display:block}}@media all and (min-width: 600px){.full-600{width:100%;display:block}.half-600{width:50%;display:block}.third-600{width:33.33333%;display:block}.two-third-600{width:66.66666%;display:block}.fourth-600{width:25%;display:block}.three-fourth-600{width:75%;display:block}.fifth-600{width:20%;display:block}.two-fifth-600{width:40%;display:block}.three-fifth-600{width:60%;display:block}.four-fifth-600{width:80%;display:block}.sixth-600{width:16.66666%;display:block}}@media all and (min-width: 700px){.full-700{width:100%;display:block}.half-700{width:50%;display:block}.third-700{width:33.33333%;display:block}.two-third-700{width:66.66666%;display:block}.fourth-700{width:25%;display:block}.three-fourth-700{width:75%;display:block}.fifth-700{width:20%;display:block}.two-fifth-700{width:40%;display:block}.three-fifth-700{width:60%;display:block}.four-fifth-700{width:80%;display:block}.sixth-700{width:16.66666%;display:block}}@media all and (min-width: 800px){.full-800{width:100%;display:block}.half-800{width:50%;display:block}.third-800{width:33.33333%;display:block}.two-third-800{width:66.66666%;display:block}.fourth-800{width:25%;display:block}.three-fourth-800{width:75%;display:block}.fifth-800{width:20%;display:block}.two-fifth-800{width:40%;display:block}.three-fifth-800{width:60%;display:block}.four-fifth-800{width:80%;display:block}.sixth-800{width:16.66666%;display:block}}@media all and (min-width: 900px){.full-900{width:100%;display:block}.half-900{width:50%;display:block}.third-900{width:33.33333%;display:block}.two-third-900{width:66.66666%;display:block}.fourth-900{width:25%;display:block}.three-fourth-900{width:75%;display:block}.fifth-900{width:20%;display:block}.two-fifth-900{width:40%;display:block}.three-fifth-900{width:60%;display:block}.four-fifth-900{width:80%;display:block}.sixth-900{width:16.66666%;display:block}}@media all and (min-width: 1000px){.full-1000{width:100%;display:block}.half-1000{width:50%;display:block}.third-1000{width:33.33333%;display:block}.two-third-1000{width:66.66666%;display:block}.fourth-1000{width:25%;display:block}.three-fourth-1000{width:75%;display:block}.fifth-1000{width:20%;display:block}.two-fifth-1000{width:40%;display:block}.three-fifth-1000{width:60%;display:block}.four-fifth-1000{width:80%;display:block}.sixth-1000{width:16.66666%;display:block}}@media all and (min-width: 1100px){.full-1100{width:100%;display:block}.half-1100{width:50%;display:block}.third-1100{width:33.33333%;display:block}.two-third-1100{width:66.66666%;display:block}.fourth-1100{width:25%;display:block}.three-fourth-1100{width:75%;display:block}.fifth-1100{width:20%;display:block}.two-fifth-1100{width:40%;display:block}.three-fifth-1100{width:60%;display:block}.four-fifth-1100{width:80%;display:block}.sixth-1100{width:16.66666%;display:block}}@media all and (min-width: 1200px){.full-1200{width:100%;display:block}.half-1200{width:50%;display:block}.third-1200{width:33.33333%;display:block}.two-third-1200{width:66.66666%;display:block}.fourth-1200{width:25%;display:block}.three-fourth-1200{width:75%;display:block}.fifth-1200{width:20%;display:block}.two-fifth-1200{width:40%;display:block}.three-fifth-1200{width:60%;display:block}.four-fifth-1200{width:80%;display:block}.sixth-1200{width:16.66666%;display:block}}@media all and (min-width: 1300px){.full-1300{width:100%;display:block}.half-1300{width:50%;display:block}.third-1300{width:33.33333%;display:block}.two-third-1300{width:66.66666%;display:block}.fourth-1300{width:25%;display:block}.three-fourth-1300{width:75%;display:block}.fifth-1300{width:20%;display:block}.two-fifth-1300{width:40%;display:block}.three-fifth-1300{width:60%;display:block}.four-fifth-1300{width:80%;display:block}.sixth-1300{width:16.66666%;display:block}}@media all and (min-width: 1400px){.full-1400{width:100%;display:block}.half-1400{width:50%;display:block}.third-1400{width:33.33333%;display:block}.two-third-1400{width:66.66666%;display:block}.fourth-1400{width:25%;display:block}.three-fourth-1400{width:75%;display:block}.fifth-1400{width:20%;display:block}.two-fifth-1400{width:40%;display:block}.three-fifth-1400{width:60%;display:block}.four-fifth-1400{width:80%;display:block}.sixth-1400{width:16.66666%;display:block}}@media all and (min-width: 1500px){.full-1500{width:100%;display:block}.half-1500{width:50%;display:block}.third-1500{width:33.33333%;display:block}.two-third-1500{width:66.66666%;display:block}.fourth-1500{width:25%;display:block}.three-fourth-1500{width:75%;display:block}.fifth-1500{width:20%;display:block}.two-fifth-1500{width:40%;display:block}.three-fifth-1500{width:60%;display:block}.four-fifth-1500{width:80%;display:block}.sixth-1500{width:16.66666%;display:block}}@media all and (min-width: 1600px){.full-1600{width:100%;display:block}.half-1600{width:50%;display:block}.third-1600{width:33.33333%;display:block}.two-third-1600{width:66.66666%;display:block}.fourth-1600{width:25%;display:block}.three-fourth-1600{width:75%;display:block}.fifth-1600{width:20%;display:block}.two-fifth-1600{width:40%;display:block}.three-fifth-1600{width:60%;display:block}.four-fifth-1600{width:80%;display:block}.sixth-1600{width:16.66666%;display:block}}@media all and (min-width: 1700px){.full-1700{width:100%;display:block}.half-1700{width:50%;display:block}.third-1700{width:33.33333%;display:block}.two-third-1700{width:66.66666%;display:block}.fourth-1700{width:25%;display:block}.three-fourth-1700{width:75%;display:block}.fifth-1700{width:20%;display:block}.two-fifth-1700{width:40%;display:block}.three-fifth-1700{width:60%;display:block}.four-fifth-1700{width:80%;display:block}.sixth-1700{width:16.66666%;display:block}}@media all and (min-width: 1800px){.full-1800{width:100%;display:block}.half-1800{width:50%;display:block}.third-1800{width:33.33333%;display:block}.two-third-1800{width:66.66666%;display:block}.fourth-1800{width:25%;display:block}.three-fourth-1800{width:75%;display:block}.fifth-1800{width:20%;display:block}.two-fifth-1800{width:40%;display:block}.three-fifth-1800{width:60%;display:block}.four-fifth-1800{width:80%;display:block}.sixth-1800{width:16.66666%;display:block}}@media all and (min-width: 1900px){.full-1900{width:100%;display:block}.half-1900{width:50%;display:block}.third-1900{width:33.33333%;display:block}.two-third-1900{width:66.66666%;display:block}.fourth-1900{width:25%;display:block}.three-fourth-1900{width:75%;display:block}.fifth-1900{width:20%;display:block}.two-fifth-1900{width:40%;display:block}.three-fifth-1900{width:60%;display:block}.four-fifth-1900{width:80%;display:block}.sixth-1900{width:16.66666%;display:block}}@media all and (min-width: 2000px){.full-2000{width:100%;display:block}.half-2000{width:50%;display:block}.third-2000{width:33.33333%;display:block}.two-third-2000{width:66.66666%;display:block}.fourth-2000{width:25%;display:block}.three-fourth-2000{width:75%;display:block}.fifth-2000{width:20%;display:block}.two-fifth-2000{width:40%;display:block}.three-fifth-2000{width:60%;display:block}.four-fifth-2000{width:80%;display:block}.sixth-2000{width:16.66666%;display:block}}@media all and (min-width: 500px){.none-500{display:none}}@media all and (min-width: 600px){.none-600{display:none}}@media all and (min-width: 700px){.none-700{display:none}}@media all and (min-width: 800px){.none-800{display:none}}@media all and (min-width: 900px){.none-900{display:none}}@media all and (min-width: 1000px){.none-1000{display:none}}@media all and (min-width: 1100px){.none-1100{display:none}}@media all and (min-width: 1200px){.none-1200{display:none}}@media all and (min-width: 1300px){.none-1300{display:none}}@media all and (min-width: 1400px){.none-1400{display:none}}@media all and (min-width: 1500px){.none-1500{display:none}}@media all and (min-width: 1600px){.none-1600{display:none}}@media all and (min-width: 1700px){.none-1700{display:none}}@media all and (min-width: 1800px){.none-1800{display:none}}@media all and (min-width: 1900px){.none-1900{display:none}}@media all and (min-width: 2000px){.none-2000{display:none}}.off-none{margin-left:0}.off-half{margin-left:50%}.off-third{margin-left:33.33333%}.off-two-third{margin-left:66.66666%}.off-fourth{margin-left:25%}.off-three-fourth{margin-left:75%}.off-fifth{margin-left:20%}.off-two-fifth{margin-left:40%}.off-three-fifth{margin-left:60%}.off-four-fifth{margin-left:80%}.off-sixth{margin-left:16.66666%}@media all and (min-width: 500px){.off-none-500{margin-left:0}.off-half-500{margin-left:50%}.off-third-500{margin-left:33.33333%}.off-two-third-500{margin-left:66.66666%}.off-fourth-500{margin-left:25%}.off-three-fourth-500{margin-left:75%}.off-fifth-500{margin-left:20%}.off-two-fifth-500{margin-left:40%}.off-three-fifth-500{margin-left:60%}.off-four-fifth-500{margin-left:80%}.off-sixth-500{margin-left:16.66666%}}@media all and (min-width: 600px){.off-none-600{margin-left:0}.off-half-600{margin-left:50%}.off-third-600{margin-left:33.33333%}.off-two-third-600{margin-left:66.66666%}.off-fourth-600{margin-left:25%}.off-three-fourth-600{margin-left:75%}.off-fifth-600{margin-left:20%}.off-two-fifth-600{margin-left:40%}.off-three-fifth-600{margin-left:60%}.off-four-fifth-600{margin-left:80%}.off-sixth-600{margin-left:16.66666%}}@media all and (min-width: 700px){.off-none-700{margin-left:0}.off-half-700{margin-left:50%}.off-third-700{margin-left:33.33333%}.off-two-third-700{margin-left:66.66666%}.off-fourth-700{margin-left:25%}.off-three-fourth-700{margin-left:75%}.off-fifth-700{margin-left:20%}.off-two-fifth-700{margin-left:40%}.off-three-fifth-700{margin-left:60%}.off-four-fifth-700{margin-left:80%}.off-sixth-700{margin-left:16.66666%}}@media all and (min-width: 800px){.off-none-800{margin-left:0}.off-half-800{margin-left:50%}.off-third-800{margin-left:33.33333%}.off-two-third-800{margin-left:66.66666%}.off-fourth-800{margin-left:25%}.off-three-fourth-800{margin-left:75%}.off-fifth-800{margin-left:20%}.off-two-fifth-800{margin-left:40%}.off-three-fifth-800{margin-left:60%}.off-four-fifth-800{margin-left:80%}.off-sixth-800{margin-left:16.66666%}}@media all and (min-width: 900px){.off-none-900{margin-left:0}.off-half-900{margin-left:50%}.off-third-900{margin-left:33.33333%}.off-two-third-900{margin-left:66.66666%}.off-fourth-900{margin-left:25%}.off-three-fourth-900{margin-left:75%}.off-fifth-900{margin-left:20%}.off-two-fifth-900{margin-left:40%}.off-three-fifth-900{margin-left:60%}.off-four-fifth-900{margin-left:80%}.off-sixth-900{margin-left:16.66666%}}@media all and (min-width: 1000px){.off-none-1000{margin-left:0}.off-half-1000{margin-left:50%}.off-third-1000{margin-left:33.33333%}.off-two-third-1000{margin-left:66.66666%}.off-fourth-1000{margin-left:25%}.off-three-fourth-1000{margin-left:75%}.off-fifth-1000{margin-left:20%}.off-two-fifth-1000{margin-left:40%}.off-three-fifth-1000{margin-left:60%}.off-four-fifth-1000{margin-left:80%}.off-sixth-1000{margin-left:16.66666%}}@media all and (min-width: 1100px){.off-none-1100{margin-left:0}.off-half-1100{margin-left:50%}.off-third-1100{margin-left:33.33333%}.off-two-third-1100{margin-left:66.66666%}.off-fourth-1100{margin-left:25%}.off-three-fourth-1100{margin-left:75%}.off-fifth-1100{margin-left:20%}.off-two-fifth-1100{margin-left:40%}.off-three-fifth-1100{margin-left:60%}.off-four-fifth-1100{margin-left:80%}.off-sixth-1100{margin-left:16.66666%}}@media all and (min-width: 1200px){.off-none-1200{margin-left:0}.off-half-1200{margin-left:50%}.off-third-1200{margin-left:33.33333%}.off-two-third-1200{margin-left:66.66666%}.off-fourth-1200{margin-left:25%}.off-three-fourth-1200{margin-left:75%}.off-fifth-1200{margin-left:20%}.off-two-fifth-1200{margin-left:40%}.off-three-fifth-1200{margin-left:60%}.off-four-fifth-1200{margin-left:80%}.off-sixth-1200{margin-left:16.66666%}}@media all and (min-width: 1300px){.off-none-1300{margin-left:0}.off-half-1300{margin-left:50%}.off-third-1300{margin-left:33.33333%}.off-two-third-1300{margin-left:66.66666%}.off-fourth-1300{margin-left:25%}.off-three-fourth-1300{margin-left:75%}.off-fifth-1300{margin-left:20%}.off-two-fifth-1300{margin-left:40%}.off-three-fifth-1300{margin-left:60%}.off-four-fifth-1300{margin-left:80%}.off-sixth-1300{margin-left:16.66666%}}@media all and (min-width: 1400px){.off-none-1400{margin-left:0}.off-half-1400{margin-left:50%}.off-third-1400{margin-left:33.33333%}.off-two-third-1400{margin-left:66.66666%}.off-fourth-1400{margin-left:25%}.off-three-fourth-1400{margin-left:75%}.off-fifth-1400{margin-left:20%}.off-two-fifth-1400{margin-left:40%}.off-three-fifth-1400{margin-left:60%}.off-four-fifth-1400{margin-left:80%}.off-sixth-1400{margin-left:16.66666%}}@media all and (min-width: 1500px){.off-none-1500{margin-left:0}.off-half-1500{margin-left:50%}.off-third-1500{margin-left:33.33333%}.off-two-third-1500{margin-left:66.66666%}.off-fourth-1500{margin-left:25%}.off-three-fourth-1500{margin-left:75%}.off-fifth-1500{margin-left:20%}.off-two-fifth-1500{margin-left:40%}.off-three-fifth-1500{margin-left:60%}.off-four-fifth-1500{margin-left:80%}.off-sixth-1500{margin-left:16.66666%}}@media all and (min-width: 1600px){.off-none-1600{margin-left:0}.off-half-1600{margin-left:50%}.off-third-1600{margin-left:33.33333%}.off-two-third-1600{margin-left:66.66666%}.off-fourth-1600{margin-left:25%}.off-three-fourth-1600{margin-left:75%}.off-fifth-1600{margin-left:20%}.off-two-fifth-1600{margin-left:40%}.off-three-fifth-1600{margin-left:60%}.off-four-fifth-1600{margin-left:80%}.off-sixth-1600{margin-left:16.66666%}}@media all and (min-width: 1700px){.off-none-1700{margin-left:0}.off-half-1700{margin-left:50%}.off-third-1700{margin-left:33.33333%}.off-two-third-1700{margin-left:66.66666%}.off-fourth-1700{margin-left:25%}.off-three-fourth-1700{margin-left:75%}.off-fifth-1700{margin-left:20%}.off-two-fifth-1700{margin-left:40%}.off-three-fifth-1700{margin-left:60%}.off-four-fifth-1700{margin-left:80%}.off-sixth-1700{margin-left:16.66666%}}@media all and (min-width: 1800px){.off-none-1800{margin-left:0}.off-half-1800{margin-left:50%}.off-third-1800{margin-left:33.33333%}.off-two-third-1800{margin-left:66.66666%}.off-fourth-1800{margin-left:25%}.off-three-fourth-1800{margin-left:75%}.off-fifth-1800{margin-left:20%}.off-two-fifth-1800{margin-left:40%}.off-three-fifth-1800{margin-left:60%}.off-four-fifth-1800{margin-left:80%}.off-sixth-1800{margin-left:16.66666%}}@media all and (min-width: 1900px){.off-none-1900{margin-left:0}.off-half-1900{margin-left:50%}.off-third-1900{margin-left:33.33333%}.off-two-third-1900{margin-left:66.66666%}.off-fourth-1900{margin-left:25%}.off-three-fourth-1900{margin-left:75%}.off-fifth-1900{margin-left:20%}.off-two-fifth-1900{margin-left:40%}.off-three-fifth-1900{margin-left:60%}.off-four-fifth-1900{margin-left:80%}.off-sixth-1900{margin-left:16.66666%}}@media all and (min-width: 2000px){.off-none-2000{margin-left:0}.off-half-2000{margin-left:50%}.off-third-2000{margin-left:33.33333%}.off-two-third-2000{margin-left:66.66666%}.off-fourth-2000{margin-left:25%}.off-three-fourth-2000{margin-left:75%}.off-fifth-2000{margin-left:20%}.off-two-fifth-2000{margin-left:40%}.off-three-fifth-2000{margin-left:60%}.off-four-fifth-2000{margin-left:80%}.off-sixth-2000{margin-left:16.66666%}}nav{position:fixed;top:0;left:0;right:0;height:3em;padding:0 .6em;background:#fff;box-shadow:0 0 0.2em rgba(17,17,17,0.2);z-index:10000;transition:all .3s;transform-style:preserve-3d}nav .brand,nav .menu,nav .burger{float:right;position:relative;top:50%;-webkit-transform:translateY(-50%);transform:translateY(-50%)}nav .brand{font-weight:700;float:left;padding:0 .6em;max-width:50%;white-space:nowrap;color:inherit}nav .brand *{vertical-align:middle}nav .logo{height:2em;margin-right:.3em}nav .select::after{height:calc(100% - 1px);padding:0;line-height:2.4em}nav .menu>*{margin-right:.6em}nav .burger{display:none}@media all and (max-width: 60em){nav .burger{display:inline-block;cursor:pointer;bottom:-1000em;margin:0}nav .burger ~ .menu,nav .show:checked ~ .burger{position:fixed;min-height:100%;top:0;right:0;bottom:-1000em;margin:0;background:#fff;transition:all .5s ease;transform:none}nav .burger ~ .menu{z-index:11}nav .show:checked ~ .burger{color:transparent;width:100%;border-radius:0;background:rgba(0,0,0,0.2);transition:all .5s ease}nav .show ~ .menu{width:70%;max-width:300px;transform-origin:center right;transition:all .25s ease;transform:scaleX(0)}nav .show ~ .menu>*{transform:translateX(100%);transition:all 0s ease .5s}nav .show:checked ~ .menu>*:nth-child(1){transition:all .5s cubic-bezier(0.645, 0.045, 0.355, 1) 0s}nav .show:checked ~ .menu>*:nth-child(2){transition:all .5s cubic-bezier(0.645, 0.045, 0.355, 1) .1s}nav .show:checked ~ .menu>*:nth-child(3){transition:all .5s cubic-bezier(0.645, 0.045, 0.355, 1) .2s}nav .show:checked ~ .menu>*:nth-child(4){transition:all .5s cubic-bezier(0.645, 0.045, 0.355, 1) .3s}nav .show:checked ~ .menu>*:nth-child(5){transition:all .5s cubic-bezier(0.645, 0.045, 0.355, 1) .4s}nav .show:checked ~ .menu>*:nth-child(6){transition:all .5s cubic-bezier(0.645, 0.045, 0.355, 1) .5s}nav .show:checked ~ .menu{transform:scaleX(1)}nav .show:checked ~ .menu>*{transform:translateX(0);transition:all .5s ease-in-out .6s}nav .burger ~ .menu>*{display:block;margin:.3em;text-align:left;max-width:calc(100% - .6em)}nav .burger ~ .menu>a{padding:.3em .9em}}.stack,.stack .toggle{margin-top:0;margin-bottom:0;display:block;width:100%;text-align:left;border-radius:0}.stack:first-child,.stack:first-child .toggle{border-top-left-radius:.2em;border-top-right-radius:.2em}.stack:last-child,.stack:last-child .toggle{border-bottom-left-radius:.2em;border-bottom-right-radius:.2em}input.stack,textarea.stack,select.stack{transition:border-bottom 0 ease 0;border-bottom-width:0}input.stack:last-child,textarea.stack:last-child,select.stack:last-child{border-bottom-width:1px}input.stack:focus+input,input.stack:focus+textarea,input.stack:focus+select,textarea.stack:focus+input,textarea.stack:focus+textarea,textarea.stack:focus+select,select.stack:focus+input,select.stack:focus+textarea,select.stack:focus+select{border-top-color:#0074d9}.card,.modal .overlay ~ *{position:relative;box-shadow:0;border-radius:.2em;border:1px solid #ccc;overflow:hidden;text-align:left;background:#fff;margin-bottom:.6em;padding:0;transition:all .3s ease}.hidden.card,.modal .overlay ~ .hidden,:checked+.card,.modal .overlay ~ :checked+*,.modal .overlay:checked+*{font-size:0;padding:0;margin:0;border:0}.card>*,.modal .overlay ~ *>*{max-width:100%;display:block}.card>*:last-child,.modal .overlay ~ *>*:last-child{margin-bottom:0}.card header,.modal .overlay ~ * header,.card section,.modal .overlay ~ * section,.card>p,.modal .overlay ~ *>p{padding:.6em .8em}.card section,.modal .overlay ~ * section{padding:.6em .8em 0}.card hr,.modal .overlay ~ * hr{border:none;height:1px;background-color:#eee}.card header,.modal .overlay ~ * header{font-weight:bold;position:relative;border-bottom:1px solid #eee}.card header h1,.modal .overlay ~ * header h1,.card header h2,.modal .overlay ~ * header h2,.card header h3,.modal .overlay ~ * header h3,.card header h4,.modal .overlay ~ * header h4,.card header h5,.modal .overlay ~ * header h5,.card header h6,.modal .overlay ~ * header h6{padding:0;margin:0 2em 0 0;line-height:1;display:inline-block;vertical-align:text-bottom}.card header:last-child,.modal .overlay ~ * header:last-child{border-bottom:0}.card footer,.modal .overlay ~ * footer{padding:.8em}.card p,.modal .overlay ~ * p{margin:.3em 0}.card p:first-child,.modal .overlay ~ * p:first-child{margin-top:0}.card p:last-child,.modal .overlay ~ * p:last-child{margin-bottom:0}.card>p,.modal .overlay ~ *>p{margin:0;padding-right:2.5em}.card .close,.modal .overlay ~ * .close{position:absolute;top:.4em;right:.3em;font-size:1.2em;padding:0 .5em;cursor:pointer;width:auto}.card .close:hover,.modal .overlay ~ * .close:hover{color:#ff4136}.card h1+.close,.modal .overlay ~ * h1+.close{margin:.2em}.card h2+.close,.modal .overlay ~ * h2+.close{margin:.1em}.card .dangerous,.modal .overlay ~ * .dangerous{background:#ff4136;float:right}.modal{text-align:center}.modal>input{display:none}.modal>input ~ *{opacity:0;max-height:0;overflow:hidden}.modal .overlay{top:0;left:0;bottom:0;right:0;position:fixed;margin:0;border-radius:0;background:rgba(17,17,17,0.6);transition:all 0.3s;z-index:100000}.modal .overlay:before,.modal .overlay:after{display:none}.modal .overlay ~ *{border:0;position:fixed;top:50%;left:50%;transform:translateX(-50%) translateY(-50%) scale(0.2, 0.2);z-index:1000000;transition:all 0.3s}.modal>input:checked ~ *{display:block;opacity:1;max-height:10000px;transition:all 0.3s}.modal>input:checked ~ .overlay ~ *{max-height:90%;overflow:auto;-webkit-transform:translateX(-50%) translateY(-50%) scale(1, 1);transform:translateX(-50%) translateY(-50%) scale(1, 1)}@media (max-width: 60em){.modal .overlay ~ *{min-width:90%}}.dropimage{position:relative;display:block;padding:0;padding-bottom:56.25%;overflow:hidden;cursor:pointer;border:0;margin:.3em 0;border-radius:.2em;background-color:#ddd;background-size:cover;background-position:center center;background-image:url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2NDAiIGhlaWdodD0iNjQwIiB2ZXJzaW9uPSIxLjEiPjxnIHN0eWxlPSJmaWxsOiMzMzMiPjxwYXRoIGQ9Ik0gMTg3IDIzMCBDIDE3NSAyMzAgMTY1IDI0MCAxNjUgMjUyIEwgMTY1IDMwMCBMIDE2NSA0MDggQyAxNjUgNDIwIDE3NSA0MzAgMTg3IDQzMCBMIDQ2MyA0MzAgQyA0NzUgNDMwIDQ4NSA0MjAgNDg1IDQwOCBMIDQ4NSAzMDAgTCA0ODUgMjUyIEMgNDg1IDI0MCA0NzUgMjMwIDQ2MyAyMzAgTCAxODcgMjMwIHogTSAzNjAgMjU2IEEgNzAgNzIgMCAwIDEgNDMwIDMyOCBBIDcwIDcyIDAgMCAxIDM2MCA0MDAgQSA3MCA3MiAwIDAgMSAyOTAgMzI4IEEgNzAgNzIgMCAwIDEgMzYwIDI1NiB6Ii8+PGNpcmNsZSBjeD0iMzYwIiBjeT0iMzMwIiByPSI0MSIvPjxwYXRoIGQ9Im0yMDUgMjI1IDUtMTAgMjAgMCA1IDEwLTMwIDAiLz48cGF0aCBkPSJNMjg1IDIwMEwyNzAgMjI1IDM3NiAyMjUgMzYxIDIwMCAyODUgMjAwek0zMTAgMjA1TDMzNyAyMDUgMzM3IDIxOCAzMTAgMjE4IDMxMCAyMDV6Ii8+PHBhdGggZD0ibTQwNSAyMjUgNS0xMCAyMCAwIDUgMTAtMzAgMCIvPjwvZz48L3N2Zz4=)}.dropimage input{left:0;width:100%;height:100%;border:0;margin:0;padding:0;opacity:0;cursor:pointer;position:absolute}.tabs{position:relative;overflow:hidden}.tabs>label img{float:left;margin-left:.6em}.tabs>.row{width:calc(100% + 2 * .6em);display:table;table-layout:fixed;position:relative;padding-left:0;transition:all .3s;border-spacing:0;margin:0}.tabs>.row:before,.tabs>.row:after{display:none}.tabs>.row>*,.tabs>.row img{display:table-cell;vertical-align:top;margin:0;width:100%}.tabs>input{display:none}.tabs>input+*{width:100%}.tabs>input+label{width:auto}.two.tabs>.row{width:200%;left:-100%}.two.tabs>input:nth-of-type(1):checked ~ .row{margin-left:100%}.two.tabs>label img{width:48%;margin:4% 0 4% 4%}.three.tabs>.row{width:300%;left:-200%}.three.tabs>input:nth-of-type(1):checked ~ .row{margin-left:200%}.three.tabs>input:nth-of-type(2):checked ~ .row{margin-left:100%}.three.tabs>label img{width:30%;margin:5% 0 5% 5%}.four.tabs>.row{width:400%;left:-300%}.four.tabs>input:nth-of-type(1):checked ~ .row{margin-left:300%}.four.tabs>input:nth-of-type(2):checked ~ .row{margin-left:200%}.four.tabs>input:nth-of-type(3):checked ~ .row{margin-left:100%}.four.tabs>label img{width:22%;margin:4% 0 4% 4%}.tabs>label:first-of-type img{margin-left:0}[data-tooltip]{position:relative}[data-tooltip]:after,[data-tooltip]:before{position:absolute;z-index:10;opacity:0;border-width:0;height:0;padding:0;overflow:hidden;transition:opacity .6s ease, height 0s ease .6s;top:calc(100% - 6px);left:0;margin-top:12px}[data-tooltip]:after{margin-left:0;font-size:.8em;background:#111;content:attr(data-tooltip);white-space:nowrap}[data-tooltip]:before{content:'';width:0;height:0;border-width:0;border-style:solid;border-color:transparent transparent #111;margin-top:0;left:10px}[data-tooltip]:hover:after,[data-tooltip]:focus:after,[data-tooltip]:hover:before,[data-tooltip]:focus:before{opacity:1;border-width:6px;height:auto}[data-tooltip]:hover:after,[data-tooltip]:focus:after{padding:.45em .9em}.tooltip-top:after,.tooltip-top:before{top:auto;bottom:calc(100% - 6px);left:0;margin-bottom:12px}.tooltip-top:before{border-color:#111 transparent transparent;margin-bottom:0;left:10px}.tooltip-right:after,.tooltip-right:before{left:100%;margin-left:6px;margin-top:0;top:0}.tooltip-right:before{border-color:transparent #111 transparent transparent;margin-left:-6px;left:100%;top:7px}.tooltip-left:after,.tooltip-left:before{right:100%;margin-right:6px;left:auto;margin-top:0;top:0}.tooltip-left:before{border-color:transparent transparent transparent #111;margin-right:-6px;right:100%;top:7px} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/select2.min.css b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/select2.min.css new file mode 100644 index 0000000..60d5990 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/css/vendor/select2.min.css @@ -0,0 +1 @@ +.select2-container{box-sizing:border-box;display:inline-block;margin:0;position:relative;vertical-align:middle}.select2-container .select2-selection--single{box-sizing:border-box;cursor:pointer;display:block;height:28px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--single .select2-selection__rendered{display:block;padding-left:8px;padding-right:20px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-selection--single .select2-selection__clear{position:relative}.select2-container[dir="rtl"] .select2-selection--single .select2-selection__rendered{padding-right:8px;padding-left:20px}.select2-container .select2-selection--multiple{box-sizing:border-box;cursor:pointer;display:block;min-height:32px;user-select:none;-webkit-user-select:none}.select2-container .select2-selection--multiple .select2-selection__rendered{display:inline-block;overflow:hidden;padding-left:8px;text-overflow:ellipsis;white-space:nowrap}.select2-container .select2-search--inline{float:left}.select2-container .select2-search--inline .select2-search__field{box-sizing:border-box;border:none;font-size:100%;margin-top:5px;padding:0}.select2-container .select2-search--inline .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-dropdown{background-color:white;border:1px solid #aaa;border-radius:4px;box-sizing:border-box;display:block;position:absolute;left:-100000px;width:100%;z-index:1051}.select2-results{display:block}.select2-results__options{list-style:none;margin:0;padding:0}.select2-results__option{padding:6px;user-select:none;-webkit-user-select:none}.select2-results__option[aria-selected]{cursor:pointer}.select2-container--open .select2-dropdown{left:0}.select2-container--open .select2-dropdown--above{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--open .select2-dropdown--below{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-search--dropdown{display:block;padding:4px}.select2-search--dropdown .select2-search__field{padding:4px;width:100%;box-sizing:border-box}.select2-search--dropdown .select2-search__field::-webkit-search-cancel-button{-webkit-appearance:none}.select2-search--dropdown.select2-search--hide{display:none}.select2-close-mask{border:0;margin:0;padding:0;display:block;position:fixed;left:0;top:0;min-height:100%;min-width:100%;height:auto;width:auto;opacity:0;z-index:99;background-color:#fff;filter:alpha(opacity=0)}.select2-hidden-accessible{border:0 !important;clip:rect(0 0 0 0) !important;-webkit-clip-path:inset(50%) !important;clip-path:inset(50%) !important;height:1px !important;overflow:hidden !important;padding:0 !important;position:absolute !important;width:1px !important;white-space:nowrap !important}.select2-container--default .select2-selection--single{background-color:#fff;border:1px solid #aaa;border-radius:4px}.select2-container--default .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--default .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold}.select2-container--default .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--default .select2-selection--single .select2-selection__arrow{height:26px;position:absolute;top:1px;right:1px;width:20px}.select2-container--default .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--default[dir="rtl"] .select2-selection--single .select2-selection__arrow{left:1px;right:auto}.select2-container--default.select2-container--disabled .select2-selection--single{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection--single .select2-selection__clear{display:none}.select2-container--default.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--default .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text}.select2-container--default .select2-selection--multiple .select2-selection__rendered{box-sizing:border-box;list-style:none;margin:0;padding:0 5px;width:100%}.select2-container--default .select2-selection--multiple .select2-selection__rendered li{list-style:none}.select2-container--default .select2-selection--multiple .select2-selection__placeholder{color:#999;margin-top:5px;float:left}.select2-container--default .select2-selection--multiple .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-top:5px;margin-right:10px}.select2-container--default .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove{color:#999;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--default .select2-selection--multiple .select2-selection__choice__remove:hover{color:#333}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__placeholder,.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-search--inline{float:right}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice{margin-left:5px;margin-right:auto}.select2-container--default[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--default.select2-container--focus .select2-selection--multiple{border:solid black 1px;outline:0}.select2-container--default.select2-container--disabled .select2-selection--multiple{background-color:#eee;cursor:default}.select2-container--default.select2-container--disabled .select2-selection__choice__remove{display:none}.select2-container--default.select2-container--open.select2-container--above .select2-selection--single,.select2-container--default.select2-container--open.select2-container--above .select2-selection--multiple{border-top-left-radius:0;border-top-right-radius:0}.select2-container--default.select2-container--open.select2-container--below .select2-selection--single,.select2-container--default.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--default .select2-search--dropdown .select2-search__field{border:1px solid #aaa}.select2-container--default .select2-search--inline .select2-search__field{background:transparent;border:none;outline:0;box-shadow:none;-webkit-appearance:textfield}.select2-container--default .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--default .select2-results__option[role=group]{padding:0}.select2-container--default .select2-results__option[aria-disabled=true]{color:#999}.select2-container--default .select2-results__option[aria-selected=true]{background-color:#ddd}.select2-container--default .select2-results__option .select2-results__option{padding-left:1em}.select2-container--default .select2-results__option .select2-results__option .select2-results__group{padding-left:0}.select2-container--default .select2-results__option .select2-results__option .select2-results__option{margin-left:-1em;padding-left:2em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-2em;padding-left:3em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-3em;padding-left:4em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-4em;padding-left:5em}.select2-container--default .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option .select2-results__option{margin-left:-5em;padding-left:6em}.select2-container--default .select2-results__option--highlighted[aria-selected]{background-color:#5897fb;color:white}.select2-container--default .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic .select2-selection--single{background-color:#f7f7f7;border:1px solid #aaa;border-radius:4px;outline:0;background-image:-webkit-linear-gradient(top, #fff 50%, #eee 100%);background-image:-o-linear-gradient(top, #fff 50%, #eee 100%);background-image:linear-gradient(to bottom, #fff 50%, #eee 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic .select2-selection--single:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--single .select2-selection__rendered{color:#444;line-height:28px}.select2-container--classic .select2-selection--single .select2-selection__clear{cursor:pointer;float:right;font-weight:bold;margin-right:10px}.select2-container--classic .select2-selection--single .select2-selection__placeholder{color:#999}.select2-container--classic .select2-selection--single .select2-selection__arrow{background-color:#ddd;border:none;border-left:1px solid #aaa;border-top-right-radius:4px;border-bottom-right-radius:4px;height:26px;position:absolute;top:1px;right:1px;width:20px;background-image:-webkit-linear-gradient(top, #eee 50%, #ccc 100%);background-image:-o-linear-gradient(top, #eee 50%, #ccc 100%);background-image:linear-gradient(to bottom, #eee 50%, #ccc 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFCCCCCC', GradientType=0)}.select2-container--classic .select2-selection--single .select2-selection__arrow b{border-color:#888 transparent transparent transparent;border-style:solid;border-width:5px 4px 0 4px;height:0;left:50%;margin-left:-4px;margin-top:-2px;position:absolute;top:50%;width:0}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__clear{float:left}.select2-container--classic[dir="rtl"] .select2-selection--single .select2-selection__arrow{border:none;border-right:1px solid #aaa;border-radius:0;border-top-left-radius:4px;border-bottom-left-radius:4px;left:1px;right:auto}.select2-container--classic.select2-container--open .select2-selection--single{border:1px solid #5897fb}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow{background:transparent;border:none}.select2-container--classic.select2-container--open .select2-selection--single .select2-selection__arrow b{border-color:transparent transparent #888 transparent;border-width:0 4px 5px 4px}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--single{border-top:none;border-top-left-radius:0;border-top-right-radius:0;background-image:-webkit-linear-gradient(top, #fff 0%, #eee 50%);background-image:-o-linear-gradient(top, #fff 0%, #eee 50%);background-image:linear-gradient(to bottom, #fff 0%, #eee 50%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFFFFFFF', endColorstr='#FFEEEEEE', GradientType=0)}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--single{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0;background-image:-webkit-linear-gradient(top, #eee 50%, #fff 100%);background-image:-o-linear-gradient(top, #eee 50%, #fff 100%);background-image:linear-gradient(to bottom, #eee 50%, #fff 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#FFEEEEEE', endColorstr='#FFFFFFFF', GradientType=0)}.select2-container--classic .select2-selection--multiple{background-color:white;border:1px solid #aaa;border-radius:4px;cursor:text;outline:0}.select2-container--classic .select2-selection--multiple:focus{border:1px solid #5897fb}.select2-container--classic .select2-selection--multiple .select2-selection__rendered{list-style:none;margin:0;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__clear{display:none}.select2-container--classic .select2-selection--multiple .select2-selection__choice{background-color:#e4e4e4;border:1px solid #aaa;border-radius:4px;cursor:default;float:left;margin-right:5px;margin-top:5px;padding:0 5px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove{color:#888;cursor:pointer;display:inline-block;font-weight:bold;margin-right:2px}.select2-container--classic .select2-selection--multiple .select2-selection__choice__remove:hover{color:#555}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice{float:right;margin-left:5px;margin-right:auto}.select2-container--classic[dir="rtl"] .select2-selection--multiple .select2-selection__choice__remove{margin-left:2px;margin-right:auto}.select2-container--classic.select2-container--open .select2-selection--multiple{border:1px solid #5897fb}.select2-container--classic.select2-container--open.select2-container--above .select2-selection--multiple{border-top:none;border-top-left-radius:0;border-top-right-radius:0}.select2-container--classic.select2-container--open.select2-container--below .select2-selection--multiple{border-bottom:none;border-bottom-left-radius:0;border-bottom-right-radius:0}.select2-container--classic .select2-search--dropdown .select2-search__field{border:1px solid #aaa;outline:0}.select2-container--classic .select2-search--inline .select2-search__field{outline:0;box-shadow:none}.select2-container--classic .select2-dropdown{background-color:#fff;border:1px solid transparent}.select2-container--classic .select2-dropdown--above{border-bottom:none}.select2-container--classic .select2-dropdown--below{border-top:none}.select2-container--classic .select2-results>.select2-results__options{max-height:200px;overflow-y:auto}.select2-container--classic .select2-results__option[role=group]{padding:0}.select2-container--classic .select2-results__option[aria-disabled=true]{color:grey}.select2-container--classic .select2-results__option--highlighted[aria-selected]{background-color:#3875d7;color:#fff}.select2-container--classic .select2-results__group{cursor:default;display:block;padding:6px}.select2-container--classic.select2-container--open .select2-dropdown{border-color:#5897fb} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/dragon.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/dragon.html new file mode 100644 index 0000000..143f19f --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/dragon.html @@ -0,0 +1,82 @@ + + + + + + Dragotchi + + + + + + + + + +
+ + +

Dragotchi

+ + + +
+ +
+ +
+ +
+
+ +
+
+
+ + +
+
+
+ + + +
+ +
+ + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/dragon.js b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/dragon.js new file mode 100644 index 0000000..5df800d --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/dragon.js @@ -0,0 +1,91 @@ +// Copyright 2016 Google Inc. +// +// 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 +// +// http://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. + +function refresh(action, callback) { + var x = new XMLHttpRequest(); + x.onload = function() { + var out = x.response; + if (typeof out == 'string') { + out = JSON.parse(out); + } + callback(out); + }; + x.onerror = function() { + alert('couldn\'t fetch dragon status'); + }; + + if (action) { + x.open('POST', 'https://dragon-server.appspot.com/?action=' + action); + } else { + x.open('GET', 'https://dragon-server.appspot.com/'); + } + + x.send(); +} + +window.addEventListener('load', function() { + function createDataRow(name, value) { + var row = document.createElement('tr'); + + var th = document.createElement('th'); + th.textContent = name; + row.appendChild(th); + + var td = document.createElement('td'); + td.textContent = value; + row.appendChild(td); + + return row; + } + + var timeout; + (function work(action) { + window.clearTimeout(timeout); + timeout = window.setTimeout(work, 60 * 1000); // 60s + + if (action) { // clicked action, clear future actions + actions.textContent = ''; + } + refresh(action, function(status) { + + gold.textContent = ''; + for (var i = 0; i < status.Gold; ++i) { + var coin = document.createElement('span'); + coin.className = 'coin'; + coin.style.top = Math.random() * 100 + '%'; + coin.style.left = Math.random() * 100 + '%'; + gold.appendChild(coin); + } + + data.textContent = ''; + data.appendChild(createDataRow('Gold', status.Gold)); + data.appendChild(createDataRow('Size', status.Size + 'kg')); + + // TO DO: update size + + actions.textContent = ''; + status.Actions.forEach(function(action) { + var button = document.createElement('button'); + button.addEventListener('click', function(ev) { + ev.preventDefault(); + work(action.ID); + }); + button.textContent = action.Name; + actions.appendChild(button); + }); + + }); + + }()); +}); \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/dragon.png b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/dragon.png new file mode 100644 index 0000000000000000000000000000000000000000..339f151cc851676fe95e8b529ab370ffefdc57d6 GIT binary patch literal 20628 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 + + + + + Dragotchi + + + + + + + + +
+ + +

Dragotchi

+ + + +
+ +
+ +
+ +

Home

+ +

+ Click and play with your very own Dragon! Feed it gold and watch it grow over time. +

+

+ Soon it will be an excited little dragon eating villagers and burning houses down. +

+

+ To get started, click on the Dragotchi link! +

+ + + +

+ Dragotchi is always Under Construction!!! +

+ +
+ +
+ + + +
+ 0 + 0 + 0 + 2 + 0 + 1 + 6 +
+ + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/faq.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/faq.html new file mode 100644 index 0000000..fbd75d1 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/faq.html @@ -0,0 +1,70 @@ + + + + + + Dragotchi + + + + + + + + +
+ + +

Dragotchi

+ + + +
+ +
+ +
+ +

FAQ

+ +
+
Are you serious?
+
Yes.
+
How cool are dragons?
+
Well, it's not clear whether they are cold- or warm-blooded.
+
Why does this exist?
+
This site has been built as an example for Google Codelabs.
+
Where are the images from?
+
The logo is from Google Maps 8-bit, the "Under Construction" gif is generally available online, some assets from openclipart, other assets were built for this site.
+
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icon-192.png b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..e41ce7099352cd1d3bfbacb21feb6adfc65ae06d GIT binary patch literal 49154 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 literal 0 HcmV?d00001 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icons/movie_camera.png b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icons/movie_camera.png new file mode 100644 index 0000000000000000000000000000000000000000..3563ccf6a37add74257840b929c028bec23f3671 GIT binary patch literal 4623 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 literal 0 HcmV?d00001 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icons/movies.png b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icons/movies.png new file mode 100644 index 0000000000000000000000000000000000000000..2cea8abfa56e663aeee03e7668e629fe03ff7337 GIT binary patch literal 14666 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 literal 0 HcmV?d00001 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/manifest.json b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/manifest.json new file mode 100644 index 0000000..07be4f0 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/manifest.json @@ -0,0 +1,15 @@ +{ + "name": "The Most Awesome Dragon Site", + "short_name": "🐉🐉🐉", + "display": "minimal-ui", + "start_url": "/", + "theme_color": "#673ab6", + "background_color": "#111111", + "icons": [ + { + "src": "icon-192.png", + "sizes": "192x192", + "type": "image/png" + } + ] +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/site.js b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/site.js new file mode 100644 index 0000000..620b61b --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/site.js @@ -0,0 +1,17 @@ +// Copyright 2016 Google Inc. +// +// 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 +// +// http://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. + +navigator.serviceWorker && navigator.serviceWorker.register('./sw.js').then(function(registration) { + console.log('Excellent, registered with scope: ', registration.scope); +}); \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/styles.css b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/styles.css new file mode 100644 index 0000000..8a7f0af --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/styles.css @@ -0,0 +1,161 @@ +/* + * Copyright 2016 Google Inc. + * + * 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 + * + * http://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. + */ + + +body { + background: black url(background.jpeg) repeat-x center top; + background-size: 200px; + color: white; + text-align: center; +} + +a { + color: #ff9249; +} + +/** I'm so sorry. */ +blink { + animation: blink 1s step-end infinite; +} +@keyframes blink { + 67% { opacity: 0 } +} + +header .logo { + width: 100px; +} +header ul { + list-style: none; + padding: 0; + color: #555; +} +header li { + display: inline-block; +} +header li::before { + content: " — "; +} +header li:first-child::before { + content: ""; +} + +.counter { + display: inline-flex; + margin: 20px; +} + +.counter span { + border: 1px solid #333; + width: 12px; + font-family: Monospace; + background: linear-gradient(to top, red, blue); +} + +dl { + width: 400px; + margin: 0 auto; + text-align: left; +} + +dd { + margin-bottom: 1em; +} + +section table { + margin: 0 auto; + text-align: left; +} +section table td, +section table th { + min-width: 50px; +} + +.dragon { + box-sizing: border-box; + padding: 20px; + width: 280px; + height: 280px; + margin: 12px auto; + border: 2px solid rgba(255, 255, 255, 0.125); + background: #111; + border-radius: 2px; + position: relative; + overflow: hidden; +} +.dragon #guy { + position: absolute; + width: 100px; + margin-left: -50px; + margin-top: -50px; + z-index: 1000; + transition: transform 1s ease-in-out; +} +.dragon img { + width: 100%; + cursor: pointer; + transition: transform 0.25s; +} +.dragon img:hover { + transform: scale(1.45) rotate(10deg); +} + +.coin { + position: absolute; + opacity: 0.5; + width: 10px; + height: 5px; + margin-left: -5px; + margin-top: -2px; + background: yellow; + border-radius: 100%; /** oval */ + border-bottom: 1px solid rgba(0, 0, 0, 0.5); + border-top: 1px solid rgba(255, 255, 255, 0.5); + box-shadow: 0 1px 0 rgba(0, 0, 0, 0.75), 0 -1px 0 rgba(0, 0, 0, 0.25); +} + +.actions { + width: 280px; + height: 80px; + box-sizing: border-box; + border: 2px solid rgba(255, 255, 255, 0.125); + background: #211; + border-radius: 2px; + margin: 12px auto; + display: flex; + padding: 2px; +} + +.actions button { + border: 0; + flex-grow: 1; + width: 300%; /** forces equal size */ + margin-left: 2px; + background: rgba(255, 255, 255, 0.125); + color: white; + font-size: 16px; + padding: 12px; + opacity: 0.8; +} +.actions button:first-child { + margin-left: 0; +} +.actions button:disabled { + text-decoration: line-through; +} +.actions button:hover:not(:disabled) { + opacity: 1.0; + cursor: pointer; +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/sw.js b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/sw.js new file mode 100644 index 0000000..c8984b9 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/sw.js @@ -0,0 +1,51 @@ +// Copyright 2016 Google Inc. +// +// 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 +// +// http://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. + +self.addEventListener('install', function(e) { + e.waitUntil( + caches.open('your-magic-cache').then(function(cache) { + return cache.addAll([ + '/', + '/drindex.html', + '/dragon.html', + '/faq.html', + '/manifest.json', + '/background.jpeg', + '/construction.gif', + '/dragon.png', + '/logo.png', + '/site.js', + '/dragon.js', + '/styles.css', + ]); + }) + ); +}); + +self.addEventListener('fetch', function(event) { + if (event.request.url == 'https://dragon-server.appspot.com/') { + console.info('responding to dragon-server fetch with Service Worker! 🤓'); + event.respondWith(fetch(event.request).catch(function(e) { + let out = {Gold: 1, Size: -1, Actions: []}; + return new Response(JSON.stringify(out)); + })); + return; + } + + event.respondWith( + caches.match(event.request).then(function(response) { + return response || fetch(event.request); + }) + ); +}); \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/error.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/error.html new file mode 100644 index 0000000..b358031 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/error.html @@ -0,0 +1,59 @@ + + + + Flexinale - Error + + + + + + + +
+ +
+ + + +
Error
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
Timestamp +
Path +
Error +
Status +
Message +
Exception +
Trace +
+            
+
+ + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/film.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/film.html new file mode 100644 index 0000000..17c695f --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/film.html @@ -0,0 +1,111 @@ + + + + Flexinale - Film Details + + + + + + + +
+ + + +
+

+ +

+ + + + + + + + + + + + + +
IDTitelLinkDauer [m]
+ IMDB Link +
+
+ +
+

Vorführungen

+ + + + + + + + + + + + + + + + + + + + + + + + +
IDZeitKinoSaalTickets
+ + + + , + + Uhr + + + + + + + + + Du hast bereits Ticket(s). + + + Keine Tickets verfügbar + + + Du hast zu der Zeit bereits eine andere Vorführung. + + +
+ + + + +
+
+
+ + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/filme.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/filme.html new file mode 100644 index 0000000..32ca7a7 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/filme.html @@ -0,0 +1,41 @@ + + + + Flexinale - Film Übersicht + + + + + + + +
+ +
+

Film-Programm ist leer!

+
+ +
+

Film-Programm

+ + + + + + + + + +
TitelLink
+ + + + + IMDB Link +
+
+ + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/header.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/header.html new file mode 100644 index 0000000..20880e7 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/header.html @@ -0,0 +1,38 @@ + + + + + Flexinale + + + + + +
+
+  +
+ + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/index.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/index.html new file mode 100644 index 0000000..09cc267 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/index.html @@ -0,0 +1,33 @@ + + + + Flexinale Distributed - Besucherportal + + + + + + + +
+ +
+ + + +
Flexinale
+
+ +
+
+ + , Version + + , built on + +
+ + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/kino.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/kino.html new file mode 100644 index 0000000..cc408f8 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/kino.html @@ -0,0 +1,62 @@ + + + + Flexinale - Kino Details + + + + + + + +
+ +
+

+ +

+ + + + + + + + + + + + + +
IDNameAdresseKontakt
+ +
+

+ + Kinosäle +

+ + + + + + + + + + + +
IDNameAnzahl Plätze
+ + + + + +
+
+ +
+ + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/kinos.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/kinos.html new file mode 100644 index 0000000..c2edc20 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/kinos.html @@ -0,0 +1,44 @@ + + + + Flexinale - Kino Übersicht + + + + + + + +
+ +
+

Kino-Liste ist leer!

+
+ +
+

Kino-Liste

+ + + + + + + + + + + +
NameAdresseAnzahl Säle
+ + + + + + + +
+
+ + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/tickets.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/tickets.html new file mode 100644 index 0000000..f136f71 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/templates/tickets.html @@ -0,0 +1,78 @@ + + + + Flexinale - Tickets + + + + + + + + +
+ + + +
+

Keine Tickets vorhanden!

+
+ +
+

Du hast + + Tickets +

+ + + + +

+ + Vorführung(en) + am + + +

+ + + + + + + + + + + + + + + + + +
FilmVorführungKinoSaalAnzahl gültige TicketsAnzahl ungültige Tickets
+ + + + + + + + + + + + + +
+   +   +   +
+
+
+ + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-cacheContents.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-cacheContents.http new file mode 100644 index 0000000..b5954aa --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-cacheContents.http @@ -0,0 +1,3 @@ +# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/besucherportalCacheContents +GET http://localhost:8080/actuator/besucherportalCacheContents +Authorization: Basic admin admin1 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-events-consumed.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-events-consumed.http new file mode 100644 index 0000000..ed0421a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-events-consumed.http @@ -0,0 +1,3 @@ +# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/besucherportalEventsConsumed +GET http://localhost:8080/actuator/besucherportalEventsConsumed +Authorization: Basic admin admin1 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-events-published.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-events-published.http new file mode 100644 index 0000000..c035d57 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-events-published.http @@ -0,0 +1,3 @@ +# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/besucherportalEventsPublished +GET http://localhost:8080/actuator/besucherportalEventsPublished +Authorization: Basic admin admin1 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-health.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-health.http new file mode 100644 index 0000000..3815062 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-health.http @@ -0,0 +1,3 @@ +# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/health +GET http://localhost:8080/actuator/health +Authorization: Basic admin admin1 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-info.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-info.http new file mode 100644 index 0000000..98d95b4 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-info.http @@ -0,0 +1,3 @@ +# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/info +GET http://localhost:8080/actuator/info +Authorization: Basic admin admin1 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-metrics.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-metrics.http new file mode 100644 index 0000000..fb446e6 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-metrics.http @@ -0,0 +1,3 @@ +# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/metrics +GET http://localhost:8080/actuator/metrics +Authorization: Basic admin admin1 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-prometheus.http b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-prometheus.http new file mode 100644 index 0000000..1baab52 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/curl/besucherportal-actuator-prometheus.http @@ -0,0 +1,3 @@ +# curl -v -X GET -u admin:admin1 http://localhost:8080/actuator/prometheus +GET http://localhost:8080/actuator/prometheus +Authorization: Basic admin admin1 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java new file mode 100644 index 0000000..15f617d --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/java/de/accso/flexinale/ApplicationPropertiesFileTest.java @@ -0,0 +1,21 @@ +package de.accso.flexinale; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = {FlexinaleDistributedApplicationBesucherportal.class}) +@ActiveProfiles("smoketest") +public class ApplicationPropertiesFileTest { + + @Value("${application.version}") + private String propertyApplicationVersion; + + @Test + public void testReadsTestPropertiesFile() { + assertThat(propertyApplicationVersion).isEqualTo("test"); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportalSmokeTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportalSmokeTest.java new file mode 100644 index 0000000..df20f3d --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportalSmokeTest.java @@ -0,0 +1,20 @@ +package de.accso.flexinale; + +import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests; +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest(classes = {FlexinaleDistributedApplicationBesucherportal.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) +@ActiveProfiles("smoketest") +@DoNotCheckInArchitectureTests +class FlexinaleDistributedApplicationBesucherportalSmokeTest { + + @Test + @SuppressWarnings("EmptyMethod") + void testLoadInitialApplicationContext() { + // nope + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportalSpringConfigTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportalSpringConfigTest.java new file mode 100644 index 0000000..ea206a5 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/java/de/accso/flexinale/FlexinaleDistributedApplicationBesucherportalSpringConfigTest.java @@ -0,0 +1,31 @@ +package de.accso.flexinale; + +import de.accso.flexinale.common.application.Config; +import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.annotation.DirtiesContext; +import org.springframework.test.context.ActiveProfiles; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(classes = {FlexinaleDistributedApplicationBesucherportal.class}) +@DirtiesContext(classMode = DirtiesContext.ClassMode.BEFORE_CLASS) +@ActiveProfiles("configtest") +@DoNotCheckInArchitectureTests +class FlexinaleDistributedApplicationBesucherportalSpringConfigTest { + + @Autowired + Config config; + + @Test + void testLoadSpringConfig() { + // arrange, act + // nope, is loaded auto-magically by Spring, see application-configtest.properties + + // assert + assertThat(config.getQuoteOnline()).isEqualTo(33); // default + assertThat(config.getMinZeitZwischenVorfuehrungenInMinuten()).isEqualTo(1234); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/resources/application-configtest.properties b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/resources/application-configtest.properties new file mode 100644 index 0000000..b53061c --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/resources/application-configtest.properties @@ -0,0 +1,2 @@ +# time in minutes +de.accso.flexinale.vorfuehrung.min-zeit-zwischen-vorfuehrungen-in-minuten=1234 diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/resources/application.properties b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/resources/application.properties new file mode 100644 index 0000000..1cf6926 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/resources/application.properties @@ -0,0 +1,39 @@ +application.title=FLEXinale as Distributed Services, Besucherportal +application.version=test +spring.banner.location=classpath:/flexinale-banner.txt + +######################################################################### +# For endpoint /version +######################################################################### +build.version=@pom.version@ +build.date=@maven.build.timestamp@ + +######################################################################### +# Persistence +######################################################################### +spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/distributed-besucherportal +spring.datasource.name=flexinale +spring.datasource.username=flexinale +spring.datasource.password=flexinale +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.database=postgresql +spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect +spring.jpa.generate-ddl=true +# spring.jpa.show-sql=true +spring.jpa.hibernate.ddl-auto=create +server.port=9090 + +######################################################################### +# Web +######################################################################### +spring.thymeleaf.prefix=classpath:/templates/ +spring.thymeleaf.suffix=.html +server.error.path=/error + +######################################################################### +# flexinale properties +######################################################################### +# Quote for online kontingent in percent +de.accso.flexinale.kontingent.quote.online=33 +# time in minutes +de.accso.flexinale.vorfuehrung.min-zeit-zwischen-vorfuehrungen-in-minuten=30 \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/resources/logback-test.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/resources/logback-test.xml new file mode 100644 index 0000000..ac74e21 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/test/resources/logback-test.xml @@ -0,0 +1,20 @@ + + + + %d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/.gitignore b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/.gitignore new file mode 100644 index 0000000..b8de55e --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/.gitignore @@ -0,0 +1,4 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/pom.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/pom.xml new file mode 100644 index 0000000..3d481fd --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + + de.accso + flexinale-distributed + 2024.3.0 + ../pom.xml + + + flexinale-distributed-besucherportal_api_contract + 2024.3.0 + Flexinale Distributed Besucherportal API Contract + Flexinale - FLEX case-study "film festival", distributed services, besucherportal_api-contract + + jar + + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + de.accso + flexinale-distributed-common + 2024.3.0 + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs-maven-plugin.version} + + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs-maven-plugin.version} + + + + + + + com.github.spotbugs + spotbugs + ${spotbugs.version} + + + + + verify + + check + + + + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/java/de/accso/flexinale/besucherportal/api_contract/event/AllBesucherportalEvents.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/java/de/accso/flexinale/besucherportal/api_contract/event/AllBesucherportalEvents.java new file mode 100644 index 0000000..59d570c --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/java/de/accso/flexinale/besucherportal/api_contract/event/AllBesucherportalEvents.java @@ -0,0 +1,14 @@ +package de.accso.flexinale.besucherportal.api_contract.event; + +import de.accso.flexinale.common.api.event.Event; + +import java.util.Set; + +public final class AllBesucherportalEvents { + + public static final Set> eventTypes = + Set.of( + GutscheinEinloesenBeauftragtEvent.class + ); + +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/java/de/accso/flexinale/besucherportal/api_contract/event/GutscheinEinloesenBeauftragtEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/java/de/accso/flexinale/besucherportal/api_contract/event/GutscheinEinloesenBeauftragtEvent.java new file mode 100644 index 0000000..904ef3e --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/java/de/accso/flexinale/besucherportal/api_contract/event/GutscheinEinloesenBeauftragtEvent.java @@ -0,0 +1,53 @@ +package de.accso.flexinale.besucherportal.api_contract.event; + +import com.fasterxml.jackson.annotation.JsonGetter; +import de.accso.flexinale.besucherportal.api_contract.event.model.GutscheinEinloesenAuftragTO; +import de.accso.flexinale.common.api.event.AbstractEvent; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests; +import de.accso.flexinale.common.shared_kernel.Versionable; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +@SuppressWarnings({"unused", "CanBeFinal"}) +public class GutscheinEinloesenBeauftragtEvent extends AbstractEvent implements Event { + private static Version version = Versionable.initialVersion().inc(); + + @DoNotCheckInArchitectureTests + public GutscheinEinloesenAuftragTO gutscheinEinloesenAuftrag; + + private GutscheinEinloesenBeauftragtEvent() {} // needed for (de)serialization via Jackson + + public GutscheinEinloesenBeauftragtEvent(final GutscheinEinloesenAuftragTO gutscheinEinloesenAuftrag) { + this.gutscheinEinloesenAuftrag = gutscheinEinloesenAuftrag; + } + + @Override + @JsonGetter("version") // needed as otherwise the static field version is not (de)serialized + public Version version() { + return version; + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + final GutscheinEinloesenBeauftragtEvent that = (GutscheinEinloesenBeauftragtEvent) o; + + return new EqualsBuilder().appendSuper(super.equals(o)) + .append(this.version(), that.version()) + .append(gutscheinEinloesenAuftrag, that.gutscheinEinloesenAuftrag) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .appendSuper(super.hashCode()) + .append(this.version()) + .append(gutscheinEinloesenAuftrag) + .toHashCode(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/java/de/accso/flexinale/besucherportal/api_contract/event/model/GutscheinEinloesenAuftragTO.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/java/de/accso/flexinale/besucherportal/api_contract/event/model/GutscheinEinloesenAuftragTO.java new file mode 100644 index 0000000..f5f64ae --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/java/de/accso/flexinale/besucherportal/api_contract/event/model/GutscheinEinloesenAuftragTO.java @@ -0,0 +1,38 @@ +package de.accso.flexinale.besucherportal.api_contract.event.model; + +import de.accso.flexinale.common.shared_kernel.*; +import org.apache.commons.lang3.builder.EqualsBuilder; + +import java.io.Serializable; + +@SuppressWarnings("unused") +public record GutscheinEinloesenAuftragTO(Id id, Version version, + Id filmId, Id vorfuehrungId, Id besucherId, + AnzahlTickets anzahlTickets) + implements Identifiable, Versionable, EqualsByContent, Serializable +{ + public record AnzahlTickets(Integer raw) implements RawWrapper {} + + public enum VerkaufsKanal { + ONLINE, + ZENTRAL, + KINOKASSE + } + + @Override + public boolean equalsByContent(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + GutscheinEinloesenAuftragTO that = (GutscheinEinloesenAuftragTO) o; + + return new EqualsBuilder() + .append(id, that.id) + .append(filmId, that.filmId) + .append(vorfuehrungId, that.vorfuehrungId) + .append(besucherId, that.besucherId) + .append(anzahlTickets, that.anzahlTickets) + .isEquals(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/resources/logback.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/resources/logback.xml new file mode 100644 index 0000000..87e8669 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/main/resources/logback.xml @@ -0,0 +1,20 @@ + + + + %d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/test/java/de/accso/flexinale/besucherportal/api_contract/event/EventSerializationTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/test/java/de/accso/flexinale/besucherportal/api_contract/event/EventSerializationTest.java new file mode 100644 index 0000000..386e25c --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/test/java/de/accso/flexinale/besucherportal/api_contract/event/EventSerializationTest.java @@ -0,0 +1,67 @@ +package de.accso.flexinale.besucherportal.api_contract.event; + +import com.fasterxml.jackson.core.JsonProcessingException; +import de.accso.flexinale.besucherportal.api_contract.event.model.GutscheinEinloesenAuftragTO; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.infrastructure.eventbus.EventSerializationHelper; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.RawWrapper; +import de.accso.flexinale.common.shared_kernel.Versionable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.time.LocalDateTime; +import java.util.Set; +import java.util.stream.Stream; + +import static de.accso.flexinale.besucherportal.api_contract.event.ReflectionHelper.setField; +import static org.assertj.core.api.Assertions.assertThat; + +class EventSerializationTest { + private static Stream allGutscheinEventTypes() { + return Set.of(GutscheinEinloesenBeauftragtEvent.class).stream().map(Arguments::of); + } + + @ParameterizedTest + @MethodSource("allGutscheinEventTypes") + void testSerializeGutscheinEventClass2JsonString(final Class gutscheinEventType) + throws JsonProcessingException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchMethodException { + // arrange + Constructor constructor = gutscheinEventType.getDeclaredConstructor(); + constructor.setAccessible(true); + Event event = constructor.newInstance(); + + GutscheinEinloesenAuftragTO gutscheinEinloesenAuftragTO = createGutscheinTO(); + setField(gutscheinEventType, event, "gutscheinEinloesenAuftrag", gutscheinEinloesenAuftragTO, true); + + // act + String jsonString = EventSerializationHelper.serializeEvent2JsonString(event); + + // assert + assertThat(jsonString).contains(getExpectedJsonStringForVersion(event.version())); + assertThat(jsonString).contains(getExpectedJsonStringForRawTypeInteger("anzahlTickets", gutscheinEinloesenAuftragTO.anzahlTickets())); + } + + private static GutscheinEinloesenAuftragTO createGutscheinTO() { + GutscheinEinloesenAuftragTO.AnzahlTickets anzahlTickets = new GutscheinEinloesenAuftragTO.AnzahlTickets(42); + return new GutscheinEinloesenAuftragTO(Identifiable.Id.of(), Versionable.unknownVersion(), + Identifiable.Id.of(), Identifiable.Id.of(), Identifiable.Id.of(), + anzahlTickets); + } + + private static String getExpectedJsonStringForVersion(Versionable.Version field) { + return String.format("\"%s\":{\"version\":%d", "version", field.version()); + } + private static String getExpectedJsonStringForRawTypeString(String fieldName, RawWrapper field) { + return String.format("\"%s\":{\"raw\":\"%s\"", fieldName, field.raw()); + } + private static String getExpectedJsonStringForRawTypeInteger(String fieldName, RawWrapper field) { + return String.format("\"%s\":{\"raw\":%d", fieldName, field.raw()); + } + private static String getExpectedJsonStringForRawTypeLocalDateTime(String fieldName, RawWrapper field) { + return String.format("\"%s\":{\"raw\":\"%s\"", fieldName, field.raw().toString()); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/test/java/de/accso/flexinale/besucherportal/api_contract/event/EventStructureTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/test/java/de/accso/flexinale/besucherportal/api_contract/event/EventStructureTest.java new file mode 100644 index 0000000..dabd7e3 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/test/java/de/accso/flexinale/besucherportal/api_contract/event/EventStructureTest.java @@ -0,0 +1,52 @@ +package de.accso.flexinale.besucherportal.api_contract.event; + +import com.fasterxml.jackson.core.JsonProcessingException; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.infrastructure.eventbus.EventSerializationHelper; +import de.accso.flexinale.common.shared_kernel.EventClassHelper; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.time.LocalDateTime; +import java.util.stream.Stream; + +import static de.accso.flexinale.besucherportal.api_contract.event.ReflectionHelper.setField; +import static org.assertj.core.api.Assertions.assertThatCode; + +class EventStructureTest { + private static Stream allEventTypes() { + return AllBesucherportalEvents.eventTypes.stream().map(Arguments::of); + } + + // check for all Event subclasses if no exception is thrown for (de)serialization and reflection access + // a) default constructor is needed + // b) field 'version' is needed + + @ParameterizedTest + @MethodSource("allEventTypes") + void testCheckEventStructure(final Class eventType) + throws InstantiationException, IllegalAccessException, JsonProcessingException, NoSuchMethodException, InvocationTargetException { + // arrange + Constructor constructor = eventType.getDeclaredConstructor(); + constructor.setAccessible(true); + Event event = constructor.newInstance(); + + // set id, timestamp and version explicitely, so that (de)serialization is somewhat more realistic + setField(eventType, event, "id", Identifiable.Id.of(), false); + setField(eventType, event, "timestamp", LocalDateTime.now(), false); + + // act - check serialization and deserialization + String jsonString = EventSerializationHelper.serializeEvent2JsonString(event); + EventSerializationHelper.deserializeJsonString2Event(jsonString, eventType); + + // act - check instantiation and version attribute + assertThatCode(() -> { + EventClassHelper.getEventClazzVersion(eventType); + }) + .doesNotThrowAnyException(); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/test/java/de/accso/flexinale/besucherportal/api_contract/event/ReflectionHelper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/test/java/de/accso/flexinale/besucherportal/api_contract/event/ReflectionHelper.java new file mode 100644 index 0000000..bccbfce --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal_api_contract/src/test/java/de/accso/flexinale/besucherportal/api_contract/event/ReflectionHelper.java @@ -0,0 +1,25 @@ +package de.accso.flexinale.besucherportal.api_contract.event; + +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalArgumentException; + +import java.lang.reflect.Field; + +public final class ReflectionHelper { + public static void setField(final Class eventType, final Event event, final String fieldName, final Object fieldValue, + boolean includeDerivedFieldsFromSuperClasses) throws IllegalAccessException { + if (eventType.equals(Object.class)) { + throw new FlexinaleIllegalArgumentException("event hierarchy does not have a field " + fieldName); + } + try { + Field field = (includeDerivedFieldsFromSuperClasses) + ? eventType.getField(fieldName) + : eventType.getDeclaredField(fieldName); + field.setAccessible(true); + field.set(event, fieldValue); + } + catch (NoSuchFieldException nosfex) { + setField(eventType.getSuperclass(), event, fieldName, fieldValue, includeDerivedFieldsFromSuperClasses); + } + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/SpotbugsExcludeFilter.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/SpotbugsExcludeFilter.xml new file mode 100644 index 0000000..d102a67 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/SpotbugsExcludeFilter.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/pom.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/pom.xml new file mode 100644 index 0000000..5daa9d4 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/pom.xml @@ -0,0 +1,116 @@ + + + 4.0.0 + + + de.accso + flexinale-distributed + 2024.3.0 + ../pom.xml + + + flexinale-distributed-common + 2024.3.0 + Flexinale Distributed Common + Flexinale - FLEX case-study "film festival", distributed services, common + + jar + + + + org.springframework.boot + spring-boot + + + + org.springframework.kafka + spring-kafka + + + + org.apache.poi + poi + ${apache-poi.version} + + + org.apache.poi + poi-ooxml + ${apache-poi.version} + + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + org.slf4j + slf4j-api + ${slf4j.version} + + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${jackson.version} + + + + + + + org.apache.maven.plugins + maven-jar-plugin + ${maven-jar-plugin.version} + + + + test-jar + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + ${spotbugs-maven-plugin.version} + + + + com.h3xstream.findsecbugs + findsecbugs-plugin + ${findsecbugs-maven-plugin.version} + + + SpotbugsExcludeFilter.xml + + + + + com.github.spotbugs + spotbugs + ${spotbugs.version} + + + + + verify + + check + + + + + + + + \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/event/AbstractEvent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/event/AbstractEvent.java new file mode 100644 index 0000000..d16141a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/event/AbstractEvent.java @@ -0,0 +1,105 @@ +package de.accso.flexinale.common.api.event; + +import com.fasterxml.jackson.annotation.JsonSetter; +import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException; +import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.lang.reflect.Field; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; + +@SuppressWarnings("CanBeFinal") +public abstract class AbstractEvent implements Event { + + public final Id id = Identifiable.Id.of(); + public final Id correlationId; + @DoNotCheckInArchitectureTests + protected LocalDateTime timestamp = LocalDateTime.now(); + @DoNotCheckInArchitectureTests + protected final EventContext eventContext; + + // event is start of a new event chain + protected AbstractEvent() { + this.correlationId = Identifiable.Id.of(); + this.eventContext = new EventContext( this.correlationId, + List.of( new EventContext.EventHistoryElement(id, this.getClass())) ); + } + + // event is part of an existing event chain but without a history + protected AbstractEvent(final Id correlationId) { + this.correlationId = correlationId; + this.eventContext = new EventContext( this.correlationId, + List.of( new EventContext.EventHistoryElement(id, this.getClass())) ); + } + + // event is part of an existing event chain + protected AbstractEvent(final Event predecessorEvent) { + this(predecessorEvent.eventContext()); + } + + // event is part of an existing event chain + protected AbstractEvent(final EventContext predecessorEventContext) { + this.correlationId = predecessorEventContext.correlationId; + this.eventContext = new EventContext( this.correlationId, + List.copyOf(predecessorEventContext.eventHistory.stream().toList()), + new EventContext.EventHistoryElement(id, this.getClass()) // append this event to history + ); + } + + @Override + public Id id() { + return id; + } + + @Override + public LocalDateTime timestamp() { + return timestamp; + } + + @Override + public Id correlationId() { + return correlationId; + } + + @Override + public EventContext eventContext() { + return eventContext; + } + + @JsonSetter("version") // needed as otherwise the static field version (in one of the subclasses) is not (de)serialized + public void setVersion(Version newVersion) { + String fieldName = "version"; + try { + Field field = this.getClass().getDeclaredField(fieldName); // static field + field.setAccessible(true); + field.set(this, newVersion); + } + catch (NoSuchFieldException | IllegalAccessException ex) { + throw new DeveloperMistakeException("version field cannot be set via reflection in an Event class", ex); + } + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + if (o == null || getClass() != o.getClass()) {return false;} + + final AbstractEvent that = (AbstractEvent) o; + + return Objects.equals(id, that.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/event/Event.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/event/Event.java new file mode 100644 index 0000000..89c6dd7 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/event/Event.java @@ -0,0 +1,14 @@ +package de.accso.flexinale.common.api.event; + +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.Versionable; + +import java.io.Serializable; +import java.time.LocalDateTime; + +@SuppressWarnings("unused") +public interface Event extends Identifiable, Versionable, Serializable { + LocalDateTime timestamp(); + Id correlationId(); + EventContext eventContext(); +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/event/EventContext.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/event/EventContext.java new file mode 100644 index 0000000..74ef6bf --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/event/EventContext.java @@ -0,0 +1,65 @@ +package de.accso.flexinale.common.api.event; + +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import java.util.ArrayList; +import java.util.List; + +@SuppressWarnings("unused") +public class EventContext { + public record EventHistoryElement(Identifiable.Id id, Class eventType) {} + + public Identifiable.Id correlationId; + + public List eventHistory; + + private EventContext() {} // used by Jackson deserialization + + public EventContext(final Identifiable.Id correlationId, final List eventHistory) { + this.correlationId = correlationId; + this.eventHistory = List.copyOf(eventHistory); + } + + public EventContext(final Identifiable.Id correlationId, + final List eventHistory, final EventHistoryElement additionalHistoryElement) { + this.correlationId = correlationId; + + // pretty ugly to copy plus add to immutable list + List tempList = new ArrayList<>(eventHistory); + tempList.add(additionalHistoryElement); + this.eventHistory = List.copyOf(tempList); + } + + public boolean equalsByCorrelationId(EventContext that) { + return this.correlationId.equals(that.correlationId); + } + + @Override + public String toString() { + return ToStringBuilder.reflectionToString(this, ToStringStyle.MULTI_LINE_STYLE); + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + EventContext that = (EventContext) o; + + return new EqualsBuilder() + .append(eventHistory, that.eventHistory) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .append(this.correlationId) + .toHashCode(); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventBus.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventBus.java new file mode 100644 index 0000000..bde9d0b --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventBus.java @@ -0,0 +1,20 @@ +package de.accso.flexinale.common.api.eventbus; + +import de.accso.flexinale.common.api.event.Event; + +public interface EventBus { + + // Event type specific subscription + void subscribe(Class eventType, EventSubscriber subscriber, EventSubscriptionAtStart eventSubscriptionAtStart); + void unsubscribe(Class eventType, EventSubscriber subscriber); + + // subscription for _all_ Events (i.e. based on Event.class) + void subscribe(EventSubscriber genericSubscriber, EventSubscriptionAtStart eventSubscriptionAtStart); + void unsubscribe(EventSubscriber genericSubscriber); + + void unsubscribeAll(); + + void publish(Class eventType, E event); + + EventContextHolder getEventContextHolder(); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventBusFactory.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventBusFactory.java new file mode 100644 index 0000000..b664148 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventBusFactory.java @@ -0,0 +1,7 @@ +package de.accso.flexinale.common.api.eventbus; + +import de.accso.flexinale.common.api.event.Event; + +public interface EventBusFactory { + EventBus createOrGetEventBusFor(Class type); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventContextHolder.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventContextHolder.java new file mode 100644 index 0000000..8dd5a9a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventContextHolder.java @@ -0,0 +1,10 @@ +package de.accso.flexinale.common.api.eventbus; + +import de.accso.flexinale.common.api.event.EventContext; + +public interface EventContextHolder { + EventContext get(); + void set(EventContext eventContext); + void remove(); + boolean isEmpty(); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventNotification.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventNotification.java new file mode 100644 index 0000000..82f2ce2 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventNotification.java @@ -0,0 +1,7 @@ +package de.accso.flexinale.common.api.eventbus; + +import de.accso.flexinale.common.api.event.Event; + +public interface EventNotification { + void notify(Event event); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventPublisher.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventPublisher.java new file mode 100644 index 0000000..da2741f --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventPublisher.java @@ -0,0 +1,56 @@ +package de.accso.flexinale.common.api.eventbus; + +import de.accso.flexinale.common.api.event.Event; + +@SuppressWarnings("unused") +public interface EventPublisher { + + String getName(); + + void post(Class eventType, E1 event); + + interface EventPublisher2 + extends EventPublisher { + void post2(Class eventType, E2 event); + } + + interface EventPublisher3 + extends EventPublisher2 { + void post3(Class eventType, E3 event); + } + + interface EventPublisher4 + extends EventPublisher3 { + void post4(Class eventType, E4 event); + } + + interface EventPublisher5 + extends EventPublisher4 { + void post5(Class eventType, E5 event); + } + + interface EventPublisher6 + extends EventPublisher5 { + void post6(Class eventType, E6 event); + } + + interface EventPublisher7 + extends EventPublisher6 { + void post7(Class eventType, E7 event); + } + + interface EventPublisher8 + extends EventPublisher7 { + void post8(Class eventType, E8 event); + } + + interface EventPublisher9 + extends EventPublisher8 { + void post9(Class eventType, E9 event); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventSubscriber.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventSubscriber.java new file mode 100644 index 0000000..aa204d4 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventSubscriber.java @@ -0,0 +1,67 @@ +package de.accso.flexinale.common.api.eventbus; + +import de.accso.flexinale.common.api.event.Event; + +@SuppressWarnings("unused") // methods are called via reflection +public interface EventSubscriber { + + String getName(); + String getGroupName(); + + void receive(E1 event); + + interface EventSubscriber2 + extends EventSubscriber { + void receive2(E2 event); + } + + interface EventSubscriber3 + extends EventSubscriber2 { + void receive3(E3 event); + } + + interface EventSubscriber4 + extends EventSubscriber3 { + void receive4(E4 event); + } + + interface EventSubscriber5 + extends EventSubscriber4 { + void receive5(E5 event); + } + + interface EventSubscriber6 + extends EventSubscriber5 { + void receive6(E6 event); + } + + interface EventSubscriber7 + extends EventSubscriber6 { + void receive7(E7 event); + } + + interface EventSubscriber8 + extends EventSubscriber7 { + void receive8(E8 event); + } + + interface EventSubscriber9 + extends EventSubscriber8 { + void receive9(E9 event); + } + + String RECEIVE = "receive"; + String RECEIVE2 = "receive2"; + String RECEIVE3 = "receive3"; + String RECEIVE4 = "receive4"; + String RECEIVE5 = "receive5"; + String RECEIVE6 = "receive6"; + String RECEIVE7 = "receive7"; + String RECEIVE8 = "receive8"; + String RECEIVE9 = "receive9"; +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventSubscriptionAtStart.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventSubscriptionAtStart.java new file mode 100644 index 0000000..0480487 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/api/eventbus/EventSubscriptionAtStart.java @@ -0,0 +1,19 @@ +package de.accso.flexinale.common.api.eventbus; + +// Behaviour on how a EventSubscriber (i.e. Consumer) reads new messages, after having subscribed to the EventBus +// - How much of the old messages are read or re-read (if any)? +// - Where do we start, at which offset? +public enum EventSubscriptionAtStart { + // (re)read all messages from the EventBus from the very beginning (i.e. from offset 0) + // In Kafka this is - for a new ConsumerGroup - "earliest". + START_READING_FROM_BEGINNING, + + // read all messages from now on (so don't (re)read anything published before now) + // In Kafka this is - for a new ConsumerGroup - "latest". + START_READING_FROM_NOW, + + // read all messages from the EventBus, starting where we left off last time (so last offset we had + 1) + // (if we had not connected before at all: start from beginning, i.e. from offset 0) + // In Kafka this is - for a known ConsumerGroup - the default. + START_READING_FROM_LAST_TIME +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/Config.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/Config.java new file mode 100644 index 0000000..fc7b47e --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/Config.java @@ -0,0 +1,10 @@ +package de.accso.flexinale.common.application; + +public interface Config { + int getQuoteOnline(); + int getMinZeitZwischenVorfuehrungenInMinuten(); + + String getApplicationTitle(); + String getBuildVersion(); + String getBuildDate(); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/PersistMode.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/PersistMode.java new file mode 100644 index 0000000..3a42aff --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/PersistMode.java @@ -0,0 +1,9 @@ +package de.accso.flexinale.common.application; + +public enum PersistMode { + // updates on existing entities allowed (beware of inconsistencies with derived entities!) + UPDATE, + + // only new data can be added, no updates on existing entities allowed + ADD_ONLY +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/PersistedEntityAndResult.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/PersistedEntityAndResult.java new file mode 100644 index 0000000..b6acc5c --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/PersistedEntityAndResult.java @@ -0,0 +1,12 @@ +package de.accso.flexinale.common.application; + +import de.accso.flexinale.common.shared_kernel.Identifiable; + +public record PersistedEntityAndResult + (E entity, PersistedResult result) { + + public enum PersistedResult { + UPDATED, + ADDED + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/caching/InMemoryCache.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/caching/InMemoryCache.java new file mode 100644 index 0000000..a3988c6 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/caching/InMemoryCache.java @@ -0,0 +1,129 @@ +package de.accso.flexinale.common.application.caching; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; +import java.util.stream.Collectors; + +public final class InMemoryCache { + // as an alternative: extract interface, use Cache2K as a backing implementation + // see https://cache2k.org/docs/latest/user-guide.html#getting-started + private final ConcurrentHashMap backingMap = new ConcurrentHashMap<>(); + + private final ReentrantLock lock = new ReentrantLock(); + + public OBJECT get(final ID id) { + lock.lock(); + try { + return backingMap.get(id); + } + finally { + lock.unlock(); + } + } + + public OBJECT put(final ID id, final OBJECT o) { + lock.lock(); + try { + return backingMap.put(id, o); + } + finally { + lock.unlock(); + } + } + + public OBJECT remove(final ID id) { + lock.lock(); + try { + return backingMap.remove(id); + } + finally { + lock.unlock(); + } + } + + public Set keys() { + lock.lock(); + try { + return backingMap.keySet().stream().collect(Collectors.toUnmodifiableSet()); + } + finally { + lock.unlock(); + } + } + + public Set values() { + lock.lock(); + try { + return backingMap.values().stream().collect(Collectors.toUnmodifiableSet()); + } + finally { + lock.unlock(); + } + } + + public Set> entrySet() { + lock.lock(); + try { + return backingMap.entrySet(); + } + finally { + lock.unlock(); + } + } + + public int size() { + lock.lock(); + try { + return backingMap.size(); + } + finally { + lock.unlock(); + } + } + + public boolean isEmpty() { + lock.lock(); + try { + return backingMap.isEmpty(); + } + finally { + lock.unlock(); + } + } + + public boolean contains(final OBJECT o) { + lock.lock(); + try { + return backingMap.contains(o); + } + finally { + lock.unlock(); + } + } + + public boolean containsKey(final ID key) { + return backingMap.containsKey(key); + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + if (o == null || getClass() != o.getClass()) {return false;} + + final InMemoryCache that = (InMemoryCache) o; + + return backingMap.equals(that.backingMap); + } + + @Override + public int hashCode() { + return backingMap.hashCode(); + } + + @Override + public String toString() { + return backingMap.toString(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/services/AbstractExcelDataUploadService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/services/AbstractExcelDataUploadService.java new file mode 100644 index 0000000..bc31b76 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/services/AbstractExcelDataUploadService.java @@ -0,0 +1,131 @@ +package de.accso.flexinale.common.application.services; + +import de.accso.flexinale.common.application.PersistMode; +import de.accso.flexinale.common.application.PersistedEntityAndResult; +import de.accso.flexinale.common.domain.model.AbstractDao; +import de.accso.flexinale.common.shared_kernel.DoNotCheckInArchitectureTests; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.Mergeable; +import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; +import java.util.HashSet; +import java.util.Optional; + +import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.ADDED; +import static de.accso.flexinale.common.application.PersistedEntityAndResult.PersistedResult.UPDATED; + +@SuppressWarnings("unused") +@DoNotCheckInArchitectureTests +public abstract class AbstractExcelDataUploadService> implements ExcelDataUploadService { + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractExcelDataUploadService.class); + + @Override + public void beforeLoad(Object... o) {} + + @Override + public void afterLoad(Object... o) {} + + @Override + public final Collection loadDataFromExcelSheet(final String resourceName) throws IOException { + try (InputStream stream = AbstractExcelDataUploadService.class.getResourceAsStream(resourceName)) { + if (stream == null) { + throw new FileNotFoundException("resource '%s' not found!".formatted(resourceName)); + } + return loadDataFromExcelSheet(stream); + } + } + + @Override + public final Collection loadDataFromExcelSheet(final InputStream stream) throws IOException { + Collection dataSet = new HashSet<>(); + + // Excel sheet (the sheet, not the file!) needs to be named exactly like the entity's class name + // (example: "Kino") + String sheetName = getNameOfExcelDataType(); + + try (Workbook workbook = new XSSFWorkbook(stream)) { + // iterate through each row of first/desired sheet from the workbook + for (Row excelRow : workbook.getSheet(sheetName)) { + // ignore comments, starting with # , load all other rows + Cell firstCell = excelRow.getCell(0); + if (firstCell != null && !firstCell.getStringCellValue().startsWith("#")) { + E data = createDataFromExcelRow(excelRow); + dataSet.add(data); + } + } + } + + return dataSet; + } + + @Override + public final int loadDataFromExcelSheetAndPersist( + final String resourceName, final PersistMode mode) throws IOException { + try (InputStream stream = AbstractExcelDataUploadService.class.getResourceAsStream(resourceName)) { + if (stream == null) { + throw new IOException("resource '%s' not found!".formatted(resourceName)); + } + return loadDataFromExcelSheetAndPersist(stream, mode); + } + } + + @Override + public final int loadDataFromExcelSheetAndPersist( + final InputStream stream, final PersistMode mode) throws IOException { + return loadDataFromExcelSheetAndPersistAndReturn(stream, mode).size(); + } + + @Override + public Collection> loadDataFromExcelSheetAndPersistAndReturn( + final InputStream stream, final PersistMode mode) throws IOException { + Collection dataCollectionFromExcelSheet = loadDataFromExcelSheet(stream); + return loadDataAndPersistAndReturn(dataCollectionFromExcelSheet, mode); + } + + @Override + public Collection> loadDataAndPersistAndReturn( + final Collection dataCollection, final PersistMode mode) { + Collection> dataAddedAndUpdated = new HashSet<>(); + + AbstractDao dao = getDao(); + + String sheetName = getNameOfExcelDataType(); + + for (E dataFromExcelSheet : dataCollection) { + Identifiable.Id id = dataFromExcelSheet.id(); + + Optional optionalEntityFromDatabase = dao.findById(id); + if (optionalEntityFromDatabase.isPresent()) { + E entityFromDatabase = optionalEntityFromDatabase.get(); + switch(mode) { + case ADD_ONLY -> + LOGGER.warn("%s %s already exists. Not persisted as mode 'ADD_ONLY' is active".formatted(sheetName, id)); + case UPDATE -> { + LOGGER.info("%s %s already exists. Merged as mode 'UPDATE' is active".formatted(sheetName, id)); + E mergedEntity = entityFromDatabase.merge(dataFromExcelSheet); + dao.save(mergedEntity); + + dataAddedAndUpdated.add( new PersistedEntityAndResult<>(dataFromExcelSheet, UPDATED) ); + } + } + } + else { + LOGGER.info("%s %s does not yet exist. Added as new entity".formatted(sheetName, id)); + dao.save(dataFromExcelSheet); + + dataAddedAndUpdated.add( new PersistedEntityAndResult<>(dataFromExcelSheet, ADDED) ); + } + } + + return dataAddedAndUpdated; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/services/ExcelDataUploadService.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/services/ExcelDataUploadService.java new file mode 100644 index 0000000..820caeb --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/application/services/ExcelDataUploadService.java @@ -0,0 +1,32 @@ +package de.accso.flexinale.common.application.services; + +import de.accso.flexinale.common.application.PersistMode; +import de.accso.flexinale.common.application.PersistedEntityAndResult; +import de.accso.flexinale.common.domain.model.AbstractDao; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.apache.poi.ss.usermodel.Row; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Collection; + +@SuppressWarnings("unused") +public interface ExcelDataUploadService { + Collection loadDataFromExcelSheet(final String resourceName) throws IOException; + Collection loadDataFromExcelSheet(final InputStream stream) throws IOException; + + int loadDataFromExcelSheetAndPersist(final String resourceName, PersistMode mode) throws IOException; + int loadDataFromExcelSheetAndPersist(final InputStream stream, PersistMode mode) throws IOException; + + Collection> + loadDataFromExcelSheetAndPersistAndReturn(final InputStream stream, PersistMode mode) throws IOException; + Collection> + loadDataAndPersistAndReturn(Collection data, PersistMode mode); + + AbstractDao getDao(); + String getNameOfExcelDataType(); + + void beforeLoad(Object... o); + void afterLoad(Object... o); + E createDataFromExcelRow(final Row row); +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/domain/model/AbstractDao.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/domain/model/AbstractDao.java new file mode 100644 index 0000000..d33aa55 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/domain/model/AbstractDao.java @@ -0,0 +1,19 @@ +package de.accso.flexinale.common.domain.model; + +import de.accso.flexinale.common.shared_kernel.Identifiable; + +import java.util.List; +import java.util.Optional; + +@SuppressWarnings("unused") +public interface AbstractDao { + List findAll(); + + Optional findById(final Identifiable.Id id); + + E save(final E entity); + + void delete(final E data); + void deleteById(final Identifiable.Id id); + void deleteAll(); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/FlexinaleCommonSpringFactory.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/FlexinaleCommonSpringFactory.java new file mode 100644 index 0000000..8b98a49 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/FlexinaleCommonSpringFactory.java @@ -0,0 +1,61 @@ +package de.accso.flexinale.common.infrastructure; + +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import de.accso.flexinale.common.infrastructure.eventbus.*; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Configuration +public class FlexinaleCommonSpringFactory { + + // Eventbus via Kafka + + // default: used for scenarios using real distributed remote communication via Kafka + // so when starting all three apps Besucherportal, Backoffice and Ticketing and also for "test-distributed" + @Bean + @Profile({"!test-integrated & !testdata & !configtest & !smoketest & !local"}) + public EventBusFactory createKafkaAsyncEventBusFactory(final KafkaAvailabilityChecker kafkaAvailabilityChecker) { + return new KafkaAsyncEventBusFactory(kafkaAvailabilityChecker); + } + + @Bean + @Profile({"!test-integrated & !testdata & !configtest & !smoketest & !local"}) + public KafkaAvailabilityChecker createKafkaAvailabilityChecker() { + return new KafkaAvailabilityChecker(); + } + + // -------------------------------------------------------------------------------------------------------------- + + // Eventbus as in-memory with Spy + + // used in tests where publishers and subscribers need to communicate via an in-memory event bus + // and where we need to look into the event chain via the spy + @Bean + @Profile({"test-integrated"}) + public EventBusFactory createInMemorySyncEventBusSpyFactory() { + return new InMemorySyncEventBusSpyFactory(); + } + + // -------------------------------------------------------------------------------------------------------------- + + // Eventbus as in-memory + + // used in scenarios where publishers and subscribers need to communicated via an in-memory event bus + @Bean + @Profile({"local"}) + public EventBusFactory createInMemorySyncEventBusFactory() { + return new InMemorySyncEventBusFactory(); + } + + // -------------------------------------------------------------------------------------------------------------- + + // Eventbus doing nothing + + // used in scenarios to clean and load test data where we do not want to send any event + @Bean + @Profile({"testdata | configtest | smoketest"}) + public EventBusFactory createNopeEventBusSpyFactory() { + return new NopeEventBusSpyFactory(); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/FlexinaleSpringConfig.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/FlexinaleSpringConfig.java new file mode 100644 index 0000000..038143a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/FlexinaleSpringConfig.java @@ -0,0 +1,70 @@ +package de.accso.flexinale.common.infrastructure; + +import de.accso.flexinale.common.application.Config; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalArgumentException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class FlexinaleSpringConfig implements Config { + @Value("${application.title}") + public String applicationTitle; + + @Value("${build.version}") + public String buildVersion; + + @Value("${build.date}") + public String buildDate; + + @Override + public String getApplicationTitle() { + return applicationTitle; + } + + @Override + public String getBuildVersion() { + return buildVersion; + } + + @Override + public String getBuildDate() { + return buildDate; + } + + // ----------------------------------------------------------------------------------------------------------- + + private final int quoteOnline; + + @Override + public int getQuoteOnline() { + return quoteOnline; + } + + // ----------------------------------------------------------------------------------------------------------- + + private final int minZeitZwischenVorfuehrungenInMinuten; + + @Override + public int getMinZeitZwischenVorfuehrungenInMinuten() { + return minZeitZwischenVorfuehrungenInMinuten; + } + + // ----------------------------------------------------------------------------------------------------------- + + public FlexinaleSpringConfig(@Value("${de.accso.flexinale.kontingent.quote.online:33}") final int quoteOnline, + @Value("${de.accso.flexinale.vorfuehrung.min-zeit-zwischen-vorfuehrungen-in-minuten:30}") final int minZeitZwischenVorfuehrungenInMinuten) { + if (quoteOnline < 0 || quoteOnline > 100) { + String message = "Quote online should be a percentage, i.e. 0 <= quote online <= 100, but was " + quoteOnline; + throw new FlexinaleIllegalArgumentException(message); + } + this.quoteOnline = quoteOnline; + + if (minZeitZwischenVorfuehrungenInMinuten < 0) { + String message = "minZeitZwischenVorfuehrungenInMinuten should be positiv, but was " + + minZeitZwischenVorfuehrungenInMinuten; + throw new FlexinaleIllegalArgumentException(message); + } + this.minZeitZwischenVorfuehrungenInMinuten = minZeitZwischenVorfuehrungenInMinuten; + } + +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/EventSerializationHelper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/EventSerializationHelper.java new file mode 100644 index 0000000..dd28b7d --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/EventSerializationHelper.java @@ -0,0 +1,51 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.module.SimpleModule; +import de.accso.flexinale.common.api.event.Event; + +import java.io.IOException; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public final class EventSerializationHelper { + private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ISO_DATE_TIME; + + private static final ObjectMapper jsonMapper; + + static { + jsonMapper = new ObjectMapper(); + jsonMapper.findAndRegisterModules(); + jsonMapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY); + + // register module for date/time (de)serialization, in ISO format ISO8601, e.g. 2022-05-06T11:12:13Z + SimpleModule localDateTimeModule = new SimpleModule(); + localDateTimeModule.addSerializer(LocalDateTime.class, new JsonSerializer<>() { + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(value.format(dateTimeFormatter)); + } + }); + localDateTimeModule.addDeserializer(LocalDateTime.class, new JsonDeserializer<>() { + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return LocalDateTime.parse(p.getValueAsString(), dateTimeFormatter); + } + }); + jsonMapper.registerModule(localDateTimeModule); + } + + public static String serializeEvent2JsonString(final Event event) throws JsonProcessingException { + return jsonMapper.writeValueAsString(event); + } + + public static E deserializeJsonString2Event( + final String jsonString, final Class eventType) throws JsonProcessingException { + return jsonMapper.readValue(jsonString, eventType); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/EventSubscriberReceiveDispatcher.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/EventSubscriberReceiveDispatcher.java new file mode 100644 index 0000000..2522019 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/EventSubscriberReceiveDispatcher.java @@ -0,0 +1,88 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventSubscriber; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +final class EventSubscriberReceiveDispatcher { + + void callSubscriberReceiveMethod(final Class eventType, final E event, final EventSubscriber subscriber) { + boolean called = false; + + if (subscriber instanceof EventSubscriber.EventSubscriber9) { + called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE9); + } + if (!called && (subscriber instanceof EventSubscriber.EventSubscriber8)) { + called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE8); + } + if (!called && (subscriber instanceof EventSubscriber.EventSubscriber7)) { + called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE7); + } + if (!called && (subscriber instanceof EventSubscriber.EventSubscriber6)) { + called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE6); + } + if (!called && (subscriber instanceof EventSubscriber.EventSubscriber5)) { + called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE5); + } + if (!called && (subscriber instanceof EventSubscriber.EventSubscriber4)) { + called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE4); + } + if (!called && (subscriber instanceof EventSubscriber.EventSubscriber3)) { + called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE3); + } + if (!called && (subscriber instanceof EventSubscriber.EventSubscriber2)) { + called = tryToGetAndInvokeReceiveMethod(eventType, event, subscriber, EventSubscriber.RECEIVE2); + } + if (!called) { + subscriber.receive(event); + } + } + + private record ReflectionMethodCacheKey( + EventSubscriber subscriber, String methodName, Class eventType) {} + + private final Map, Method> reflectionMethodCache = new HashMap<>(); + + private boolean tryToGetAndInvokeReceiveMethod( + final Class eventType, final E event, final EventSubscriber subscriber, final String methodName) + { + if (subscriber == null) { + throw new DeveloperMistakeException("could not call a method for a null subscriber for event type %s, event=%s" + .formatted(eventType, event.toString())); + } + + ReflectionMethodCacheKey key = new ReflectionMethodCacheKey<>(subscriber, methodName, eventType); + Method receiveMethod = reflectionMethodCache.get(key); + try { + // we could extend this that also receive methods are found with have an event type as parameter as a + // superclass of 'eventtype', cf test case + // InMemorySyncEventBusAndEventBusSpyTest.testGenericEventSubscriberWithExtraSubscriptionCannotBeUsedDueToDispatchingError() + if (receiveMethod == null) { + receiveMethod = subscriber.getClass().getMethod(methodName, eventType); + receiveMethod.setAccessible(true); + reflectionMethodCache.put(key, receiveMethod); + } + receiveMethod.invoke(subscriber, event); + return true; + } + catch (NoSuchMethodException nsmex) { + return false; + } + // any exception which is thrown because of "wrong" reflection due to a developer mistake (there is no test for that, could not create a test setup for this) + catch (IllegalAccessException | IllegalArgumentException | NullPointerException | ExceptionInInitializerError ex) { + throw new DeveloperMistakeException(("DeveloperMistakeException when calling the receive method %s for the subscriber %s " + + "for event type %s, event=%s").formatted(methodName, subscriber, eventType, event.toString()), ex); + } + // Any other exception which is thrown at runtime - which is ok and hence not a developer mistake + // Such exceptions are always wrapped in a InvocationTargetException + catch (InvocationTargetException ex) { + throw new RuntimeException("Exception when calling the receive method '%s' for the subscriber %s for event type %s, event=%s" + .formatted(methodName, subscriber, eventType, event.toString()), ex); + } + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBus.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBus.java new file mode 100644 index 0000000..0374a4e --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBus.java @@ -0,0 +1,144 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventContextHolder; +import de.accso.flexinale.common.api.eventbus.EventSubscriber; +import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException; +import de.accso.flexinale.common.shared_kernel.Selection; +import de.accso.flexinale.common.shared_kernel.SelectionMultiMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Set; + +@SuppressWarnings("unused") +public class InMemorySyncEventBus implements EventBus { + private static final Logger LOGGER = LoggerFactory.getLogger(InMemorySyncEventBus.class); + + protected record SubscriberGroup(String consumerGroupName) { + static SubscriberGroup of(EventSubscriber subscriber) { + return new SubscriberGroup(subscriber.getGroupName()); + } + } + + protected final Class eventType; + + protected static final EventContextHolder eventContextHolder = new InMemorySyncEventContextHolder(); + + protected final EventSubscriberReceiveDispatcher subscriberReceiveDispatcher = new EventSubscriberReceiveDispatcher<>(); + protected final EventSubscriberReceiveDispatcher genericSubscriberReceiveDispatcher = new EventSubscriberReceiveDispatcher<>(); + + protected final SelectionMultiMap> subscribersPerGroupUsingSelection = + new SelectionMultiMap<>(Selection.SelectionAlgorithm.ROUND_ROBIN); + protected final SelectionMultiMap> genericSubscribersPerGroupUsingSelection = + new SelectionMultiMap<>(Selection.SelectionAlgorithm.ROUND_ROBIN); + + InMemorySyncEventBus(final Class eventType) { + this.eventType = eventType; + } + + @Override + public EventContextHolder getEventContextHolder() { + return eventContextHolder; + } + + @Override + public void subscribe(final Class eventType, final EventSubscriber subscriber, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + LOGGER.debug("InMemorySyncEventBus<%s>: subscriber %s registered".formatted(eventType.getSimpleName(), subscriber)); + + subscribersPerGroupUsingSelection.put(SubscriberGroup.of(subscriber), subscriber); + + handle_SubscriptionAtStart(eventSubscriptionAtStart); + } + + @Override + public void subscribe(final EventSubscriber genericSubscriber, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + LOGGER.debug("InMemorySyncEventBus<%s>: generic subscriber %s registered".formatted(eventType.getSimpleName(), genericSubscriber)); + + genericSubscribersPerGroupUsingSelection.put(SubscriberGroup.of(genericSubscriber), genericSubscriber); + + handle_SubscriptionAtStart(eventSubscriptionAtStart); + } + + private void handle_SubscriptionAtStart(final EventSubscriptionAtStart eventSubscriptionAtStart) { + switch (eventSubscriptionAtStart) { + case START_READING_FROM_BEGINNING, START_READING_FROM_LAST_TIME -> { + throw new DeveloperMistakeException("not implemented. Probably want to use InMemorySyncEventBusSpy"); + } + case START_READING_FROM_NOW -> { + // nope, nothing to do + } + } + } + + @Override + public void unsubscribe(final Class eventType, final EventSubscriber subscriber) { + LOGGER.debug("InMemorySyncEventBus<%s>: subscriber %s unregistered".formatted(eventType.getSimpleName(), subscriber)); + + subscribersPerGroupUsingSelection.removeMapping(SubscriberGroup.of(subscriber), subscriber); + } + + @Override + public void unsubscribe(EventSubscriber genericSubscriber) { + LOGGER.debug("InMemorySyncEventBus<%s>: subscriber %s unregistered".formatted(eventType.getSimpleName(), genericSubscriber)); + + genericSubscribersPerGroupUsingSelection.removeMapping(SubscriberGroup.of(genericSubscriber), genericSubscriber); + } + + @Override + public void unsubscribeAll() { + LOGGER.debug("InMemorySyncEventBus<%s>: all subscribers and generic subscribers unregistered".formatted(eventType.getSimpleName())); + + subscribersPerGroupUsingSelection.clear(); + genericSubscribersPerGroupUsingSelection.clear(); + } + + @Override + public void publish(final Class eventType, final E event) { + LOGGER.info("InMemorySyncEventBus<%s>: publish new event %s".formatted(eventType, event)); + + // publish event to subscribers (depending on the selection algorithm, e.g. RoundRobin) + try { + eventContextHolder.set(event.eventContext()); + + // for each group of the subscribers, select one (or more) subscribers, using the Selection algorithm + subscribersPerGroupUsingSelection.keySet() + .forEach(subscriberGroup -> { + Selection> allSubscribersInGroup = + (Selection>) subscribersPerGroupUsingSelection.get(subscriberGroup); + Set> subscribersToSendTo = allSubscribersInGroup.next(); + subscribersToSendTo.forEach(subscriberToSendTo -> + internal_sendToSubscriber(eventType, event, subscriberToSendTo)); + }); + + // for each group of the generic subscribers, select one (or more) subscribers, using the Selection algorithm + genericSubscribersPerGroupUsingSelection.keySet() + .forEach(subscriberGroup -> { + Selection> allGenericSubscribersInGroup = + (Selection>) genericSubscribersPerGroupUsingSelection.get(subscriberGroup); + Set> genericSubscribersToSendTo = allGenericSubscribersInGroup.next(); + genericSubscribersToSendTo.forEach(genericSubscriberToSendTo -> + internal_sendToGenericSubscriber(event, genericSubscriberToSendTo)); + }); + } + finally { + eventContextHolder.remove(); + } + } + + // ----------------------------------------------------------------------------------------------------------------- + + // send event to one subscriber + protected void internal_sendToSubscriber(Class eventType, E event, EventSubscriber subscriber) { + subscriberReceiveDispatcher.callSubscriberReceiveMethod(eventType, event, subscriber); + } + + // send event to one generic subscriber + protected void internal_sendToGenericSubscriber(E event, EventSubscriber subscriber) { + genericSubscriberReceiveDispatcher.callSubscriberReceiveMethod(Event.class, event, subscriber); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBusFactory.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBusFactory.java new file mode 100644 index 0000000..930d7e9 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBusFactory.java @@ -0,0 +1,23 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import de.accso.flexinale.common.api.event.Event; + +import java.util.HashMap; +import java.util.Map; + +public class InMemorySyncEventBusFactory implements EventBusFactory { + protected final Map, EventBus> eventTypeToEventBusMap = new HashMap<>(); + + @SuppressWarnings("unchecked") + @Override + public EventBus createOrGetEventBusFor(final Class eventType) { + EventBus eventBus = eventTypeToEventBusMap.get(eventType); + if (eventBus == null) { + eventBus = new InMemorySyncEventBus<>(eventType); + eventTypeToEventBusMap.put(eventType, eventBus); + } + return (EventBus) eventBus; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBusSpy.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBusSpy.java new file mode 100644 index 0000000..ea87097 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBusSpy.java @@ -0,0 +1,130 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventSubscriber; +import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.ThreadSafeCounterMap; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.concurrent.ConcurrentLinkedQueue; + +public class InMemorySyncEventBusSpy extends InMemorySyncEventBus { + private static final Logger LOGGER = LoggerFactory.getLogger(InMemorySyncEventBusSpy.class); + + // localEventQueue is used as an in-memory storage and in tests as a spy + private final ConcurrentLinkedQueue localEventQueue = new ConcurrentLinkedQueue<>(); // TODO is currently never cleared - might want to use Cache implementation with automatic time-to-live? + + private final ThreadSafeCounterMap consumerGroupOffsetMap = new ThreadSafeCounterMap<>(); + + InMemorySyncEventBusSpy(final Class eventType) { + super(eventType); + } + + public List allEvents() { + return localEventQueue.stream().toList(); + } + + public void clear() { + localEventQueue.clear(); + } + + public int size() { + return localEventQueue.size(); + } + + // ---------------------------------------------------------------------------------------------------------------- + + @Override + public void subscribe(final Class eventType, final EventSubscriber subscriber, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + LOGGER.debug("InMemorySyncEventBusSpy<%s>: subscriber %s registered".formatted(eventType.getSimpleName(), subscriber)); + + subscribersPerGroupUsingSelection.put(SubscriberGroup.of(subscriber), subscriber); + + handle_SubscriptionAtStart(eventType, subscriber, eventSubscriptionAtStart); + } + + private void handle_SubscriptionAtStart(final Class eventType, + final EventSubscriber subscriber, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + switch (eventSubscriptionAtStart) { + case START_READING_FROM_BEGINNING -> { + // re-publish all events from beginning + localEventQueue.forEach(event -> this.internal_sendToSubscriber(eventType, event, subscriber)); + } + case START_READING_FROM_NOW -> { + // nope, nothing to do + } + case START_READING_FROM_LAST_TIME -> { + long offset = consumerGroupOffsetMap.get(SubscriberGroup.of(subscriber)); + + // includes the case, that a consumer group was not known before - + // as then offset from the map is 0 + // so nothing skipped but same behaviour as START_READING_FROM_BEGINNING + localEventQueue.stream(). + skip(offset). + forEach(event -> this.internal_sendToSubscriber(eventType, event, subscriber)); + } + } + } + + @Override + public void subscribe(EventSubscriber genericSubscriber, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + LOGGER.debug("InMemorySyncEventBusSpy<%s>: generic subscriber %s registered".formatted(eventType.getSimpleName(), genericSubscriber)); + + genericSubscribersPerGroupUsingSelection.put(SubscriberGroup.of(genericSubscriber), genericSubscriber); + + handle_SubscriptionAtStart(genericSubscriber, eventSubscriptionAtStart); + } + + private void handle_SubscriptionAtStart(final EventSubscriber genericSubscriber, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + switch (eventSubscriptionAtStart) { + case START_READING_FROM_BEGINNING -> { + // re-publish all events from beginning + localEventQueue.forEach(event -> this.internal_sendToGenericSubscriber(event, genericSubscriber)); + } + case START_READING_FROM_NOW -> { + // nope, nothing to do + } + case START_READING_FROM_LAST_TIME -> { + long offset = consumerGroupOffsetMap.get(SubscriberGroup.of(genericSubscriber)); + + // includes the case, that a consumer group was not known before, ie. offset is 0, + // so nothing skipped but same behaviour as START_READING_FROM_BEGINNING + localEventQueue.stream(). + skip(offset). + forEach(event -> this.internal_sendToGenericSubscriber(event, genericSubscriber)); + } + } + } + + @Override + public void publish(final Class eventType, final E event) { + LOGGER.info("InMemorySyncEventBusSpy<%s>: publish new event %s".formatted(eventType, event)); + + super.publish(eventType, event); + + // persist event in local (in-memory) storage (i.e. spy) + localEventQueue.add(event); + } + + // ----------------------------------------------------------------------------------------------------------------- + + // send event to one subscriber + @Override + protected void internal_sendToSubscriber(final Class eventType, final E event, final EventSubscriber subscriber) { + subscriberReceiveDispatcher.callSubscriberReceiveMethod(eventType, event, subscriber); + consumerGroupOffsetMap.increment(SubscriberGroup.of(subscriber)); + } + + // send event to one generic subscriber + @Override + protected void internal_sendToGenericSubscriber(final E event, final EventSubscriber genericSubscriber) { + genericSubscriberReceiveDispatcher.callSubscriberReceiveMethod(Event.class, event, genericSubscriber); + consumerGroupOffsetMap.increment(SubscriberGroup.of(genericSubscriber)); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBusSpyFactory.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBusSpyFactory.java new file mode 100644 index 0000000..410aee1 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventBusSpyFactory.java @@ -0,0 +1,17 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.event.Event; + +public class InMemorySyncEventBusSpyFactory extends InMemorySyncEventBusFactory { + @SuppressWarnings("unchecked") + @Override + public EventBus createOrGetEventBusFor(final Class eventType) { + EventBus eventBus = eventTypeToEventBusMap.get(eventType); + if (eventBus == null) { + eventBus = new InMemorySyncEventBusSpy<>(eventType); + eventTypeToEventBusMap.put(eventType, eventBus); + } + return (EventBus) eventBus; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventContextHolder.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventContextHolder.java new file mode 100644 index 0000000..145bc0f --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventContextHolder.java @@ -0,0 +1,52 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventContextHolder; +import de.accso.flexinale.common.api.event.EventContext; +import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException; + +import java.util.concurrent.atomic.AtomicInteger; + +public class InMemorySyncEventContextHolder implements EventContextHolder { + + // this implementation holds only one context: + // - as long as a context with the same correlation id is used, it is kept, i.e. counter is increased (so that a remove is not deleting it yet) + // - as soon as a context with a different correlation id is used, it is reset, i.e. counter is set to 1 + // -> So it is not possible to use a separate event chain and go back - for that, we could change that to a stack + // where context instances are pushed as new and popped to be cleared + private EventContext CONTEXT = null; + private AtomicInteger counter = new AtomicInteger(0); + + @Override + public EventContext get() { + return CONTEXT; + } + + @Override + public void set(final EventContext eventContext) { + if (CONTEXT != null && CONTEXT.equalsByCorrelationId(eventContext)) { + counter.incrementAndGet(); + } + else { + counter = new AtomicInteger(1); + CONTEXT = eventContext; + } + } + + @Override + public void remove() { + if (counter.get() == 0) { + throw new DeveloperMistakeException("remove on InMemorySyncEventContextHolder once too much, should not happen"); + } + else { + counter.decrementAndGet(); + if (counter.get() == 0) { + CONTEXT = null; + } + } + } + + @Override + public boolean isEmpty() { + return CONTEXT == null; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaAsyncEventBus.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaAsyncEventBus.java new file mode 100644 index 0000000..517c65a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaAsyncEventBus.java @@ -0,0 +1,140 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventContextHolder; +import de.accso.flexinale.common.api.eventbus.EventSubscriber; +import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@SuppressWarnings("unused") +public final class KafkaAsyncEventBus implements EventBus { + private static final Logger LOGGER = LoggerFactory.getLogger(KafkaAsyncEventBus.class); + + private static final EventContextHolder eventContextHolder = new ThreadLocalEventContextHolder(); + + private KafkaProducerAdapter kafkaProducerAdapter; + private KafkaConsumerAdapter kafkaConsumerAdapter; + + private final KafkaAvailabilityChecker kafkaAvailabilityChecker; + + private final Class eventType; // TODO might want to use this in order to simplify the interface and get rid of the method's first parameter + + KafkaAsyncEventBus(final Class eventType, final KafkaAvailabilityChecker kafkaAvailabilityChecker) { + this.eventType = eventType; + this.kafkaAvailabilityChecker = kafkaAvailabilityChecker; + } + + @Override + public void subscribe(final Class eventType, final EventSubscriber subscriber, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + LOGGER.info("KafkaAsyncEventBus<%s>: subscriber %s registered".formatted(eventType, subscriber)); + + if (kafkaConsumerAdapter == null) { + kafkaConsumerAdapter = createKafkaConsumerAdapter(eventType, subscriber, eventSubscriptionAtStart); + startKafkaConsumerAdapterInSeparateThread(kafkaConsumerAdapter); + } + kafkaConsumerAdapter.addSubscriber(subscriber); + } + + @Override + public void subscribe(EventSubscriber genericSubscriber, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + LOGGER.info("KafkaAsyncEventBus<%s>: generic subscriber %s registered".formatted(eventType, genericSubscriber)); + + if (kafkaConsumerAdapter == null) { + kafkaConsumerAdapter = createKafkaConsumerAdapter(eventType, genericSubscriber, eventSubscriptionAtStart); + startKafkaConsumerAdapterInSeparateThread(kafkaConsumerAdapter); + } + kafkaConsumerAdapter.addGenericSubscriber(genericSubscriber); + } + + @Override + public void unsubscribe(final Class eventType, final EventSubscriber subscriber) { + LOGGER.info("KafkaAsyncEventBus<%s>: subscriber %s unregistered".formatted(eventType, subscriber)); + + if (kafkaConsumerAdapter == null) { + throw new DeveloperMistakeException("unsubscribe a subscriber from non-existing kafkaConsumerAdapter"); + } + kafkaConsumerAdapter.removeSubscriber(subscriber); + } + + @Override + public void unsubscribe(EventSubscriber genericSubscriber) { + LOGGER.info("KafkaAsyncEventBus<%s>: generic subscriber %s unregistered".formatted(eventType, genericSubscriber)); + + if (kafkaConsumerAdapter == null) { + throw new DeveloperMistakeException("unsubscribe a generic subscriber from non-existing kafkaConsumerAdapter"); + } + kafkaConsumerAdapter.removeGenericSubscriber(genericSubscriber); + } + + @Override + public void unsubscribeAll() { + LOGGER.info("KafkaAsyncEventBus<%s>: all subscribers unregistered".formatted(eventType)); + + if (kafkaConsumerAdapter == null) { + throw new DeveloperMistakeException("unsubscribe all subscribers from non-existing kafkaConsumerAdapter"); + } + kafkaConsumerAdapter.removeAllSubscribers(); + kafkaConsumerAdapter.removeAllGenericSubscribers(); + kafkaConsumerAdapter.shutdown(); // shutdown adapter with last subscriber + kafkaConsumerAdapter = null; + + // might want to add a common shutdown method? + } + + @Override + public void publish(final Class eventType, final E event) { + LOGGER.info("KafkaAsyncEventBus<%s>: publish new event %s".formatted(eventType, event)); + + if (kafkaProducerAdapter == null) { + kafkaProducerAdapter = createKafkaProducerAdapter(eventType); + } + + kafkaProducerAdapter.publish(eventType, event); + } + + @Override + public EventContextHolder getEventContextHolder() { + return eventContextHolder; + } + + // -------------------------------------------------------------------------------------------------------------- + + private KafkaConsumerAdapter createKafkaConsumerAdapter(final Class eventType, final EventSubscriber subscriber, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + // check this once for every new KafkaConsumerAdapter + checkKafkaAvailability(); + return new KafkaConsumerAdapter<>(eventType, subscriber, eventContextHolder, eventSubscriptionAtStart); + } + + private void startKafkaConsumerAdapterInSeparateThread(final KafkaConsumerAdapter kafkaConsumerAdapter) { + String threadName = "thread4%s<%s>".formatted(kafkaConsumerAdapter.getClass().getName(), eventType); + new Thread(this.kafkaConsumerAdapter, threadName).start(); + } + + // -------------------------------------------------------------------------------------------------------------- + + private KafkaProducerAdapter createKafkaProducerAdapter(final Class eventType) { + // check this once for every new KafkaProducerAdapter + checkKafkaAvailability(); + return new KafkaProducerAdapter<>(eventType); + } + + // -------------------------------------------------------------------------------------------------------------- + + private void checkKafkaAvailability() { + try { + if (! kafkaAvailabilityChecker.isKafkaAvailable()) { + throw new FlexinaleIllegalStateException("Kafka is not available."); + } + } + catch (Exception ex) { + throw new FlexinaleIllegalStateException("Kafka is not available.", ex); + } + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaAsyncEventBusFactory.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaAsyncEventBusFactory.java new file mode 100644 index 0000000..429f27c --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaAsyncEventBusFactory.java @@ -0,0 +1,30 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventBusFactory; +import de.accso.flexinale.common.api.event.Event; + +import java.util.HashMap; +import java.util.Map; + +@SuppressWarnings("unused") +public final class KafkaAsyncEventBusFactory implements EventBusFactory { + private final Map < Class, + EventBus > eventTypeToEventBusMap = new HashMap<>(); + + private final KafkaAvailabilityChecker kafkaAvailabilityChecker; + + public KafkaAsyncEventBusFactory(final KafkaAvailabilityChecker kafkaAvailabilityChecker) { + this.kafkaAvailabilityChecker = kafkaAvailabilityChecker; + } + + @Override + public EventBus createOrGetEventBusFor(final Class eventType) { + EventBus eventBus = eventTypeToEventBusMap.get(eventType); + if (eventBus == null) { + eventBus = new KafkaAsyncEventBus<>(eventType, kafkaAvailabilityChecker); + eventTypeToEventBusMap.put(eventType, eventBus); + } + return (EventBus) eventBus; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaAvailabilityChecker.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaAvailabilityChecker.java new file mode 100644 index 0000000..3e23848 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaAvailabilityChecker.java @@ -0,0 +1,54 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException; +import org.apache.kafka.clients.admin.AdminClient; +import org.apache.kafka.clients.admin.DescribeClusterOptions; + +import java.util.Map; +import java.util.concurrent.ExecutionException; + +import static de.accso.flexinale.common.infrastructure.eventbus.KafkaConfiguration.adminConfig; +import static de.accso.flexinale.common.infrastructure.eventbus.KafkaConfiguration.kafkaTimeoutInMs; + +public final class KafkaAvailabilityChecker { + + // Kafka availability is checked intentionally in Spring init phase + // so the whole application (or test) won't start if no Kafka is available + + public KafkaAvailabilityChecker() { + try { + if (!isKafkaAvailable()) { + // intentionally not written to Log as this is checked in init phase where not even a logger + // might be available yet. + System.err.println("ERROR - Kafka is not available! Start Kafka before starting this application or test!"); + throw new FlexinaleIllegalStateException("Kafka is not available."); + } + } + catch (Exception ex) { + System.err.println("ERROR - Kafka is not available! Start Kafka before starting this application or test!"); + throw new FlexinaleIllegalStateException("Kafka is not available.", ex); + } + } + + @SuppressWarnings({"SameReturnValue", "BooleanMethodIsAlwaysInverted"}) + public boolean isKafkaAvailable() throws ExecutionException { + Map kafkaAdminConfig = adminConfig(); + + try (var adminClient = AdminClient.create( kafkaAdminConfig )) { + adminClient.describeCluster(new DescribeClusterOptions().timeoutMs(kafkaTimeoutInMs)).nodes().get(); + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + return true; + } + +/* + public static void main(String[] args) throws ExecutionException { + boolean kafkaAvailable = new KafkaAvailabilityChecker().isKafkaAvailable(); + if (kafkaAvailable) { + System.out.println("All fine, Kafka is available"); + } + } +*/ +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaConfiguration.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaConfiguration.java new file mode 100644 index 0000000..9dacae3 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaConfiguration.java @@ -0,0 +1,92 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventSubscriber; +import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException; + +import java.util.HashMap; +import java.util.Map; + +import static de.accso.flexinale.common.shared_kernel.Identifiable.Id.uuidString; +import static org.apache.kafka.clients.CommonClientConfigs.BOOTSTRAP_SERVERS_CONFIG; +import static org.apache.kafka.clients.consumer.ConsumerConfig.*; +import static org.apache.kafka.clients.producer.ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG; +import static org.apache.kafka.clients.producer.ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG; + +//TODO externalize to kafka properties file (but than we have it fixed for all topics - in the Java code we could use different configs) +//Note that in the application.properties there is something configured already (like the (De)Serializer classes) +public final class KafkaConfiguration { + + public static final int kafkaTimeoutInMs = 30_000; + + public static final String KEY_BOOTSTRAP_SERVERS = BOOTSTRAP_SERVERS_CONFIG; + public static final String defaultBootstrapServers = "localhost:29092"; + + public static final String KEY_SERIALIZER = KEY_SERIALIZER_CLASS_CONFIG; + public static final String keySerializer = "org.apache.kafka.common.serialization.StringSerializer"; + public static final String VALUE_SERIALIZER = VALUE_SERIALIZER_CLASS_CONFIG; + public static final String valueSerializer = "org.apache.kafka.common.serialization.StringSerializer"; + + public static final String KEY_DESERIALIZER = KEY_DESERIALIZER_CLASS_CONFIG; + public static final String keyDeserializer = "org.apache.kafka.common.serialization.StringDeserializer"; + public static final String VALUE_DESERIALIZER = VALUE_DESERIALIZER_CLASS_CONFIG; + public static final String valueDeserializer = "org.apache.kafka.common.serialization.StringDeserializer"; + + public static final String CONSUMER_GROUP_ID_CONFIG = GROUP_ID_CONFIG; + + public static Map producerConfig() { + return Map.of( + KEY_BOOTSTRAP_SERVERS, getBootstrapServers(), + KEY_SERIALIZER, getClassFor(keySerializer), + VALUE_SERIALIZER, getClassFor(valueSerializer) + ); + } + + @SuppressWarnings("SameParameterValue") + public static Map consumerConfig(final Class eventType, final EventSubscriber subscriber, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + Map config = new HashMap<>(); + + config.put(KEY_BOOTSTRAP_SERVERS, getBootstrapServers()); + config.put(KEY_DESERIALIZER, getClassFor(keyDeserializer)); + config.put(VALUE_DESERIALIZER, getClassFor(valueDeserializer)); + + switch (eventSubscriptionAtStart) { + case START_READING_FROM_BEGINNING -> { + // make new consumer group and read from earliest offset (works only for new consumer groups) + config.put(CONSUMER_GROUP_ID_CONFIG, subscriber.getGroupName() + "<" + eventType + ">" + uuidString()); + config.put(AUTO_OFFSET_RESET_CONFIG, "earliest"); + } + case START_READING_FROM_NOW -> { + // make new consumer group and read from latest offset (works only for new consumer groups) + config.put(CONSUMER_GROUP_ID_CONFIG, subscriber.getGroupName() + "<" + eventType + ">" + uuidString()); + config.put(AUTO_OFFSET_RESET_CONFIG, "latest"); + } + case START_READING_FROM_LAST_TIME -> { + config.put(CONSUMER_GROUP_ID_CONFIG, subscriber.getGroupName() + "<" + eventType + ">"); + } + } + + return config; + } + + public static String getBootstrapServers() { + String systemPropertyBootstrapServers = System.getProperty(BOOTSTRAP_SERVERS_CONFIG); + return (systemPropertyBootstrapServers == null) ? defaultBootstrapServers : systemPropertyBootstrapServers; + } + + public static Map adminConfig() { + return Map.of( + KEY_BOOTSTRAP_SERVERS, getBootstrapServers() + ); + } + + private static Class getClassFor(final String classAsString) { + try { + return Class.forName(classAsString); + } + catch (ClassNotFoundException ex) { + throw new FlexinaleIllegalStateException(ex); + } + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaConsumerAdapter.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaConsumerAdapter.java new file mode 100644 index 0000000..3a85b7b --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaConsumerAdapter.java @@ -0,0 +1,182 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventContextHolder; +import de.accso.flexinale.common.api.eventbus.EventSubscriber; +import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart; +import de.accso.flexinale.common.api.event.Event; +import org.apache.kafka.clients.consumer.ConsumerRecords; +import org.apache.kafka.clients.consumer.KafkaConsumer; +import org.apache.kafka.common.errors.WakeupException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.listener.CommonErrorHandler; + +import java.time.Duration; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import static de.accso.flexinale.common.infrastructure.eventbus.KafkaConfiguration.CONSUMER_GROUP_ID_CONFIG; + +final class KafkaConsumerAdapter implements Runnable { + private static final Logger LOGGER = LoggerFactory.getLogger(KafkaConsumerAdapter.class); + + private final KafkaConsumer internal_KafkaConsumer; + private final static Lock lock4KafkaConsumer = new ReentrantLock(); // the internal_KafkaConsumer is not thread-safe + + private final String kafkaTopicName; + + private final AtomicBoolean consumerIsShutdownAndNoLongerConsumingFromKafka = new AtomicBoolean(false); + private final CommonErrorHandler errorHandler = new KafkaConsumerErrorHandler(); + + private final Class eventType; + private final EventContextHolder eventContextHolder; + + private final ConcurrentLinkedQueue> subscribers = new ConcurrentLinkedQueue<>(); + private final ConcurrentLinkedQueue> genericSubscribers = new ConcurrentLinkedQueue<>(); + + private final EventSubscriberReceiveDispatcher eventSubscriberReceiveDispatcher = new EventSubscriberReceiveDispatcher<>(); + private final EventSubscriberReceiveDispatcher eventGenericSubscriberReceiveDispatcher = new EventSubscriberReceiveDispatcher<>(); + + // -------------------------------------------------------------------------------------------------------------- + + KafkaConsumerAdapter(final Class eventType, final EventSubscriber subscriber, EventContextHolder eventContextHolder, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + this.eventType = eventType; + this.eventContextHolder = eventContextHolder; //TODO This is probably not working correctly. + // As each KafkaConsumerAdapter is running in its own thread, they cannot share the same ThreadLocal. + // The eventContextHolder must not be given from the bus but instantiated in each KafkaConsumerAdapter separately. + + this.kafkaTopicName = KafkaTopicHelper.createTopicNameFromEventClazz(eventType); + + final Map kafkaConfig = KafkaConfiguration.consumerConfig(eventType, subscriber, eventSubscriptionAtStart); + final String kafkaConsumerGroupId = (String) kafkaConfig.get(CONSUMER_GROUP_ID_CONFIG); + + LOGGER.info("create new subscription from %s (%s), for Kafka topic %s, using Kafka consumer group id %s". + formatted(subscriber.getName(), + subscriber.getGroupName(), + kafkaTopicName, + kafkaConsumerGroupId)); + + internal_KafkaConsumer = new KafkaConsumer<>(kafkaConfig); + + addShutDownHook(); + } + + @Override + public void run() { + // keep running forever or until shutdown() is called from another thread. + try { + internal_KafkaConsumer.subscribe(List.of(kafkaTopicName)); + Duration kafkaTimeoutInMs = Duration.ofMillis( KafkaConfiguration.kafkaTimeoutInMs ); + + // does not necessarily read messages "from beginning", i.e. already existing records which had + // been published before the consumer was connected to the topic! + while (!consumerIsShutdownAndNoLongerConsumingFromKafka.get()) { + ConsumerRecords kafkaRecords = internal_KafkaConsumer.poll(kafkaTimeoutInMs); + processAllKafkaRecords(kafkaTopicName, kafkaRecords); + } + } + catch (WakeupException ex) { + if (!consumerIsShutdownAndNoLongerConsumingFromKafka.get()) { + throw ex; // ignore exception if closing + } + } + } + + private void processAllKafkaRecords(final String kafkaTopicName, final ConsumerRecords records) { + LOGGER.debug("consuming from %s now %d records ...".formatted(kafkaTopicName, records.count())); + records.forEach(record -> { + try { + LOGGER.debug("consuming from %s: %s".formatted(kafkaTopicName, record)); + E event = EventSerializationHelper.deserializeJsonString2Event(record.value(), eventType); + processEventAndCallReceiveOnAllSubscribers(eventType, event); + } + catch (Exception ex) { + this.errorHandler.handleOne(ex, record, + /* not used */ null, + /* not used */ null); + } + }); + LOGGER.debug("consuming from %s now %d records ... done".formatted(kafkaTopicName, records.count())); + } + + private void processEventAndCallReceiveOnAllSubscribers(final Class eventType, final E event) { + eventContextHolder.set(event.eventContext()); + + try { + subscribers.forEach(subscriber -> + eventSubscriberReceiveDispatcher.callSubscriberReceiveMethod(eventType, event, subscriber)); + genericSubscribers.forEach(subscriber -> + eventGenericSubscriberReceiveDispatcher.callSubscriberReceiveMethod(Event.class, event, subscriber)); + } + finally { + eventContextHolder.remove(); + } + } + + // -------------------------------------------------------------------------------------------------------------- + + void addSubscriber(final EventSubscriber subscriber) { + subscribers.add(subscriber); + } + void addGenericSubscriber(final EventSubscriber genericSubscriber) { + genericSubscribers.add(genericSubscriber); + } + + void removeSubscriber(final EventSubscriber subscriber) { + subscribers.remove(subscriber); + } + void removeGenericSubscriber(final EventSubscriber genericSubscriber) { + genericSubscribers.remove(genericSubscriber); + } + + void removeAllSubscribers() { + subscribers.forEach(subscribers::remove); + } + void removeAllGenericSubscribers() { + genericSubscribers.forEach(genericSubscribers::remove); + } + + // -------------------------------------------------------------------------------------------------------------- + + private void addShutDownHook() { + Runtime.getRuntime().addShutdownHook(new Thread(this::shutdown)); + } + + // note that currently there is no way of reactiving the KafkaConsumerAdapter once it has been shutdown + void shutdown() { + try { + +/** + * the lock does not seem to help, one still sees (with Java 21) these kind of errors: + * + * 20240327 14:42:20.284 [Thread-5] ERROR o.a.k.clients.consumer.KafkaConsumer - caught exception during shutdown + * java.util.ConcurrentModificationException: KafkaConsumer is not safe for multi-threaded access. currentThread(name: Thread-5, id: 43) otherThread(id: 44) + * at org.apache.kafka.clients.consumer.KafkaConsumer.acquire(KafkaConsumer.java:2484) + * at org.apache.kafka.clients.consumer.KafkaConsumer.close(KafkaConsumer.java:2343) + * at org.apache.kafka.clients.consumer.KafkaConsumer.close(KafkaConsumer.java:2321) + * at de.accso.flexinale.common.infrastructure.eventbus.KafkaConsumerAdapter.shutdown(KafkaConsumerAdapter.java:147) + * at java.base/java.lang.Thread.run(Thread.java:1583) + */ + + boolean isLockAcquired = lock4KafkaConsumer.tryLock(1, TimeUnit.SECONDS); + if (isLockAcquired) { + try { + consumerIsShutdownAndNoLongerConsumingFromKafka.set(true); + internal_KafkaConsumer.close(); + } finally { + lock4KafkaConsumer.unlock(); + } + } + } + catch (Exception ex) { + LOGGER.error("caught exception during shutdown", ex); + } + } + +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaConsumerErrorHandler.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaConsumerErrorHandler.java new file mode 100644 index 0000000..1bb9ac2 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaConsumerErrorHandler.java @@ -0,0 +1,76 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import org.apache.kafka.clients.consumer.Consumer; +import org.apache.kafka.clients.consumer.ConsumerRecord; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.listener.CommonErrorHandler; +import org.springframework.kafka.listener.DeadLetterPublishingRecoverer; +import org.springframework.kafka.listener.MessageListenerContainer; + +// Error handling for Kafka consumers using retry pattern and finally writing error messages to DLT + +/* +A typical Spring Kafka error handler would look like below, but this only works for consumers +which are annotated with @KafkaListener + +... + private int numberOfMaxRetriesForRetryableErrors = 3; + + @Bean + DefaultErrorHandler kafkaConsumerErrorHandler() { + DefaultErrorHandler errorHandler = new DefaultErrorHandler((record, ex) -> { + LOGGER.error("error while processing Kafka record " + record + ex); + + // finally send to dead letter topics + new DeadLetterPublishingRecoverer(internal_KafkaErrorDLTTemplate).accept(record, ex); + }, + // Default error handling is to retry + new ExponentialBackOffWithMaxRetries(numberOfMaxRetriesForRetryableErrors)); + + // ... but do not retry on FlexinaleKafkaNotRetryableException and its subclasses + errorHandler.addNotRetryableExceptions(FlexinaleKafkaNotRetryableException.class); + + return errorHandler; + } +... + + */ + +public final class KafkaConsumerErrorHandler implements CommonErrorHandler { + private static final Logger LOGGER = LoggerFactory.getLogger(KafkaConsumerErrorHandler.class); + + // Kafka template for sending error message to DLT + static final KafkaTemplate internal_KafkaErrorDLTTemplate = new KafkaTemplate<>( + new DefaultKafkaProducerFactory<>( + KafkaConfiguration.producerConfig() + )); + + @Override + public boolean handleOne(Exception thrownException, ConsumerRecord record, Consumer consumer, + MessageListenerContainer container) { + try { + LOGGER.error("error while processing Kafka record %s, now sending to DLT, error: %s".formatted(record, thrownException)); + new DeadLetterPublishingRecoverer(internal_KafkaErrorDLTTemplate).accept(record, thrownException); + return true; + } + catch (Exception ex) { + return false; + } + } +} + +abstract class FlexinaleKafkaNotRetryableException extends RuntimeException { + public FlexinaleKafkaNotRetryableException(final String message, final Throwable cause) { + super(message, cause); + } +} + +@SuppressWarnings("unused") +class FlexinaleKafkaMessageDeserializationException extends FlexinaleKafkaNotRetryableException { + public FlexinaleKafkaMessageDeserializationException(final String message, final Throwable cause) { + super(message, cause); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaProducerAdapter.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaProducerAdapter.java new file mode 100644 index 0000000..7d4edde --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaProducerAdapter.java @@ -0,0 +1,60 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; + +final class KafkaProducerAdapter { + private static final Logger LOGGER = LoggerFactory.getLogger(KafkaProducerAdapter.class); + + private final KafkaTemplate internal_KafkaTemplate; + private final String kafkaTopicName; + + private final Class eventType; + + public KafkaProducerAdapter(final Class eventType) { + this.eventType = eventType; + + this.kafkaTopicName = KafkaTopicHelper.createTopicNameFromEventClazz(eventType); + LOGGER.info("create new %s for Kafka topic %s".formatted(getName(), kafkaTopicName)); + + this.internal_KafkaTemplate = new KafkaTemplate<>( + new DefaultKafkaProducerFactory<>( + KafkaConfiguration.producerConfig() + )); + } + + public void publish(final Class eventType, final E event) { + try { + Identifiable.Id kafkaKey = event.id(); + String kafkaMessage = EventSerializationHelper.serializeEvent2JsonString(event); + internal_KafkaTemplate + .send(kafkaTopicName, kafkaKey.id(), kafkaMessage) + .whenComplete((result, ex) -> { + if (ex == null) { + LOGGER.debug(String.format("message was sent to topic %s " + + "for event type %s, event id %s", + kafkaTopicName, + eventType.getCanonicalName(), + event.id())); + } else { + LOGGER.error(String.format("Kafka message could not be sent to topic %s " + + "for event type %s, event id %s - error: %s", + kafkaTopicName, eventType.getCanonicalName(), + event.id(), + ex.getMessage())); + } + }); + } + catch (Exception ex) { + LOGGER.error("Kafka message will not sent out for event " + event, ex); + } + } + + String getName() { + return "KafkaProducerAdapter" + "<" + eventType + ">"; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaTopicHelper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaTopicHelper.java new file mode 100644 index 0000000..29d9c90 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/KafkaTopicHelper.java @@ -0,0 +1,57 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.EventClassHelper; +import org.apache.kafka.clients.admin.AdminClient; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import static de.accso.flexinale.common.infrastructure.eventbus.KafkaConfiguration.adminConfig; +import static de.accso.flexinale.common.infrastructure.eventbus.KafkaConfiguration.kafkaTimeoutInMs; + +@SuppressWarnings("unused") +public final class KafkaTopicHelper { + private static final Logger LOGGER = LoggerFactory.getLogger(KafkaTopicHelper.class); + + static String createTopicNameFromEventClazz(final Class eventType) { + return EventClassHelper.getEventClazzName(eventType) + + ".v" + + EventClassHelper.getEventClazzVersion(eventType).version(); + } + + static String createTopicNameFromEventClazzAndInstance(final Class eventType, final Event event) { + return eventType.getCanonicalName() + + ".v" + + event.version().version(); + } + + public static void deleteKafkaTopicsWithPrefixAndSuffix(final String prefix, final String suffix) + throws ExecutionException, TimeoutException + { + Map kafkaAdminConfig = adminConfig(); + + try (var adminClient = AdminClient.create( kafkaAdminConfig )) { + List topicNames = adminClient.listTopics().names().get() + .stream() + .filter(name -> name.startsWith(prefix) && name.endsWith(suffix)) + .toList(); + adminClient.deleteTopics(topicNames).all().get(kafkaTimeoutInMs, TimeUnit.SECONDS); + + if (topicNames.isEmpty()) { + LOGGER.info("did not delete any Kafka topics"); + } + else { + LOGGER.info("tried to delete these Kafka topics: " + topicNames); + } + } + catch (InterruptedException ex) { + Thread.currentThread().interrupt(); + } + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/NopeContextHolder.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/NopeContextHolder.java new file mode 100644 index 0000000..040a7a1 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/NopeContextHolder.java @@ -0,0 +1,32 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventContextHolder; +import de.accso.flexinale.common.api.event.EventContext; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class NopeContextHolder implements EventContextHolder { + private static final Logger LOGGER = LoggerFactory.getLogger(NopeContextHolder.class); + + @Override + public EventContext get() { + LOGGER.debug("NopeContextHolder.get() does nothing, always returns null"); + return null; + } + + @Override + public void set(EventContext eventContext) { + LOGGER.debug("NopeContextHolder.set() does nothing"); + } + + @Override + public void remove() { + LOGGER.debug("NopeContextHolder.remove() does nothing"); + } + + @Override + public boolean isEmpty() { + LOGGER.debug("NopeContextHolder.isEmpty() does nothing, always returns true"); + return true; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/NopeEventBusSpy.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/NopeEventBusSpy.java new file mode 100644 index 0000000..b86d151 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/NopeEventBusSpy.java @@ -0,0 +1,64 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.eventbus.EventContextHolder; +import de.accso.flexinale.common.api.eventbus.EventSubscriber; +import de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart; +import de.accso.flexinale.common.api.event.Event; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@SuppressWarnings("unused") +public final class NopeEventBusSpy implements EventBus { + private static final Logger LOGGER = LoggerFactory.getLogger(NopeEventBusSpy.class); + + private static final EventContextHolder eventContextHolder = new NopeContextHolder(); + + private final Class eventType; // might want to use this simplify the interface and get rid of the method's first parameter + + NopeEventBusSpy(final Class eventType) { + this.eventType = eventType; + } + + @Override + public void subscribe(final Class eventType, final EventSubscriber subscriber, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + LOGGER.debug("NopeEventBusSpy<%s>: subscriber %s registered but does nothing" + .formatted(eventType.getSimpleName(), subscriber)); + } + + @Override + public void subscribe(final EventSubscriber genericSubscriber, + final EventSubscriptionAtStart eventSubscriptionAtStart) { + LOGGER.debug("NopeEventBusSpy<%s>: subscriber %s registered but does nothing" + .formatted(eventType.getSimpleName(), genericSubscriber)); + } + + @Override + public void unsubscribe(final Class eventType, final EventSubscriber subscriber) { + LOGGER.debug("NopeEventBusSpy<%s>: subscriber %s unregistered but does nothing" + .formatted(eventType.getSimpleName(), subscriber)); + } + + @Override + public void unsubscribe(EventSubscriber genericSubscriber) { + LOGGER.debug("NopeEventBusSpy<%s>: subscriber %s unregistered but does nothing" + .formatted(eventType.getSimpleName(), genericSubscriber)); + } + + @Override + public void unsubscribeAll() { + LOGGER.debug("NopeEventBusSpy<%s>: all subscribers unregistered but does nothing" + .formatted(eventType.getSimpleName())); + } + + @Override + public void publish(final Class eventType, final E event) { + LOGGER.info("NopeEventBusSpy<%s>: publish new event %s but does nothing".formatted(eventType, event)); + } + + @Override + public EventContextHolder getEventContextHolder() { + return eventContextHolder; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/NopeEventBusSpyFactory.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/NopeEventBusSpyFactory.java new file mode 100644 index 0000000..a681b81 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/NopeEventBusSpyFactory.java @@ -0,0 +1,17 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventBus; +import de.accso.flexinale.common.api.event.Event; + +public final class NopeEventBusSpyFactory extends InMemorySyncEventBusFactory { + @SuppressWarnings("unchecked") + @Override + public EventBus createOrGetEventBusFor(final Class eventType) { + EventBus eventBus = eventTypeToEventBusMap.get(eventType); + if (eventBus == null) { + eventBus = new NopeEventBusSpy<>(eventType); + eventTypeToEventBusMap.put(eventType, eventBus); + } + return (EventBus) eventBus; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/ThreadLocalEventContextHolder.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/ThreadLocalEventContextHolder.java new file mode 100644 index 0000000..679cce0 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/infrastructure/eventbus/ThreadLocalEventContextHolder.java @@ -0,0 +1,28 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventContextHolder; +import de.accso.flexinale.common.api.event.EventContext; + +public final class ThreadLocalEventContextHolder implements EventContextHolder { + private static final ThreadLocal CONTEXT = new ThreadLocal<>(); + + @Override + public EventContext get() { + return CONTEXT.get(); + } + + @Override + public void set(EventContext eventContext) { + CONTEXT.set(eventContext); + } + + @Override + public void remove() { + CONTEXT.remove(); + } + + @Override + public boolean isEmpty() { + return CONTEXT.get() == null; + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/ClazzHelper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/ClazzHelper.java new file mode 100644 index 0000000..d0de80c --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/ClazzHelper.java @@ -0,0 +1,19 @@ +package de.accso.flexinale.common.shared_kernel; + +import java.util.Arrays; + +public final class ClazzHelper { + public static boolean clazzImplementsInterface(final Class implementationClazz, final Class interfaceClazz) { + if (implementationClazz.equals(Object.class)) return false; + + Class[] directInterfaces = implementationClazz.getInterfaces(); // _only_ the direct interfaces, not from super class! + boolean implementsEvent = Arrays.stream(directInterfaces).toList().contains(interfaceClazz); + if (implementsEvent) { + return true; + } + else { + // recursive call + return clazzImplementsInterface(implementationClazz.getSuperclass(), interfaceClazz); + } + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/DateTimeHelper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/DateTimeHelper.java new file mode 100644 index 0000000..95c2631 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/DateTimeHelper.java @@ -0,0 +1,14 @@ +package de.accso.flexinale.common.shared_kernel; + +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +public final class DateTimeHelper { + public static LocalDateTime fromEpochSeconds(final long epochSeconds) { + return LocalDateTime.ofEpochSecond(epochSeconds, 0, ZoneOffset.UTC); + } + + public static long toEpochSeconds(final LocalDateTime dateTime) { + return dateTime.toEpochSecond(ZoneOffset.UTC); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/DeveloperMistakeException.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/DeveloperMistakeException.java new file mode 100644 index 0000000..6f760f2 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/DeveloperMistakeException.java @@ -0,0 +1,24 @@ +package de.accso.flexinale.common.shared_kernel; + +public class DeveloperMistakeException extends RuntimeException { + public DeveloperMistakeException(final String message) { + super(message); + } + + public DeveloperMistakeException() { + super(); + } + + public DeveloperMistakeException(final String message, final Throwable cause) { + super(message, cause); + } + + public DeveloperMistakeException(final Throwable cause) { + super(cause); + } + + protected DeveloperMistakeException(final String message, final Throwable cause, + final boolean enableSuppression, final boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/DoNotCheckInArchitectureTests.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/DoNotCheckInArchitectureTests.java new file mode 100644 index 0000000..cd42c16 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/DoNotCheckInArchitectureTests.java @@ -0,0 +1,8 @@ +package de.accso.flexinale.common.shared_kernel; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(value = RetentionPolicy.RUNTIME) +public @interface DoNotCheckInArchitectureTests { +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/EqualsByContent.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/EqualsByContent.java new file mode 100644 index 0000000..ad6917c --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/EqualsByContent.java @@ -0,0 +1,6 @@ +package de.accso.flexinale.common.shared_kernel; + +@SuppressWarnings("unused") +public interface EqualsByContent { + boolean equalsByContent(final Object o); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/EventClassHelper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/EventClassHelper.java new file mode 100644 index 0000000..af0191a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/EventClassHelper.java @@ -0,0 +1,58 @@ +package de.accso.flexinale.common.shared_kernel; + +import de.accso.flexinale.common.api.event.Event; + +import java.lang.reflect.Constructor; + +import static de.accso.flexinale.common.shared_kernel.ClazzHelper.clazzImplementsInterface; + +public final class EventClassHelper { + @SuppressWarnings("unchecked") + public static Class getClassFor(final String clazzName) { + try { + return (Class) Class.forName(clazzName); + } + catch (ClassNotFoundException ex) { + throw new FlexinaleIllegalStateException(ex); + } + } + + public static String getEventClazzName(final String clazzName) { + Class clazz = getClassFor(clazzName); + + if (!clazzImplementsInterface(clazz, Event.class)) { + throw new FlexinaleIllegalArgumentException("wrong clazz %s used to retrieve Event's clazz name and version" + .formatted(clazzName)); + } + + return clazz.getCanonicalName(); + } + + public static String getEventClazzName(final Class clazz) { + return clazz.getCanonicalName(); + } + + public static Versionable.Version getEventClazzVersion(String clazzName) { + Class clazz = getClassFor(clazzName); + + if (!clazzImplementsInterface(clazz, Event.class)) { + throw new FlexinaleIllegalArgumentException("need to use Event class %s to retrieve class name" + .formatted(clazzName)); + } + + return getEventClazzVersion(clazz); + } + + public static Versionable.Version getEventClazzVersion(final Class clazz) { + try { + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + Event e = constructor.newInstance(); + return e.version(); + } + catch (Exception ex) { + throw new FlexinaleIllegalArgumentException("error when instantiating Event class %s or retrieving its version" + .formatted(getEventClazzName(clazz)), ex); + } + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/FlexinaleIllegalArgumentException.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/FlexinaleIllegalArgumentException.java new file mode 100644 index 0000000..56b188f --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/FlexinaleIllegalArgumentException.java @@ -0,0 +1,24 @@ +package de.accso.flexinale.common.shared_kernel; + +public class FlexinaleIllegalArgumentException extends DeveloperMistakeException { + public FlexinaleIllegalArgumentException() { + super(); + } + + public FlexinaleIllegalArgumentException(final String message) { + super(message); + } + + public FlexinaleIllegalArgumentException(final String message, final Throwable cause) { + super(message, cause); + } + + public FlexinaleIllegalArgumentException(final Throwable cause) { + super(cause); + } + + protected FlexinaleIllegalArgumentException(final String message, final Throwable cause, + final boolean enableSuppression, final boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/FlexinaleIllegalStateException.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/FlexinaleIllegalStateException.java new file mode 100644 index 0000000..ac0bff6 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/FlexinaleIllegalStateException.java @@ -0,0 +1,20 @@ +package de.accso.flexinale.common.shared_kernel; + +@SuppressWarnings("unused") +public class FlexinaleIllegalStateException extends IllegalStateException { + public FlexinaleIllegalStateException() { + super(); + } + + public FlexinaleIllegalStateException(final String message) { + super(message); + } + + public FlexinaleIllegalStateException(final String message, final Throwable cause) { + super(message, cause); + } + + public FlexinaleIllegalStateException(final Throwable cause) { + super(cause); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Identifiable.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Identifiable.java new file mode 100644 index 0000000..656689d --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Identifiable.java @@ -0,0 +1,31 @@ +package de.accso.flexinale.common.shared_kernel; + +import java.io.Serializable; +import java.util.UUID; + +public interface Identifiable { + record Id(String id) implements Serializable, RawWrapper { + public static Id of() { + return uuid(); + } + + public static Id of(String id) { + return new Id(id); + } + + public static Id uuid() { + return Id.of(UUID.randomUUID().toString()); + } + + public static String uuidString() { + return UUID.randomUUID().toString(); + } + + @Override + public String raw() { + return id; + } + } + + Id id(); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/MapWithCounter.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/MapWithCounter.java new file mode 100644 index 0000000..557cc03 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/MapWithCounter.java @@ -0,0 +1,21 @@ +package de.accso.flexinale.common.shared_kernel; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public final class MapWithCounter { + private final ConcurrentHashMap backingMap = new ConcurrentHashMap<>(); + + public long increment(K key) { + return backingMap.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet(); + } + + public long get(K key) { + AtomicLong counter = backingMap.get(key); + return counter != null ? counter.get() : 0; + } + + public void set(K key, Long value) { + backingMap.put(key, new AtomicLong(value)); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Mergeable.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Mergeable.java new file mode 100644 index 0000000..9500fcf --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Mergeable.java @@ -0,0 +1,5 @@ +package de.accso.flexinale.common.shared_kernel; + +public interface Mergeable { + E merge(E newData); +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/RawWrapper.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/RawWrapper.java new file mode 100644 index 0000000..372c888 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/RawWrapper.java @@ -0,0 +1,25 @@ +package de.accso.flexinale.common.shared_kernel; + +import java.io.Serializable; + +public interface RawWrapper extends Serializable { + T raw(); + + default String print() { // unfortunately one cannot override toString() here, results in compiler error + return raw().toString(); + } + + @SuppressWarnings("unchecked") + static T getRawOrNull(Object o) { + if (o == null) + return null; + else + try { + RawWrapper castedObject = (RawWrapper)o; + return castedObject.raw(); + } + catch (ClassCastException ccex) { + throw new DeveloperMistakeException(ccex); + } + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Selection.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Selection.java new file mode 100644 index 0000000..5eb62de --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Selection.java @@ -0,0 +1,71 @@ +package de.accso.flexinale.common.shared_kernel; + +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; + +public interface Selection extends Set { + Set next(); // select one or more options from the list based on a selection algorithm + + enum SelectionAlgorithm { + RANDOM, CONSTANT_FIRST, CONSTANT_LAST, ROUND_ROBIN, ALL + } + + final class RandomSelector extends SynchronizedSet implements Selection { + @Override + public Set next() { + if (this.size() == 0) return Set.of(); + + int randomIndex = ThreadLocalRandom.current().nextInt(this.size()); + return Set.of(this.stream() + .skip(randomIndex) + .findFirst() + .orElseThrow(DeveloperMistakeException::new)); + } + } + + final class ConstantFirstSelector extends SynchronizedSet implements Selection { + @Override + public Set next() { + if (this.size() == 0) return Set.of(); + + return Set.of(this.stream() + .findFirst() + .orElseThrow(DeveloperMistakeException::new)); + } + } + + final class ConstantLastSelector extends SynchronizedSet implements Selection { + @Override + public Set next() { + if (this.size() == 0) return Set.of(); + + return Set.of(this.stream() + .skip(this.size()-1) + .findFirst() + .orElseThrow(DeveloperMistakeException::new)); + } + } + + final class RoundRobinSelector extends SynchronizedSet implements Selection { + private int roundRobinIndex; + + @Override + public Set next() { + if (this.size() == 0) return Set.of(); + + T result = this.stream().toList().get(roundRobinIndex++); + if (roundRobinIndex == this.size()) roundRobinIndex = 0; + + return Set.of(result); + } + } + + final class AllSelector extends SynchronizedSet implements Selection { + @Override + public Set next() { + if (this.size() == 0) return Set.of(); + + return this; + } + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/SelectionMultiMap.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/SelectionMultiMap.java new file mode 100644 index 0000000..1f3e7d4 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/SelectionMultiMap.java @@ -0,0 +1,30 @@ +package de.accso.flexinale.common.shared_kernel; + +import org.apache.commons.collections4.multimap.AbstractSetValuedMap; + +import java.util.*; + +public final class SelectionMultiMap extends AbstractSetValuedMap { + private final Selection.SelectionAlgorithm selectionAlgorithm; + + public SelectionMultiMap(Selection.SelectionAlgorithm selectionAlgorithm) { + super(new HashMap>()); + this.selectionAlgorithm = selectionAlgorithm; + } + + @Override + protected Set createCollection() { + return switch (selectionAlgorithm) { + case RANDOM -> new Selection.RandomSelector<>(); + case CONSTANT_FIRST -> new Selection.ConstantFirstSelector<>(); + case CONSTANT_LAST -> new Selection.ConstantLastSelector<>(); + case ROUND_ROBIN -> new Selection.RoundRobinSelector<>(); + case ALL -> new Selection.AllSelector<>(); + }; + } + + @Override + public Set get(final K key) { + return getMap().get(key); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/SynchronizedSet.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/SynchronizedSet.java new file mode 100644 index 0000000..1b6e3c7 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/SynchronizedSet.java @@ -0,0 +1,127 @@ +package de.accso.flexinale.common.shared_kernel; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; + +// needed as one cannot derive from Collections.synchronizedSet()'s result type (as package-protected, sigh) +class SynchronizedSet implements Set { + + private final Set backingSet = new HashSet(); + private final Object lock = new Object(); + + @Override + public boolean add(T object) { + synchronized (lock) { + return backingSet.add(object); + } + } + + @Override + public boolean addAll(Collection collection) { + synchronized (lock) { + return backingSet.addAll(collection); + } + } + + @Override + public void clear() { + synchronized (lock) { + backingSet.clear(); + } + } + + @Override + public boolean contains(Object object) { + synchronized (lock) { + return backingSet.contains(object); + } + } + + @Override + public boolean containsAll(Collection collection) { + synchronized (lock) { + return backingSet.containsAll(collection); + } + } + + @Override + public boolean isEmpty() { + synchronized (lock) { + return backingSet.isEmpty(); + } + } + + // needs to get synchronized outside! + @Override + public Iterator iterator() { + return backingSet.iterator(); + } + + @Override + public T[] toArray() { + synchronized (lock) { + return (T[]) backingSet.toArray(); + } + } + + @Override + public T[] toArray(Object[] object) { + synchronized (lock) { + return (T[]) backingSet.toArray(object); + } + } + + @Override + public boolean remove(Object object) { + synchronized (lock) { + return backingSet.remove(object); + } + } + + @Override + public boolean removeAll(Collection collection) { + synchronized (lock) { + return backingSet.removeAll(collection); + } + } + + @Override + public boolean retainAll(Collection collection) { + synchronized (lock) { + return backingSet.retainAll(collection); + } + } + + @Override + public int size() { + synchronized (lock) { + return backingSet.size(); + } + } + + @Override + public boolean equals(Object object) { + synchronized (lock) { + if (object == this) { + return true; + } + return backingSet.equals(object); + } + } + + @Override + public int hashCode() { + synchronized (lock) { + return backingSet.hashCode(); + } + } + + @Override + public String toString() { + synchronized (lock) { + return backingSet.toString(); + } + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/ThreadSafeCounterMap.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/ThreadSafeCounterMap.java new file mode 100644 index 0000000..3aed43c --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/ThreadSafeCounterMap.java @@ -0,0 +1,21 @@ +package de.accso.flexinale.common.shared_kernel; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +public final class ThreadSafeCounterMap { + private final ConcurrentHashMap backingMap = new ConcurrentHashMap<>(); + + public long increment(K key) { + return backingMap.computeIfAbsent(key, k -> new AtomicLong()).incrementAndGet(); + } + + public long get(K key) { + AtomicLong counter = backingMap.get(key); + return counter != null ? counter.get() : 0; + } + + public void set(K key, Long value) { + backingMap.put(key, new AtomicLong(value)); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/TimeFormatter.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/TimeFormatter.java new file mode 100644 index 0000000..893d4b8 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/TimeFormatter.java @@ -0,0 +1,14 @@ +package de.accso.flexinale.common.shared_kernel; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public class TimeFormatter { + public static String calculateString(final LocalDateTime from, final Integer durationInMinutes) { + DateTimeFormatter hourMinuteFormatter = DateTimeFormatter.ofPattern("HH:mm"); + + return hourMinuteFormatter.format(from) + + " - " + + hourMinuteFormatter.format(from.plusMinutes(durationInMinutes)); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Versionable.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Versionable.java new file mode 100644 index 0000000..9961ec1 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/java/de/accso/flexinale/common/shared_kernel/Versionable.java @@ -0,0 +1,32 @@ +package de.accso.flexinale.common.shared_kernel; + +import java.io.Serializable; + +public interface Versionable { + record Version(Integer version) implements Serializable, RawWrapper { + public static Version of(Integer version) { + return new Version(version); + } + + public Version inc() { + return new Version(this.version()+1); + } + + @Override + public Integer raw() { + return version; + } + } + + Version version(); + + static Version unknownVersion() { + Integer UNKNOWN = -1; + return Version.of(UNKNOWN); + } + + static Version initialVersion() { + Integer INITIAL = 0; + return Version.of(INITIAL); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/resources/logback.xml b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/resources/logback.xml new file mode 100644 index 0000000..87e8669 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/main/resources/logback.xml @@ -0,0 +1,20 @@ + + + + %d{YYYYMMdd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/event/EventTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/event/EventTest.java new file mode 100644 index 0000000..ef92d43 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/event/EventTest.java @@ -0,0 +1,78 @@ +package de.accso.flexinale.common.api.event; + +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class EventTest { + + @Test + void testEventConstructor() { + // arrange + LocalDateTime now = LocalDateTime.now(); + + // act + EventTestdata.MyNumberEvent event = new EventTestdata.MyNumberEvent(11, 12); + + // assert + assertThat(event.id).isNotNull(); + assertThat(event.correlationId()).isNotNull(); + assertThat(event.timestamp).isAfterOrEqualTo(now); + assertThat(event.eventContext().eventHistory.size()).isEqualTo(1); + assertThat(event.eventContext().eventHistory.getFirst()).isEqualTo( + new EventContext.EventHistoryElement(event.id, EventTestdata.MyNumberEvent.class)); + } + + @Test + void testEventConstructorWithHistory() { + // arrange + LocalDateTime now = LocalDateTime.now(); + + // act + EventTestdata.MyNumberEvent startEvent = + new EventTestdata.MyNumberEvent(11, 12); + + EventTestdata.MyMixedEvent nextEvent = + new EventTestdata.MyMixedEvent(startEvent, "13", 14); + + EventTestdata.MyNumberEvent lastEvent = + new EventTestdata.MyNumberEvent(nextEvent, 15, 16); + + // assert + assertThat(lastEvent.id).isNotNull(); + assertThat(lastEvent.id).isNotEqualTo( nextEvent.id); + assertThat(lastEvent.id).isNotEqualTo(startEvent.id); + assertThat(nextEvent.id).isNotEqualTo(startEvent.id); + + assertThat(lastEvent.correlationId()).isNotNull(); + assertThat(lastEvent.correlationId()).isEqualTo( nextEvent.correlationId()); + assertThat(nextEvent.correlationId()).isEqualTo(startEvent.correlationId()); + + assertThat( lastEvent.timestamp).isAfterOrEqualTo( nextEvent.timestamp); + assertThat( nextEvent.timestamp).isAfterOrEqualTo(startEvent.timestamp); + assertThat(startEvent.timestamp).isAfterOrEqualTo(now); + + assertThat( lastEvent.eventContext().eventHistory.size()).isEqualTo(3); + assertThat( nextEvent.eventContext().eventHistory.size()).isEqualTo(2); + assertThat(startEvent.eventContext().eventHistory.size()).isEqualTo(1); + + assertThat(lastEvent.eventContext().eventHistory.get(0)).isEqualTo( + new EventContext.EventHistoryElement(startEvent.id, startEvent.getClass())); + assertThat(lastEvent.eventContext().eventHistory.get(1)).isEqualTo( + new EventContext.EventHistoryElement(nextEvent.id, nextEvent.getClass())); + assertThat(lastEvent.eventContext().eventHistory.get(2)).isEqualTo( + new EventContext.EventHistoryElement(lastEvent.id, lastEvent.getClass())); + + assertThat(nextEvent.eventContext().eventHistory.get(0)).isEqualTo( + new EventContext.EventHistoryElement(startEvent.id, startEvent.getClass())); + assertThat(nextEvent.eventContext().eventHistory.get(1)).isEqualTo( + new EventContext.EventHistoryElement(nextEvent.id, nextEvent.getClass())); + + assertThat(startEvent.eventContext().eventHistory.getFirst()).isEqualTo( + new EventContext.EventHistoryElement(startEvent.id, startEvent.getClass())); + } + +} + diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/event/EventTestdata.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/event/EventTestdata.java new file mode 100644 index 0000000..262c86d --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/event/EventTestdata.java @@ -0,0 +1,159 @@ +package de.accso.flexinale.common.api.event; + +import de.accso.flexinale.common.api.event.AbstractEvent; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.shared_kernel.Versionable; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; + +public final class EventTestdata { + + public static final class MyNumberEvent extends AbstractEvent { + public final Integer value1; + public final Integer value2; + + public final Version version = Versionable.initialVersion(); + + public MyNumberEvent(final Integer value1, final Integer value2) { + this.value1 = value1; + this.value2 = value2; + } + + public MyNumberEvent(final Event predecessorEvent, final Integer value1, final Integer value2) { + super(predecessorEvent); + this.value1 = value1; + this.value2 = value2; + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + final MyNumberEvent that = (MyNumberEvent) o; + + return new EqualsBuilder() + .appendSuper(super.equals(o)) + .append(this.version(), that.version()) + .append(value1, that.value1) + .append(value2, that.value2) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .appendSuper(super.hashCode()) + .append(this.version()) + .append(value1).append(value2) + .toHashCode(); + } + + @Override + public Version version() { + return version; + } + } + + public static final class MyTextEvent extends AbstractEvent { + public final String text1; + public final String text2; + + public final Version version = Versionable.initialVersion(); + + public MyTextEvent(final String text1, final String text2) { + this.text1 = text1; + this.text2 = text2; + } + + public MyTextEvent(final Id correlationId, final String text1, final String text2) { + super(correlationId); + this.text1 = text1; + this.text2 = text2; + } + + public MyTextEvent(final Event predecessorEvent, final String text1, final String text2) { + super(predecessorEvent); + this.text1 = text1; + this.text2 = text2; + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + final MyTextEvent that = (MyTextEvent) o; + + return new EqualsBuilder() + .appendSuper(super.equals(o)) + .append(this.version(), that.version()) + .append(text1, that.text1) + .append(text2, that.text2) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .appendSuper(super.hashCode()) + .append(this.version()) + .append(text1).append(text2) + .toHashCode(); + } + + @Override + public Version version() { + return version; + } + } + + public static final class MyMixedEvent extends AbstractEvent { + public final String text; + public final Integer number; + + public final Version version = Versionable.initialVersion(); + + public MyMixedEvent(final String text, final Integer number) { + this.text = text; + this.number = number; + } + + public MyMixedEvent(final Event predecessorEvent, final String text, final Integer number) { + super(predecessorEvent); + this.text = text; + this.number = number; + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + final MyMixedEvent that = (MyMixedEvent) o; + + return new EqualsBuilder() + .appendSuper(super.equals(o)) + .append(this.version(), that.version()) + .append(text, that.text) + .append(number, that.number).isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .appendSuper(super.hashCode()) + .append(this.version()) + .append(text).append(number) + .toHashCode(); + } + + @Override + public Version version() { + return version; + } + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/InMemorySyncEventBusAndSpyHandlingMultipleEventTypesTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/InMemorySyncEventBusAndSpyHandlingMultipleEventTypesTest.java new file mode 100644 index 0000000..ca5cb79 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/InMemorySyncEventBusAndSpyHandlingMultipleEventTypesTest.java @@ -0,0 +1,282 @@ +package de.accso.flexinale.common.api.eventbus; + +import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.MyNumberAndMixedEventPublisher; +import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.MyNumberAndMixedEventSubscriber; +import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.MyNumberAndTextAndMixedEventSubscriber; +import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.MyTextEventPublisher; +import de.accso.flexinale.common.api.event.AbstractEvent; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.api.event.EventTestdata.MyMixedEvent; +import de.accso.flexinale.common.api.event.EventTestdata.MyNumberEvent; +import de.accso.flexinale.common.api.event.EventTestdata.MyTextEvent; +import de.accso.flexinale.common.infrastructure.eventbus.InMemorySyncEventBus; +import de.accso.flexinale.common.infrastructure.eventbus.InMemorySyncEventBusFactory; +import de.accso.flexinale.common.shared_kernel.Versionable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.START_READING_FROM_NOW; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@SuppressWarnings("unused") +class InMemorySyncEventBusAndSpyHandlingMultipleEventTypesTest { + + InMemorySyncEventBusFactory factory; + + InMemorySyncEventBus numberBus; + InMemorySyncEventBus textBus; + InMemorySyncEventBus mixedBus; + + @BeforeEach + public void setUp() { + factory = new InMemorySyncEventBusFactory(); + + numberBus = (InMemorySyncEventBus) factory.createOrGetEventBusFor(MyNumberEvent.class); + textBus = (InMemorySyncEventBus) factory.createOrGetEventBusFor(MyTextEvent.class); + mixedBus = (InMemorySyncEventBus) factory.createOrGetEventBusFor(MyMixedEvent.class); + } + + @Test + void testOneEventTypeWithTwoInOnePublisherAndTwoInOneSubscriber() { + // arrange + Random random = new Random(); + + int numEventsToPublish = 10; + + MyNumberAndMixedEventPublisher twoInOnePublisher = new MyNumberAndMixedEventPublisher(numberBus, mixedBus); + MyNumberAndMixedEventSubscriber twoInOneSubscriber = new MyNumberAndMixedEventSubscriber(); + + // act + numberBus.subscribe(MyNumberEvent.class, twoInOneSubscriber, START_READING_FROM_NOW); + + MyNumberEvent lastNumberEvent = null; + for (int counter=0; counter E1bus = factory.createOrGetEventBusFor(E1.class); + EventBus E2bus = factory.createOrGetEventBusFor(E2.class); + EventBus E3bus = factory.createOrGetEventBusFor(E3.class); + EventBus E4bus = factory.createOrGetEventBusFor(E4.class); + EventBus E5bus = factory.createOrGetEventBusFor(E5.class); + EventBus E6bus = factory.createOrGetEventBusFor(E6.class); + EventBus E7bus = factory.createOrGetEventBusFor(E7.class); + EventBus E8bus = factory.createOrGetEventBusFor(E8.class); + EventBus E9bus = factory.createOrGetEventBusFor(E9.class); + + class Publisher9 implements EventPublisher.EventPublisher9 { + @Override + public String getName() { + return Publisher9.class.getName(); + } + + @Override + public void post (final Class eventType, final E1 event) { E1bus.publish(eventType, event); } + @Override + public void post2(final Class eventType, final E2 event) { E2bus.publish(eventType, event); } + @Override + public void post3(final Class eventType, final E3 event) { E3bus.publish(eventType, event); } + @Override + public void post4(final Class eventType, final E4 event) { E4bus.publish(eventType, event); } + @Override + public void post5(final Class eventType, final E5 event) { E5bus.publish(eventType, event); } + @Override + public void post6(final Class eventType, final E6 event) { E6bus.publish(eventType, event); } + @Override + public void post7(final Class eventType, final E7 event) { E7bus.publish(eventType, event); } + @Override + public void post8(final Class eventType, final E8 event) { E8bus.publish(eventType, event); } + @Override + public void post9(final Class eventType, final E9 event) { E9bus.publish(eventType, event); } + } + + class Subscriber9 implements EventSubscriber.EventSubscriber9 { + final Map,Integer> event2CounterMap = new HashMap<>(); + + private void incEventCounter(Class eventType) { + Integer counter = event2CounterMap.get(eventType); + if (event2CounterMap.get(eventType) == null) { + event2CounterMap.put(eventType, 1); + } else { + event2CounterMap.put(eventType, counter+1); + } + } + + @Override + public String getName() { + return Subscriber9.class.getName(); + } + + @Override + public String getGroupName() { + return InMemorySyncEventBusAndSpyHandlingMultipleEventTypesTest.class.getPackageName(); + } + + @Override + public void receive (final E1 event) { incEventCounter(E1.class); } + @Override + public void receive2(final E2 event) { incEventCounter(E2.class); } + @Override + public void receive3(final E3 event) { incEventCounter(E3.class); } + @Override + public void receive4(final E4 event) { incEventCounter(E4.class); } + @Override + public void receive5(final E5 event) { incEventCounter(E5.class); } + @Override + public void receive6(final E6 event) { incEventCounter(E6.class); } + @Override + public void receive7(final E7 event) { incEventCounter(E7.class); } + @Override + public void receive8(final E8 event) { incEventCounter(E8.class); } + @Override + public void receive9(final E9 event) { incEventCounter(E9.class); } + } + + // ---------------------------------------------------------------------------------------------------------- + + // arrange, 2 + + Publisher9 publisher9 = new Publisher9(); + Subscriber9 subscriber9 = new Subscriber9(); + + E1bus.subscribe(E1.class, subscriber9, START_READING_FROM_NOW); + E2bus.subscribe(E2.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW); + E3bus.subscribe(E3.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW); + E4bus.subscribe(E4.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW); + E5bus.subscribe(E5.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW); + E6bus.subscribe(E6.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW); + E7bus.subscribe(E7.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW); + E8bus.subscribe(E8.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW); + E9bus.subscribe(E9.class, (EventSubscriber) subscriber9, START_READING_FROM_NOW); + + // act + int numEventsToPublish = 100; + for (int counter=0; counter> classes = subscriber9.event2CounterMap.keySet(); + classes.forEach(eventClass -> { + assertThat(subscriber9.event2CounterMap.get(eventClass)).isEqualTo(numEventsToPublish); + }); + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/InMemorySyncEventBusAndSpySubscriptionTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/InMemorySyncEventBusAndSpySubscriptionTest.java new file mode 100644 index 0000000..d8dc5c7 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/InMemorySyncEventBusAndSpySubscriptionTest.java @@ -0,0 +1,423 @@ +package de.accso.flexinale.common.api.eventbus; + +import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.*; +import de.accso.flexinale.common.api.event.EventTestdata.MyNumberEvent; +import de.accso.flexinale.common.infrastructure.eventbus.InMemorySyncEventBusFactory; +import de.accso.flexinale.common.infrastructure.eventbus.InMemorySyncEventBusSpy; +import de.accso.flexinale.common.infrastructure.eventbus.InMemorySyncEventBusSpyFactory; +import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class InMemorySyncEventBusAndSpySubscriptionTest { + + @Test + void testSubscriptionAtStart_START_READING_FROM_NOW() { + // arrange + int numEventsToPublish = 100; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + internal_testSubscriberReceivesMultipleEvents_START_READING_FROM_NOW(numEventsToPublish, sut); + } + + @Test + void testSubscriptionAtStart_START_READING_FROM_NOW_Spy() { + // arrange + int numEventsToPublish = 100; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + MyNumberEvent myEvent = internal_testSubscriberReceivesMultipleEvents_START_READING_FROM_NOW(numEventsToPublish, sut); + + // ... and assert spy functionality + InMemorySyncEventBusSpy spy = (InMemorySyncEventBusSpy) sut; + List allEvents = spy.allEvents(); + assertThat(allEvents.getLast()).isEqualTo(myEvent); + assertThat(spy.size()).isEqualTo(numEventsToPublish); + } + + private static MyNumberEvent internal_testSubscriberReceivesMultipleEvents_START_READING_FROM_NOW( + final int numEventsToPublish, final EventBus sut) { + + MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut); + + MyNumberEventSubscriber mySubscriberSubscribedBeforePublish = new MyNumberEventSubscriber(true); + MyNumberEventSubscriber mySubscriberSubscribedAfterPublish = new MyNumberEventSubscriber(true); + + // act + sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedBeforePublish, START_READING_FROM_NOW); + MyNumberEvent myEvent = null; + for (int counter = 0; counter < numEventsToPublish; counter++) { + myEvent = new MyNumberEvent(counter, counter); + myPublisher.post(MyNumberEvent.class, myEvent); + } + sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedAfterPublish, START_READING_FROM_NOW); + + // assert + assertThat(mySubscriberSubscribedBeforePublish.lastReceivedEvent).isEqualTo(myEvent); + assertThat(mySubscriberSubscribedBeforePublish.counterOfRetrievedEvents).isEqualTo(numEventsToPublish); + + assertThat(mySubscriberSubscribedAfterPublish.lastReceivedEvent).isNull(); + assertThat(mySubscriberSubscribedAfterPublish.counterOfRetrievedEvents).isEqualTo(0); + + return myEvent; + } + + // ----------------------------------------------------------------------------------------------------------- + + @Test + void testSubscriptionAtStart_START_READING_FROM_BEGINNING_throwsException() { + // arrange + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(true); + + // act and assert + Assertions.assertThatThrownBy(() -> { + sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_BEGINNING); + }) + .isInstanceOf(DeveloperMistakeException.class); + } + + @Test + void testSubscriptionAtStart_START_READING_FROM_BEGINNING_Spy() { + // arrange + int numEventsToPublish = 100; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + InMemorySyncEventBusSpy spy = (InMemorySyncEventBusSpy) sut; + MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut); + + MyNumberEventSubscriber mySubscriberSubscribedBeforePublish = new MyNumberEventSubscriber(true); + MyNumberEventSubscriber mySubscriberSubscribedAfterPublish = new MyNumberEventSubscriber(true); + + // act - one subscriber before the publish, the other afterwards - with START_READING_FROM_BEGINNING both get the same + sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedBeforePublish, START_READING_FROM_BEGINNING); + MyNumberEvent myEvent = null; + for (int counter = 0; counter < numEventsToPublish; counter++) { + myEvent = new MyNumberEvent(counter, counter); + myPublisher.post(MyNumberEvent.class, myEvent); + } + sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedAfterPublish, START_READING_FROM_BEGINNING); + + // assert + assertThat(mySubscriberSubscribedBeforePublish.lastReceivedEvent).isEqualTo(myEvent); + assertThat(mySubscriberSubscribedBeforePublish.counterOfRetrievedEvents).isEqualTo(numEventsToPublish); + + assertThat(mySubscriberSubscribedAfterPublish.lastReceivedEvent).isEqualTo(myEvent); + assertThat(mySubscriberSubscribedAfterPublish.counterOfRetrievedEvents).isEqualTo(numEventsToPublish); + + // ... and assert spy functionality + List allEvents = spy.allEvents(); + assertThat(allEvents.getLast()).isEqualTo(myEvent); + assertThat(spy.size()).isEqualTo(numEventsToPublish); + } + + // ----------------------------------------------------------------------------------------------------------- + + @Test + void testSubscriptionAtStart_START_READING_FROM_LAST_TIME_throwsException() { + // arrange + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(true); + + // act and assert + Assertions.assertThatThrownBy(() -> { + sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_LAST_TIME); + }) + .isInstanceOf(DeveloperMistakeException.class); + } + + @Test + void testSubscriptionAtStart_START_READING_FROM_LAST_TIME_Spy_oneConsumer() { + // arrange + int numEventsToPublish = 100; + int counter; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + InMemorySyncEventBusSpy spy = (InMemorySyncEventBusSpy) sut; + MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut); + + MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(true); + + // act + + // 1) subscribe + sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_LAST_TIME); + + // 2) sent first half + MyNumberEvent myEvent = null; + for (counter = 0; counter < numEventsToPublish / 2; counter++) { + myEvent = new MyNumberEvent(counter, counter); + myPublisher.post(MyNumberEvent.class, myEvent); + } + + assertThat(mySubscriber.lastReceivedEvent).isEqualTo(myEvent); + assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish / 2); + + // 3) unsubscribe + sut.unsubscribe(MyNumberEvent.class, mySubscriber); + + // 4) sent second half + for (; counter < numEventsToPublish; counter++) { + myEvent = new MyNumberEvent(counter, counter); + myPublisher.post(MyNumberEvent.class, myEvent); + } + + // 5) subscribe again -> get the second half + sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_LAST_TIME); + + // 6) unsubscribe + sut.unsubscribe(MyNumberEvent.class, mySubscriber); + assertThat(mySubscriber.lastReceivedEvent).isEqualTo(myEvent); + assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish); + + // 7) subscribe again -> get nothing else + sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_LAST_TIME); + assertThat(mySubscriber.lastReceivedEvent).isEqualTo(myEvent); + assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish); + + // ... and assert spy functionality + List allEvents = spy.allEvents(); + assertThat(allEvents.getLast()).isEqualTo(myEvent); + assertThat(spy.size()).isEqualTo(numEventsToPublish); + } + + @Test + void testSubscriptionAtStart_START_READING_FROM_LAST_TIME_Spy_twoConsumersInOneGroup() { + // arrange + int numEventsToPublish = 100; + int counter; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + InMemorySyncEventBusSpy spy = (InMemorySyncEventBusSpy) sut; + + MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut); + + MyNumberEventSubscriber mySubscriber1 = new MyNumberEventSubscriber(true); + MyNumberEventSubscriber mySubscriber2 = new MyNumberEventSubscriber(true); + + // act + + // 1) subscribe + sut.subscribe(MyNumberEvent.class, mySubscriber1, START_READING_FROM_LAST_TIME); + sut.subscribe(MyNumberEvent.class, mySubscriber2, START_READING_FROM_LAST_TIME); + + // 2) sent first half + MyNumberEvent myEvent50 = null; + for (counter = 0; counter < numEventsToPublish / 2; counter++) { + myEvent50 = new MyNumberEvent(counter, counter); + myPublisher.post(MyNumberEvent.class, myEvent50); + } + + // only one of the subscribers has the last event + Set lastReceivedEvents = Set.of(mySubscriber1.lastReceivedEvent, mySubscriber2.lastReceivedEvent); + assertThat(lastReceivedEvents.size()).isEqualTo(2); // Set.of() already guarantees that the two elements are different (otherwise "java.lang.IllegalArgumentException: duplicate element") + assertThat(lastReceivedEvents.contains(myEvent50)).isTrue(); + + // both subscribers received all events in total + assertThat(mySubscriber1.counterOfRetrievedEvents + mySubscriber2.counterOfRetrievedEvents) + .isEqualTo(numEventsToPublish / 2); + + int counterOfRetrievedEventsOfSubscriber1AfterFirstHalf = mySubscriber1.counterOfRetrievedEvents; + MyNumberEvent mySubscriber1LastReceivedEventAfterFirstHalf = mySubscriber1.lastReceivedEvent; + int counterOfRetrievedEventsOfSubscriber2AfterFirstHalf = mySubscriber2.counterOfRetrievedEvents; + MyNumberEvent mySubscriber2LastReceivedEventAfterFirstHalf = mySubscriber2.lastReceivedEvent; + + // 3) unsubscribe + sut.unsubscribe(MyNumberEvent.class, mySubscriber1); + sut.unsubscribe(MyNumberEvent.class, mySubscriber2); + + // 4) sent second half + MyNumberEvent myEvent100 = null; + for (; counter < numEventsToPublish; counter++) { + myEvent100 = new MyNumberEvent(counter, counter); + myPublisher.post(MyNumberEvent.class, myEvent100); + } + + // 5) subscribe subscriber 1 again -> gets the second half completely + sut.subscribe(MyNumberEvent.class, mySubscriber1, START_READING_FROM_LAST_TIME); + assertThat(mySubscriber1.lastReceivedEvent).isEqualTo(myEvent100); + assertThat(mySubscriber1.counterOfRetrievedEvents) + .isEqualTo(counterOfRetrievedEventsOfSubscriber1AfterFirstHalf + numEventsToPublish/2); + + // the second subscriber get's nothing as both subscribers are in one group + // (wait fail, when the following subscribe comes to early -> then add to wait a little here) + sut.subscribe(MyNumberEvent.class, mySubscriber2, START_READING_FROM_LAST_TIME); + assertThat(mySubscriber2.lastReceivedEvent).isEqualTo(mySubscriber2LastReceivedEventAfterFirstHalf); + assertThat(mySubscriber2.counterOfRetrievedEvents) + .isEqualTo(counterOfRetrievedEventsOfSubscriber2AfterFirstHalf); + + int counterOfRetrievedEventsOfSubscriber1AfterSecondHalf = mySubscriber1.counterOfRetrievedEvents; + int counterOfRetrievedEventsOfSubscriber2AfterSecondHalf = mySubscriber2.counterOfRetrievedEvents; + + assertThat(counterOfRetrievedEventsOfSubscriber1AfterSecondHalf + counterOfRetrievedEventsOfSubscriber2AfterSecondHalf) + .isEqualTo(numEventsToPublish); + + // 6) unsubscribe + sut.unsubscribe(MyNumberEvent.class, mySubscriber1); + sut.unsubscribe(MyNumberEvent.class, mySubscriber2); + + // 7) subscribe again -> get nothing else + sut.subscribe(MyNumberEvent.class, mySubscriber1, START_READING_FROM_LAST_TIME); + assertThat(mySubscriber1.lastReceivedEvent).isEqualTo(myEvent100); + assertThat(mySubscriber1.counterOfRetrievedEvents) + .isEqualTo(counterOfRetrievedEventsOfSubscriber1AfterSecondHalf); + sut.subscribe(MyNumberEvent.class, mySubscriber2, START_READING_FROM_LAST_TIME); + assertThat(mySubscriber2.lastReceivedEvent) + .isEqualTo(mySubscriber2LastReceivedEventAfterFirstHalf); + assertThat(mySubscriber2.counterOfRetrievedEvents) + .isEqualTo(counterOfRetrievedEventsOfSubscriber2AfterSecondHalf); + + // ... and assert spy functionality + List allEvents = spy.allEvents(); + assertThat(allEvents.getLast()).isEqualTo(myEvent100); + assertThat(spy.size()).isEqualTo(numEventsToPublish); + } + + @Test + void testSubscriptionAtStart_START_READING_FROM_LAST_TIME_Spy_twoConsumersInSeparateGroups() { + // arrange + int numEventsToPublish = 100; + int counter; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + InMemorySyncEventBusSpy spy = (InMemorySyncEventBusSpy) sut; + + MyNumberEventSubscriber mySubscriber1 = new MyNumberEventSubscriber(true); + MyEventSubscriberEachInstanceInItsOwnGroup mySubscriber2 = new MyEventSubscriberEachInstanceInItsOwnGroup(true); + MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut); + + // act + + // 1) subscribe + sut.subscribe(MyNumberEvent.class, mySubscriber1, START_READING_FROM_LAST_TIME); + sut.subscribe(MyNumberEvent.class, mySubscriber2, START_READING_FROM_LAST_TIME); + + // 2) sent first half + MyNumberEvent myEvent50 = null; + for (counter = 0; counter < numEventsToPublish / 2; counter++) { + myEvent50 = new MyNumberEvent(counter, counter); + myPublisher.post(MyNumberEvent.class, myEvent50); + } + + assertThat(mySubscriber1.lastReceivedEvent).isEqualTo(myEvent50); + assertThat(mySubscriber1.counterOfRetrievedEvents).isEqualTo(numEventsToPublish / 2); + assertThat(mySubscriber2.lastReceivedEvent).isEqualTo(myEvent50); + assertThat(mySubscriber2.counterOfRetrievedEvents).isEqualTo(numEventsToPublish / 2); + + // 3) unsubscribe + sut.unsubscribe(MyNumberEvent.class, mySubscriber1); + sut.unsubscribe(MyNumberEvent.class, mySubscriber2); + + // 4) sent second half + MyNumberEvent myEvent100 = null; + for (; counter < numEventsToPublish; counter++) { + myEvent100 = new MyNumberEvent(counter, counter); + myPublisher.post(MyNumberEvent.class, myEvent100); + } + + // 5) subscribe again -> get the second half + sut.subscribe(MyNumberEvent.class, mySubscriber1, START_READING_FROM_LAST_TIME); + assertThat(mySubscriber1.lastReceivedEvent).isEqualTo(myEvent100); + assertThat(mySubscriber1.counterOfRetrievedEvents).isEqualTo(numEventsToPublish); + + // the second subscriber get's same as both subscribers are in separate groups + sut.subscribe(MyNumberEvent.class, mySubscriber2, START_READING_FROM_LAST_TIME); + assertThat(mySubscriber2.lastReceivedEvent).isEqualTo(myEvent100); + assertThat(mySubscriber2.counterOfRetrievedEvents).isEqualTo(numEventsToPublish); + + // 6) unsubscribe + sut.unsubscribe(MyNumberEvent.class, mySubscriber1); + sut.unsubscribe(MyNumberEvent.class, mySubscriber2); + + // 7) subscribe again -> get nothing else + sut.subscribe(MyNumberEvent.class, mySubscriber1, START_READING_FROM_LAST_TIME); + assertThat(mySubscriber1.lastReceivedEvent).isEqualTo(myEvent100); + assertThat(mySubscriber1.counterOfRetrievedEvents).isEqualTo(numEventsToPublish); + sut.subscribe(MyNumberEvent.class, mySubscriber2, START_READING_FROM_LAST_TIME); + assertThat(mySubscriber2.lastReceivedEvent).isEqualTo(myEvent100); + assertThat(mySubscriber2.counterOfRetrievedEvents).isEqualTo(numEventsToPublish); + + // ... and assert spy functionality + List allEvents = spy.allEvents(); + assertThat(allEvents.getLast()).isEqualTo(myEvent100); + assertThat(spy.size()).isEqualTo(numEventsToPublish); + } + + @Test + void testSubscriptionAtStart_START_READING_FROM_BEGINNING_and_LAST_TIME_Spy() { + // arrange + int numEventsToPublish = 100; + int counter; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + InMemorySyncEventBusSpy spy = (InMemorySyncEventBusSpy) sut; + + // false, as we get some events twice + MyNumberEventSubscriber mySubscriberSubscribedBeforeAndAfterPublish = new MyNumberEventSubscriber(false); + MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut); + + // act + + // 1) subscribe + sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedBeforeAndAfterPublish, START_READING_FROM_LAST_TIME); + + // 2) sent first half + MyNumberEvent myEvent = null; + for (counter = 0; counter < numEventsToPublish / 2; counter++) { + myEvent = new MyNumberEvent(counter, counter); + myPublisher.post(MyNumberEvent.class, myEvent); + } + + // 3) unsubscribe + sut.unsubscribe(MyNumberEvent.class, mySubscriberSubscribedBeforeAndAfterPublish); + + // 4) sent second half + for (; counter < numEventsToPublish; counter++) { + myEvent = new MyNumberEvent(counter, counter); + myPublisher.post(MyNumberEvent.class, myEvent); + } + + // 5) subscribe again -> get it all, i.e. the first half and the second half + sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedBeforeAndAfterPublish, START_READING_FROM_BEGINNING); + + // assert + assertThat(mySubscriberSubscribedBeforeAndAfterPublish.lastReceivedEvent).isEqualTo(myEvent); + assertThat(mySubscriberSubscribedBeforeAndAfterPublish.counterOfRetrievedEvents) + .isEqualTo(numEventsToPublish/2 + numEventsToPublish); + + // 6) unsubscribe + sut.unsubscribe(MyNumberEvent.class, mySubscriberSubscribedBeforeAndAfterPublish); + + // 7) subscribe again -> get nothing else + sut.subscribe(MyNumberEvent.class, mySubscriberSubscribedBeforeAndAfterPublish, START_READING_FROM_LAST_TIME); + + // assert + assertThat(mySubscriberSubscribedBeforeAndAfterPublish.lastReceivedEvent).isEqualTo(myEvent); + assertThat(mySubscriberSubscribedBeforeAndAfterPublish.counterOfRetrievedEvents) + .isEqualTo(numEventsToPublish/2 + numEventsToPublish); + + // ... and assert spy functionality + List allEvents = spy.allEvents(); + assertThat(allEvents.getLast()).isEqualTo(myEvent); + assertThat(spy.size()).isEqualTo(numEventsToPublish); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/InMemorySyncEventBusAndSpyTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/InMemorySyncEventBusAndSpyTest.java new file mode 100644 index 0000000..602c0f1 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/InMemorySyncEventBusAndSpyTest.java @@ -0,0 +1,571 @@ +package de.accso.flexinale.common.api.eventbus; + +import de.accso.flexinale.common.api.eventbus.PublisherAndSubscriberTestdata.*; +import de.accso.flexinale.common.api.event.EventTestdata.MyMixedEvent; +import de.accso.flexinale.common.api.event.EventTestdata.MyNumberEvent; +import de.accso.flexinale.common.api.event.EventTestdata.MyTextEvent; +import de.accso.flexinale.common.infrastructure.eventbus.*; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; + +import static de.accso.flexinale.common.api.eventbus.EventSubscriptionAtStart.*; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class InMemorySyncEventBusAndSpyTest { + + @Test + void testCreateAndGetEventBus() { + // arrange + InMemorySyncEventBusFactory sut = new InMemorySyncEventBusFactory(); + + // act + InMemorySyncEventBus eventBus = + (InMemorySyncEventBus) sut.createOrGetEventBusFor(MyNumberEvent.class); + InMemorySyncEventBus retrievedEventBus = + (InMemorySyncEventBus) sut.createOrGetEventBusFor(MyNumberEvent.class); + + // assert + assertThat(eventBus).isNotNull(); + assertThat(retrievedEventBus).isEqualTo(eventBus); + } + + @Test + void testCreateAndGetEventBusSpy() { + // arrange + InMemorySyncEventBusSpyFactory sut = new InMemorySyncEventBusSpyFactory(); + + // act + InMemorySyncEventBusSpy eventBus = + (InMemorySyncEventBusSpy) sut.createOrGetEventBusFor(MyNumberEvent.class); + InMemorySyncEventBusSpy retrievedEventBus = + (InMemorySyncEventBusSpy) sut.createOrGetEventBusFor(MyNumberEvent.class); + + // assert + assertThat(eventBus).isNotNull(); + assertThat(retrievedEventBus).isEqualTo(eventBus); + } + + // ----------------------------------------------------------------------------------------------------------- + + @Test + void testSubscriberReceivesEventOfCorrectType() { + // arrange + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + internal_testSubscriberReceivesEventOfCorrectType(sut); + } + + @Test + void testSubscriberReceivesEventOfCorrectTypeSpy() { + // arrange + InMemorySyncEventBusSpyFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + MyNumberEvent myEvent = internal_testSubscriberReceivesEventOfCorrectType(sut); + + // ... and assert spy functionality + InMemorySyncEventBusSpy spy = (InMemorySyncEventBusSpy) sut; + assertThat(spy.allEvents()).isEqualTo(List.of(myEvent)); + assertThat(spy.size()).isEqualTo(1); + } + + private MyNumberEvent internal_testSubscriberReceivesEventOfCorrectType(final EventBus sut) { + MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(); + MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut); + + // act + sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW); + MyNumberEvent myEvent = new MyNumberEvent(11, 22); + myPublisher.post(MyNumberEvent.class, myEvent); + + // assert + assertThat(mySubscriber.lastReceivedEvent).isEqualTo(myEvent); + assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(1); + return myEvent; + } + + @SuppressWarnings("unchecked") + @Test + void testEventSubscriberProducesRuntimeException() { + // arrange + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + EventBus numberBus = factory.createOrGetEventBusFor(MyNumberEvent.class); + + MyNumberEventPublisher myNumberPublisher = new MyNumberEventPublisher(numberBus); + + MyNumberEventThrowingRuntimeExceptionSubscriber erroneousSubscriber = new MyNumberEventThrowingRuntimeExceptionSubscriber(); + numberBus.subscribe(MyNumberEvent.class, (EventSubscriber) erroneousSubscriber, START_READING_FROM_NOW); + + MyNumberEvent myNumberEvent = new MyNumberEvent(11, 22); + Identifiable.Id id = myNumberEvent.id(); + + // act and assert that receive method throws RuntimeException (wrapped in a RuntimeException by the dispatcher) + Exception caughtException = null; + + try { + myNumberPublisher.post(MyNumberEvent.class, myNumberEvent); + } catch (Exception ex) { + caughtException = ex; + } + assertThat(caughtException).isNotNull(); // assertThatThrownBy would be nicer, but we need to the check the exception's contents + + Throwable cause = caughtException.getCause(); + // ... and wrapped again in a InvocationTargetException + assertThat(cause).isInstanceOf(java.lang.reflect.InvocationTargetException.class); + // so here we go with the root cause: + assertThat(cause.getCause()).isInstanceOf(MyNumberEventRuntimeException.class); + assertThat(cause.getCause()).hasMessageContaining("RuntimeException for event with Id=" + id); + } + + // ----------------------------------------------------------------------------------------------------------- + + @Test + void testGenericSubscriberReceivesAllEventsIndependentOfItsType() { + // arrange + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + EventBus numberBus = factory.createOrGetEventBusFor(MyNumberEvent.class); + EventBus textBus = factory.createOrGetEventBusFor(MyTextEvent.class); + + MyNumberEventSubscriber myNumberSubscriber = new MyNumberEventSubscriber(); + MyNumberEventPublisher myNumberPublisher = new MyNumberEventPublisher(numberBus); + MyTextEventSubscriber myTextSubscriber = new MyTextEventSubscriber(); + MyTextEventPublisher myTextPublisher = new MyTextEventPublisher(textBus); + + numberBus.subscribe(MyNumberEvent.class, myNumberSubscriber, START_READING_FROM_NOW); + textBus.subscribe(MyTextEvent.class, myTextSubscriber, START_READING_FROM_NOW); + + GenericEventSubscriber myGenericSubscriber = new GenericEventSubscriber(); + + numberBus.subscribe(myGenericSubscriber, START_READING_FROM_NOW); + textBus.subscribe(myGenericSubscriber, START_READING_FROM_NOW); + + // act + MyNumberEvent myNumberEvent = new MyNumberEvent(11, 22); + myNumberPublisher.post(MyNumberEvent.class, myNumberEvent); + + // assert + assertThat(myNumberSubscriber.lastReceivedEvent).isEqualTo(myNumberEvent); + assertThat(myNumberSubscriber.counterOfRetrievedEvents).isEqualTo(1); + + assertThat(myGenericSubscriber.lastReceivedEvent.getClass()).isEqualTo(MyNumberEvent.class); + assertThat(myGenericSubscriber.lastReceivedEvent).isEqualTo(myNumberEvent); + assertThat(myGenericSubscriber.counterOfRetrievedEvents).isEqualTo(1); + + // act + MyTextEvent myTextEvent = new MyTextEvent("11", "22"); + myTextPublisher.post(MyTextEvent.class, myTextEvent); + + // assert + assertThat(myTextSubscriber.lastReceivedEvent).isEqualTo(myTextEvent); + assertThat(myTextSubscriber.counterOfRetrievedEvents).isEqualTo(1); + + assertThat(myGenericSubscriber.lastReceivedEvent.getClass()).isEqualTo(MyTextEvent.class); + assertThat(myGenericSubscriber.lastReceivedEvent).isEqualTo(myTextEvent); + assertThat(myGenericSubscriber.counterOfRetrievedEvents).isEqualTo(2); + } + + // ----------------------------------------------------------------------------------------------------------- + + @Test + void testEventChainKeepsCorrelationIdAndEventHistory() { + // arrange + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + + EventBus textBus = factory.createOrGetEventBusFor(MyTextEvent.class); + EventBus numberBus = factory.createOrGetEventBusFor(MyNumberEvent.class); + + MyNumberEventSubscriberAndMyTextEventPublisher numberSubscriberAndTextPublisher = + new MyNumberEventSubscriberAndMyTextEventPublisher(textBus); + MyTextEventSubscriber myTextEventSubscriber = new MyTextEventSubscriber(); + + numberBus.subscribe(MyNumberEvent.class, numberSubscriberAndTextPublisher, START_READING_FROM_NOW); + textBus.subscribe(MyTextEvent.class, myTextEventSubscriber, START_READING_FROM_NOW); + + // act - send MyNumberEvent so that internally a MyTextEvent is sent + MyNumberEvent firstEvent = new MyNumberEvent(11, 22); + new MyNumberEventPublisher(numberBus).post(MyNumberEvent.class, firstEvent); + assertThat(numberSubscriberAndTextPublisher.lastReceivedEvent).isEqualTo(firstEvent); + + // assert that the event chain keeps the correlation Id and the event history + assertThat(numberSubscriberAndTextPublisher.lastSentEvent).isEqualTo(myTextEventSubscriber.lastReceivedEvent); + MyTextEvent secondEvent = numberSubscriberAndTextPublisher.lastSentEvent; + + assertThat(secondEvent.correlationId()).isEqualTo(firstEvent.correlationId()); + assertThat(secondEvent.eventContext().eventHistory.getFirst().id()).isEqualTo(firstEvent.id()); + assertThat(secondEvent.eventContext().eventHistory.getFirst().eventType()).isEqualTo(MyNumberEvent.class); + assertThat(secondEvent.text1).isEqualTo("11"); + assertThat(secondEvent.text2).isEqualTo("22"); + + // assert that the context of the bus is cleared + assertThat(textBus.getEventContextHolder().isEmpty()).isTrue(); + assertThat(numberBus.getEventContextHolder().isEmpty()).isTrue(); + } + + // ----------------------------------------------------------------------------------------------------------- + + @Test + void testSubscriberReceivesMultipleEvents() { + // arrange + int numEventsToPublish = 100; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + internal_testSubscriberReceivesMultipleEvents(numEventsToPublish, sut); + } + + @Test + void testSubscriberReceivesMultipleEventsSpy() { + // arrange + int numEventsToPublish = 100; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + MyNumberEvent myEvent = internal_testSubscriberReceivesMultipleEvents(numEventsToPublish, sut); + + // ... and assert spy functionality + InMemorySyncEventBusSpy spy = (InMemorySyncEventBusSpy) sut; + List allEvents = spy.allEvents(); + assertThat(allEvents.getLast()).isEqualTo(myEvent); + assertThat(spy.size()).isEqualTo(numEventsToPublish); + } + + private static MyNumberEvent internal_testSubscriberReceivesMultipleEvents( + final int numEventsToPublish, final EventBus sut) { + + MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(true); + MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut); + + // act + sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW); + MyNumberEvent myEvent = null; + for (int counter = 0; counter < numEventsToPublish; counter++) { + myEvent = new MyNumberEvent(counter, counter); + myPublisher.post(MyNumberEvent.class, myEvent); + } + + // assert + assertThat(mySubscriber.lastReceivedEvent).isEqualTo(myEvent); + assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish); + return myEvent; + } + + // ----------------------------------------------------------------------------------------------------------- + + @Test + void testSubscriberDoesNotReceiveOtherEventTypes() { + // arrange + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + EventBus bus1 = factory.createOrGetEventBusFor(MyNumberEvent.class); + EventBus bus2 = factory.createOrGetEventBusFor(MyMixedEvent.class); + + internal_testSubscriberDoesNotReceiveOtherEventTypes(bus1, bus2); + } + + @Test + void testSubscriberDoesNotReceiveOtherEventTypesSpy() { + // arrange + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus bus1 = factory.createOrGetEventBusFor(MyNumberEvent.class); + EventBus bus2 = factory.createOrGetEventBusFor(MyMixedEvent.class); + + MyMixedEvent myEvent = internal_testSubscriberDoesNotReceiveOtherEventTypes(bus1, bus2); + + // ... and assert spy functionality + InMemorySyncEventBusSpy spy1 = (InMemorySyncEventBusSpy) bus1; + InMemorySyncEventBusSpy spy2 = (InMemorySyncEventBusSpy) bus2; + assertThat(spy1.allEvents().isEmpty()).isTrue(); + assertThat(spy1.size()).isEqualTo(0); + assertThat(spy2.allEvents()).isEqualTo(List.of(myEvent)); + assertThat(spy2.size()).isEqualTo(1); + } + + private static MyMixedEvent internal_testSubscriberDoesNotReceiveOtherEventTypes( + final EventBus bus1, final EventBus bus2) { + + MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(); + MyMixedEventPublisher myPublisher = new MyMixedEventPublisher(bus2); + + // act + bus1.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW); + MyMixedEvent myEvent = new MyMixedEvent("abc", 22); + myPublisher.post(MyMixedEvent.class, myEvent); + + // assert + assertThat(mySubscriber.lastReceivedEvent).isNull(); + assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(0); + return myEvent; + } + + // ----------------------------------------------------------------------------------------------------------- + + @Test + void testSubscriberReceivedEventsOnlyAfterSubscribing() { + // arrange + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + MyNumberEvent firstEvent = new MyNumberEvent(11, 22); + MyNumberEvent secondEvent = new MyNumberEvent(12, 23); + + internal_testSubscriberReceivedEventsOnlyAfterSubscribing(sut, firstEvent, secondEvent); + } + + @Test + void testSubscriberReceivedEventsOnlyAfterSubscribingSpy() { + // arrange + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + MyNumberEvent firstEvent = new MyNumberEvent(11, 22); + MyNumberEvent secondEvent = new MyNumberEvent(12, 23); + + internal_testSubscriberReceivedEventsOnlyAfterSubscribing(sut, firstEvent, secondEvent); + + // ... and assert spy functionality + InMemorySyncEventBusSpy spy = (InMemorySyncEventBusSpy) sut; + assertThat(spy.allEvents()).isEqualTo(List.of(firstEvent, secondEvent)); + } + + private static void internal_testSubscriberReceivedEventsOnlyAfterSubscribing( + final EventBus sut, + final MyNumberEvent firstEvent, final MyNumberEvent secondEvent) { + MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(); + MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut); + + // act + myPublisher.post(MyNumberEvent.class, firstEvent); // published but not received + + sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW); + + myPublisher.post(MyNumberEvent.class, secondEvent); // published and received + + // assert + assertThat(mySubscriber.lastReceivedEvent).isEqualTo(secondEvent); + assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(1); + } + + // ----------------------------------------------------------------------------------------------------------- + + @Test + void testSubscriberReceivesNoMoreEventsAfterUnsubscribing() { + // arrange + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + MyNumberEvent firstEvent = new MyNumberEvent(11, 22); + MyNumberEvent secondEvent = new MyNumberEvent(12, 23); + + internal_testSubscriberReceivesNoMoreEventsAfterUnsubscribing(sut, firstEvent, secondEvent); + } + + @Test + void testSubscriberReceivesNoMoreEventsAfterUnsubscribingSpy() { + // arrange + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + MyNumberEvent firstEvent = new MyNumberEvent(11, 22); + MyNumberEvent secondEvent = new MyNumberEvent(12, 23); + + internal_testSubscriberReceivesNoMoreEventsAfterUnsubscribing(sut, firstEvent, secondEvent); + + // ... and assert spy functionality + InMemorySyncEventBusSpy spy = (InMemorySyncEventBusSpy) sut; + assertThat(spy.allEvents()).isEqualTo(List.of(firstEvent, secondEvent)); + } + + private static void internal_testSubscriberReceivesNoMoreEventsAfterUnsubscribing( + final EventBus sut, + final MyNumberEvent firstEvent, final MyNumberEvent secondEvent) { + MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(); + MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut); + + // act + sut.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW); + myPublisher.post(MyNumberEvent.class, firstEvent); // published but not received + assertThat(mySubscriber.lastReceivedEvent).isEqualTo(firstEvent); + sut.unsubscribe(MyNumberEvent.class, mySubscriber); + myPublisher.post(MyNumberEvent.class, secondEvent); // published and received + + // assert + assertThat(mySubscriber.lastReceivedEvent).isEqualTo(firstEvent); + assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(1); + } + + // ----------------------------------------------------------------------------------------------------------- + + @Test + void testMultipleSubscribersInSeparateGroupsAllReceiveTheSameEvents() { + // arrange + int numSubscribers = 5; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + List events = List.of(new MyNumberEvent(11, 22), new MyNumberEvent(12, 23), + new MyNumberEvent(13, 24), new MyNumberEvent(14, 25)); + + internal_testListOfSubscribersAllReceiveTheSameEvents(numSubscribers, sut, events.toArray(new MyNumberEvent[0])); + } + + @Test + void testMultipleSubscribersInSeparateGroupsAllReceiveTheSameEventsSpy() { + // arrange + int numSubscribers = 5; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus sut = factory.createOrGetEventBusFor(MyNumberEvent.class); + + List events = List.of(new MyNumberEvent(11, 22), new MyNumberEvent(12, 23), + new MyNumberEvent(13, 24), new MyNumberEvent(14, 25)); + + internal_testListOfSubscribersAllReceiveTheSameEvents(numSubscribers, sut, events.toArray(new MyNumberEvent[0])); + + // ... and assert spy functionality + InMemorySyncEventBusSpy spy = (InMemorySyncEventBusSpy) sut; + assertThat(spy.allEvents()).isEqualTo(events); + } + + private static void internal_testListOfSubscribersAllReceiveTheSameEvents( + final int numSubscribers, final EventBus sut, + final MyNumberEvent... events) { + + List mySubscribers = new ArrayList<>(); + for (int counter = 0; counter < numSubscribers; counter++) { + MyEventSubscriberEachInstanceInItsOwnGroup mySubscriberEachInItsOwnGroup = new MyEventSubscriberEachInstanceInItsOwnGroup(true); + mySubscribers.add(mySubscriberEachInItsOwnGroup); + sut.subscribe(MyNumberEvent.class, mySubscriberEachInItsOwnGroup, START_READING_FROM_NOW); + } + + MyNumberEventPublisher myPublisher = new MyNumberEventPublisher(sut); + + for (MyNumberEvent event: events) { + // act - first event + myPublisher.post(MyNumberEvent.class, event); + + // assert + for (MyNumberEventSubscriber mySubscriber : mySubscribers) { + assertThat(mySubscriber.lastReceivedEvent).isEqualTo(event); + } + } + } + + // ----------------------------------------------------------------------------------------------------------- + + @Test + void testMultipleSubscribersReceiveMultipleEventsForMultipleTypes() { + // arrange + int numEventsToPublish = 9_999; + int numSubscribersPerType = 1_999; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusFactory(); + EventBus numberEventBus = factory.createOrGetEventBusFor(MyNumberEvent.class); + EventBus mixedEventBus = factory.createOrGetEventBusFor(MyMixedEvent.class); + + // create subscribers + internal_testMultipleSubscribersReceiveMultipleEventsForMultipleTypes(numEventsToPublish, numSubscribersPerType, + numberEventBus, mixedEventBus); + } + + @Test + void testMultipleSubscribersReceiveMultipleEventsForMultipleTypesSpy() { + // arrange + int numEventsToPublish = 9_999; + int numSubscribersPerType = 1_999; + + InMemorySyncEventBusFactory factory = new InMemorySyncEventBusSpyFactory(); + EventBus numberEventBus = factory.createOrGetEventBusFor(MyNumberEvent.class); + EventBus mixedEventBus = factory.createOrGetEventBusFor(MyMixedEvent.class); + + internal_testMultipleSubscribersReceiveMultipleEventsForMultipleTypes(numEventsToPublish, numSubscribersPerType, + numberEventBus, mixedEventBus); + + // ... and assert spy functionality + InMemorySyncEventBusSpy numberEventBusSpy = (InMemorySyncEventBusSpy) numberEventBus; + InMemorySyncEventBusSpy mixedEventBusSpy = (InMemorySyncEventBusSpy) mixedEventBus; + assertThat(numberEventBusSpy.size()).isEqualTo(numEventsToPublish); + assertThat(mixedEventBusSpy.size()).isEqualTo(numEventsToPublish); + } + + private static void internal_testMultipleSubscribersReceiveMultipleEventsForMultipleTypes( + final int numEventsToPublish, final int numSubscribersPerType, + final EventBus numberEventBus, final EventBus mixedEventBus) { + + // LocalDateTime start = LocalDateTime.now(); + + Random random = new Random(); + + // create subscribers, all instances in the same group + List myNumberEventSubscribers = new ArrayList<>(); + List myMixedEventSubscribers = new ArrayList<>(); + + // create subscribers, all instances in the separate groups (each in one group) + List mySubscribersEachInItsOwnGroup = new ArrayList<>(); + + for (int counter = 0; counter < numSubscribersPerType; counter++) { + MyNumberEventSubscriber mySubscriber = new MyNumberEventSubscriber(); + myNumberEventSubscribers.add(mySubscriber); + numberEventBus.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW); + } + for (int counter = 0; counter < numSubscribersPerType; counter++) { + MyMixedEventSubscriber mySubscriber = new MyMixedEventSubscriber(); + myMixedEventSubscribers.add(mySubscriber); + mixedEventBus.subscribe(MyMixedEvent.class, mySubscriber, START_READING_FROM_NOW); + } + for (int counter = 0; counter < numSubscribersPerType; counter++) { + MyEventSubscriberEachInstanceInItsOwnGroup mySubscriber = new MyEventSubscriberEachInstanceInItsOwnGroup(); + mySubscribersEachInItsOwnGroup.add(mySubscriber); + numberEventBus.subscribe(MyNumberEvent.class, mySubscriber, START_READING_FROM_NOW); + } + + // create publisher and post events + MyNumberEventPublisher myNumberEventPublisher = new MyNumberEventPublisher(numberEventBus); + MyMixedEventPublisher myMixedEventPublisher = new MyMixedEventPublisher(mixedEventBus); + + MyNumberEvent lastNumberEvent = null; + for (int eventCounter = 0; eventCounter < numEventsToPublish; eventCounter++) { + lastNumberEvent = new MyNumberEvent(random.nextInt(), random.nextInt()); + myNumberEventPublisher.post(MyNumberEvent.class, lastNumberEvent); + } + MyMixedEvent lastMixedEvent = null; + for (int eventCounter = 0; eventCounter < numEventsToPublish; eventCounter++) { + lastMixedEvent = new MyMixedEvent("" + random.nextInt(), random.nextInt()); + myMixedEventPublisher.post(MyMixedEvent.class, lastMixedEvent); + } + + // assert + List lastNumberReceivedEvents = new ArrayList<>(); + int counterOfRetrievedNumberEvents = 0; + for (MyNumberEventSubscriber mySubscriber : myNumberEventSubscribers) { + lastNumberReceivedEvents.add(mySubscriber.lastReceivedEvent); + counterOfRetrievedNumberEvents += mySubscriber.counterOfRetrievedEvents; + } + assertThat(lastNumberReceivedEvents.contains(lastNumberEvent)).isTrue(); + assertThat(counterOfRetrievedNumberEvents).isEqualTo(numEventsToPublish); + + List lastMixedReceivedEvents = new ArrayList<>(); + int counterOfRetrievedMixedEvents = 0; + for (MyMixedEventSubscriber mySubscriber : myMixedEventSubscribers) { + lastMixedReceivedEvents.add(mySubscriber.lastReceivedEvent); + counterOfRetrievedMixedEvents += mySubscriber.counterOfRetrievedEvents; + } + assertThat(lastMixedReceivedEvents.contains(lastMixedEvent)).isTrue(); + assertThat(counterOfRetrievedMixedEvents).isEqualTo(numEventsToPublish); + + // assert + for (MyEventSubscriberEachInstanceInItsOwnGroup mySubscriber : mySubscribersEachInItsOwnGroup) { + assertThat(mySubscriber.lastReceivedEvent).isEqualTo(lastNumberEvent); + assertThat(mySubscriber.counterOfRetrievedEvents).isEqualTo(numEventsToPublish); + } + + // LocalDateTime end = LocalDateTime.now(); + // System.out.println("test duration was " + (Duration.between(start,end).toMillis()) + " ms"); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/PublisherAndSubscriberTestdata.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/PublisherAndSubscriberTestdata.java new file mode 100644 index 0000000..8e434db --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/api/eventbus/PublisherAndSubscriberTestdata.java @@ -0,0 +1,398 @@ +package de.accso.flexinale.common.api.eventbus; + +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.api.event.EventTestdata.MyMixedEvent; +import de.accso.flexinale.common.api.event.EventTestdata.MyNumberEvent; +import de.accso.flexinale.common.api.event.EventTestdata.MyTextEvent; +import de.accso.flexinale.common.shared_kernel.FlexinaleIllegalStateException; + +import static de.accso.flexinale.common.shared_kernel.Identifiable.Id.uuidString; + +@SuppressWarnings("unused") +public final class PublisherAndSubscriberTestdata { + public static class MyNumberEventSubscriber implements EventSubscriber { + MyNumberEvent lastReceivedEvent = null; + int counterOfRetrievedEvents = 0; + + final boolean checkForAscendingNumbersInEvents; + + MyNumberEventSubscriber() { + this(false); + } + + MyNumberEventSubscriber(boolean checkForAscendingNumbersInEvents) { + this.checkForAscendingNumbersInEvents = checkForAscendingNumbersInEvents; + } + + @Override + public String getName() { + return MyNumberEventSubscriber.class.getName(); + } + + @Override + public String getGroupName() { + return getName(); + } + + @Override + public void receive(final MyNumberEvent event) { + if (checkForAscendingNumbersInEvents && lastReceivedEvent != null) { + if ( this.lastReceivedEvent.value1 >= event.value1 + || this.lastReceivedEvent.value2 >= event.value2) { + throw new FlexinaleIllegalStateException( + ("no ascending value when receiving next MyNumberEvent " + + "(last received event = %s, next received event = %s") + .formatted(lastReceivedEvent, event)); + } + } + this.lastReceivedEvent = event; + counterOfRetrievedEvents++; + } + } + + public static class MyEventSubscriberEachInstanceInItsOwnGroup extends MyNumberEventSubscriber { + // not static, as otherwise all instances of MyDifferentEventSubscriber would be in the same group, + // but we want them in different groups + private final String RANDOM_SUFFIX = uuidString(); + + MyEventSubscriberEachInstanceInItsOwnGroup() { + this(false); + } + + MyEventSubscriberEachInstanceInItsOwnGroup(boolean checkForAscendingNumbersInEvents) { + super(checkForAscendingNumbersInEvents); + } + + @Override + public String getGroupName() { + // don't use anything random here, as called in each subscribe() method (so group name would change) + return getName() + RANDOM_SUFFIX; + } + } + + @SuppressWarnings("unused") + public static final class MyTextEventSubscriber implements EventSubscriber { + MyTextEvent lastReceivedEvent = null; + int counterOfRetrievedEvents = 0; + + @Override + public String getName() { + return MyTextEventSubscriber.class.getName(); + } + + @Override + public String getGroupName() { + return getName(); + } + + @Override + public void receive(final MyTextEvent event) { + this.lastReceivedEvent = event; + counterOfRetrievedEvents++; + } + } + + // ----------------------------------------------------------------------------------------------------------- + + @SuppressWarnings("unused") + public static final class MyNumberEventThrowingRuntimeExceptionSubscriber + implements EventSubscriber.EventSubscriber2 { + MyNumberEvent lastReceivedEvent = null; + int counterOfRetrievedEvents = 0; + + @Override + public String getName() { + return MyNumberEventThrowingRuntimeExceptionSubscriber.class.getName(); + } + + @Override + public String getGroupName() { + return getName(); + } + + @Override + public void receive(final MyTextEvent event) { + // do nothing + } + + @Override + public void receive2(final MyNumberEvent event) { + this.lastReceivedEvent = event; + counterOfRetrievedEvents++; + throw new MyNumberEventRuntimeException("RuntimeException for event with Id=" + event.id()); + } + } + + public static class MyNumberEventRuntimeException extends RuntimeException { + public MyNumberEventRuntimeException(String message) { + super(message); + } + } + + // ----------------------------------------------------------------------------------------------------------- + + @SuppressWarnings("unused") + public static final class MyMixedEventSubscriber implements EventSubscriber { + MyMixedEvent lastReceivedEvent = null; + int counterOfRetrievedEvents = 0; + + @Override + public String getName() { + return MyMixedEventSubscriber.class.getName(); + } + + @Override + public String getGroupName() { + return getName(); + } + + @Override + public void receive(final MyMixedEvent event) { + this.lastReceivedEvent = event; + counterOfRetrievedEvents++; + } + } + + @SuppressWarnings("unused") + public static final class MyNumberAndMixedEventSubscriber implements + EventSubscriber.EventSubscriber2 { + MyNumberEvent lastReceivedNumberEvent = null; + MyMixedEvent lastReceivedMixedEvent = null; + int counterOfRetrievedEvents = 0; + + @Override + public String getName() { + return MyNumberAndMixedEventSubscriber.class.getName(); + } + + @Override + public String getGroupName() { + return getName(); + } + + @Override + public void receive(final MyNumberEvent event) { + this.lastReceivedNumberEvent = event; + counterOfRetrievedEvents++; + } + + @Override + public void receive2(final MyMixedEvent event) { + this.lastReceivedMixedEvent = event; + counterOfRetrievedEvents++; + } + } + + @SuppressWarnings("unused") + public static final class MyNumberAndTextAndMixedEventSubscriber implements + EventSubscriber.EventSubscriber3 { + MyNumberEvent lastReceivedNumberEvent = null; + MyTextEvent lastReceivedTextEvent = null; + MyMixedEvent lastReceivedMixedEvent = null; + int counterOfRetrievedEvents = 0; + + @Override + public String getName() { + return MyNumberAndTextAndMixedEventSubscriber.class.getName(); + } + + @Override + public String getGroupName() { + return getName(); + } + + @Override + public void receive(final MyNumberEvent event) { + this.lastReceivedNumberEvent = event; + counterOfRetrievedEvents++; + } + + @Override + public void receive2(final MyTextEvent event) { + this.lastReceivedTextEvent = event; + counterOfRetrievedEvents++; + } + + @Override + public void receive3(final MyMixedEvent event) { + this.lastReceivedMixedEvent = event; + counterOfRetrievedEvents++; + } + } + + @SuppressWarnings("unused") + public static final class GenericEventSubscriber implements EventSubscriber { + Event lastReceivedEvent = null; + int counterOfRetrievedEvents = 0; + + @Override + public String getName() { + return GenericEventSubscriber.class.getName(); + } + + @Override + public String getGroupName() { + return getName(); + } + + @Override + public void receive(final Event event) { + this.lastReceivedEvent = event; + counterOfRetrievedEvents++; + } + } + + // does not work, see expected exception in test + @SuppressWarnings("unused") + public static final class GenericEventSubscriberWithExtraSubscription + implements EventSubscriber.EventSubscriber2 { // Event has to be the first generic type otherwise does not compile + Event lastReceivedEvent = null; + int counterOfRetrievedEvents = 0; + + @Override + public String getName() { + return GenericEventSubscriberWithExtraSubscription.class.getName(); + } + + @Override + public String getGroupName() { + return getName(); + } + + @Override + public void receive(final Event event) { + this.lastReceivedEvent = event; + counterOfRetrievedEvents++; + } + + @Override + public void receive2(final MyTextEvent event) { + this.lastReceivedEvent = event; + counterOfRetrievedEvents++; + } + } + + // ----------------------------------------------------------------------------------------------------------- + + public static final class MyNumberEventPublisher implements EventPublisher { + private final EventBus bus; + + public MyNumberEventPublisher(EventBus bus) { + this.bus = bus; + } + + @Override + public String getName() { + return MyNumberEventPublisher.class.getName(); + } + + @Override + public void post(final Class eventType, final MyNumberEvent event) { + bus.publish(MyNumberEvent.class, event); + } + } + + public static final class MyTextEventPublisher implements EventPublisher { + private final EventBus bus; + + public MyTextEventPublisher(EventBus bus) { + this.bus = bus; + } + + @Override + public String getName() { + return MyTextEventPublisher.class.getName(); + } + + @Override + public void post(final Class eventType, final MyTextEvent event) { + bus.publish(MyTextEvent.class, event); + } + } + + public static final class MyMixedEventPublisher implements EventPublisher { + private final EventBus bus; + + MyMixedEventPublisher(EventBus bus) { + this.bus = bus; + } + + @Override + public String getName() { + return MyMixedEventPublisher.class.getName(); + } + + @Override + public void post(final Class eventType, final MyMixedEvent event) { + bus.publish(MyMixedEvent.class, event); + } + } + + public static final class MyNumberAndMixedEventPublisher implements + EventPublisher.EventPublisher2 { + private final EventBus numberBus; + private final EventBus mixedBus; + + public MyNumberAndMixedEventPublisher(final EventBus numberBus, + final EventBus mixedBus) { + this.numberBus = numberBus; + this.mixedBus = mixedBus; + } + + @Override + public String getName() { + return MyNumberAndMixedEventPublisher.class.getName(); + } + + @Override + public void post(final Class eventType, final MyNumberEvent event) { + numberBus.publish(MyNumberEvent.class, event); + } + + @Override + public void post2(final Class eventType, final MyMixedEvent event) { + mixedBus.publish(MyMixedEvent.class, event); + } + } + + // ----------------------------------------------------------------------------------------------------------- + + @SuppressWarnings("unused") + public static final class MyNumberEventSubscriberAndMyTextEventPublisher implements + EventSubscriber, + EventPublisher { + + private final EventBus textBus; + + public MyNumberEventSubscriberAndMyTextEventPublisher(final EventBus textBus) { + this.textBus = textBus; + } + + MyNumberEvent lastReceivedEvent = null; + MyTextEvent lastSentEvent = null; + + @Override + public String getName() { + return MyNumberEventSubscriberAndMyTextEventPublisher.class.getName(); + } + + @Override + public String getGroupName() { + return getName(); + } + + @Override + public void receive(final MyNumberEvent event) { + this.lastReceivedEvent = event; + + // create and post new Event using the received event as the predecessor, implicitely using propagating its correlationId + this.lastSentEvent = new MyTextEvent(event, ""+event.value1, ""+ event.value2); + post(MyTextEvent.class, lastSentEvent); + } + + @Override + public void post(final Class eventType, final MyTextEvent event) { + textBus.publish(MyTextEvent.class, event); + } + } +} diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/application/caching/InMemoryCacheTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/application/caching/InMemoryCacheTest.java new file mode 100644 index 0000000..17173ca --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/application/caching/InMemoryCacheTest.java @@ -0,0 +1,293 @@ +package de.accso.flexinale.common.application.caching; + +import de.accso.flexinale.common.application.caching.InMemoryCache; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.junit.jupiter.api.Test; + +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class InMemoryCacheTest { + + private record Data(String text, int number) { } + + @Test + void testEmptyCacheContainsNothing() { + // arrange + Random random = new Random(); + + // act + InMemoryCache sut = new InMemoryCache<>(); + + // assert + assertThat(sut.size()).isEqualTo(0); + + Data data = new Data("" + random.nextInt(), random.nextInt()); + assertThat(sut.contains(data)).isFalse(); + + assertThat(sut.keys().isEmpty()).isTrue(); + assertThat(sut.values().isEmpty()).isTrue(); + assertThat(sut.entrySet().isEmpty()).isTrue(); + } + + @Test + void testGet() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + Identifiable.Id id = Identifiable.Id.of(); + Data data = new Data("text", 42); + sut.put(id, data); + + // act + Data retrievedData = sut.get(id); + + // assert + assertThat(retrievedData).isEqualTo(data); + } + + @Test + void testGetForNotPuttedId() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + Identifiable.Id id = Identifiable.Id.of(); + Data data = new Data("text", 42); + sut.put(id, data); + + // act + Identifiable.Id newId = Identifiable.Id.of(); + Data retrievedData = sut.get(newId); + + // assert + assertThat(retrievedData).isNull(); + } + + @Test + void testPutOnce() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + Identifiable.Id id = Identifiable.Id.of(); + Data data = new Data("text", 42); + + // act + Data oldData = sut.put(id, data); + + // assert + assertThat(oldData).isNull(); + } + + @Test + void testPutTwice() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + Identifiable.Id id = Identifiable.Id.of(); + Data data1 = new Data("text1", 42); + Data data2 = new Data("text2", 21); + + // act + Data oldData0 = sut.put(id, data1); + Data oldData1 = sut.put(id, data2); + + // assert + assertThat(oldData0).isNull(); + assertThat(oldData1).isEqualTo(data1); + } + + @Test + void testRemoveExistingData() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + Identifiable.Id id = Identifiable.Id.of(); + Data data = new Data("text", 42); + + // act + Data oldData0 = sut.put(id, data); + Data removedData = sut.remove(id); + Data notFoundData = sut.get(id); + + // assert + assertThat(oldData0).isNull(); + assertThat(removedData).isEqualTo(data); + assertThat(notFoundData).isNull(); + } + + @Test + void testRemoveNotExistingDataDoesNothing() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + Identifiable.Id id = Identifiable.Id.of(); + + // act + Data removedData = sut.remove(id); + Data notFoundData = sut.get(id); + + // assert + assertThat(removedData).isNull(); + assertThat(notFoundData).isNull(); + } + + @Test + void testKeys() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + Identifiable.Id id1 = Identifiable.Id.of(); + Data data1 = new Data("text", 42); + Identifiable.Id id2 = Identifiable.Id.of(); + Data data2 = new Data("abc", 21); + + sut.put(id1, data1); + sut.put(id2, data2); + + // act + Set retrievedKeys = sut.keys(); + + // assert + assertThat(retrievedKeys).isEqualTo(Set.of(id1, id2)); + } + + @Test + void testValues() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + Identifiable.Id id1 = Identifiable.Id.of(); + Data data1 = new Data("text", 42); + Identifiable.Id id2 = Identifiable.Id.of(); + Data data2 = new Data("abc", 21); + + sut.put(id1, data1); + sut.put(id2, data2); + + // act + Set retrievedKeys = sut.values(); + + // assert + assertThat(retrievedKeys).isEqualTo(Set.of(data1, data2)); + } + + @Test + void testEntrySet() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + Identifiable.Id id1 = Identifiable.Id.of(); + Data data1 = new Data("text", 42); + Identifiable.Id id2 = Identifiable.Id.of(); + Data data2 = new Data("abc", 21); + + sut.put(id1, data1); + sut.put(id2, data2); + + // act + Set> retrievedEntries = sut.entrySet(); + + // assert + assertThat(retrievedEntries.size()).isEqualTo(2); + Set retrievedKeys = retrievedEntries.stream() + .map(Map.Entry::getKey).collect(Collectors.toUnmodifiableSet()); + Set retrievedValues = retrievedEntries.stream() + .map(Map.Entry::getValue).collect(Collectors.toSet()); + + assertThat(retrievedKeys).isEqualTo(Set.of(id1, id2)); + assertThat(retrievedValues).isEqualTo(Set.of(data1, data2)); + } + + @Test + void testSize() { + // arrange + Random random = new Random(); + + InMemoryCache sut = new InMemoryCache<>(); + + for (int counter=0; counter<100; counter++) { + Identifiable.Id id = Identifiable.Id.of(); + Data data = new Data("" + random.nextInt(), random.nextInt()); + sut.put(id, data); + } + + // act + int expectedSize = sut.size(); + + // assert + assertThat(expectedSize).isEqualTo(100); + } + + @Test + void testSizeDoesNotChangeAfterPutWithSameId() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + + Identifiable.Id id = Identifiable.Id.of(); + Data data = new Data("text", 42); + sut.put(id, data); + int size = sut.size(); + assertThat(size).isEqualTo(1); + + // act + sut.put(id, data); + int expectedSize = sut.size(); + + // assert + assertThat(expectedSize).isEqualTo(size); + } + + @Test + void testIsEmptyInitially() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + + // act + boolean expectedEmpty = sut.isEmpty(); + + // assert + assertThat(expectedEmpty).isTrue(); + } + + @Test + void testIsNotEmptyAfterPuttingData() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + Identifiable.Id id = Identifiable.Id.of(); + Data data = new Data("text", 42); + sut.put(id, data); + + // act + boolean expectedEmpty = sut.isEmpty(); + + // assert + assertThat(expectedEmpty).isFalse(); + } + + @Test + void testContains() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + Identifiable.Id id = Identifiable.Id.of(); + Data data = new Data("text", 42); + sut.put(id, data); + + // act + boolean expectedContains = sut.contains(data); + + // assert + assertThat(expectedContains).isTrue(); + } + + @Test + void testContainsForNotPuttedId() { + // arrange + InMemoryCache sut = new InMemoryCache<>(); + Identifiable.Id id = Identifiable.Id.of(); + Data data = new Data("text", 42); + sut.put(id, data); + + // act + Data newData = new Data("def", 21); + boolean expectedContains = sut.contains(newData); + + // assert + assertThat(expectedContains).isFalse(); + } + +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/infrastructure/eventbus/EventSerializationHelperTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/infrastructure/eventbus/EventSerializationHelperTest.java new file mode 100644 index 0000000..06acdfa --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/infrastructure/eventbus/EventSerializationHelperTest.java @@ -0,0 +1,167 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import com.fasterxml.jackson.core.JsonProcessingException; +import de.accso.flexinale.common.api.event.AbstractEvent; +import de.accso.flexinale.common.api.event.Event; +import de.accso.flexinale.common.api.event.EventContext; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import de.accso.flexinale.common.shared_kernel.Versionable; +import org.apache.commons.lang3.builder.EqualsBuilder; +import org.apache.commons.lang3.builder.HashCodeBuilder; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +import static org.assertj.core.api.Assertions.assertThat; + +class EventSerializationHelperTest { + + @Test + void testSerializeEventClass2JsonString() throws JsonProcessingException { + // arrange + MyNumberEventImplementedAsSubclassOfAbstractEvent myNumberEvent = + new MyNumberEventImplementedAsSubclassOfAbstractEvent(21, 42); + + // act + String jsonString = EventSerializationHelper.serializeEvent2JsonString(myNumberEvent); + + // assert + String expectedJsonString = + ("{\"id\":{\"id\":\"%s\"}," + + "\"correlationId\":{\"id\":\"%s\"}," + + "\"timestamp\":\"%s\"," + + "\"eventContext\":{\"correlationId\":{\"id\":\"%s\"}," + + "\"eventHistory\":[{\"id\":{\"id\":\"%s\"}," + + "\"eventType\":\"%s\"}]}," + + "\"value1\":21,\"value2\":42,\"version\":{\"version\":0}}") + .formatted(myNumberEvent.id().id(), + myNumberEvent.correlationId().id(), + timestamp2String(myNumberEvent.timestamp()), + myNumberEvent.correlationId().id(), + myNumberEvent.id().id(), + MyNumberEventImplementedAsSubclassOfAbstractEvent.class.getName()); + + assertThat(jsonString).isEqualTo(expectedJsonString); + } + + @Test + void testSerializeEventRecord2JsonString() throws JsonProcessingException { + // arrange + MyNumberEventImplementedAsRecord myNumberEvent = + new MyNumberEventImplementedAsRecord( + Identifiable.Id.of(), Versionable.initialVersion(), LocalDateTime.now(), Identifiable.Id.of(), null, + 21, 42); + + // act + String jsonString = EventSerializationHelper.serializeEvent2JsonString(myNumberEvent); + + // assert + String expectedJsonString = ("{\"id\":{\"id\":\"%s\"}," + + "\"version\":{\"version\":0}," + + "\"timestamp\":\"%s\"," + + "\"correlationId\":{\"id\":\"%s\"}," + + "\"eventContext\":null,\"value1\":21,\"value2\":42}") + .formatted(myNumberEvent.id().id(), + timestamp2String(myNumberEvent.timestamp()), + myNumberEvent.correlationId().id()); + + assertThat(jsonString).isEqualTo(expectedJsonString); + } + + @Test + void testDeserializeJsonString2EventClass() throws JsonProcessingException { + // arrange + MyNumberEventImplementedAsSubclassOfAbstractEvent myNumberEvent = + new MyNumberEventImplementedAsSubclassOfAbstractEvent(21, 42); + String jsonString = EventSerializationHelper.serializeEvent2JsonString(myNumberEvent); + + // act + MyNumberEventImplementedAsSubclassOfAbstractEvent deserializedEvent = + EventSerializationHelper.deserializeJsonString2Event( + jsonString, MyNumberEventImplementedAsSubclassOfAbstractEvent.class); + + // assert + assertThat(deserializedEvent).isEqualTo(myNumberEvent); + } + + @Test + void testDeserializeJsonString2EventRecord() throws JsonProcessingException { + // arrange + MyNumberEventImplementedAsRecord myNumberEvent = + new MyNumberEventImplementedAsRecord( + Identifiable.Id.of(), Versionable.initialVersion(), LocalDateTime.now(), Identifiable.Id.of(), null, + 21, 42); + String jsonString = EventSerializationHelper.serializeEvent2JsonString(myNumberEvent); + + // act + MyNumberEventImplementedAsRecord deserializedEvent = + EventSerializationHelper.deserializeJsonString2Event( + jsonString, MyNumberEventImplementedAsRecord.class); + + // assert + assertThat(deserializedEvent).isEqualTo(myNumberEvent); + } + + private static String timestamp2String(final LocalDateTime timestamp) { + return timestamp.format(DateTimeFormatter.ISO_DATE_TIME); + } + +} + +final class MyNumberEventImplementedAsSubclassOfAbstractEvent extends AbstractEvent { + public Integer value1; + public Integer value2; + + private final Version version = Versionable.initialVersion(); + + // needed for Jackson de/serialization + private MyNumberEventImplementedAsSubclassOfAbstractEvent() {} + + public MyNumberEventImplementedAsSubclassOfAbstractEvent(final Integer value1, final Integer value2) { + this.value1 = value1; + this.value2 = value2; + } + + public MyNumberEventImplementedAsSubclassOfAbstractEvent(final Event predecessorEvent, final Integer value1, final Integer value2) { + super(predecessorEvent); + this.value1 = value1; + this.value2 = value2; + } + + @Override + public boolean equals(final Object o) { + if (this == o) {return true;} + + if (o == null || getClass() != o.getClass()) {return false;} + + final MyNumberEventImplementedAsSubclassOfAbstractEvent that = (MyNumberEventImplementedAsSubclassOfAbstractEvent) o; + + return new EqualsBuilder() + .appendSuper(super.equals(o)) + .append(this.version(), that.version()) + .append(value1, that.value1) + .append(value2, that.value2) + .isEquals(); + } + + @Override + public int hashCode() { + return new HashCodeBuilder(17, 37) + .appendSuper(super.hashCode()) + .append(this.version()) + .append(value1).append(value2) + .toHashCode(); + } + + @Override + public Version version() { + return version; + } +} + + +@SuppressWarnings("ALL") +record MyNumberEventImplementedAsRecord( + Id id, Version version, LocalDateTime timestamp, Id correlationId, EventContext eventContext, + Integer value1, Integer value2) implements Event { } \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventContextHolderTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventContextHolderTest.java new file mode 100644 index 0000000..ad22ab4 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/infrastructure/eventbus/InMemorySyncEventContextHolderTest.java @@ -0,0 +1,105 @@ +package de.accso.flexinale.common.infrastructure.eventbus; + +import de.accso.flexinale.common.api.eventbus.EventContextHolder; +import de.accso.flexinale.common.api.event.EventContext; +import de.accso.flexinale.common.shared_kernel.DeveloperMistakeException; +import de.accso.flexinale.common.shared_kernel.Identifiable; +import org.junit.jupiter.api.Test; + +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class InMemorySyncEventContextHolderTest { + + @Test + void testInitialHolderDoesNotContainAnything() { + // arrange + EventContextHolder sut = new InMemorySyncEventContextHolder(); + + // act + EventContext eventContext = sut.get(); + + // assert + assertThat(eventContext).isNull(); + } + + @Test + void testSetAndGetOnce() { + // arrange + EventContextHolder sut = new InMemorySyncEventContextHolder(); + EventContext newContext = new EventContext(Identifiable.Id.of("123"), Collections.emptyList()); + + // act + sut.set(newContext); + EventContext eventContext = sut.get(); + + // assert + assertThat(eventContext).isEqualTo(newContext); + + // act + sut.remove(); + eventContext = sut.get(); + + // assert + assertThat(eventContext).isNull(); + } + + @Test + void testSetAndGetTwice() { + // arrange + EventContextHolder sut = new InMemorySyncEventContextHolder(); + EventContext newContext = new EventContext(Identifiable.Id.of("123"), Collections.emptyList()); + + // act set twice + sut.set(newContext); + sut.set(newContext); + + // assert + assertThat(sut.get()).isEqualTo(newContext); + + // act remove first time + sut.remove(); + + // assert + assertThat(sut.get()).isEqualTo(newContext); + + // act second first time + sut.remove(); + + // assert + assertThat(sut.get()).isNull(); + } + + @Test + void testRemoveOnceToOftenIsAnError() { + // arrange + int maxSets = 10; + + EventContextHolder sut = new InMemorySyncEventContextHolder(); + EventContext newContext = new EventContext(Identifiable.Id.of("123"), Collections.emptyList()); + + // act + for (int i=0; i { + sut.remove(); + }).isInstanceOf(DeveloperMistakeException.class) + .hasMessageContaining("remove on InMemorySyncEventContextHolder once too much, should not happen"); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/ClazzHelperTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/ClazzHelperTest.java new file mode 100644 index 0000000..0766120 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/ClazzHelperTest.java @@ -0,0 +1,118 @@ +package de.accso.flexinale.common.shared_kernel; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ClazzHelperTest { + + interface MyInterface1 { } + interface MyInterface2 { } + + static class MyClazzImplementingTheInterface1 implements MyInterface1 { } + static class MyClazzImplementingTheInterface2 implements MyInterface2 { } + static class MyClazzImplementingTheInterfaces implements MyInterface1, MyInterface2 { } + static class MyClazzNotImplementingAnyInterface { } + + record MyRecordImplementingTheInterface1() implements MyInterface1 { } + record MyRecordImplementingTheInterface2() implements MyInterface2 { } + record MyRecordImplementingTheInterfaces() implements MyInterface1, MyInterface2 { } + record MyRecordNotImplementingAnyInterface() { } + + @Test + void testClazzImplementsInterface() { + boolean result; + + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyClazzImplementingTheInterface1.class, MyInterface1.class); + // assert + assertThat(result).isTrue(); + + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyClazzImplementingTheInterface2.class, MyInterface2.class); + // assert + assertThat(result).isTrue(); + + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyClazzImplementingTheInterfaces.class, MyInterface1.class); + // assert + assertThat(result).isTrue(); + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyClazzImplementingTheInterfaces.class, MyInterface2.class); + // assert + assertThat(result).isTrue(); + } + + @Test + void testClazzNotImplementsInterface() { + boolean result; + + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyClazzImplementingTheInterface1.class, MyInterface2.class); + // assert + assertThat(result).isFalse(); + + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyClazzImplementingTheInterface2.class, MyInterface1.class); + // assert + assertThat(result).isFalse(); + + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyClazzNotImplementingAnyInterface.class, MyInterface1.class); + // assert + assertThat(result).isFalse(); + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyClazzNotImplementingAnyInterface.class, MyInterface2.class); + // assert + assertThat(result).isFalse(); + } + + + @Test + void testRecordImplementsInterface() { + boolean result; + + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyRecordImplementingTheInterface1.class, MyInterface1.class); + // assert + assertThat(result).isTrue(); + + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyRecordImplementingTheInterface2.class, MyInterface2.class); + // assert + assertThat(result).isTrue(); + + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyRecordImplementingTheInterfaces.class, MyInterface1.class); + // assert + assertThat(result).isTrue(); + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyRecordImplementingTheInterfaces.class, MyInterface2.class); + // assert + assertThat(result).isTrue(); + } + + @Test + void testRecordNotImplementsInterface() { + boolean result; + + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyRecordImplementingTheInterface1.class, MyInterface2.class); + // assert + assertThat(result).isFalse(); + + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyRecordImplementingTheInterface2.class, MyInterface1.class); + // assert + assertThat(result).isFalse(); + + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyRecordNotImplementingAnyInterface.class, MyInterface1.class); + // assert + assertThat(result).isFalse(); + // arrange, act + result = ClazzHelper.clazzImplementsInterface(MyRecordNotImplementingAnyInterface.class, MyInterface2.class); + // assert + assertThat(result).isFalse(); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/EventClassHelperTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/EventClassHelperTest.java new file mode 100644 index 0000000..06064fc --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/EventClassHelperTest.java @@ -0,0 +1,142 @@ +package de.accso.flexinale.common.shared_kernel; + +import de.accso.flexinale.common.api.event.AbstractEvent; +import de.accso.flexinale.common.api.event.Event; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class EventClassHelperTest { + public static final Versionable.Version VERSION = Versionable.Version.of(42); + + private static Stream allEventClassesToTest() { + return Stream.of( + Arguments.of(MyEvent.class) + , Arguments.of(MyEventWithHierarchyClass.class) + , Arguments.of(MyEventExtendsAbstractClassButDoesNotImplementInterface.class) + , Arguments.of(MyEventWithHierarchySuperClass.class) + ); + } + + @ParameterizedTest + @MethodSource("allEventClassesToTest") + void testGetClassForEventClass(Class eventClass) { + // arrange + String clazzName = eventClass.getCanonicalName(); + + // act + Class classForClazzName = EventClassHelper.getClassFor(clazzName); + + // assert + assertThat(classForClazzName).isEqualTo(eventClass); + } + + @Test + void testGetClassForNonEventClass() { + // arrange + String clazzName = String.class.getCanonicalName(); + + // act + Class classForClazzName = EventClassHelper.getClassFor(clazzName); + + // assert + assertThat(classForClazzName).isEqualTo(String.class); + } + + @ParameterizedTest + @MethodSource("allEventClassesToTest") + void testGetClassNameForEventClass(Class eventClass) { + // arrange + String clazzName = eventClass.getCanonicalName(); + + // act + String eventClazzName = EventClassHelper.getEventClazzName(clazzName); + + // assert + assertThat(eventClazzName).isEqualTo(clazzName); + } + + + @Test + void testGetClassNameForNonEventClassThrowsException() { + // arrange + String clazzName = Long.class.getCanonicalName(); + + assertThatThrownBy(() -> { + // act + EventClassHelper.getEventClazzName(clazzName); + + // assert + }).isInstanceOf(FlexinaleIllegalArgumentException.class) + .hasMessageContaining("wrong clazz"); + } + + @ParameterizedTest + @MethodSource("allEventClassesToTest") + void testGetVersionForEventClass(Class eventClass) { + // arrange + String clazzName = eventClass.getCanonicalName(); + + // act + Versionable.Version eventClazzVersion = EventClassHelper.getEventClazzVersion(clazzName); + + // assert + assertThat(eventClazzVersion).isEqualTo(VERSION); + } + + @Test + void testGetVersionForNonEventClassThrowsException() { + // arrange + String clazzName = Integer.class.getCanonicalName(); + + assertThatThrownBy(() -> { + // act + EventClassHelper.getEventClazzVersion(clazzName); + + // assert + }).isInstanceOf(FlexinaleIllegalArgumentException.class) + .hasMessageContaining("need to use Event class"); + } +} + + +class MyEvent extends AbstractEvent implements Event { + static final Version version = EventClassHelperTest.VERSION; + + @Override + public Version version() { + return version; + } +} + +class MyEventExtendsAbstractClassButDoesNotImplementInterface extends AbstractEvent { + static final Version version = EventClassHelperTest.VERSION; + + @Override + public Version version() { + return version; + } +} + +class MyEventWithHierarchySuperClass extends AbstractEvent implements Event { + static final Version version = EventClassHelperTest.VERSION; + + @Override + public Version version() { + return version; + } +} +class MyEventWithHierarchyClass extends MyEventWithHierarchySuperClass { + static final Version version = EventClassHelperTest.VERSION; + + @Override + public Version version() { + return version; + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/MapWithCounterTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/MapWithCounterTest.java new file mode 100644 index 0000000..07b912a --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/MapWithCounterTest.java @@ -0,0 +1,69 @@ +package de.accso.flexinale.common.shared_kernel; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class MapWithCounterTest { + + @Test + public void testInitialMap() { + // arrange + MapWithCounter sut = new MapWithCounter<>(); + + // act + Long key = sut.get("nothere"); + + // assert + assertThat(key).isEqualTo(0L); + } + + @Test + public void testSetAndGet() { + // arrange + MapWithCounter sut = new MapWithCounter<>(); + Long fortyTwo = 42L; + + // act + sut.set("key", fortyTwo); + + // assert + assertThat(sut.get("key")).isEqualTo(fortyTwo); + assertThat(sut.get("notthere")).isEqualTo(0L); + } + + @Test + public void testIncrementInitial() { + // arrange + MapWithCounter sut = new MapWithCounter<>(); + + // act + long value = sut.increment("key"); + + // assert + assertThat(value).isEqualTo(1); + assertThat(sut.get("key")).isEqualTo(1); + + // act + value = sut.increment("key"); + + // assert + assertThat(value).isEqualTo(1+1); + assertThat(sut.get("key")).isEqualTo(1+1); + } + + @Test + public void testIncrementAfterSet() { + // arrange + MapWithCounter sut = new MapWithCounter<>(); + Long fortyTwo = 42L; + sut.set("key", fortyTwo); + + // act + long value = sut.increment("key"); + + // assert + assertThat(value).isEqualTo(fortyTwo+1); + assertThat(sut.get("key")).isEqualTo(fortyTwo+1); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/RawWrapperTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/RawWrapperTest.java new file mode 100644 index 0000000..f466ce1 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/RawWrapperTest.java @@ -0,0 +1,61 @@ +package de.accso.flexinale.common.shared_kernel; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class RawWrapperTest { + + private record StringWrapper(String raw) implements RawWrapper {} + private record IntegerWrapper(Integer raw) implements RawWrapper {} + + @Test + public void testStringWrapperWithString() { + // arrange + StringWrapper sut = new StringWrapper("test"); + + // act + String rawOrNull = RawWrapper.getRawOrNull(sut); + + // assert + assertThat(rawOrNull.getClass()).isEqualTo(String.class); + assertThat(rawOrNull).isEqualTo("test"); + } + + @Test + public void testStringWrapperWithNull() { + // arrange + StringWrapper sut = new StringWrapper(null); + + // act + String rawOrNull = RawWrapper.getRawOrNull(sut); + + // assert + assertThat(rawOrNull).isNull(); + } + + @Test + public void testIntegerWrapperWithInteger() { + // arrange + IntegerWrapper sut = new IntegerWrapper(1); + + // act + Integer rawOrNull = RawWrapper.getRawOrNull(sut); + + // assert + assertThat(rawOrNull.getClass()).isEqualTo(Integer.class); + assertThat(rawOrNull).isEqualTo(1); + } + + @Test + public void testIntegerWrapperWithNull() { + // arrange + IntegerWrapper sut = new IntegerWrapper(null); + + // act + Integer rawOrNull = RawWrapper.getRawOrNull(sut); + + // assert + assertThat(rawOrNull).isNull(); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/SelectionMultiMapTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/SelectionMultiMapTest.java new file mode 100644 index 0000000..d2835c1 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/SelectionMultiMapTest.java @@ -0,0 +1,93 @@ +package de.accso.flexinale.common.shared_kernel; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.Set; +import java.util.stream.Stream; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class SelectionMultiMapTest { + + private record VALUE(String s) {} + + private static Stream allAlgorithms() { + return Stream.of( + Arguments.of(Selection.SelectionAlgorithm.RANDOM), + Arguments.of(Selection.SelectionAlgorithm.CONSTANT_FIRST), + Arguments.of(Selection.SelectionAlgorithm.CONSTANT_LAST), + Arguments.of(Selection.SelectionAlgorithm.ROUND_ROBIN), + Arguments.of(Selection.SelectionAlgorithm.ALL) + ); + } + + @ParameterizedTest + @MethodSource("allAlgorithms") + public void testInitialMap(Selection.SelectionAlgorithm algorithm) { + // arrange + SelectionMultiMap sut = new SelectionMultiMap<>(algorithm); + + // act + Set result = sut.get("notthere"); + + // assert + assertThat(result).isNull(); + } + + @ParameterizedTest + @MethodSource("allAlgorithms") + public void testSetAndGet(Selection.SelectionAlgorithm algorithm) { + // arrange + SelectionMultiMap sut = new SelectionMultiMap<>(algorithm); + VALUE fortyTwo = new VALUE("42"); + + // act + sut.put("key", fortyTwo); + + // assert + assertThat(sut.get("key").size()).isEqualTo(1); + assertThat(sut.get("key").contains(fortyTwo)).isTrue(); + assertThat(sut.get("notthere")).isNull(); + } + + @ParameterizedTest + @MethodSource("allAlgorithms") + public void testSetDuplicatesAndGet(Selection.SelectionAlgorithm algorithm) { + // arrange + SelectionMultiMap sut = new SelectionMultiMap<>(algorithm); + VALUE fortyTwo = new VALUE("42"); + + // act + sut.put("key", fortyTwo); + sut.put("key", fortyTwo); + + // assert + assertThat(sut.get("key").size()).isEqualTo(1); + assertThat(sut.get("key").contains(fortyTwo)).isTrue(); + assertThat(sut.get("notthere")).isNull(); + } + + @ParameterizedTest + @MethodSource("allAlgorithms") + public void testSetAndGetMultipleValues(Selection.SelectionAlgorithm algorithm) { + // arrange + SelectionMultiMap sut = new SelectionMultiMap<>(algorithm); + VALUE fortyTwo = new VALUE("42"); + VALUE fortyThree = new VALUE("43"); + VALUE fortyFour = new VALUE("44"); + + // act + sut.put("key", fortyTwo); + sut.put("key", fortyThree); + sut.put("key", fortyFour); + + // assert + assertThat(sut.get("key").size()).isEqualTo(3); + assertThat(sut.get("key").contains(fortyTwo)).isTrue(); + assertThat(sut.get("key").contains(fortyThree)).isTrue(); + assertThat(sut.get("key").contains(fortyFour)).isTrue(); + assertThat(sut.get("notthere")).isNull(); + } +} \ No newline at end of file diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/SelectionTest.java b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/SelectionTest.java new file mode 100644 index 0000000..d823971 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-common/src/test/java/de/accso/flexinale/common/shared_kernel/SelectionTest.java @@ -0,0 +1,568 @@ +package de.accso.flexinale.common.shared_kernel; + +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +class SelectionTest { + + private record OPTION(Integer i) {} + + @Test + void testRandomSelectorOneOption() { + // arrange + Selection.RandomSelector