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 0000000..058d463 Binary files /dev/null and b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/background.jpeg differ diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/clear.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/clear.html new file mode 100644 index 0000000..81f1224 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/clear.html @@ -0,0 +1,44 @@ + + + + + + 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 0000000..3689979 Binary files /dev/null and b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/construction.gif differ diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/crypto.js b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/crypto.js new file mode 100644 index 0000000..3025225 --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/crypto.js @@ -0,0 +1,92 @@ +// Copyright 2017 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 urlB64ToUint8Array(base64String) { + const suffixLength = 4 - base64String.length % 4; + const base64 = (base64String + '='.repeat(suffixLength)) + .replace(/\-/g, '+') + .replace(/_/g, '/'); + + const raw = window.atob(base64); + + const output = new Uint8Array(raw.length); + Array.from(raw).forEach((c, i) => 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,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw==");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 0000000..339f151 Binary files /dev/null and b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/dragon.png differ diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/drindex.html b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/drindex.html new file mode 100644 index 0000000..999d04c --- /dev/null +++ b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/drindex.html @@ -0,0 +1,85 @@ + + + + + + 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 0000000..e41ce70 Binary files /dev/null and b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icon-192.png differ diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icons/movie.png b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icons/movie.png new file mode 100644 index 0000000..d12d33e Binary files /dev/null and b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icons/movie.png differ 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 0000000..3563ccf Binary files /dev/null and b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icons/movie_camera.png differ 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 0000000..2cea8ab Binary files /dev/null and b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/icons/movies.png differ diff --git a/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/logo.png b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/logo.png new file mode 100644 index 0000000..286f02c Binary files /dev/null and b/flex-training-flexinale/flexinale-distributed/flexinale-distributed-besucherportal/src/main/resources/static/logo.png differ 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