From 445f98a47362a0692682a3e77ee3991e6674d3f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aleksander=20Kova=C4=8D?= Date: Sun, 2 Jul 2023 22:24:58 +0200 Subject: [PATCH] initial commit --- .dockerignore | 26 +++ .gitattributes | 10 + .gitignore | 41 ++++ .vscode/launch.json | 28 +++ .vscode/settings.json | 17 ++ .vscode/tasks.json | 31 +++ Dockerfile | 33 +++ LICENSE | 201 ++++++++++++++++++ compose-in-memory.yaml | 9 + compose-oracle.yaml | 33 +++ compose-sqlserver.yaml | 34 +++ pom.xml | 87 ++++++++ readme.md | 55 +++++ scripts/build_docker.sh | 24 +++ scripts/common.sh | 36 ++++ scripts/sqlserver/cmd.sh | 7 + scripts/sqlserver/configure-db.sh | 15 ++ scripts/sqlserver/healthcheck.sh | 12 ++ scripts/sqlserver/setup.sql | 11 + .../domain/author_related/AuthorDbo.java | 22 ++ .../domain/author_related/AuthorDto.java | 18 ++ .../author_related/AuthorDtoMapper.java | 14 ++ .../sample/domain/book_related/BookDbo.java | 37 ++++ .../sample/domain/book_related/BookDto.java | 26 +++ .../domain/book_related/BookDtoMapper.java | 27 +++ .../si/zpiz/sample/domain/misc/ErrorDto.java | 14 ++ .../si/zpiz/sample/domain/misc/Paged.java | 47 ++++ .../sample/domain/persistence/DboBase.java | 44 ++++ .../persistence/GrantedAuthorityImpl.java | 21 ++ .../GrantedAuthorityImplListConverter.java | 46 ++++ .../sample/domain/persistence/IDboBase.java | 5 + .../domain/persistence/InstantConverter.java | 26 +++ .../persistence/OptionalInstantConverter.java | 28 +++ .../domain/persistence/UUIDConverter.java | 20 ++ .../user_details_related/UserDetailsDbo.java | 86 ++++++++ .../user_details_related/UserDetailsDto.java | 25 +++ .../UserDetailsDtoMapper.java | 18 ++ .../user_related/AuthenticationTokenDto.java | 12 ++ .../author_related/AuthorDboRepository.java | 14 ++ .../command_query/CreateAuthorCommand.java | 22 ++ .../command_query/CreateAuthorHandler.java | 30 +++ .../command_query/DeleteAuthorCommand.java | 17 ++ .../command_query/DeleteAuthorHandler.java | 25 +++ .../command_query/GetAuthorHandler.java | 26 +++ .../command_query/GetAuthorQuery.java | 17 ++ .../command_query/UpdateAuthorCommand.java | 28 +++ .../command_query/UpdateAuthorHandler.java | 34 +++ .../book_related/BookDboRepository.java | 26 +++ .../command_query/CreateBookCommand.java | 31 +++ .../command_query/CreateBookHandler.java | 42 ++++ .../command_query/DeleteBookCommand.java | 15 ++ .../command_query/DeleteBookHandler.java | 25 +++ .../command_query/GetBookHandler.java | 25 +++ .../command_query/GetBookQuery.java | 19 ++ .../command_query/GetBooksHandler.java | 46 ++++ .../command_query/GetBooksQuery.java | 25 +++ .../command_query/GetBooksQueryMode.java | 7 + .../command_query/UpdateBookCommand.java | 35 +++ .../command_query/UpdateBookHandler.java | 47 ++++ .../exceptions/MediatorException.java | 15 ++ .../InitializeSampleDataCommand.java | 10 + .../InitializeSampleDataHandler.java | 53 +++++ .../mediator/IMediatorHandler.java | 10 + .../mediator/IMediatorRequest.java | 5 + .../infrastructure/mediator/Mediator.java | 71 +++++++ .../misc/CustomAuthenticationEntryPoint.java | 39 ++++ .../misc/EnvironmentPropertiesPrinter.java | 23 ++ .../misc/GeneralHelperService.java | 11 + .../misc/GlobalExceptionHandler.java | 101 +++++++++ .../misc/RequestLoggingFilterConfig.java | 25 +++ .../UserDetailsDboRepository.java | 13 ++ .../CreateUserDetailsCommand.java | 22 ++ .../CreateUserDetailsHandler.java | 39 ++++ .../command_query/AuthenticateCommand.java | 18 ++ .../command_query/AuthenticateHandler.java | 69 ++++++ .../java/si/zpiz/sample/webapi/WebApi.java | 12 ++ .../author_related/AuthorController.java | 104 +++++++++ .../webapi/book_related/BookController.java | 104 +++++++++ .../sample/webapi/config/SecurityConfig.java | 126 +++++++++++ .../sample/webapi/config/WebApiConfig.java | 21 ++ .../InitializationController.java | 52 +++++ .../AuthenticationController.java | 64 ++++++ src/main/resources/app.key | 28 +++ src/main/resources/app.pub | 9 + src/main/resources/application-h2.properties | 12 ++ .../resources/application-oracle.properties | 8 + .../application-sqlserver.properties | 8 + src/main/resources/application.properties | 18 ++ .../IntegrationTestConfiguration.java | 17 ++ .../author_related/AuthorControllerTest.java | 40 ++++ .../author_related/DeleteAuthorTest.java | 55 +++++ .../book_related/BookDboRepositoryTest.java | 70 ++++++ .../book_related/CreateBookTest.java | 54 +++++ .../AuthenticationControllerTest.java | 66 ++++++ src/test/java/unit/UnitTestConfiguration.java | 12 ++ .../unit/book_related/BookDtoMapperTest.java | 36 ++++ .../unit/general/UnderstandingTestsTest.java | 35 +++ src/test/resources/app.key | 28 +++ src/test/resources/app.pub | 9 + src/test/resources/application-h2.properties | 8 + .../application-sqlserver.properties | 8 + src/test/resources/application.properties | 11 + 102 files changed, 3341 insertions(+) create mode 100644 .dockerignore create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json create mode 100644 .vscode/tasks.json create mode 100644 Dockerfile create mode 100644 LICENSE create mode 100644 compose-in-memory.yaml create mode 100644 compose-oracle.yaml create mode 100644 compose-sqlserver.yaml create mode 100644 pom.xml create mode 100644 readme.md create mode 100644 scripts/build_docker.sh create mode 100644 scripts/common.sh create mode 100644 scripts/sqlserver/cmd.sh create mode 100644 scripts/sqlserver/configure-db.sh create mode 100644 scripts/sqlserver/healthcheck.sh create mode 100644 scripts/sqlserver/setup.sql create mode 100644 src/main/java/si/zpiz/sample/domain/author_related/AuthorDbo.java create mode 100644 src/main/java/si/zpiz/sample/domain/author_related/AuthorDto.java create mode 100644 src/main/java/si/zpiz/sample/domain/author_related/AuthorDtoMapper.java create mode 100644 src/main/java/si/zpiz/sample/domain/book_related/BookDbo.java create mode 100644 src/main/java/si/zpiz/sample/domain/book_related/BookDto.java create mode 100644 src/main/java/si/zpiz/sample/domain/book_related/BookDtoMapper.java create mode 100644 src/main/java/si/zpiz/sample/domain/misc/ErrorDto.java create mode 100644 src/main/java/si/zpiz/sample/domain/misc/Paged.java create mode 100644 src/main/java/si/zpiz/sample/domain/persistence/DboBase.java create mode 100644 src/main/java/si/zpiz/sample/domain/persistence/GrantedAuthorityImpl.java create mode 100644 src/main/java/si/zpiz/sample/domain/persistence/GrantedAuthorityImplListConverter.java create mode 100644 src/main/java/si/zpiz/sample/domain/persistence/IDboBase.java create mode 100644 src/main/java/si/zpiz/sample/domain/persistence/InstantConverter.java create mode 100644 src/main/java/si/zpiz/sample/domain/persistence/OptionalInstantConverter.java create mode 100644 src/main/java/si/zpiz/sample/domain/persistence/UUIDConverter.java create mode 100644 src/main/java/si/zpiz/sample/domain/user_details_related/UserDetailsDbo.java create mode 100644 src/main/java/si/zpiz/sample/domain/user_details_related/UserDetailsDto.java create mode 100644 src/main/java/si/zpiz/sample/domain/user_details_related/UserDetailsDtoMapper.java create mode 100644 src/main/java/si/zpiz/sample/domain/user_related/AuthenticationTokenDto.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/author_related/AuthorDboRepository.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/CreateAuthorCommand.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/CreateAuthorHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/DeleteAuthorCommand.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/DeleteAuthorHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/GetAuthorHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/GetAuthorQuery.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/UpdateAuthorCommand.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/UpdateAuthorHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/book_related/BookDboRepository.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/CreateBookCommand.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/CreateBookHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/DeleteBookCommand.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/DeleteBookHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBookHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBookQuery.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBooksHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBooksQuery.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBooksQueryMode.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/UpdateBookCommand.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/UpdateBookHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/exceptions/MediatorException.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/initialization_related/InitializeSampleDataCommand.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/initialization_related/InitializeSampleDataHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/mediator/IMediatorHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/mediator/IMediatorRequest.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/mediator/Mediator.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/misc/CustomAuthenticationEntryPoint.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/misc/EnvironmentPropertiesPrinter.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/misc/GeneralHelperService.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/misc/GlobalExceptionHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/misc/RequestLoggingFilterConfig.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/user_details_related/UserDetailsDboRepository.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/user_details_related/command_query/CreateUserDetailsCommand.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/user_details_related/command_query/CreateUserDetailsHandler.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/user_related/command_query/AuthenticateCommand.java create mode 100644 src/main/java/si/zpiz/sample/infrastructure/user_related/command_query/AuthenticateHandler.java create mode 100644 src/main/java/si/zpiz/sample/webapi/WebApi.java create mode 100644 src/main/java/si/zpiz/sample/webapi/author_related/AuthorController.java create mode 100644 src/main/java/si/zpiz/sample/webapi/book_related/BookController.java create mode 100644 src/main/java/si/zpiz/sample/webapi/config/SecurityConfig.java create mode 100644 src/main/java/si/zpiz/sample/webapi/config/WebApiConfig.java create mode 100644 src/main/java/si/zpiz/sample/webapi/initialization_related/InitializationController.java create mode 100644 src/main/java/si/zpiz/sample/webapi/user_related/AuthenticationController.java create mode 100644 src/main/resources/app.key create mode 100644 src/main/resources/app.pub create mode 100644 src/main/resources/application-h2.properties create mode 100644 src/main/resources/application-oracle.properties create mode 100644 src/main/resources/application-sqlserver.properties create mode 100644 src/main/resources/application.properties create mode 100644 src/test/java/integration/IntegrationTestConfiguration.java create mode 100644 src/test/java/integration/author_related/AuthorControllerTest.java create mode 100644 src/test/java/integration/author_related/DeleteAuthorTest.java create mode 100644 src/test/java/integration/book_related/BookDboRepositoryTest.java create mode 100644 src/test/java/integration/book_related/CreateBookTest.java create mode 100644 src/test/java/integration/user_related/AuthenticationControllerTest.java create mode 100644 src/test/java/unit/UnitTestConfiguration.java create mode 100644 src/test/java/unit/book_related/BookDtoMapperTest.java create mode 100644 src/test/java/unit/general/UnderstandingTestsTest.java create mode 100644 src/test/resources/app.key create mode 100644 src/test/resources/app.pub create mode 100644 src/test/resources/application-h2.properties create mode 100644 src/test/resources/application-sqlserver.properties create mode 100644 src/test/resources/application.properties diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..48ad3c5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,26 @@ +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md +**/appsettings.*.json diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..53bf484 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +############################################################ +# Normalize line endings for text files: +# - on check out, use current OS style line endings +# - on check in, use Unix style line endings +############################################################ +* text=auto +*.java text eol=auto +*.sh text eol=lf +*.sql text eol=lf +*.dockerignore text eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..788331e --- /dev/null +++ b/.gitignore @@ -0,0 +1,41 @@ +*# +*.iml +*.ipr +*.iws +*.jar +*.sw? +*~ +.#* +.*.md.html +.DS_Store +.attach_pid* +.classpath +.factorypath +.gradle +.idea +.metadata +.project +.recommenders +.settings +.springBeans +/code +MANIFEST.MF +_site/ +activemq-data +bin +build +!/**/src/**/bin +!/**/src/**/build +build.log +dependency-reduced-pom.xml +dump.rdb +interpolated*.xml +lib/ +manifest.yml +out +overridedb.* +target +.flattened-pom.xml +secrets.yml +.gradletasknamecache +.sts4-cache \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..5499098 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "java", + "name": "Debug (Attach)", + "request": "attach", + "hostName": "localhost", + "port": 8000 + }, + { + "type": "java", + "name": "JavaSpringBootApiSample", + "request": "launch", + "cwd": "${workspaceFolder}", + "mainClass": "si.zpiz.sample.webapi.WebApi", + "projectName": "JavaSpringBootApiSample", + "args": "", + "env": { + "spring_profiles_active":"h2" + }, + "envFile": "${workspaceFolder}/.env" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f2afd1b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "java.compile.nullAnalysis.mode": "automatic", + "java.configuration.updateBuildConfiguration": "automatic", + "java.test.config": [ + { + "name": "spring-boot-h2", + "env": + { + "spring_profiles_active": "h2" + } + } + ], + "java.test.defaultConfig": "spring-boot-h2", + "[java]": { + "editor.formatOnSave": true + } +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..91d30eb --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,31 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "debug", + "type": "shell", + "command": "mvn spring-boot:run '-Dspring-boot.run.jvmArguments=\"-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=8000\"'", + "group": "build" + }, + { + "label": "build", + "type": "shell", + "command": "mvn clean install", + "group": "build" + }, + { + "label": "debug test", + "type": "shell", + "command": "mvnDebug test -DforkMode=never", + "group": "test" + }, + { + "label": "test", + "type": "shell", + "command": "mvn test", + "group": "test" + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8a7531d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +ARG BUILD_IMAGE='' +ARG RUNTIME_IMAGE='' + +FROM ${BUILD_IMAGE} AS builder + +RUN apk add --no-cache \ + maven + +WORKDIR /app +COPY pom.xml /app +COPY src /app/src + +RUN mvn clean package + +FROM ${RUNTIME_IMAGE} AS runner +ARG UID=1000 +ARG USER=webapi-user + +RUN apk add --no-cache icu-libs krb5-libs libgcc libintl libssl1.1 libstdc++ zlib + +WORKDIR /app + +RUN addgroup --system "$USER" && \ + adduser --disabled-password --gecos "" --ingroup "$USER" --uid "$UID" --system "$USER" && \ + chown -R $UID:$UID /app + +WORKDIR /app + +COPY --chown=$UID:$UID --from=builder /app/target/JavaSpringBootApiSample-1.0.0.jar /app + +USER $USER +EXPOSE 8080 +CMD ["java", "-jar", "JavaSpringBootApiSample-1.0.0.jar"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/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/compose-in-memory.yaml b/compose-in-memory.yaml new file mode 100644 index 0000000..9c0ecc1 --- /dev/null +++ b/compose-in-memory.yaml @@ -0,0 +1,9 @@ +version: '3.8' +services: + javaspringboot-api-in-memory: + image: sample/javaspringboot-api + container_name: javaspringboot-api-in-memory + ports: + - 8080:8080 + environment: + - SPRING_PROFILES_ACTIVE=h2 \ No newline at end of file diff --git a/compose-oracle.yaml b/compose-oracle.yaml new file mode 100644 index 0000000..7462730 --- /dev/null +++ b/compose-oracle.yaml @@ -0,0 +1,33 @@ +version: '3.8' +services: + oracle: + image: gvenzl/oracle-free:slim + container_name: oracle-for-javaspringboot-api + environment: + ORACLE_PASSWORD: password@123! + APP_USER: sa + APP_USER_PASSWORD: password@123! + healthcheck: + test: [ "CMD", "/opt/oracle/healthcheck.sh"] + interval: 5s + timeout: 5s + retries: 20 + ports: + - 1521:1521 + volumes: + - javaspringboot-api-oracle-db-data:/opt/oracle/oradata + javaspringboot-api-oracle: + image: sample/javaspringboot-api + container_name: javaspringboot-api-oracle + ports: + - 8080:8080 + depends_on: + oracle: + condition: service_healthy + environment: + - SPRING_PROFILES_ACTIVE=oracle + - SPRING_DATASOURCE_URL=jdbc:oracle:thin:@//oracle:1521/FREEPDB1 + - SPRING_DATASOURCE_USERNAME=sa + - SPRING_DATASOURCE_PASSWORD=password@123! +volumes: + javaspringboot-api-oracle-db-data: \ No newline at end of file diff --git a/compose-sqlserver.yaml b/compose-sqlserver.yaml new file mode 100644 index 0000000..ae25175 --- /dev/null +++ b/compose-sqlserver.yaml @@ -0,0 +1,34 @@ +version: '3.8' +services: + sql-server: + image: mcr.microsoft.com/mssql/server:2019-latest + container_name: sql-server-for-javaspringboot-api + environment: + SA_PASSWORD: YourStrong@Passw0rd + ACCEPT_EULA: Y + ports: + - 14333:1433 + command: /bin/bash "/usr/config/cmd.sh" + healthcheck: + test: [ "CMD", "/usr/config/healthcheck.sh"] + interval: 5s + timeout: 5s + retries: 20 + volumes: + - javaspringboot-api-sqlserver-db-data:/var/opt/mssql + - ./scripts/sqlserver:/usr/config + javaspringboot-api-sqlserver: + image: sample/javaspringboot-api + container_name: javaspringboot-api-sqlserver + ports: + - 8080:8080 + environment: + - SPRING_PROFILES_ACTIVE=sqlserver + - SPRING_DATASOURCE_URL=jdbc:sqlserver://sql-server;databaseName=JavaSample;trustServerCertificate=True + - SPRING_DATASOURCE_USERNAME=sa + - SPRING_DATASOURCE_PASSWORD=YourStrong@Passw0rd + depends_on: + sql-server: + condition: service_healthy +volumes: + javaspringboot-api-sqlserver-db-data: \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..e0321ff --- /dev/null +++ b/pom.xml @@ -0,0 +1,87 @@ + + + 4.0.0 + si.zpiz.sample + JavaSpringBootApiSample + 1.0.0 + jar + JavaSpringBootApiSample + Api sample for Java Spring Boot + + org.springframework.boot + spring-boot-starter-parent + 3.1.1 + + + + UTF-8 + UTF-8 + 20 + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-oauth2-resource-server + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-validation + + + io.jsonwebtoken + jjwt + 0.9.1 + + + org.springframework.boot + spring-boot-starter-test + test + + + com.h2database + h2 + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.1.0 + + + org.projectlombok + lombok + 1.18.28 + provided + + + com.microsoft.sqlserver + mssql-jdbc + 12.2.0.jre11 + + + com.oracle.database.jdbc + ojdbc11 + 23.2.0.0 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-resources-plugin + 3.2.0 + + + + \ No newline at end of file diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..67ca8fd --- /dev/null +++ b/readme.md @@ -0,0 +1,55 @@ +# Java Spring Boot API sample + +This project contains a Java Spring Boot API sample. The following is demonstrated: + +* authentication, +* authorization, +* mediator, command and query patterns, +* database repository pattern, +* transaction handling, +* error handling, +* model validation, +* unit testing, +* integration testing, +* some Swagger tricks, +* in-memory database, +* multiple database types, +* integration tests cover multiple databases, +* ... + +To start the sample, run the following command in solution root: + +* `mvn spring-boot:run` + +Then navigate to: `http://localhost:8080/swagger-ui/index.html` + +Supported users using `password`: + +* admin - full access +* user - not allowed to delete data +* unauthenticated users - may only initialize data + +Authorization header pattern: `` + +Examine command handler `si.zpiz.sample.infrastructure.initialization_related.InitializeSampleDataHandler` to understand how data is initialized. + +The following is TODO: + +* localization, +* generating openapi.json during build, +* generating Java client from openapi.json during build, +* integration testing using generated .Java client, +* ... + +For development in VS Code install the following plugins: + +* Extension Pack for Java from Microsoft, +* Spring Boot Extension Pack from VMware, +* ... + +Once plugins are installed, the project can be debugged from Spring Boot Dashboard and tests can be debugged from Testing dashboard. + +Additional commands: + +* Run tests with profile: `mvn test '-Dspring.profiles.active=h2'` +* Run application with profile: `mvn spring-boot:run '-Dspring-boot.run.profiles=h2'` diff --git a/scripts/build_docker.sh b/scripts/build_docker.sh new file mode 100644 index 0000000..649e80a --- /dev/null +++ b/scripts/build_docker.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +# Run this script to build docker image locally +# ./build_docker.sh + +scriptdir=$(dirname "$0") + +# Load common code +source "$scriptdir"/common.sh + +fn_say_wrn "**********************" +fn_say_wrn "Building docker images" +fn_say_wrn "**********************" +echo + +docker_file="$scriptdir/../Dockerfile" + +docker build -t "sample/javaspringboot-api" -f "$docker_file" "$scriptdir/.." \ + --build-arg BUILD_IMAGE="eclipse-temurin:20-jdk-alpine"\ + --build-arg RUNTIME_IMAGE="eclipse-temurin:20-jre-alpine" + +fn_say_success "*******" +fn_say_success "Success" +fn_say_success "*******" \ No newline at end of file diff --git a/scripts/common.sh b/scripts/common.sh new file mode 100644 index 0000000..8993916 --- /dev/null +++ b/scripts/common.sh @@ -0,0 +1,36 @@ +#!/bin/bash +set -euo pipefail + +############################################################ +# This is a script which contains common code for all other +# scripts in the project. +############################################################ + +# Configure colors +export TXT_RED="\e[31m" +export TXT_YELLOW="\e[0;33m" +export TXT_CLEAR="\e[0m" +export TXT_GREEN="\e[0;32m" + +fn_say() { + echo -e "${TXT_CLEAR}${1}${TXT_CLEAR}" +} + +fn_say_wrn() { + echo -e "${TXT_YELLOW}${1}${TXT_CLEAR}" +} + +fn_say_err() { + # Print to stderr + echo -e "${TXT_RED}${1}${TXT_CLEAR}" >&2 + exit 1 +} + +fn_say_success() { + echo -e "${TXT_GREEN}${1}${TXT_CLEAR}" +} + +fn_debug() +{ + echo -e "DEBUG [$( caller )] ${TXT_CLEAR}${1}${TXT_CLEAR}" +} \ No newline at end of file diff --git a/scripts/sqlserver/cmd.sh b/scripts/sqlserver/cmd.sh new file mode 100644 index 0000000..4f32120 --- /dev/null +++ b/scripts/sqlserver/cmd.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Start the script to create the DB and user +/usr/config/configure-db.sh & + +# Start SQL Server +/opt/mssql/bin/sqlservr \ No newline at end of file diff --git a/scripts/sqlserver/configure-db.sh b/scripts/sqlserver/configure-db.sh new file mode 100644 index 0000000..ca642e7 --- /dev/null +++ b/scripts/sqlserver/configure-db.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +DBSTATUS=1 +ERRCODE=1 + +while [[ $DBSTATUS -ne 0 ]] && [[ $ERRCODE -ne 0 ]]; do + i=$i+1 + DBSTATUS=$(/opt/mssql-tools/bin/sqlcmd -h -1 -t 1 -U sa -P $SA_PASSWORD -Q "SET NOCOUNT ON; Select SUM(state) from sys.databases") + ERRCODE=$? + echo "Waiting for server to start" + sleep 1 +done + +# Run the setup script to create the DB and the schema in the DB +/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -d master -i /usr/config/setup.sql diff --git a/scripts/sqlserver/healthcheck.sh b/scripts/sqlserver/healthcheck.sh new file mode 100644 index 0000000..2d93d87 --- /dev/null +++ b/scripts/sqlserver/healthcheck.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +DBSTATUS=1 +ERRCODE=1 + +DBSTATUS=$(/opt/mssql-tools/bin/sqlcmd -h -1 -t 1 -U sa -P $SA_PASSWORD -Q "SET NOCOUNT ON; Select SUM(state) from sys.databases") +ERRCODE=$? + +if [[ $DBSTATUS -ne 0 ]] || [[ $ERRCODE -ne 0 ]]; then + echo "Healthcheck failed" + exit 1 +fi \ No newline at end of file diff --git a/scripts/sqlserver/setup.sql b/scripts/sqlserver/setup.sql new file mode 100644 index 0000000..dcb208e --- /dev/null +++ b/scripts/sqlserver/setup.sql @@ -0,0 +1,11 @@ +IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'JavaSample') +BEGIN + CREATE DATABASE JavaSample; +END +GO + +IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = 'JavaSampleTest') +BEGIN + CREATE DATABASE JavaSampleTest; +END +GO \ No newline at end of file diff --git a/src/main/java/si/zpiz/sample/domain/author_related/AuthorDbo.java b/src/main/java/si/zpiz/sample/domain/author_related/AuthorDbo.java new file mode 100644 index 0000000..0665b71 --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/author_related/AuthorDbo.java @@ -0,0 +1,22 @@ +package si.zpiz.sample.domain.author_related; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import si.zpiz.sample.domain.persistence.DboBase; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@Entity +@Table(name = "Authors") +public class AuthorDbo extends DboBase { + @Column(nullable = false) + private String firstName; + + @Column(nullable = false) + private String lastName; +} diff --git a/src/main/java/si/zpiz/sample/domain/author_related/AuthorDto.java b/src/main/java/si/zpiz/sample/domain/author_related/AuthorDto.java new file mode 100644 index 0000000..dfa76d9 --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/author_related/AuthorDto.java @@ -0,0 +1,18 @@ +package si.zpiz.sample.domain.author_related; + +import java.util.UUID; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NonNull; + +@Data +@AllArgsConstructor +public class AuthorDto { + @NonNull + private String firstName; + @NonNull + private String lastName; + @NonNull + private UUID uniqueId; +} diff --git a/src/main/java/si/zpiz/sample/domain/author_related/AuthorDtoMapper.java b/src/main/java/si/zpiz/sample/domain/author_related/AuthorDtoMapper.java new file mode 100644 index 0000000..a346d7f --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/author_related/AuthorDtoMapper.java @@ -0,0 +1,14 @@ +package si.zpiz.sample.domain.author_related; + +import org.springframework.stereotype.Component; + +@Component +public class AuthorDtoMapper { + public AuthorDto fromDbo(AuthorDbo dbo) { + AuthorDto dto = new AuthorDto( + dbo.getFirstName(), + dbo.getLastName(), + dbo.getUniqueId()); + return dto; + } +} diff --git a/src/main/java/si/zpiz/sample/domain/book_related/BookDbo.java b/src/main/java/si/zpiz/sample/domain/book_related/BookDbo.java new file mode 100644 index 0000000..257a2b2 --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/book_related/BookDbo.java @@ -0,0 +1,37 @@ +package si.zpiz.sample.domain.book_related; + +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.domain.persistence.DboBase; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@Entity +@Table(name = "Books") +public class BookDbo extends DboBase { + @Column(nullable = false) + private String title; + + private int year; + + @Column(nullable = false) + private String publisher; + + /* Deleting the author will result in FKs being set to null by the database */ + @ManyToOne(fetch = FetchType.EAGER, optional = true) + @JoinColumn(name = "author_id", referencedColumnName = "id", nullable = true) + @OnDelete(action = OnDeleteAction.SET_NULL) + private AuthorDbo author; +} diff --git a/src/main/java/si/zpiz/sample/domain/book_related/BookDto.java b/src/main/java/si/zpiz/sample/domain/book_related/BookDto.java new file mode 100644 index 0000000..5a506d2 --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/book_related/BookDto.java @@ -0,0 +1,26 @@ +package si.zpiz.sample.domain.book_related; + +import java.util.UUID; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NonNull; +import si.zpiz.sample.domain.author_related.AuthorDto; + +@Data +@AllArgsConstructor +public class BookDto { + @NonNull + private String title; + + // Optional + private AuthorDto author; + + private int year; + + @NonNull + private String publisher; + + @NonNull + private UUID uniqueId; +} diff --git a/src/main/java/si/zpiz/sample/domain/book_related/BookDtoMapper.java b/src/main/java/si/zpiz/sample/domain/book_related/BookDtoMapper.java new file mode 100644 index 0000000..5af1581 --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/book_related/BookDtoMapper.java @@ -0,0 +1,27 @@ +package si.zpiz.sample.domain.book_related; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import si.zpiz.sample.domain.author_related.AuthorDto; +import si.zpiz.sample.domain.author_related.AuthorDtoMapper; + +@Component +public class BookDtoMapper { + @Autowired + private AuthorDtoMapper authorDtoMapper; + + public BookDto fromDbo(BookDbo dbo) { + AuthorDto author = null; + if (dbo.getAuthor() != null) { + author = authorDtoMapper.fromDbo(dbo.getAuthor()); + } + + BookDto dto = new BookDto(dbo.getTitle(), + author, + dbo.getYear(), + dbo.getPublisher(), + dbo.getUniqueId()); + return dto; + } +} diff --git a/src/main/java/si/zpiz/sample/domain/misc/ErrorDto.java b/src/main/java/si/zpiz/sample/domain/misc/ErrorDto.java new file mode 100644 index 0000000..890964b --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/misc/ErrorDto.java @@ -0,0 +1,14 @@ +package si.zpiz.sample.domain.misc; + +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class ErrorDto { + private String message; + private int status; + private String fromClass; + private String controllerName; + private String methodName; +} diff --git a/src/main/java/si/zpiz/sample/domain/misc/Paged.java b/src/main/java/si/zpiz/sample/domain/misc/Paged.java new file mode 100644 index 0000000..5191f4e --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/misc/Paged.java @@ -0,0 +1,47 @@ +package si.zpiz.sample.domain.misc; + +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class Paged { + @Min(0) + private int page; + + @Min(0) + @Max(1000) + @Schema(defaultValue = "20") + private int size = 20; + + private String[] sort; + + private Direction[] sortDirection; + + @JsonIgnore + public Pageable getPageable() { + Sort sorter = Sort.unsorted(); + if (sort != null && sortDirection != null && sort.length != sortDirection.length) { + throw new IllegalStateException("sort and sortDirection must be of same length"); + } + + if (sort != null && sortDirection != null) { + for (int i = 0; i < sort.length; i++) { + sorter = sorter.and(Sort.by(sortDirection[i], sort[i])); + } + } + + return org.springframework.data.domain.PageRequest.of(page, size, sorter); + } +} diff --git a/src/main/java/si/zpiz/sample/domain/persistence/DboBase.java b/src/main/java/si/zpiz/sample/domain/persistence/DboBase.java new file mode 100644 index 0000000..4d95de3 --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/persistence/DboBase.java @@ -0,0 +1,44 @@ +package si.zpiz.sample.domain.persistence; + +import java.time.Instant; +import java.util.Optional; +import java.util.UUID; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PreUpdate; +import jakarta.persistence.Version; +import lombok.Data; + +@Data +@MappedSuperclass +public abstract class DboBase implements IDboBase { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + protected Long id; + + @Column(nullable = false, unique = true) + @Convert(converter = UUIDConverter.class) + protected UUID uniqueId = UUID.randomUUID(); + + @Version + protected int rowVersion; + + @Column(nullable = false) + protected Instant createdOnUtc = Instant.now(); + + /* Example converter usage */ + @Column(nullable = true) + @Convert(converter = OptionalInstantConverter.class) + protected Optional modifiedOnUtc = Optional.empty(); + + @PreUpdate + public void onSavingModified() { + rowVersion += 1; + modifiedOnUtc = Optional.of(Instant.now()); + } +} diff --git a/src/main/java/si/zpiz/sample/domain/persistence/GrantedAuthorityImpl.java b/src/main/java/si/zpiz/sample/domain/persistence/GrantedAuthorityImpl.java new file mode 100644 index 0000000..a7d545c --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/persistence/GrantedAuthorityImpl.java @@ -0,0 +1,21 @@ +package si.zpiz.sample.domain.persistence; + +import org.springframework.security.core.GrantedAuthority; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class GrantedAuthorityImpl implements GrantedAuthority { + @NotNull + private String authority; + + @Override + public String getAuthority() { + return authority; + } +} \ No newline at end of file diff --git a/src/main/java/si/zpiz/sample/domain/persistence/GrantedAuthorityImplListConverter.java b/src/main/java/si/zpiz/sample/domain/persistence/GrantedAuthorityImplListConverter.java new file mode 100644 index 0000000..030b0c1 --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/persistence/GrantedAuthorityImplListConverter.java @@ -0,0 +1,46 @@ +package si.zpiz.sample.domain.persistence; + +import java.util.ArrayList; +import java.util.List; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class GrantedAuthorityImplListConverter implements AttributeConverter, String> { + + @Override + public String convertToDatabaseColumn(List instance) { + if (instance == null) { + return null; + } + + ObjectMapper mapper = new ObjectMapper(); + try { + return mapper.writeValueAsString(instance); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Error converting object to JSON string", e); + } + } + + @Override + public List convertToEntityAttribute(String dbData) { + if (dbData == null) { + return null; + } + + ObjectMapper mapper = new ObjectMapper(); + + @SuppressWarnings("unused") + List result = new ArrayList<>(); + + try { + return mapper.readValue(dbData, + mapper.getTypeFactory().constructCollectionType(List.class, GrantedAuthorityImpl.class)); + } catch (JsonProcessingException e) { + throw new IllegalArgumentException("Error converting JSON string to object", e); + } + } +} \ No newline at end of file diff --git a/src/main/java/si/zpiz/sample/domain/persistence/IDboBase.java b/src/main/java/si/zpiz/sample/domain/persistence/IDboBase.java new file mode 100644 index 0000000..948891d --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/persistence/IDboBase.java @@ -0,0 +1,5 @@ +package si.zpiz.sample.domain.persistence; + +public interface IDboBase { + void onSavingModified(); +} diff --git a/src/main/java/si/zpiz/sample/domain/persistence/InstantConverter.java b/src/main/java/si/zpiz/sample/domain/persistence/InstantConverter.java new file mode 100644 index 0000000..bfd88ef --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/persistence/InstantConverter.java @@ -0,0 +1,26 @@ +package si.zpiz.sample.domain.persistence; + +import java.sql.Timestamp; +import java.time.Instant; +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class InstantConverter implements AttributeConverter { + + @Override + public Timestamp convertToDatabaseColumn(Instant instance) { + if (instance == null) { + return null; + } + return Timestamp.from(instance); + } + + @Override + public Instant convertToEntityAttribute(Timestamp dbData) { + if (dbData == null) { + return null; + } + return dbData.toInstant(); + } +} diff --git a/src/main/java/si/zpiz/sample/domain/persistence/OptionalInstantConverter.java b/src/main/java/si/zpiz/sample/domain/persistence/OptionalInstantConverter.java new file mode 100644 index 0000000..3db64eb --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/persistence/OptionalInstantConverter.java @@ -0,0 +1,28 @@ +package si.zpiz.sample.domain.persistence; + +import java.sql.Timestamp; +import java.time.Instant; +import java.util.Optional; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class OptionalInstantConverter implements AttributeConverter, Timestamp> { + + @Override + public Timestamp convertToDatabaseColumn(Optional instance) { + if (instance == null) { + return null; + } + return instance.map(Timestamp::from).orElse(null); + } + + @Override + public Optional convertToEntityAttribute(Timestamp dbData) { + if (dbData == null) { + return Optional.empty(); + } + return Optional.ofNullable(dbData).map(Timestamp::toInstant); + } +} \ No newline at end of file diff --git a/src/main/java/si/zpiz/sample/domain/persistence/UUIDConverter.java b/src/main/java/si/zpiz/sample/domain/persistence/UUIDConverter.java new file mode 100644 index 0000000..443404a --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/persistence/UUIDConverter.java @@ -0,0 +1,20 @@ +package si.zpiz.sample.domain.persistence; + +import java.util.UUID; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; + +@Converter(autoApply = true) +public class UUIDConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(UUID instance) { + return instance.toString(); + } + + @Override + public UUID convertToEntityAttribute(String dbData) { + return UUID.fromString(dbData); + } +} \ No newline at end of file diff --git a/src/main/java/si/zpiz/sample/domain/user_details_related/UserDetailsDbo.java b/src/main/java/si/zpiz/sample/domain/user_details_related/UserDetailsDbo.java new file mode 100644 index 0000000..7d93136 --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/user_details_related/UserDetailsDbo.java @@ -0,0 +1,86 @@ +package si.zpiz.sample.domain.user_details_related; + +import java.util.Collection; +import java.util.List; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import jakarta.persistence.Column; +import jakarta.persistence.Convert; +import jakarta.persistence.Entity; +import jakarta.persistence.Table; +import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.NoArgsConstructor; +import si.zpiz.sample.domain.persistence.DboBase; +import si.zpiz.sample.domain.persistence.GrantedAuthorityImpl; +import si.zpiz.sample.domain.persistence.GrantedAuthorityImplListConverter; + +@Data +@EqualsAndHashCode(callSuper = true) +@NoArgsConstructor +@Entity +@Table(name = "UserDetails") +public class UserDetailsDbo extends DboBase implements UserDetails { + @Column(nullable = false) + @Convert(converter = GrantedAuthorityImplListConverter.class) + private List authorities; + + @Column(nullable = false) + private String password; + + @Column(nullable = false, unique = true) + private String username; + + @Column(nullable = false) + private boolean isAccountNonExpired; + + @Column(nullable = false) + private boolean isAccountNonLocked; + + @Column(nullable = false) + private boolean isCredentialsNonExpired; + + @Column(nullable = false) + private boolean isEnabled; + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getPassword() { + return password; + } + + @Override + public String getUsername() { + return username; + } + + @Override + public boolean isAccountNonExpired() { + return isAccountNonExpired; + } + + @Override + public boolean isAccountNonLocked() { + return isAccountNonLocked; + } + + @Override + public boolean isCredentialsNonExpired() { + return isCredentialsNonExpired; + } + + @Override + public boolean isEnabled() { + return isEnabled; + } + + public String toStringWithObscuredPassword() { + return toString().replaceFirst("password=.*?,", "password=***,"); + } +} diff --git a/src/main/java/si/zpiz/sample/domain/user_details_related/UserDetailsDto.java b/src/main/java/si/zpiz/sample/domain/user_details_related/UserDetailsDto.java new file mode 100644 index 0000000..b2226a4 --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/user_details_related/UserDetailsDto.java @@ -0,0 +1,25 @@ +package si.zpiz.sample.domain.user_details_related; + +import java.util.List; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class UserDetailsDto { + @NotNull + private List authorities; + + @NotNull + private String password; + + @NotNull + private String username; + + private boolean isAccountNonExpired; + private boolean isAccountNonLocked; + private boolean isCredentialsNonExpired; + private boolean isEnabled; +} diff --git a/src/main/java/si/zpiz/sample/domain/user_details_related/UserDetailsDtoMapper.java b/src/main/java/si/zpiz/sample/domain/user_details_related/UserDetailsDtoMapper.java new file mode 100644 index 0000000..c999f86 --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/user_details_related/UserDetailsDtoMapper.java @@ -0,0 +1,18 @@ +package si.zpiz.sample.domain.user_details_related; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +@Component +public class UserDetailsDtoMapper { + public UserDetailsDto fromDbo(UserDetailsDbo dbo) { + return new UserDetailsDto( + dbo.getAuthorities().stream().map(GrantedAuthority::getAuthority).toList(), + dbo.getPassword(), + dbo.getUsername(), + dbo.isAccountNonExpired(), + dbo.isAccountNonLocked(), + dbo.isCredentialsNonExpired(), + dbo.isEnabled()); + } +} diff --git a/src/main/java/si/zpiz/sample/domain/user_related/AuthenticationTokenDto.java b/src/main/java/si/zpiz/sample/domain/user_related/AuthenticationTokenDto.java new file mode 100644 index 0000000..6420272 --- /dev/null +++ b/src/main/java/si/zpiz/sample/domain/user_related/AuthenticationTokenDto.java @@ -0,0 +1,12 @@ +package si.zpiz.sample.domain.user_related; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NonNull; + +@Data +@AllArgsConstructor +public class AuthenticationTokenDto { + @NonNull + private String token; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/author_related/AuthorDboRepository.java b/src/main/java/si/zpiz/sample/infrastructure/author_related/AuthorDboRepository.java new file mode 100644 index 0000000..db9d996 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/author_related/AuthorDboRepository.java @@ -0,0 +1,14 @@ +package si.zpiz.sample.infrastructure.author_related; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; + +import si.zpiz.sample.domain.author_related.AuthorDbo; + +public interface AuthorDboRepository extends JpaRepository { + Optional findByUniqueId(UUID uniqueId); + + void deleteByUniqueId(UUID uniqueId); +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/CreateAuthorCommand.java b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/CreateAuthorCommand.java new file mode 100644 index 0000000..75d0d7a --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/CreateAuthorCommand.java @@ -0,0 +1,22 @@ +package si.zpiz.sample.infrastructure.author_related.command_query; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.infrastructure.mediator.IMediatorRequest; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreateAuthorCommand implements IMediatorRequest { + @NotBlank + @Size(min = 5, max = 20) + private String firstName; + + @NotBlank + @Size(min = 5, max = 20) + private String lastName; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/CreateAuthorHandler.java b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/CreateAuthorHandler.java new file mode 100644 index 0000000..367f14b --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/CreateAuthorHandler.java @@ -0,0 +1,30 @@ +package si.zpiz.sample.infrastructure.author_related.command_query; + +import org.springframework.stereotype.Service; + +import jakarta.transaction.Transactional; +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.infrastructure.author_related.AuthorDboRepository; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.IMediatorHandler; + +@Service +public class CreateAuthorHandler implements IMediatorHandler { + private AuthorDboRepository authorDboRepository; + + public CreateAuthorHandler(AuthorDboRepository authorDboRepository) { + this.authorDboRepository = authorDboRepository; + } + + @Override + @Transactional + public AuthorDbo handle(CreateAuthorCommand request) throws MediatorException { + AuthorDbo author = new AuthorDbo(); + author.setFirstName(request.getFirstName()); + author.setLastName(request.getLastName()); + + author = authorDboRepository.save(author); + return author; + } + +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/DeleteAuthorCommand.java b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/DeleteAuthorCommand.java new file mode 100644 index 0000000..ca6ead2 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/DeleteAuthorCommand.java @@ -0,0 +1,17 @@ +package si.zpiz.sample.infrastructure.author_related.command_query; + +import java.util.UUID; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import si.zpiz.sample.infrastructure.mediator.IMediatorRequest; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DeleteAuthorCommand implements IMediatorRequest { + @NotNull + private UUID uniqueId; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/DeleteAuthorHandler.java b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/DeleteAuthorHandler.java new file mode 100644 index 0000000..0deb1d4 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/DeleteAuthorHandler.java @@ -0,0 +1,25 @@ +package si.zpiz.sample.infrastructure.author_related.command_query; + +import org.springframework.stereotype.Service; + +import jakarta.transaction.Transactional; +import si.zpiz.sample.infrastructure.author_related.AuthorDboRepository; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.IMediatorHandler; + +@Service +public class DeleteAuthorHandler implements IMediatorHandler { + private AuthorDboRepository authorDboRepository; + + public DeleteAuthorHandler(AuthorDboRepository authorDboRepository) { + this.authorDboRepository = authorDboRepository; + } + + @Override + @Transactional + public Void handle(DeleteAuthorCommand request) throws MediatorException { + authorDboRepository.deleteByUniqueId(request.getUniqueId()); + return null; + } + +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/GetAuthorHandler.java b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/GetAuthorHandler.java new file mode 100644 index 0000000..38bc6bd --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/GetAuthorHandler.java @@ -0,0 +1,26 @@ +package si.zpiz.sample.infrastructure.author_related.command_query; + +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.infrastructure.author_related.AuthorDboRepository; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.IMediatorHandler; + +@Service +public class GetAuthorHandler implements IMediatorHandler> { + private AuthorDboRepository authorDboRepository; + + public GetAuthorHandler(AuthorDboRepository authorDboRepository) { + this.authorDboRepository = authorDboRepository; + } + + @Override + public Optional handle(GetAuthorQuery request) throws MediatorException { + Optional author = authorDboRepository.findByUniqueId(request.getUniqueId()); + return author; + } + +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/GetAuthorQuery.java b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/GetAuthorQuery.java new file mode 100644 index 0000000..2f73ec4 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/GetAuthorQuery.java @@ -0,0 +1,17 @@ +package si.zpiz.sample.infrastructure.author_related.command_query; + +import java.util.Optional; +import java.util.UUID; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.infrastructure.mediator.IMediatorRequest; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GetAuthorQuery implements IMediatorRequest> { + private UUID uniqueId; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/UpdateAuthorCommand.java b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/UpdateAuthorCommand.java new file mode 100644 index 0000000..d0efe1b --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/UpdateAuthorCommand.java @@ -0,0 +1,28 @@ +package si.zpiz.sample.infrastructure.author_related.command_query; + +import java.util.UUID; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.infrastructure.mediator.IMediatorRequest; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UpdateAuthorCommand implements IMediatorRequest { + @NotBlank + @Size(min = 5, max = 20) + private String firstName; + + @NotBlank + @Size(min = 5, max = 20) + private String lastName; + + @NotNull + private UUID uniqueId; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/UpdateAuthorHandler.java b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/UpdateAuthorHandler.java new file mode 100644 index 0000000..9e0e940 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/author_related/command_query/UpdateAuthorHandler.java @@ -0,0 +1,34 @@ +package si.zpiz.sample.infrastructure.author_related.command_query; + +import org.springframework.stereotype.Service; + +import jakarta.persistence.EntityNotFoundException; +import jakarta.transaction.Transactional; +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.infrastructure.author_related.AuthorDboRepository; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.IMediatorHandler; + +@Service +public class UpdateAuthorHandler implements IMediatorHandler { + private AuthorDboRepository authorDboRepository; + + public UpdateAuthorHandler(AuthorDboRepository authorDboRepository) { + this.authorDboRepository = authorDboRepository; + } + + @Override + @Transactional + public AuthorDbo handle(UpdateAuthorCommand request) throws MediatorException { + AuthorDbo author = authorDboRepository.findByUniqueId(request.getUniqueId()) + .orElseThrow(() -> new EntityNotFoundException( + String.format("Author with unique id %s was not found", request.getUniqueId()))); + + author.setFirstName(request.getFirstName()); + author.setLastName(request.getLastName()); + + author = authorDboRepository.save(author); + return author; + } + +} \ No newline at end of file diff --git a/src/main/java/si/zpiz/sample/infrastructure/book_related/BookDboRepository.java b/src/main/java/si/zpiz/sample/infrastructure/book_related/BookDboRepository.java new file mode 100644 index 0000000..fe26e0b --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/book_related/BookDboRepository.java @@ -0,0 +1,26 @@ +package si.zpiz.sample.infrastructure.book_related; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import si.zpiz.sample.domain.book_related.BookDbo; + +public interface BookDboRepository extends JpaRepository { + void deleteByUniqueId(UUID uniqueId); + + Optional findByUniqueId(UUID uniqueId); + + // Supports wildcard searches + // HQL + @Query(value = "FROM BookDbo b WHERE LOWER(b.title) LIKE LOWER(CONCAT('%', :value, '%')) OR LOWER(b.author) LIKE LOWER(CONCAT('%', :value, '%')) OR LOWER(b.publisher) LIKE LOWER(CONCAT('%', :value, '%'))", countQuery = "SELECT COUNT(b) FROM BookDbo b WHERE LOWER(b.title) LIKE LOWER(CONCAT('%', :value, '%')) OR LOWER(b.author) LIKE LOWER(CONCAT('%', :value, '%')) OR LOWER(b.publisher) LIKE LOWER(CONCAT('%', :value, '%'))") + Page findByStringsContainingIgnoreCase(@Param("value") String value, Pageable pageable); + + // Does not support wildcard searches + Page findByTitleContainingIgnoreCase(String title, Pageable pageable); +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/CreateBookCommand.java b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/CreateBookCommand.java new file mode 100644 index 0000000..cde1def --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/CreateBookCommand.java @@ -0,0 +1,31 @@ +package si.zpiz.sample.infrastructure.book_related.command_query; + +import java.util.UUID; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.infrastructure.mediator.IMediatorRequest; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreateBookCommand implements IMediatorRequest { + @NotBlank + @Size(min = 5, max = 20) + private String title; + + @Min(2000) + private int year; + + @NotBlank + @Size(min = 5, max = 20) + private String publisher; + + // Optional + private UUID authorUniqueId; +} \ No newline at end of file diff --git a/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/CreateBookHandler.java b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/CreateBookHandler.java new file mode 100644 index 0000000..87ad64a --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/CreateBookHandler.java @@ -0,0 +1,42 @@ +package si.zpiz.sample.infrastructure.book_related.command_query; + +import org.springframework.stereotype.Service; + +import jakarta.persistence.EntityNotFoundException; +import jakarta.transaction.Transactional; +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.infrastructure.author_related.AuthorDboRepository; +import si.zpiz.sample.infrastructure.book_related.BookDboRepository; +import si.zpiz.sample.infrastructure.mediator.IMediatorHandler; + +@Service +public class CreateBookHandler implements IMediatorHandler { + private final BookDboRepository bookDboRepository; + private AuthorDboRepository authorDboRepository; + + public CreateBookHandler(BookDboRepository bookDboRepository, AuthorDboRepository authorDboRepository) { + this.bookDboRepository = bookDboRepository; + this.authorDboRepository = authorDboRepository; + } + + @Transactional + @Override + public BookDbo handle(CreateBookCommand request) { + AuthorDbo author = null; + if (request.getAuthorUniqueId() != null) { + author = authorDboRepository.findByUniqueId(request.getAuthorUniqueId()) + .orElseThrow(() -> new EntityNotFoundException( + String.format("Author with unique id %s was not found", request.getAuthorUniqueId()))); + } + + BookDbo book = new BookDbo(); + book.setTitle(request.getTitle()); + book.setAuthor(author); + book.setYear(request.getYear()); + book.setPublisher(request.getPublisher()); + + book = bookDboRepository.save(book); + return book; + } +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/DeleteBookCommand.java b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/DeleteBookCommand.java new file mode 100644 index 0000000..d7807ef --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/DeleteBookCommand.java @@ -0,0 +1,15 @@ +package si.zpiz.sample.infrastructure.book_related.command_query; + +import java.util.UUID; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import si.zpiz.sample.infrastructure.mediator.IMediatorRequest; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class DeleteBookCommand implements IMediatorRequest { + public UUID uniqueId; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/DeleteBookHandler.java b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/DeleteBookHandler.java new file mode 100644 index 0000000..6b9cb67 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/DeleteBookHandler.java @@ -0,0 +1,25 @@ +package si.zpiz.sample.infrastructure.book_related.command_query; + +import org.springframework.stereotype.Service; + +import jakarta.transaction.Transactional; +import si.zpiz.sample.infrastructure.book_related.BookDboRepository; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.IMediatorHandler; + +@Service +public class DeleteBookHandler implements IMediatorHandler { + private BookDboRepository bookDboRepository; + + public DeleteBookHandler(BookDboRepository bookDboRepository) { + this.bookDboRepository = bookDboRepository; + } + + @Override + @Transactional + public Void handle(DeleteBookCommand request) throws MediatorException { + bookDboRepository.deleteByUniqueId(request.getUniqueId()); + return null; + } + +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBookHandler.java b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBookHandler.java new file mode 100644 index 0000000..1853fe6 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBookHandler.java @@ -0,0 +1,25 @@ +package si.zpiz.sample.infrastructure.book_related.command_query; + +import java.util.Optional; + +import org.springframework.stereotype.Service; + +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.infrastructure.book_related.BookDboRepository; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.IMediatorHandler; + +@Service +public class GetBookHandler implements IMediatorHandler> { + private final BookDboRepository bookDboRepository; + + public GetBookHandler(BookDboRepository bookDboRepository) { + this.bookDboRepository = bookDboRepository; + } + + @Override + public Optional handle(GetBookQuery request) throws MediatorException { + return bookDboRepository.findByUniqueId(request.getUniqueId()); + } + +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBookQuery.java b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBookQuery.java new file mode 100644 index 0000000..51a4733 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBookQuery.java @@ -0,0 +1,19 @@ +package si.zpiz.sample.infrastructure.book_related.command_query; + +import java.util.Optional; +import java.util.UUID; + +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.infrastructure.mediator.IMediatorRequest; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GetBookQuery implements IMediatorRequest> { + @NotNull + private UUID uniqueId; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBooksHandler.java b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBooksHandler.java new file mode 100644 index 0000000..3603158 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBooksHandler.java @@ -0,0 +1,46 @@ +package si.zpiz.sample.infrastructure.book_related.command_query; + +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Service; + +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.infrastructure.book_related.BookDboRepository; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.IMediatorHandler; + +@Service +public class GetBooksHandler implements IMediatorHandler> { + private final BookDboRepository bookDboRepository; + + public GetBooksHandler(BookDboRepository bookDboRepository) { + this.bookDboRepository = bookDboRepository; + } + + @Override + public Page handle(GetBooksQuery request) throws MediatorException { + if (request.getMode() == GetBooksQueryMode.BY_TITLE_CONTAINING_IGNORE_CASE) { + if (request.getQuery() == null || request.getQuery().isEmpty()) { + throw new MediatorException("Query string is empty"); + } + + return bookDboRepository.findByTitleContainingIgnoreCase(request.getQuery(), + request.getPaged().getPageable()); + } + + if (request.getMode() == GetBooksQueryMode.STRINGS_CONTAINING_IGNORE_CASE) { + if (request.getQuery() == null || request.getQuery().isEmpty()) { + throw new MediatorException("Query string is empty"); + } + + return bookDboRepository.findByStringsContainingIgnoreCase(request.getQuery(), + request.getPaged().getPageable()); + } + + if (request.getMode() == GetBooksQueryMode.ALL) { + return bookDboRepository.findAll(request.getPaged().getPageable()); + } + + throw new MediatorException("Unknown query mode"); + } + +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBooksQuery.java b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBooksQuery.java new file mode 100644 index 0000000..480c35a --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBooksQuery.java @@ -0,0 +1,25 @@ +package si.zpiz.sample.infrastructure.book_related.command_query; + +import org.springframework.data.domain.Page; + +import jakarta.validation.Valid; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.domain.misc.Paged; +import si.zpiz.sample.infrastructure.mediator.IMediatorRequest; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class GetBooksQuery implements IMediatorRequest> { + /** + * Must not be empty if mode is BY_TITLE_CONTAINING_IGNORE_CASE or + * STRINGS_CONTAINING_IGNORE_CASE. + */ + private String query; + private GetBooksQueryMode mode; + @Valid + private Paged paged; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBooksQueryMode.java b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBooksQueryMode.java new file mode 100644 index 0000000..dec2696 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/GetBooksQueryMode.java @@ -0,0 +1,7 @@ +package si.zpiz.sample.infrastructure.book_related.command_query; + +public enum GetBooksQueryMode { + BY_TITLE_CONTAINING_IGNORE_CASE, + STRINGS_CONTAINING_IGNORE_CASE, + ALL +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/UpdateBookCommand.java b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/UpdateBookCommand.java new file mode 100644 index 0000000..042f07a --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/UpdateBookCommand.java @@ -0,0 +1,35 @@ +package si.zpiz.sample.infrastructure.book_related.command_query; + +import java.util.UUID; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.infrastructure.mediator.IMediatorRequest; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class UpdateBookCommand implements IMediatorRequest { + @NotBlank + @Size(min = 5, max = 20) + private String title; + + @Min(2000) + private int year; + + @NotBlank + @Size(min = 5, max = 20) + private String publisher; + + // Optional + private UUID authorUniqueId; + + @NotNull + private UUID uniqueId; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/UpdateBookHandler.java b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/UpdateBookHandler.java new file mode 100644 index 0000000..5eef442 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/book_related/command_query/UpdateBookHandler.java @@ -0,0 +1,47 @@ +package si.zpiz.sample.infrastructure.book_related.command_query; + +import org.springframework.stereotype.Service; + +import jakarta.persistence.EntityNotFoundException; +import jakarta.transaction.Transactional; +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.infrastructure.author_related.AuthorDboRepository; +import si.zpiz.sample.infrastructure.book_related.BookDboRepository; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.IMediatorHandler; + +@Service +public class UpdateBookHandler implements IMediatorHandler { + private final BookDboRepository bookDboRepository; + private AuthorDboRepository authorDboRepository; + + public UpdateBookHandler(BookDboRepository bookDboRepository, AuthorDboRepository authorDboRepository) { + this.bookDboRepository = bookDboRepository; + this.authorDboRepository = authorDboRepository; + } + + @Transactional + @Override + public BookDbo handle(UpdateBookCommand request) throws MediatorException { + BookDbo dbo = bookDboRepository.findByUniqueId(request.getUniqueId()) + .orElseThrow(() -> new EntityNotFoundException( + String.format("Book with unique id %s was not found", request.getUniqueId()))); + + AuthorDbo author = null; + if (request.getAuthorUniqueId() != null) { + author = authorDboRepository.findByUniqueId(request.getAuthorUniqueId()) + .orElseThrow(() -> new EntityNotFoundException( + String.format("Author with unique id %s was not found", request.getAuthorUniqueId()))); + } + + dbo.setTitle(request.getTitle()); + dbo.setAuthor(author); + dbo.setYear(request.getYear()); + dbo.setPublisher(request.getPublisher()); + + dbo = bookDboRepository.save(dbo); + return dbo; + } + +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/exceptions/MediatorException.java b/src/main/java/si/zpiz/sample/infrastructure/exceptions/MediatorException.java new file mode 100644 index 0000000..1b1c88b --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/exceptions/MediatorException.java @@ -0,0 +1,15 @@ +package si.zpiz.sample.infrastructure.exceptions; + +public class MediatorException extends Exception { + public MediatorException() { + super(); + } + + public MediatorException(String message) { + super(message); + } + + public MediatorException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/initialization_related/InitializeSampleDataCommand.java b/src/main/java/si/zpiz/sample/infrastructure/initialization_related/InitializeSampleDataCommand.java new file mode 100644 index 0000000..86201f3 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/initialization_related/InitializeSampleDataCommand.java @@ -0,0 +1,10 @@ +package si.zpiz.sample.infrastructure.initialization_related; + +import lombok.Data; +import lombok.NoArgsConstructor; +import si.zpiz.sample.infrastructure.mediator.IMediatorRequest; + +@Data +@NoArgsConstructor +public class InitializeSampleDataCommand implements IMediatorRequest { +} \ No newline at end of file diff --git a/src/main/java/si/zpiz/sample/infrastructure/initialization_related/InitializeSampleDataHandler.java b/src/main/java/si/zpiz/sample/infrastructure/initialization_related/InitializeSampleDataHandler.java new file mode 100644 index 0000000..f498a95 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/initialization_related/InitializeSampleDataHandler.java @@ -0,0 +1,53 @@ +package si.zpiz.sample.infrastructure.initialization_related; + +import org.springframework.stereotype.Service; + +import jakarta.transaction.Transactional; +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.infrastructure.author_related.command_query.CreateAuthorCommand; +import si.zpiz.sample.infrastructure.book_related.command_query.CreateBookCommand; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.IMediatorHandler; +import si.zpiz.sample.infrastructure.mediator.Mediator; +import si.zpiz.sample.infrastructure.user_details_related.command_query.CreateUserDetailsCommand; + +@Service +public class InitializeSampleDataHandler implements IMediatorHandler { + private Mediator mediator; + + public InitializeSampleDataHandler(Mediator mediator) { + this.mediator = mediator; + } + + @Override + @Transactional + public Void handle(InitializeSampleDataCommand request) throws MediatorException { + CreateAuthorCommand createAuthorCommand = new CreateAuthorCommand("John", "Doe"); + AuthorDbo author = mediator.send(createAuthorCommand); + + for (int i = 0; i < 20; i++) { + CreateBookCommand createBookCommand = new CreateBookCommand( + "Title " + i, + 2000 + i, + "Publisher " + i, + author.getUniqueId()); + + mediator.send(createBookCommand); + } + + CreateUserDetailsCommand createAdminUserDetailsCommand = new CreateUserDetailsCommand( + "admin", + "password", + java.util.Arrays.asList("ADMIN", "USER")); + mediator.send(createAdminUserDetailsCommand); + + CreateUserDetailsCommand createUserUserDetailsCommand = new CreateUserDetailsCommand( + "user", + "password", + java.util.Arrays.asList("USER")); + mediator.send(createUserUserDetailsCommand); + + return null; + } + +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/mediator/IMediatorHandler.java b/src/main/java/si/zpiz/sample/infrastructure/mediator/IMediatorHandler.java new file mode 100644 index 0000000..8568ab8 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/mediator/IMediatorHandler.java @@ -0,0 +1,10 @@ +package si.zpiz.sample.infrastructure.mediator; + +import si.zpiz.sample.infrastructure.exceptions.MediatorException; + +/** + * Handlers must be singleton Services. + */ +public interface IMediatorHandler, TResponse> { + public TResponse handle(TRequest request) throws MediatorException; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/mediator/IMediatorRequest.java b/src/main/java/si/zpiz/sample/infrastructure/mediator/IMediatorRequest.java new file mode 100644 index 0000000..5c2455d --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/mediator/IMediatorRequest.java @@ -0,0 +1,5 @@ +package si.zpiz.sample.infrastructure.mediator; + +public interface IMediatorRequest { + public String toString(); +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/mediator/Mediator.java b/src/main/java/si/zpiz/sample/infrastructure/mediator/Mediator.java new file mode 100644 index 0000000..f8dc45f --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/mediator/Mediator.java @@ -0,0 +1,71 @@ +package si.zpiz.sample.infrastructure.mediator; + +import java.lang.reflect.Method; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.context.ApplicationContext; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; + +@Service +@Slf4j +public class Mediator { + + private ApplicationContext applicationContext; + private ConcurrentHashMap, IMediatorHandler> handlerCache = new ConcurrentHashMap<>(); + + public Mediator(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @SuppressWarnings("all") + public , TResponse> TResponse send( + IMediatorRequest request) throws MediatorException { + + Object bean = handlerCache.get(request.getClass()); + + if (bean == null) { + Map beans = applicationContext.getBeansOfType(IMediatorHandler.class); + for (IMediatorHandler handler : beans.values()) { + Method[] methods = handler.getClass().getMethods(); + for (Method method : methods) { + if (method.getName().equals("handle") + && method.getParameterTypes()[0].equals(request.getClass())) { + handlerCache.put(request.getClass(), handler); + bean = handler; + } + } + } + } + + if (bean != null) { + return handleSend(request, + (IMediatorHandler, TResponse>) bean); + } + + throw new MediatorException("Handler not found for request: " + request.getClass().getName()); + } + + private TResponse handleSend(IMediatorRequest request, + IMediatorHandler, TResponse> handler) + throws MediatorException { + if (log.isDebugEnabled()) { + log.debug("Handling request: {} {}", request.getClass().getName(), request.toString()); + } + + TResponse response = handler.handle(request); + + if (log.isDebugEnabled()) { + if (response != null) { + log.debug("Request handled: {} {}", response.getClass().getName(), response.toString()); + } else { + log.debug("Request handled: null"); + } + } + + return response; + } +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/misc/CustomAuthenticationEntryPoint.java b/src/main/java/si/zpiz/sample/infrastructure/misc/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..392df6b --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/misc/CustomAuthenticationEntryPoint.java @@ -0,0 +1,39 @@ +package si.zpiz.sample.infrastructure.misc; + +import java.io.IOException; +import org.springframework.http.HttpStatus; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import si.zpiz.sample.domain.misc.ErrorDto; + +@Slf4j +public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException, ServletException { + log.error("Authentication error", authException); + + ErrorDto errorDto = new ErrorDto(); + errorDto.setMessage(authException.getMessage()); + errorDto.setStatus(HttpStatus.UNAUTHORIZED.value()); + errorDto.setFromClass(authException.getStackTrace()[0].getClassName()); + errorDto.setControllerName(null); + errorDto.setMethodName(null); + + ObjectMapper mapper = new ObjectMapper(); + String json = mapper.writeValueAsString(errorDto); + + response.setContentType("application/json"); + response.setStatus(HttpStatus.UNAUTHORIZED.value()); + response.getWriter().write(json); + } + +} \ No newline at end of file diff --git a/src/main/java/si/zpiz/sample/infrastructure/misc/EnvironmentPropertiesPrinter.java b/src/main/java/si/zpiz/sample/infrastructure/misc/EnvironmentPropertiesPrinter.java new file mode 100644 index 0000000..176fa2a --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/misc/EnvironmentPropertiesPrinter.java @@ -0,0 +1,23 @@ +package si.zpiz.sample.infrastructure.misc; + +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import jakarta.annotation.PostConstruct; +import lombok.extern.slf4j.Slf4j; + +@Component +@Slf4j +public class EnvironmentPropertiesPrinter { + private Environment env; + + public EnvironmentPropertiesPrinter(Environment env) { + this.env = env; + } + + @PostConstruct + public void logApplicationProperties() throws Exception { + log.info("{}={}", "spring.jpa.hibernate.ddl-auto", env.getProperty("spring.jpa.hibernate.ddl-auto")); + log.info("{}={}", "spring.profiles.active", env.getProperty("spring.profiles.active")); + } +} \ No newline at end of file diff --git a/src/main/java/si/zpiz/sample/infrastructure/misc/GeneralHelperService.java b/src/main/java/si/zpiz/sample/infrastructure/misc/GeneralHelperService.java new file mode 100644 index 0000000..612dc70 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/misc/GeneralHelperService.java @@ -0,0 +1,11 @@ +package si.zpiz.sample.infrastructure.misc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class GeneralHelperService { + @Autowired + @SuppressWarnings("unused") + private EnvironmentPropertiesPrinter environmentPropertiesPrinter; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/misc/GlobalExceptionHandler.java b/src/main/java/si/zpiz/sample/infrastructure/misc/GlobalExceptionHandler.java new file mode 100644 index 0000000..1982681 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/misc/GlobalExceptionHandler.java @@ -0,0 +1,101 @@ +package si.zpiz.sample.infrastructure.misc; + +import java.util.stream.Collectors; + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.HandlerMethod; + +import jakarta.persistence.EntityNotFoundException; +import jakarta.servlet.http.HttpServletRequest; +import lombok.extern.slf4j.Slf4j; +import si.zpiz.sample.domain.misc.ErrorDto; + +@RestControllerAdvice +@Slf4j +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidationErrors(MethodArgumentNotValidException ex, + HandlerMethod handlerMethod, HttpServletRequest request) { + log.error("Validation error", ex); + + String controllerName = handlerMethod.getMethod().getDeclaringClass().getName(); + String methodName = handlerMethod.getMethod().getName(); + + String message = ex.getFieldErrors().stream() + .map(fe -> fe.getField() + " " + fe.getDefaultMessage()) + .collect(Collectors.joining(", ")); + + ErrorDto errorDto = new ErrorDto(); + errorDto.setMessage(message); + errorDto.setStatus(HttpStatus.BAD_REQUEST.value()); + errorDto.setFromClass(ex.getStackTrace()[0].getClassName()); + errorDto.setControllerName(controllerName); + errorDto.setMethodName(methodName); + + return new ResponseEntity<>(errorDto, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(EntityNotFoundException.class) + public ResponseEntity handleValidationErrors(EntityNotFoundException ex, + HandlerMethod handlerMethod, HttpServletRequest request) { + log.error("Entity not found error", ex); + + String controllerName = handlerMethod.getMethod().getDeclaringClass().getName(); + String methodName = handlerMethod.getMethod().getName(); + + ErrorDto errorDto = new ErrorDto(); + errorDto.setMessage(ex.getMessage()); + errorDto.setStatus(HttpStatus.NOT_FOUND.value()); + errorDto.setFromClass(ex.getStackTrace()[0].getClassName()); + errorDto.setControllerName(controllerName); + errorDto.setMethodName(methodName); + + return new ResponseEntity<>(errorDto, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(AccessDeniedException.class) + public ResponseEntity handleAccessDeniedException(AccessDeniedException ex, + HandlerMethod handlerMethod, HttpServletRequest request) { + log.error("Access denied error", ex); + + String controllerName = handlerMethod.getMethod().getDeclaringClass().getName(); + String methodName = handlerMethod.getMethod().getName(); + + ErrorDto errorDto = new ErrorDto(); + errorDto.setMessage(ex.getMessage()); + errorDto.setStatus(HttpStatus.FORBIDDEN.value()); + errorDto.setFromClass(ex.getStackTrace()[0].getClassName()); + errorDto.setControllerName(controllerName); + errorDto.setMethodName(methodName); + + return new ResponseEntity<>(errorDto, HttpStatus.FORBIDDEN); + } + + // ***** + // AuthenticationException is handled by CustomAuthenticationEntryPoint + // ***** + + @ExceptionHandler(Exception.class) + public ResponseEntity generalExceptionHandler(Exception ex, HandlerMethod handlerMethod, + HttpServletRequest request) { + log.error("General error", ex); + + String controllerName = handlerMethod.getMethod().getDeclaringClass().getName(); + String methodName = handlerMethod.getMethod().getName(); + + ErrorDto errorDto = new ErrorDto(); + errorDto.setMessage(ex.getMessage()); + errorDto.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); + errorDto.setFromClass(ex.getStackTrace()[0].getClassName()); + errorDto.setControllerName(controllerName); + errorDto.setMethodName(methodName); + + return new ResponseEntity(errorDto, HttpStatus.INTERNAL_SERVER_ERROR); + } +} \ No newline at end of file diff --git a/src/main/java/si/zpiz/sample/infrastructure/misc/RequestLoggingFilterConfig.java b/src/main/java/si/zpiz/sample/infrastructure/misc/RequestLoggingFilterConfig.java new file mode 100644 index 0000000..123db2a --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/misc/RequestLoggingFilterConfig.java @@ -0,0 +1,25 @@ +package si.zpiz.sample.infrastructure.misc; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.filter.CommonsRequestLoggingFilter; + +@Configuration +public class RequestLoggingFilterConfig { + + /* + * Enable the bean with + * org.springframework.web.filter.CommonsRequestLoggingFilter.level=DEBUG + */ + + @Bean + public CommonsRequestLoggingFilter logFilter() { + CommonsRequestLoggingFilter filter = new CommonsRequestLoggingFilter(); + filter.setIncludeQueryString(true); + filter.setIncludePayload(true); + filter.setMaxPayloadLength(10000); + filter.setIncludeHeaders(true); + filter.setAfterMessagePrefix("REQUEST DATA: "); + return filter; + } +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/user_details_related/UserDetailsDboRepository.java b/src/main/java/si/zpiz/sample/infrastructure/user_details_related/UserDetailsDboRepository.java new file mode 100644 index 0000000..cd844bc --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/user_details_related/UserDetailsDboRepository.java @@ -0,0 +1,13 @@ +package si.zpiz.sample.infrastructure.user_details_related; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.data.jpa.repository.JpaRepository; +import si.zpiz.sample.domain.user_details_related.UserDetailsDbo; + +public interface UserDetailsDboRepository extends JpaRepository { + Optional findByUsernameIgnoreCase(String username); + + Optional findByUniqueId(UUID uniqueId); +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/user_details_related/command_query/CreateUserDetailsCommand.java b/src/main/java/si/zpiz/sample/infrastructure/user_details_related/command_query/CreateUserDetailsCommand.java new file mode 100644 index 0000000..c5f5a01 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/user_details_related/command_query/CreateUserDetailsCommand.java @@ -0,0 +1,22 @@ +package si.zpiz.sample.infrastructure.user_details_related.command_query; + +import java.util.List; + +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import si.zpiz.sample.domain.user_details_related.UserDetailsDbo; +import si.zpiz.sample.infrastructure.mediator.IMediatorRequest; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class CreateUserDetailsCommand implements IMediatorRequest { + @NotEmpty + private String username; + @NotEmpty + private String password; + @NotEmpty + private List authorities; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/user_details_related/command_query/CreateUserDetailsHandler.java b/src/main/java/si/zpiz/sample/infrastructure/user_details_related/command_query/CreateUserDetailsHandler.java new file mode 100644 index 0000000..a26a538 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/user_details_related/command_query/CreateUserDetailsHandler.java @@ -0,0 +1,39 @@ +package si.zpiz.sample.infrastructure.user_details_related.command_query; + +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import jakarta.transaction.Transactional; +import si.zpiz.sample.domain.persistence.GrantedAuthorityImpl; +import si.zpiz.sample.domain.user_details_related.UserDetailsDbo; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.IMediatorHandler; +import si.zpiz.sample.infrastructure.user_details_related.UserDetailsDboRepository; + +@Service +public class CreateUserDetailsHandler implements IMediatorHandler { + + private UserDetailsDboRepository userDetailsDboRepository; + private BCryptPasswordEncoder bCryptPasswordEncoder; + + public CreateUserDetailsHandler(UserDetailsDboRepository userDetailsDboRepository, + BCryptPasswordEncoder bCryptPasswordEncoder) { + this.userDetailsDboRepository = userDetailsDboRepository; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; + } + + @Override + @Transactional + public UserDetailsDbo handle(CreateUserDetailsCommand request) throws MediatorException { + UserDetailsDbo userDetailsDbo = new UserDetailsDbo(); + userDetailsDbo.setUsername(request.getUsername()); + userDetailsDbo.setPassword(bCryptPasswordEncoder.encode(request.getPassword())); + userDetailsDbo.setAuthorities(request.getAuthorities().stream().map(GrantedAuthorityImpl::new) + .collect(java.util.stream.Collectors.toList())); + userDetailsDbo.setEnabled(true); + + userDetailsDbo = userDetailsDboRepository.save(userDetailsDbo); + return userDetailsDbo; + } + +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/user_related/command_query/AuthenticateCommand.java b/src/main/java/si/zpiz/sample/infrastructure/user_related/command_query/AuthenticateCommand.java new file mode 100644 index 0000000..8736e90 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/user_related/command_query/AuthenticateCommand.java @@ -0,0 +1,18 @@ +package si.zpiz.sample.infrastructure.user_related.command_query; + +import org.springframework.security.oauth2.jwt.Jwt; +import jakarta.validation.constraints.NotEmpty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import si.zpiz.sample.infrastructure.mediator.IMediatorRequest; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class AuthenticateCommand implements IMediatorRequest { + @NotEmpty + private String username; + @NotEmpty + private String password; +} diff --git a/src/main/java/si/zpiz/sample/infrastructure/user_related/command_query/AuthenticateHandler.java b/src/main/java/si/zpiz/sample/infrastructure/user_related/command_query/AuthenticateHandler.java new file mode 100644 index 0000000..565dbf4 --- /dev/null +++ b/src/main/java/si/zpiz/sample/infrastructure/user_related/command_query/AuthenticateHandler.java @@ -0,0 +1,69 @@ +package si.zpiz.sample.infrastructure.user_related.command_query; + +import java.time.Instant; +import java.util.stream.Collectors; + +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.jwt.JwtClaimsSet; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.JwtEncoderParameters; +import org.springframework.stereotype.Service; + +import lombok.extern.slf4j.Slf4j; +import si.zpiz.sample.domain.user_details_related.UserDetailsDbo; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.IMediatorHandler; +import si.zpiz.sample.infrastructure.user_details_related.UserDetailsDboRepository; + +@Service +@Slf4j +public class AuthenticateHandler implements IMediatorHandler { + + private BCryptPasswordEncoder bCryptPasswordEncoder; + private JwtEncoder encoder; + private UserDetailsDboRepository userDetailsDboRepository; + + public AuthenticateHandler(UserDetailsDboRepository userDetailsDboRepository, + BCryptPasswordEncoder bCryptPasswordEncoder, + JwtEncoder encoder) { + this.userDetailsDboRepository = userDetailsDboRepository; + this.bCryptPasswordEncoder = bCryptPasswordEncoder; + this.encoder = encoder; + } + + @Override + public Jwt handle(AuthenticateCommand request) throws MediatorException { + log.debug("username: {}", request.getUsername()); + + UserDetailsDbo user = userDetailsDboRepository.findByUsernameIgnoreCase(request.getUsername()) + .orElseThrow(() -> new UsernameNotFoundException( + String.format("User %s was not found", request.getUsername()))); + + // TODO check if user is enabled etc. + + if (bCryptPasswordEncoder.matches(request.getPassword(), user.getPassword())) { + Instant now = Instant.now(); + long expiry = 36000L; + String scope = user.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(" ")); + JwtClaimsSet claims = JwtClaimsSet.builder() + .issuer("self") + .issuedAt(now) + .expiresAt(now.plusSeconds(expiry)) + .subject(request.getUsername()) + // the scope claim is a standard way of conveying the authorities granted to the + // user in spring security + .claim("scope", scope) + .build(); + return this.encoder.encode(JwtEncoderParameters.from(claims)); + } + + throw new BadCredentialsException("Bad credentials"); + } + +} diff --git a/src/main/java/si/zpiz/sample/webapi/WebApi.java b/src/main/java/si/zpiz/sample/webapi/WebApi.java new file mode 100644 index 0000000..111f650 --- /dev/null +++ b/src/main/java/si/zpiz/sample/webapi/WebApi.java @@ -0,0 +1,12 @@ +package si.zpiz.sample.webapi; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class WebApi { + public static void main(String[] args) { + SpringApplication.run(WebApi.class, args); + } +} + diff --git a/src/main/java/si/zpiz/sample/webapi/author_related/AuthorController.java b/src/main/java/si/zpiz/sample/webapi/author_related/AuthorController.java new file mode 100644 index 0000000..1ddd91d --- /dev/null +++ b/src/main/java/si/zpiz/sample/webapi/author_related/AuthorController.java @@ -0,0 +1,104 @@ +package si.zpiz.sample.webapi.author_related; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.domain.author_related.AuthorDto; +import si.zpiz.sample.domain.author_related.AuthorDtoMapper; +import si.zpiz.sample.domain.misc.ErrorDto; +import si.zpiz.sample.infrastructure.author_related.command_query.CreateAuthorCommand; +import si.zpiz.sample.infrastructure.author_related.command_query.DeleteAuthorCommand; +import si.zpiz.sample.infrastructure.author_related.command_query.GetAuthorQuery; +import si.zpiz.sample.infrastructure.author_related.command_query.UpdateAuthorCommand; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.Mediator; + +@RestController +@RequestMapping("/api/authors") +@ApiResponses(value = { + @ApiResponse(responseCode = "400", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "401", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "403", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "404", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "500", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }) +}) +public class AuthorController { + @Autowired + private AuthorDtoMapper authorDtoMapper; + + @Autowired + private Mediator mediator; + + @PostMapping(path = "/createAuthor") + @ApiResponse(responseCode = "201") + public ResponseEntity createAuthor(@RequestBody @Valid CreateAuthorCommand author) + throws MediatorException { + AuthorDbo dbo = mediator.send(author); + AuthorDto dto = authorDtoMapper.fromDbo(dbo); + + return new ResponseEntity<>(dto, HttpStatus.CREATED); + } + + @GetMapping(path = "/getAuthor") + @ApiResponse(responseCode = "200") + public ResponseEntity getAuthor(@RequestParam @NotNull UUID uniqueId) throws MediatorException { + Optional dbo = mediator.send(new GetAuthorQuery(uniqueId)); + Optional dto = dbo.map(book -> authorDtoMapper.fromDbo(book)); + + if (dto.isEmpty()) { + ErrorDto error = new ErrorDto(); + error.setMessage("Author not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + return new ResponseEntity<>(dto.get(), HttpStatus.OK); + } + + @PutMapping(path = "/updateAuthor") + @ApiResponse(responseCode = "200") + public ResponseEntity updateAuthor(@RequestBody @Valid UpdateAuthorCommand author) + throws MediatorException { + AuthorDbo dbo = mediator.send(author); + AuthorDto dto = authorDtoMapper.fromDbo(dbo); + + return new ResponseEntity<>(dto, HttpStatus.OK); + } + + @DeleteMapping(path = "/deleteAuthor") + @PreAuthorize("hasAuthority('ADMIN')") + @ApiResponse(responseCode = "200") + public ResponseEntity deleteAuthor(@RequestParam @NotNull UUID uniqueId) throws MediatorException { + mediator.send(new DeleteAuthorCommand(uniqueId)); + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/src/main/java/si/zpiz/sample/webapi/book_related/BookController.java b/src/main/java/si/zpiz/sample/webapi/book_related/BookController.java new file mode 100644 index 0000000..b6df4cb --- /dev/null +++ b/src/main/java/si/zpiz/sample/webapi/book_related/BookController.java @@ -0,0 +1,104 @@ +package si.zpiz.sample.webapi.book_related; + +import java.util.Optional; +import java.util.UUID; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotNull; +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.domain.book_related.BookDto; +import si.zpiz.sample.domain.book_related.BookDtoMapper; +import si.zpiz.sample.domain.misc.ErrorDto; +import si.zpiz.sample.infrastructure.book_related.command_query.CreateBookCommand; +import si.zpiz.sample.infrastructure.book_related.command_query.GetBookQuery; +import si.zpiz.sample.infrastructure.book_related.command_query.GetBooksQuery; +import si.zpiz.sample.infrastructure.book_related.command_query.UpdateBookCommand; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.Mediator; + +@RestController +@RequestMapping("/api/books") +@ApiResponses(value = { + @ApiResponse(responseCode = "400", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "401", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "403", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "404", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "500", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }) +}) +public class BookController { + @Autowired + private BookDtoMapper bookDtoMapper; + + @Autowired + private Mediator mediator; + + @PostMapping(path = "/createBook") + @ApiResponse(responseCode = "201") + public ResponseEntity createBook(@RequestBody @Valid CreateBookCommand book) throws MediatorException { + BookDbo dbo = mediator.send(book); + BookDto dto = bookDtoMapper.fromDbo(dbo); + + return new ResponseEntity<>(dto, HttpStatus.CREATED); + } + + @PutMapping(path = "/updateBook") + @ApiResponse(responseCode = "200") + public ResponseEntity updateBook(@RequestBody @Valid UpdateBookCommand book) throws MediatorException { + BookDbo dbo = mediator.send(book); + BookDto dto = bookDtoMapper.fromDbo(dbo); + + return new ResponseEntity<>(dto, HttpStatus.OK); + } + + @GetMapping(path = "/getBook") + @ApiResponse(responseCode = "200") + public ResponseEntity getBook(@RequestParam @NotNull UUID uniqueId) throws MediatorException { + Optional dbo = mediator.send(new GetBookQuery(uniqueId)); + Optional dto = dbo.map(book -> bookDtoMapper.fromDbo(book)); + + if (dto.isEmpty()) { + ErrorDto error = new ErrorDto(); + error.setMessage("Book not found"); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + return new ResponseEntity<>(dto.get(), HttpStatus.OK); + } + + @PostMapping(path = "/getBooks") + @ApiResponse(responseCode = "200") + public ResponseEntity> getBooks(@RequestBody @Valid GetBooksQuery query) + throws MediatorException { + Page dbos = mediator.send(query); + Page dtos = dbos.map(book -> bookDtoMapper.fromDbo(book)); + + return new ResponseEntity<>(dtos, HttpStatus.OK); + } +} \ No newline at end of file diff --git a/src/main/java/si/zpiz/sample/webapi/config/SecurityConfig.java b/src/main/java/si/zpiz/sample/webapi/config/SecurityConfig.java new file mode 100644 index 0000000..9a6d2ca --- /dev/null +++ b/src/main/java/si/zpiz/sample/webapi/config/SecurityConfig.java @@ -0,0 +1,126 @@ +package si.zpiz.sample.webapi.config; + +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.util.Collection; + +import com.nimbusds.jose.jwk.JWK; +import com.nimbusds.jose.jwk.JWKSet; +import com.nimbusds.jose.jwk.RSAKey; +import com.nimbusds.jose.jwk.source.ImmutableJWKSet; +import com.nimbusds.jose.jwk.source.JWKSource; +import com.nimbusds.jose.proc.SecurityContext; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.security.SecurityRequirement; +import io.swagger.v3.oas.models.security.SecurityScheme; +import si.zpiz.sample.infrastructure.misc.CustomAuthenticationEntryPoint; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.convert.converter.Converter; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.oauth2.jwt.JwtDecoder; +import org.springframework.security.oauth2.jwt.JwtEncoder; +import org.springframework.security.oauth2.jwt.NimbusJwtDecoder; +import org.springframework.security.oauth2.jwt.NimbusJwtEncoder; +import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter; +import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.oauth2.jwt.Jwt; + +@Configuration +@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true, prePostEnabled = true) +public class SecurityConfig { + + @Value("${jwt.public.key}") + public RSAPublicKey key; + + @Value("${jwt.private.key}") + public RSAPrivateKey priv; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .authorizeHttpRequests((authorize) -> authorize + .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", + "/api/authentication/token", + "/api/initialization/initializeSampleData", + "/h2-console/**") + .permitAll() + .anyRequest().authenticated()) + .csrf((csrf) -> csrf.disable()) + // .csrf((csrf) -> .ignoringRequestMatchers("/api/authentication/token", + // "/api/initialization/initializeSampleData")) + .httpBasic(Customizer.withDefaults()) + .oauth2ResourceServer((oauth2) -> oauth2 + .jwt(Customizer.withDefaults())) + .sessionManagement((session) -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .exceptionHandling((exceptions) -> exceptions + .authenticationEntryPoint(new CustomAuthenticationEntryPoint())); + return http.build(); + } + + @Bean + public JwtDecoder jwtDecoder() { + return NimbusJwtDecoder.withPublicKey(this.key).build(); + } + + @Bean + public JwtEncoder jwtEncoder() { + JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build(); + JWKSource jwks = new ImmutableJWKSet<>(new JWKSet(jwk)); + return new NimbusJwtEncoder(jwks); + } + + @Bean + public BCryptPasswordEncoder bCryptPasswordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public Converter> jwtGrantedAuthoritiesConverter() { + JwtGrantedAuthoritiesConverter converter = new JwtGrantedAuthoritiesConverter(); + // make sure to remove prefix from authority so it is easier to use, e.g. + // @PreAuthorize("hasAuthority('ADMIN')") for a given ADMIN authority + converter.setAuthorityPrefix(""); + return converter; + } + + @Bean + public JwtAuthenticationConverter customJwtAuthenticationConverter() { + JwtAuthenticationConverter converter = new JwtAuthenticationConverter(); + converter.setJwtGrantedAuthoritiesConverter(jwtGrantedAuthoritiesConverter()); + return converter; + } + + @Bean + public OpenAPI apiInfo() { + final var securitySchemeName = "bearerAuth"; + return new OpenAPI() + .addSecurityItem(new SecurityRequirement().addList(securitySchemeName)) + .components( + new Components() + .addSecuritySchemes( + securitySchemeName, + new SecurityScheme() + .name(securitySchemeName) + .type(SecurityScheme.Type.HTTP) + .scheme("bearer") + .bearerFormat("JWT"))) + .info( + new Info() + .title("Sample Rest Api") + .description("Rest Api for sample web application") + .version("1.0")); + } +} diff --git a/src/main/java/si/zpiz/sample/webapi/config/WebApiConfig.java b/src/main/java/si/zpiz/sample/webapi/config/WebApiConfig.java new file mode 100644 index 0000000..d9ea3ab --- /dev/null +++ b/src/main/java/si/zpiz/sample/webapi/config/WebApiConfig.java @@ -0,0 +1,21 @@ +package si.zpiz.sample.webapi.config; + +import org.springframework.boot.autoconfigure.domain.EntityScan; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.data.web.PageableHandlerMethodArgumentResolver; +import org.springframework.transaction.annotation.EnableTransactionManagement; + +@Configuration +@EnableJpaRepositories({ "si.zpiz.sample.*" }) +@EnableTransactionManagement +@ComponentScan("si.zpiz.sample.*") +@EntityScan(basePackages = "si.zpiz.sample.*") +public class WebApiConfig { + @Bean + public PageableHandlerMethodArgumentResolver pageableResolver() { + return new PageableHandlerMethodArgumentResolver(); + } +} diff --git a/src/main/java/si/zpiz/sample/webapi/initialization_related/InitializationController.java b/src/main/java/si/zpiz/sample/webapi/initialization_related/InitializationController.java new file mode 100644 index 0000000..fb3ff0c --- /dev/null +++ b/src/main/java/si/zpiz/sample/webapi/initialization_related/InitializationController.java @@ -0,0 +1,52 @@ +package si.zpiz.sample.webapi.initialization_related; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import si.zpiz.sample.domain.misc.ErrorDto; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.initialization_related.InitializeSampleDataCommand; +import si.zpiz.sample.infrastructure.mediator.Mediator; + +@RestController +@RequestMapping("/api/initialization") +@ApiResponses(value = { + @ApiResponse(responseCode = "400", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "401", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "403", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "404", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "500", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }) +}) +public class InitializationController { + @Autowired + private Mediator mediator; + + @PostMapping(path = "/initializeSampleData") + @ApiResponse(responseCode = "200") + public ResponseEntity initializeSampleData(@RequestBody @Valid InitializeSampleDataCommand cmd) + throws MediatorException { + mediator.send(cmd); + + return new ResponseEntity<>(HttpStatus.OK); + } +} diff --git a/src/main/java/si/zpiz/sample/webapi/user_related/AuthenticationController.java b/src/main/java/si/zpiz/sample/webapi/user_related/AuthenticationController.java new file mode 100644 index 0000000..b93a6a2 --- /dev/null +++ b/src/main/java/si/zpiz/sample/webapi/user_related/AuthenticationController.java @@ -0,0 +1,64 @@ +package si.zpiz.sample.webapi.user_related; + +import java.util.Collection; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.oauth2.jwt.Jwt; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import jakarta.validation.Valid; +import si.zpiz.sample.domain.misc.ErrorDto; +import si.zpiz.sample.domain.user_related.AuthenticationTokenDto; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.Mediator; +import si.zpiz.sample.infrastructure.user_related.command_query.AuthenticateCommand; + +@RestController +@RequestMapping("/api/authentication") +@ApiResponses(value = { + @ApiResponse(responseCode = "400", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "401", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "403", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "404", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }), + @ApiResponse(responseCode = "500", content = { + @Content(mediaType = "application/json", schema = @Schema(implementation = ErrorDto.class)) + }) +}) +public class AuthenticationController { + @Autowired + private Mediator mediator; + + @PostMapping("/token") + @ApiResponse(responseCode = "200") + public ResponseEntity token(@RequestBody @Valid AuthenticateCommand command) + throws MediatorException { + Jwt jwt = mediator.send(command); + return ResponseEntity.ok(new AuthenticationTokenDto(jwt.getTokenValue())); + } + + @GetMapping("/getCurrentAuthorities") + @ApiResponse(responseCode = "200") + public ResponseEntity> getCurrentAuthorities() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + return ResponseEntity.ok(authentication.getAuthorities()); + } +} diff --git a/src/main/resources/app.key b/src/main/resources/app.key new file mode 100644 index 0000000..5351007 --- /dev/null +++ b/src/main/resources/app.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA +iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM +g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK +LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF +oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc +3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn ++jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE +E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek +lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG +mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7 +62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0 +bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA ++Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH +Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA +8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd +I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY +QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d +rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk +HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA +Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN +HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a +FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF +snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H +c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM +TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR +47jndeyIaMTNETEmOnms+as17g== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/src/main/resources/app.pub b/src/main/resources/app.pub new file mode 100644 index 0000000..0b2ee7b --- /dev/null +++ b/src/main/resources/app.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FlqJr5TRskIQIgdE3Dd +7D9lboWdcTUT8a+fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRv +c5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4/1tfRgG6ii4Uhxh6 +iI8qNMJQX+fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2 +kJdJ/ZIV+WW4noDdzpKqHcwmB8FsrumlVY/DNVvUSDIipiq9PbP4H99TXN1o746o +RaNa07rq1hoCgMSSy+85SagCoxlmyE+D+of9SsMY8Ol9t0rdzpobBuhyJ/o5dfvj +KwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/src/main/resources/application-h2.properties b/src/main/resources/application-h2.properties new file mode 100644 index 0000000..42ca265 --- /dev/null +++ b/src/main/resources/application-h2.properties @@ -0,0 +1,12 @@ +# h2 database +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=update + +# h2 web console +spring.h2.console.enabled=true + diff --git a/src/main/resources/application-oracle.properties b/src/main/resources/application-oracle.properties new file mode 100644 index 0000000..dc498a2 --- /dev/null +++ b/src/main/resources/application-oracle.properties @@ -0,0 +1,8 @@ +# Oracle database +spring.datasource.url=jdbc:oracle:thin:@//localhost:1521/FREEPDB1 +spring.datasource.driverClassName=oracle.jdbc.OracleDriver +spring.datasource.username=javasample +spring.datasource.password=password@123! + +spring.jpa.database-platform=org.hibernate.dialect.OracleDialect +spring.jpa.hibernate.ddl-auto=update \ No newline at end of file diff --git a/src/main/resources/application-sqlserver.properties b/src/main/resources/application-sqlserver.properties new file mode 100644 index 0000000..5d6499d --- /dev/null +++ b/src/main/resources/application-sqlserver.properties @@ -0,0 +1,8 @@ +# SQL Server database +spring.datasource.url=jdbc:sqlserver://localhost:14333;databaseName=JavaSample;trustServerCertificate=True +spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver +spring.datasource.username=sa +spring.datasource.password=YourStrong@Passw0rd + +spring.jpa.database-platform=org.hibernate.dialect.SQLServerDialect +spring.jpa.hibernate.ddl-auto=update \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..534b01f --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,18 @@ +# spring boot debug mode +debug=false + +# database +spring.jpa.properties.hibernate.globally_quoted_identifiers=true +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# logging +logging.level.org.springframework.web.*=DEBUG +logging.level.org.springframework.http.*=DEBUG +logging.level.org.springframework.web.filter.CommonsRequestLoggingFilter=DEBUG +logging.level.si.zpiz.sample.infrastructure.mediator.Mediator=DEBUG +logging.level.org.springframework.jdbc.datasource.init.ScriptUtils=DEBUG + +# jwt +jwt.private.key: classpath:app.key +jwt.public.key: classpath:app.pub diff --git a/src/test/java/integration/IntegrationTestConfiguration.java b/src/test/java/integration/IntegrationTestConfiguration.java new file mode 100644 index 0000000..f76a937 --- /dev/null +++ b/src/test/java/integration/IntegrationTestConfiguration.java @@ -0,0 +1,17 @@ +package integration; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +import si.zpiz.sample.infrastructure.misc.EnvironmentPropertiesPrinter; + +@Configuration +@EnableAutoConfiguration +@ComponentScan("si.zpiz.sample.*") +public class IntegrationTestConfiguration { + @Autowired + @SuppressWarnings("unused") + private EnvironmentPropertiesPrinter environmentPropertiesPrinter; +} diff --git a/src/test/java/integration/author_related/AuthorControllerTest.java b/src/test/java/integration/author_related/AuthorControllerTest.java new file mode 100644 index 0000000..155df7a --- /dev/null +++ b/src/test/java/integration/author_related/AuthorControllerTest.java @@ -0,0 +1,40 @@ +package integration.author_related; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.util.LinkedMultiValueMap; + +import integration.IntegrationTestConfiguration; +import si.zpiz.sample.infrastructure.author_related.command_query.DeleteAuthorCommand; +import si.zpiz.sample.infrastructure.mediator.Mediator; + +@SpringBootTest +@AutoConfigureMockMvc +@ContextConfiguration(classes = IntegrationTestConfiguration.class) +public class AuthorControllerTest { + @Autowired + private MockMvc mockMvc; + + @MockBean + private Mediator mediator; + + public void deleteAuthor_Works() throws Exception { + when(mediator.send(any(DeleteAuthorCommand.class))).thenReturn(null); + + LinkedMultiValueMap requestParams = new LinkedMultiValueMap<>(); + requestParams.add("uniqueId", "29d1b123-b679-4ee5-8e22-2d4d89684244"); + + mockMvc.perform(delete("/api/authors/deleteAuthor") + .params(requestParams)) + .andExpect(status().isOk()); + } +} diff --git a/src/test/java/integration/author_related/DeleteAuthorTest.java b/src/test/java/integration/author_related/DeleteAuthorTest.java new file mode 100644 index 0000000..c285c61 --- /dev/null +++ b/src/test/java/integration/author_related/DeleteAuthorTest.java @@ -0,0 +1,55 @@ +package integration.author_related; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.Assert; + +import integration.IntegrationTestConfiguration; +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.infrastructure.author_related.command_query.CreateAuthorCommand; +import si.zpiz.sample.infrastructure.author_related.command_query.DeleteAuthorCommand; +import si.zpiz.sample.infrastructure.book_related.command_query.CreateBookCommand; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.Mediator; + +@SpringBootTest +@ContextConfiguration(classes = IntegrationTestConfiguration.class) +public class DeleteAuthorTest { + @Autowired + private Mediator mediator; + + @Test + private void deleting_Author_Sets_Book_Author_To_Null() throws MediatorException { + CreateAuthorCommand createAuthorCommand = new CreateAuthorCommand(); + AuthorDbo author = mediator.send(createAuthorCommand); + + List books = new ArrayList<>(); + for (int i = 0; i < 5; i++) { + CreateBookCommand createBookCommand = new CreateBookCommand(); + createBookCommand.setAuthorUniqueId(author.getUniqueId()); + createBookCommand.setPublisher("publisher"); + createBookCommand.setTitle("title"); + createBookCommand.setYear(2000); + BookDbo book = mediator.send(createBookCommand); + books.add(book); + + Assert.isTrue(book.getAuthor() != null, "AuthorDbo should not be null"); + Assert.isTrue(book.getRowVersion() == 0, "RowVersion should be 0"); + } + + DeleteAuthorCommand deleteAuthorCommand = new DeleteAuthorCommand(); + deleteAuthorCommand.setUniqueId(author.getUniqueId()); + mediator.send(deleteAuthorCommand); + + for (BookDbo book : books) { + Assert.isTrue(book.getAuthor() == null, "AuthorDbo should be null"); + Assert.isTrue(book.getRowVersion() == 1, "RowVersion should be 1"); + } + } +} diff --git a/src/test/java/integration/book_related/BookDboRepositoryTest.java b/src/test/java/integration/book_related/BookDboRepositoryTest.java new file mode 100644 index 0000000..3e7b071 --- /dev/null +++ b/src/test/java/integration/book_related/BookDboRepositoryTest.java @@ -0,0 +1,70 @@ +package integration.book_related; + +import java.util.Optional; +import java.util.UUID; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; + +import integration.IntegrationTestConfiguration; +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.infrastructure.book_related.BookDboRepository; + +@SpringBootTest +@ContextConfiguration(classes = IntegrationTestConfiguration.class) +@TestMethodOrder(MethodOrderer.MethodName.class) +public class BookDboRepositoryTest { + private static UUID uuid1; + + @Autowired + private BookDboRepository bookDboRepository; + + @BeforeAll + public static void beforeAll() { + uuid1 = UUID.randomUUID(); + } + + @Test + public void test1_create_BookDbo_Works() { + BookDbo dbo = new BookDbo(); + dbo.setUniqueId(uuid1); + dbo.setTitle("title"); + dbo.setAuthor(null); + dbo.setYear(2020); + dbo.setPublisher("publisher"); + + bookDboRepository.save(dbo); + + Assert.isTrue(bookDboRepository.findByUniqueId(uuid1).isPresent(), "book dbo was not saved"); + } + + @Test + public void test2_verify_BookDbo_Exists() { + Optional dbo = bookDboRepository.findByUniqueId(uuid1); + + Assert.isTrue(dbo.isPresent(), "book dbo was not saved"); + } + + /* This test should delete all books but then transaction should roll back */ + @Test + @Transactional + public void test3_delete_BookDbo_Works() { + bookDboRepository.deleteByUniqueId(uuid1); + Optional dbo = bookDboRepository.findByUniqueId(uuid1); + Assert.isTrue(dbo.isEmpty(), "book dbo was not deleted"); + } + + @Test + public void test4_verify_BookDbo_Exists() { + Optional dbo = bookDboRepository.findByUniqueId(uuid1); + + Assert.isTrue(dbo.isPresent(), "book dbo should exist"); + } +} diff --git a/src/test/java/integration/book_related/CreateBookTest.java b/src/test/java/integration/book_related/CreateBookTest.java new file mode 100644 index 0000000..2f6f435 --- /dev/null +++ b/src/test/java/integration/book_related/CreateBookTest.java @@ -0,0 +1,54 @@ +package integration.book_related; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.Assert; + +import integration.IntegrationTestConfiguration; +import si.zpiz.sample.domain.author_related.AuthorDbo; +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.infrastructure.author_related.command_query.CreateAuthorCommand; +import si.zpiz.sample.infrastructure.book_related.command_query.CreateBookCommand; +import si.zpiz.sample.infrastructure.exceptions.MediatorException; +import si.zpiz.sample.infrastructure.mediator.Mediator; + +@SpringBootTest +@ContextConfiguration(classes = IntegrationTestConfiguration.class) +public class CreateBookTest { + @Autowired + private Mediator mediator; + + @Test + public void create_WithoutAuthor_Works() throws MediatorException { + CreateBookCommand command = new CreateBookCommand(); + command.setAuthorUniqueId(null); + command.setPublisher("publisher"); + command.setTitle("title"); + command.setYear(2000); + + BookDbo dbo = mediator.send(command); + Assert.notNull(dbo, "BookDbo is null"); + Assert.isTrue(dbo.getId() != null, "BookDbo was not saved to db"); + } + + @Test + public void create_WithAuthor_Works() throws MediatorException { + CreateAuthorCommand authorCommand = new CreateAuthorCommand(); + authorCommand.setFirstName("first"); + authorCommand.setLastName("last"); + AuthorDbo author = mediator.send(authorCommand); + + CreateBookCommand command = new CreateBookCommand(); + command.setAuthorUniqueId(author.getUniqueId()); + command.setPublisher("publisher"); + command.setTitle("title"); + command.setYear(2000); + + BookDbo dbo = mediator.send(command); + Assert.notNull(dbo, "BookDbo is null"); + Assert.isTrue(dbo.getId() != null, "BookDbo was not saved to db"); + Assert.isTrue(dbo.getAuthor() != null, "AuthorDbo should not be null"); + } +} diff --git a/src/test/java/integration/user_related/AuthenticationControllerTest.java b/src/test/java/integration/user_related/AuthenticationControllerTest.java new file mode 100644 index 0000000..d29044e --- /dev/null +++ b/src/test/java/integration/user_related/AuthenticationControllerTest.java @@ -0,0 +1,66 @@ +package integration.user_related; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.hamcrest.Matchers.containsString; + +import java.util.Arrays; +import java.util.Optional; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import com.fasterxml.jackson.databind.ObjectMapper; + +import integration.IntegrationTestConfiguration; +import si.zpiz.sample.domain.persistence.GrantedAuthorityImpl; +import si.zpiz.sample.domain.user_details_related.UserDetailsDbo; +import si.zpiz.sample.infrastructure.user_details_related.UserDetailsDboRepository; +import si.zpiz.sample.infrastructure.user_related.command_query.AuthenticateCommand; + +@SpringBootTest +@AutoConfigureMockMvc +@ContextConfiguration(classes = IntegrationTestConfiguration.class) +public class AuthenticationControllerTest { + @Autowired + private MockMvc mockMvc; + + @Autowired + private BCryptPasswordEncoder passwordEncoder; + + @MockBean + private UserDetailsDboRepository userDetailsDboRepository; + + @Test + public void token_Works() throws Exception { + UserDetailsDbo userDetailsDbo = new UserDetailsDbo(); + userDetailsDbo.setUsername("admin"); + userDetailsDbo.setPassword(passwordEncoder.encode("password")); + userDetailsDbo.setEnabled(true); + userDetailsDbo + .setAuthorities(Arrays.asList(new GrantedAuthorityImpl("ADMIN"), new GrantedAuthorityImpl("USER"))); + + when(userDetailsDboRepository.findByUsernameIgnoreCase(any())).thenReturn(Optional.of(userDetailsDbo)); + + AuthenticateCommand command = new AuthenticateCommand(); + command.setUsername("admin"); + command.setPassword("password"); + + ObjectMapper objectMapper = new ObjectMapper(); + + mockMvc.perform(post("/api/authentication/token") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(command))) + .andExpect(status().isOk()) + .andExpect(content().string(containsString("token"))); + } +} diff --git a/src/test/java/unit/UnitTestConfiguration.java b/src/test/java/unit/UnitTestConfiguration.java new file mode 100644 index 0000000..b2db3a3 --- /dev/null +++ b/src/test/java/unit/UnitTestConfiguration.java @@ -0,0 +1,12 @@ +package unit; + +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableAutoConfiguration +@ComponentScan("si.zpiz.sample.*") +public class UnitTestConfiguration { + +} diff --git a/src/test/java/unit/book_related/BookDtoMapperTest.java b/src/test/java/unit/book_related/BookDtoMapperTest.java new file mode 100644 index 0000000..4d261d4 --- /dev/null +++ b/src/test/java/unit/book_related/BookDtoMapperTest.java @@ -0,0 +1,36 @@ +package unit.book_related; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; + +import si.zpiz.sample.domain.book_related.BookDbo; +import si.zpiz.sample.domain.book_related.BookDto; +import si.zpiz.sample.domain.book_related.BookDtoMapper; +import unit.UnitTestConfiguration; + +@SpringBootTest +@ContextConfiguration(classes = UnitTestConfiguration.class) +public class BookDtoMapperTest { + @Autowired + private BookDtoMapper bookDtoMapper; + + @Test + public void testMapToDto() { + BookDbo dbo = new BookDbo(); + dbo.setTitle("Title"); + dbo.setAuthor(null); + dbo.setYear(2021); + dbo.setPublisher("Publisher"); + + BookDto dto = bookDtoMapper.fromDbo(dbo); + assertEquals("Title", dto.getTitle()); + assertEquals(null, dto.getAuthor()); + assertEquals(2021, dto.getYear()); + assertEquals("Publisher", dto.getPublisher()); + assertEquals(dbo.getUniqueId(), dto.getUniqueId()); + } +} \ No newline at end of file diff --git a/src/test/java/unit/general/UnderstandingTestsTest.java b/src/test/java/unit/general/UnderstandingTestsTest.java new file mode 100644 index 0000000..497f0f4 --- /dev/null +++ b/src/test/java/unit/general/UnderstandingTestsTest.java @@ -0,0 +1,35 @@ +package unit.general; + +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.util.Assert; + +import unit.UnitTestConfiguration; + +/* + * In JUnit, each test method is executed in a new instance of the test class, + * so each test is initialized from scratch and does not share any context with other tests + */ + +@SpringBootTest +@ContextConfiguration(classes = UnitTestConfiguration.class) +@TestMethodOrder(MethodOrderer.MethodName.class) +public class UnderstandingTestsTest { + + private int value = 0; + + @Test + public void increaseValue1() { + value++; + Assert.isTrue(value == 1, "value should be 1"); + } + + @Test + public void increaseValue2() { + value++; + Assert.isTrue(value == 1, "value should be 1"); + } +} diff --git a/src/test/resources/app.key b/src/test/resources/app.key new file mode 100644 index 0000000..5351007 --- /dev/null +++ b/src/test/resources/app.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDcWWomvlNGyQhA +iB0TcN3sP2VuhZ1xNRPxr58lHswC9Cbtdc2hiSbe/sxAvU1i0O8vaXwICdzRZ1JM +g1TohG9zkqqjZDhyw1f1Ic6YR/OhE6NCpqERy97WMFeW6gJd1i5inHj/W19GAbqK +LhSHGHqIjyo0wlBf58t+qFt9h/EFBVE/LAGQBsg/jHUQCxsLoVI2aSELGIw2oSDF +oiljwLaQl0n9khX5ZbiegN3OkqodzCYHwWyu6aVVj8M1W9RIMiKmKr09s/gf31Nc +3WjvjqhFo1rTuurWGgKAxJLL7zlJqAKjGWbIT4P6h/1Kwxjw6X23St3OmhsG6HIn ++jl1++MrAgMBAAECggEBAMf820wop3pyUOwI3aLcaH7YFx5VZMzvqJdNlvpg1jbE +E2Sn66b1zPLNfOIxLcBG8x8r9Ody1Bi2Vsqc0/5o3KKfdgHvnxAB3Z3dPh2WCDek +lCOVClEVoLzziTuuTdGO5/CWJXdWHcVzIjPxmK34eJXioiLaTYqN3XKqKMdpD0ZG +mtNTGvGf+9fQ4i94t0WqIxpMpGt7NM4RHy3+Onggev0zLiDANC23mWrTsUgect/7 +62TYg8g1bKwLAb9wCBT+BiOuCc2wrArRLOJgUkj/F4/gtrR9ima34SvWUyoUaKA0 +bi4YBX9l8oJwFGHbU9uFGEMnH0T/V0KtIB7qetReywkCgYEA9cFyfBIQrYISV/OA ++Z0bo3vh2aL0QgKrSXZ924cLt7itQAHNZ2ya+e3JRlTczi5mnWfjPWZ6eJB/8MlH +Gpn12o/POEkU+XjZZSPe1RWGt5g0S3lWqyx9toCS9ACXcN9tGbaqcFSVI73zVTRA +8J9grR0fbGn7jaTlTX2tnlOTQ60CgYEA5YjYpEq4L8UUMFkuj+BsS3u0oEBnzuHd +I9LEHmN+CMPosvabQu5wkJXLuqo2TxRnAznsA8R3pCLkdPGoWMCiWRAsCn979TdY +QbqO2qvBAD2Q19GtY7lIu6C35/enQWzJUMQE3WW0OvjLzZ0l/9mA2FBRR+3F9A1d +rBdnmv0c3TcCgYEAi2i+ggVZcqPbtgrLOk5WVGo9F1GqUBvlgNn30WWNTx4zIaEk +HSxtyaOLTxtq2odV7Kr3LGiKxwPpn/T+Ief+oIp92YcTn+VfJVGw4Z3BezqbR8lA +Uf/+HF5ZfpMrVXtZD4Igs3I33Duv4sCuqhEvLWTc44pHifVloozNxYfRfU0CgYBN +HXa7a6cJ1Yp829l62QlJKtx6Ymj95oAnQu5Ez2ROiZMqXRO4nucOjGUP55Orac1a +FiGm+mC/skFS0MWgW8evaHGDbWU180wheQ35hW6oKAb7myRHtr4q20ouEtQMdQIF +snV39G1iyqeeAsf7dxWElydXpRi2b68i3BIgzhzebQKBgQCdUQuTsqV9y/JFpu6H +c5TVvhG/ubfBspI5DhQqIGijnVBzFT//UfIYMSKJo75qqBEyP2EJSmCsunWsAFsM +TszuiGTkrKcZy9G0wJqPztZZl2F2+bJgnA6nBEV7g5PA4Af+QSmaIhRwqGDAuROR +47jndeyIaMTNETEmOnms+as17g== +-----END PRIVATE KEY----- \ No newline at end of file diff --git a/src/test/resources/app.pub b/src/test/resources/app.pub new file mode 100644 index 0000000..0b2ee7b --- /dev/null +++ b/src/test/resources/app.pub @@ -0,0 +1,9 @@ +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3FlqJr5TRskIQIgdE3Dd +7D9lboWdcTUT8a+fJR7MAvQm7XXNoYkm3v7MQL1NYtDvL2l8CAnc0WdSTINU6IRv +c5Kqo2Q4csNX9SHOmEfzoROjQqahEcve1jBXluoCXdYuYpx4/1tfRgG6ii4Uhxh6 +iI8qNMJQX+fLfqhbfYfxBQVRPywBkAbIP4x1EAsbC6FSNmkhCxiMNqEgxaIpY8C2 +kJdJ/ZIV+WW4noDdzpKqHcwmB8FsrumlVY/DNVvUSDIipiq9PbP4H99TXN1o746o +RaNa07rq1hoCgMSSy+85SagCoxlmyE+D+of9SsMY8Ol9t0rdzpobBuhyJ/o5dfvj +KwIDAQAB +-----END PUBLIC KEY----- \ No newline at end of file diff --git a/src/test/resources/application-h2.properties b/src/test/resources/application-h2.properties new file mode 100644 index 0000000..e85ff9b --- /dev/null +++ b/src/test/resources/application-h2.properties @@ -0,0 +1,8 @@ +# h2 +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=password + +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/src/test/resources/application-sqlserver.properties b/src/test/resources/application-sqlserver.properties new file mode 100644 index 0000000..62cbb8f --- /dev/null +++ b/src/test/resources/application-sqlserver.properties @@ -0,0 +1,8 @@ +# SQL Server database +spring.datasource.url=jdbc:sqlserver://localhost:14333;databaseName=JavaSampleTest;trustServerCertificate=True +spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver +spring.datasource.username=sa +spring.datasource.password=YourStrong@Passw0rd + +spring.jpa.database-platform=org.hibernate.dialect.SQLServerDialect +spring.jpa.hibernate.ddl-auto=create-drop \ No newline at end of file diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..35c3cc7 --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1,11 @@ +# spring boot debug mode +debug=false + +# database +spring.jpa.properties.hibernate.globally_quoted_identifiers=true +spring.jpa.show-sql=true +spring.jpa.properties.hibernate.format_sql=true + +# jwt +jwt.private.key: classpath:app.key +jwt.public.key: classpath:app.pub \ No newline at end of file