From c6920bb4f772f335e9b6204460b201c19fb7e2ec Mon Sep 17 00:00:00 2001 From: Chris Dombroski Date: Thu, 21 Nov 2024 20:22:38 -0500 Subject: [PATCH] Import spring petclinic backend --- .envrc | 1 + .gitattributes | 9 + .gitignore | 190 ++ backend/.editorconfig | 12 + backend/.gitignore | 13 + backend/LICENSE.txt | 201 ++ backend/build.gradle.kts | 120 + backend/docker-compose.yml | 26 + backend/petclinic-ermodel.png | Bin 0 -> 71318 bytes backend/readme.md | 223 ++ .../petclinic/PetClinicApplication.java | 14 + .../petclinic/config/SwaggerConfig.java | 66 + .../samples/petclinic/mapper/OwnerMapper.java | 29 + .../samples/petclinic/mapper/PetMapper.java | 39 + .../petclinic/mapper/PetTypeMapper.java | 27 + .../petclinic/mapper/SpecialtyMapper.java | 22 + .../samples/petclinic/mapper/UserMapper.java | 32 + .../samples/petclinic/mapper/VetMapper.java | 24 + .../samples/petclinic/mapper/VisitMapper.java | 28 + .../samples/petclinic/model/BaseEntity.java | 49 + .../samples/petclinic/model/NamedEntity.java | 51 + .../samples/petclinic/model/Owner.java | 153 ++ .../samples/petclinic/model/Person.java | 56 + .../samples/petclinic/model/Pet.java | 102 + .../samples/petclinic/model/PetType.java | 29 + .../samples/petclinic/model/Role.java | 39 + .../samples/petclinic/model/Specialty.java | 30 + .../samples/petclinic/model/User.java | 74 + .../samples/petclinic/model/Vet.java | 79 + .../samples/petclinic/model/Visit.java | 116 + .../samples/petclinic/model/package-info.java | 5 + .../petclinic/repository/OwnerRepository.java | 81 + .../petclinic/repository/PetRepository.java | 78 + .../repository/PetTypeRepository.java | 41 + .../repository/SpecialtyRepository.java | 43 + .../petclinic/repository/UserRepository.java | 9 + .../petclinic/repository/VetRepository.java | 49 + .../petclinic/repository/VisitRepository.java | 53 + .../jdbc/JdbcOwnerRepositoryImpl.java | 196 ++ .../petclinic/repository/jdbc/JdbcPet.java | 48 + .../jdbc/JdbcPetRepositoryImpl.java | 169 ++ .../repository/jdbc/JdbcPetRowMapper.java | 41 + .../jdbc/JdbcPetTypeRepositoryImpl.java | 145 ++ .../jdbc/JdbcPetVisitExtractor.java | 54 + .../jdbc/JdbcSpecialtyRepositoryImpl.java | 121 + .../jdbc/JdbcUserRepositoryImpl.java | 68 + .../jdbc/JdbcVetRepositoryImpl.java | 174 ++ .../jdbc/JdbcVisitRepositoryImpl.java | 177 ++ .../repository/jdbc/JdbcVisitRowMapper.java | 41 + .../repository/jdbc/package-info.java | 6 + .../jpa/JpaOwnerRepositoryImpl.java | 96 + .../repository/jpa/JpaPetRepositoryImpl.java | 84 + .../jpa/JpaPetTypeRepositoryImpl.java | 92 + .../jpa/JpaSpecialtyRepositoryImpl.java | 80 + .../repository/jpa/JpaUserRepositoryImpl.java | 27 + .../repository/jpa/JpaVetRepositoryImpl.java | 71 + .../jpa/JpaVisitRepositoryImpl.java | 84 + .../repository/jpa/package-info.java | 6 + .../springdatajpa/PetRepositoryOverride.java | 32 + .../PetTypeRepositoryOverride.java | 32 + .../SpecialtyRepositoryOverride.java | 32 + .../SpringDataOwnerRepository.java | 44 + .../SpringDataPetRepository.java | 41 + .../SpringDataPetRepositoryImpl.java | 46 + .../SpringDataPetTypeRepository.java | 32 + .../SpringDataPetTypeRepositoryImpl.java | 56 + .../SpringDataSpecialtyRepository.java | 33 + .../SpringDataSpecialtyRepositoryImpl.java | 44 + .../SpringDataUserRepository.java | 11 + .../SpringDataVetRepository.java | 32 + .../SpringDataVisitRepository.java | 32 + .../SpringDataVisitRepositoryImpl.java | 47 + .../VisitRepositoryOverride.java | 32 + .../advice/ExceptionControllerAdvice.java | 109 + .../controller/BindingErrorsResponse.java | 137 + .../rest/controller/OwnerRestController.java | 177 ++ .../rest/controller/PetRestController.java | 109 + .../controller/PetTypeRestController.java | 105 + .../rest/controller/RootRestController.java | 47 + .../controller/SpecialtyRestController.java | 108 + .../rest/controller/UserRestController.java | 57 + .../rest/controller/VetRestController.java | 123 + .../rest/controller/VisitRestController.java | 110 + .../samples/petclinic/rest/package-info.java | 5 + .../security/BasicAuthenticationConfig.java | 58 + .../security/DisableSecurityConfig.java | 29 + .../samples/petclinic/security/Roles.java | 11 + .../petclinic/service/ClinicService.java | 70 + .../petclinic/service/ClinicServiceImpl.java | 250 ++ .../petclinic/service/UserService.java | 8 + .../petclinic/service/UserServiceImpl.java | 36 + .../petclinic/util/CallMonitoringAspect.java | 97 + .../samples/petclinic/util/EntityUtils.java | 54 + .../resources/application-hsqldb.properties | 8 + .../resources/application-mysql.properties | 7 + .../resources/application-postgres.properties | 6 + .../src/main/resources/application.properties | 44 + backend/src/main/resources/db/hsqldb/data.sql | 59 + .../src/main/resources/db/hsqldb/schema.sql | 82 + backend/src/main/resources/db/mysql/data.sql | 59 + .../db/mysql/petclinic_db_setup_mysql.txt | 36 + .../src/main/resources/db/mysql/schema.sql | 72 + .../src/main/resources/db/postgres/data.sql | 59 + .../postgres/petclinic_db_setup_postgres.txt | 22 + .../src/main/resources/db/postgres/schema.sql | 102 + backend/src/main/resources/logback.xml | 22 + .../resources/messages/messages.properties | 8 + .../resources/messages/messages_de.properties | 8 + .../resources/messages/messages_en.properties | 1 + backend/src/main/resources/openapi.yml | 2220 +++++++++++++++++ .../samples/petclinic/SpringConfigTests.java | 13 + .../petclinic/model/ValidatorTests.java | 45 + .../controller/OwnerRestControllerTests.java | 433 ++++ .../controller/PetRestControllerTests.java | 250 ++ .../PetTypeRestControllerTests.java | 253 ++ .../SpecialtyRestControllerTests.java | 218 ++ .../controller/UserRestControllerTests.java | 75 + .../controller/VetRestControllerTests.java | 222 ++ .../controller/VisitRestControllerTests.java | 254 ++ .../AbstractClinicServiceTests.java | 502 ++++ .../clinicService/ApplicationTestConfig.java | 13 + .../clinicService/ClinicServiceJdbcTests.java | 32 + .../clinicService/ClinicServiceJpaTests.java | 19 + .../ClinicServiceSpringDataJpaTests.java | 17 + .../userService/AbstractUserServiceTests.java | 35 + .../userService/UserServiceJdbcTests.java | 10 + .../userService/UserServiceJpaTests.java | 10 + .../UserServiceSpringDataJpaTests.java | 10 + .../src/test/resources/application.properties | 43 + build.gradle.kts | 9 + flake/flake.lock | 57 + flake/flake.nix | 12 + gradle.properties | 1 + gradle/libs.versions.toml | 2 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43583 bytes gradle/wrapper/gradle-wrapper.properties | 7 + gradlew | 252 ++ gradlew.bat | 94 + settings.gradle.kts | 8 + 139 files changed, 11950 insertions(+) create mode 100644 .envrc create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 backend/.editorconfig create mode 100644 backend/.gitignore create mode 100644 backend/LICENSE.txt create mode 100644 backend/build.gradle.kts create mode 100644 backend/docker-compose.yml create mode 100644 backend/petclinic-ermodel.png create mode 100644 backend/readme.md create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java create mode 100755 backend/src/main/java/org/springframework/samples/petclinic/config/SwaggerConfig.java create mode 100755 backend/src/main/java/org/springframework/samples/petclinic/mapper/OwnerMapper.java create mode 100755 backend/src/main/java/org/springframework/samples/petclinic/mapper/PetMapper.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/mapper/PetTypeMapper.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/mapper/SpecialtyMapper.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/mapper/UserMapper.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/mapper/VetMapper.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/mapper/VisitMapper.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/model/BaseEntity.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/model/Owner.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/model/Person.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/model/Pet.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/model/PetType.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/model/Role.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/model/Specialty.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/model/User.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/model/Vet.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/model/Visit.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/model/package-info.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/OwnerRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/PetRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/PetTypeRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/SpecialtyRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/UserRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/VetRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/VisitRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcOwnerRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPet.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetRowMapper.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetTypeRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetVisitExtractor.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcSpecialtyRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcUserRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcVetRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcVisitRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcVisitRowMapper.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/package-info.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaOwnerRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaPetRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaPetTypeRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaSpecialtyRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaUserRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaVetRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaVisitRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/package-info.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/PetRepositoryOverride.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/PetTypeRepositoryOverride.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpecialtyRepositoryOverride.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataOwnerRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetTypeRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetTypeRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataSpecialtyRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataSpecialtyRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataUserRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataVetRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataVisitRepository.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataVisitRepositoryImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/VisitRepositoryOverride.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/rest/advice/ExceptionControllerAdvice.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/rest/controller/BindingErrorsResponse.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/rest/controller/OwnerRestController.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/rest/controller/PetRestController.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/rest/controller/PetTypeRestController.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/rest/controller/RootRestController.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/rest/controller/SpecialtyRestController.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/rest/controller/UserRestController.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/rest/controller/VetRestController.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/rest/controller/VisitRestController.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/rest/package-info.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/security/BasicAuthenticationConfig.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/security/DisableSecurityConfig.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/security/Roles.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/service/ClinicService.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/service/ClinicServiceImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/service/UserService.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/service/UserServiceImpl.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/util/CallMonitoringAspect.java create mode 100644 backend/src/main/java/org/springframework/samples/petclinic/util/EntityUtils.java create mode 100644 backend/src/main/resources/application-hsqldb.properties create mode 100644 backend/src/main/resources/application-mysql.properties create mode 100644 backend/src/main/resources/application-postgres.properties create mode 100644 backend/src/main/resources/application.properties create mode 100644 backend/src/main/resources/db/hsqldb/data.sql create mode 100644 backend/src/main/resources/db/hsqldb/schema.sql create mode 100644 backend/src/main/resources/db/mysql/data.sql create mode 100644 backend/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt create mode 100644 backend/src/main/resources/db/mysql/schema.sql create mode 100644 backend/src/main/resources/db/postgres/data.sql create mode 100644 backend/src/main/resources/db/postgres/petclinic_db_setup_postgres.txt create mode 100644 backend/src/main/resources/db/postgres/schema.sql create mode 100644 backend/src/main/resources/logback.xml create mode 100644 backend/src/main/resources/messages/messages.properties create mode 100644 backend/src/main/resources/messages/messages_de.properties create mode 100644 backend/src/main/resources/messages/messages_en.properties create mode 100755 backend/src/main/resources/openapi.yml create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/SpringConfigTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/model/ValidatorTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/rest/controller/OwnerRestControllerTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/rest/controller/PetRestControllerTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/rest/controller/PetTypeRestControllerTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/rest/controller/SpecialtyRestControllerTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/rest/controller/UserRestControllerTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/rest/controller/VetRestControllerTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/rest/controller/VisitRestControllerTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/AbstractClinicServiceTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ApplicationTestConfig.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ClinicServiceJdbcTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ClinicServiceJpaTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ClinicServiceSpringDataJpaTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/service/userService/AbstractUserServiceTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/service/userService/UserServiceJdbcTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/service/userService/UserServiceJpaTests.java create mode 100644 backend/src/test/java/org/springframework/samples/petclinic/service/userService/UserServiceSpringDataJpaTests.java create mode 100644 backend/src/test/resources/application.properties create mode 100644 build.gradle.kts create mode 100644 flake/flake.lock create mode 100644 flake/flake.nix create mode 100644 gradle.properties create mode 100644 gradle/libs.versions.toml create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle.kts diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..baf9714 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake path:flake; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..097f9f9 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,9 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1bb72fa --- /dev/null +++ b/.gitignore @@ -0,0 +1,190 @@ +.direnv/ +.idea/ +.vscode/ + +# Created by https://www.toptal.com/developers/gitignore/api/intellij,linux,node,gradle +# Edit at https://www.toptal.com/developers/gitignore?templates=intellij,linux,node,gradle + +### Linux ### +*~ + +# temporary files which can be created if a process still has a handle open of a deleted file +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder which might appear on any partition or disk +.Trash-* + +# .nfs files are created when an open file is removed but is still being accessed +.nfs* + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/intellij,linux,node,gradle diff --git a/backend/.editorconfig b/backend/.editorconfig new file mode 100644 index 0000000..8d67bc7 --- /dev/null +++ b/backend/.editorconfig @@ -0,0 +1,12 @@ +# top-most EditorConfig file +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space + +[*.{java,xml}] +indent_size = 4 +trim_trailing_whitespace = true diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..ad07b8f --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,13 @@ +target/* +.settings/* +.classpath +.project +.idea +*.iml +/target + +generated/ + +# Easier branch switching +springboot-petclinic-client/ +springboot-petclinic-server/ diff --git a/backend/LICENSE.txt b/backend/LICENSE.txt new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/backend/LICENSE.txt @@ -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/backend/build.gradle.kts b/backend/build.gradle.kts new file mode 100644 index 0000000..8dffcf0 --- /dev/null +++ b/backend/build.gradle.kts @@ -0,0 +1,120 @@ +plugins { + java + id("org.springframework.boot") version "3.3.6" + id("io.spring.dependency-management") version "1.1.6" + id("org.openapi.generator") version "7.10.0" + idea + jacoco +} + +group = "org.springframework.samples" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation("org.springframework.boot:spring-boot-starter-actuator") + implementation("org.springframework.boot:spring-boot-starter-cache") + implementation("org.springframework.boot:spring-boot-starter-data-jpa") + implementation("org.springframework.boot:spring-boot-starter-jdbc") + implementation("org.springframework.boot:spring-boot-starter-security") + implementation("org.springframework.boot:spring-boot-starter-validation") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("org.springframework.data:spring-data-jdbc-core:1.2.1.RELEASE") + implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0") + implementation("org.mapstruct:mapstruct:${project.property("mapstructVersion")}") +// implementation("org.openapitools:jackson-databind-nullable:0.2.6") + annotationProcessor("org.mapstruct:mapstruct-processor:${project.property("mapstructVersion")}") + runtimeOnly("org.hsqldb:hsqldb") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.springframework.security:spring-security-test") + testImplementation("com.jayway.jsonpath:json-path") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") +} + +openApiGenerate { + inputSpec.set("${projectDir}/src/main/resources/openapi.yml") + generatorName.set("spring") + library.set("spring-boot") + modelNameSuffix.set("Dto") + apiPackage.set("org.springframework.samples.petclinic.rest.api") + modelPackage.set("org.springframework.samples.petclinic.rest.dto") + supportingFilesConstrainedTo.set(listOf("ApiUtil.java")) + globalProperties.set( + mapOf( + "apis" to "", + "models" to "" + ) + ) + configOptions.set( + mutableMapOf( + "verbose" to "true", + "interfaceOnly" to "true", + "performBeanValidation" to "true", + "dateLibrary" to "java8", + "useSpringBoot3" to "true", + "openApiNullable" to "false", + "serializationLibrary" to "jackson", + "documentationProvider" to "springdoc", + ) + ) +} + +tasks.jacocoTestReport { + dependsOn(tasks.test) // tests are required to run before generating the report + classDirectories.setFrom( + files(classDirectories.files.map { + fileTree(it) { + exclude( + "**/org/springframework/samples/petclinic/rest/dto/**", + "**/org/springframework/samples/petclinic/rest/api/**" + ) + } + }) + ) +} + +tasks.jacocoTestCoverageVerification { + violationRules { + rule { + element = "BUNDLE" + limit { + counter = "LINE" + value = "COVEREDRATIO" + minimum = "0.85".toBigDecimal() + } + limit { + counter = "BRANCH" + value = "COVEREDRATIO" + minimum = "0.66".toBigDecimal() + } + } + } +} + +tasks.withType { + useJUnitPlatform() + finalizedBy(tasks.jacocoTestReport) // report is always generated after tests run + configure { + } +} + +java.sourceSets["main"].java { + srcDir(layout.buildDirectory.dir("generate-resources/main/src/main/java")) +} + +tasks.withType { + dependsOn(tasks.openApiGenerate) + options.compilerArgs = listOf( + "-Amapstruct.suppressGeneratorTimestamp=true", + "-Amapstruct.suppressGeneratorVersionInfoComment=true", + "-Amapstruct.defaultComponentModel=spring" + ) +} diff --git a/backend/docker-compose.yml b/backend/docker-compose.yml new file mode 100644 index 0000000..caa9b49 --- /dev/null +++ b/backend/docker-compose.yml @@ -0,0 +1,26 @@ +services: + mysql: + image: mysql:8.4 + command: --mysql-native-password=ON + ports: + - "3306:3306" + environment: + - MYSQL_ROOT_PASSWORD= + - MYSQL_ALLOW_EMPTY_PASSWORD=true + - MYSQL_USER=petclinic + - MYSQL_PASSWORD=petclinic + - MYSQL_DATABASE=petclinic + volumes: + - "./conf.d:/etc/mysql/conf.d:ro" + profiles: + - mysql + postgres: + image: postgres:16.3 + ports: + - "5432:5432" + environment: + - POSTGRES_PASSWORD=petclinic + - POSTGRES_USER=petclinic + - POSTGRES_DB=petclinic + profiles: + - postgres diff --git a/backend/petclinic-ermodel.png b/backend/petclinic-ermodel.png new file mode 100644 index 0000000000000000000000000000000000000000..6c790fb7c45f61ea9526fcf4aae5a6a0a46ec4f1 GIT binary patch literal 71318 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

6~{`5J^xCJ(MZ1#lV6N1$u>z2 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%0DO=4Jl%Ap>B zFzN^(`QhJkqJ`lcGVP|T#lXr0@cHTcdVsUp6NHQn)P8RPU9c`DGBpgnGoTXY2yh@K z>!XDQs&5o^=73Uepew-F*txqy)roW^v;IEb#js*%sbq|pG7sSWuFouhY$py#grv=K zSLun!$m;fCCORx~9LqCg)Np|XL)q%~c_5+nyVG7A7Sk^mGKbI^S|j?)Nxlj(&5ltEe6$4kP2T-c<~NeL)1 z+jDNku|0MPww@>@1&SyUz>GAXR8{=1K!bu8F0gN6413}wJ`b>|GZh0JLjlo23U_}Qq+h7;hcdhYE>iri*3S7sFU_p0-o!(Zy z3oP^~(64`{fgJkR+!J+#Hw#6IN|Lp}h5+V(F#x1yQ?;KWI2en%g5{ff-^zsbb~S}E zfH}PYU-Kf+bZh_m{)Jr%AbP<9Ii{1+Z62`kTI-fdMG|GJ2RZ}7V5@loBXrVR5JLb~ z`c_|GpPy2i|HEd}flEA;r9sjc555|yoWIZ}{5n~_N?$xHeETv!M6@0ogi7130fI#S zS;D?;3GoH;n{!P(bJ{*YCdYEBk`@q&!u#UrO~F2J>|txm0tL3nr#<+YbZ_J6ham0wkDZQpbW(k+5WBi+(cf|LT%qBPRo zB_%Bq(w)-XUDDm%-QCaJ9?!YY6Mw+_;oYD1U@+F+YmYVOyslqej}Wh!ETY!mLDz4l z2JQyZQy+h*b-l6UC8~isc`CI8TL+G!rB-yovV}9ec>tbwb0%h(w#4~}C-uNxN(3^w zGm6!1kqEjyxz`AHh^@6h5chSP7?^43_osi5GV1Ak9OjS46W)PrhP4ee&(jCiQ?%RUh5AA~-L55$~ro8VJ3k%EQo8Qb)%Bzn++-BEa10aY`V3&qu5RpuS z+(142u*EMpWxKusz0=H3SKOR%a;W7Ny&dZv*Q6r8GN9Ap4)kq#EnM7wwbt%lyrv3G zQ_qjvEy~Jwe$;b+i{c!5>qFvqFI0|&c688#KXqEPg}?0Znbpkfh#U}+feZtz=(4p< z74Em9STDz`9^01_hsz!z;U+e$EY|;ce+_`zl|<)#=tv}#NL0q8ew|RhA!xH4gFVp>`m`!Hdo`xRO4QAq@xR&$2(QUN{6SB{N+d|g-nsmx6Bc3@8@|( zVp~3q#xGS1o%0T4eQk9p?CX0U@8>gzO%tX=72T0v179LizYEpr>-Q1IbRZ`AsTF97 zIIfPf7C7E?*`R)On)iX*ZCQmVu_d!V@JGM!n)UXSiobg1C^Je&eztn1ty+w{3h*O8|V)G|xLOOAhlJ<*UZO8!{vo7>%X=Ggb5qf}*7afsSLr#*Wm!q?P$XHNwht zpAP<ZAr?GMZ$^c8baKcP%~u-%`L zCWT!6yij&M@KgtOseOt5^0H78*QF6x1mFJTHl1#E;I$a&nXe zTshMj>d7BsI48dyh8H_fWYR0<>x%41*^(LA*WAiKKvFPk&9OD(U{uxkM;#4aARmsq zN(60B>8r6LPvS516bngTFM*Fw#@00od(mp_;b!3>ukkv+az5ad56Th%Cp*`EXoWz- zB1}>i=PKq{MTHJqUq9T=AB|*DCLi>XxJ1gT0Stciz89;0+Vg*(_~5-8 zXx$HC#Z>>tp9#Sc6UIVJg8BEe`B%vXE-vLwO4E(y`HjMY9eT2X{l<^endWe&YGek@ zjx7^i9Tss#&Rl}$pUYi^01&{%Uee|j`8N?b&U^gJRONF{HwWUw9d)6zH~f3DhA4F= zoOVu>D3_P7$Si9&CgYu@-JUIu8GhWdPISo(n1IudTbZH(XXE1IT$i)WA=tu(Yq|<^ z`+kGN#b%WXz_;uHbvgmNA%?*1VXMimrp({H*(L;2sF~nQm#_mNf`@DL8u)j>hSV@+ zwBR_aRUdSGyOx!nKB@UpK|!Iptne~-MV%`9fLabl!cOl4r4dt;)%^F*+)ri>5>_Os zNsrU#a85!dp&b@%ZyiiFg2fafD%@v2trMISH@Wr)L_G9}_Q@pb$5?e@k zP80k9R6eO~Hv_w?Y4m5RxmwS(b){2{#d8KBDJfX-M z;3LL&xi2`%-m9-k6FbMV&ukfNcPLpjjP%j`L@F|UWe5rQr(!z6ZZO*nKez5p8?^QIq*}QmKpnY`v(uouiA1C z$^obX)^Ap$Wp`{A8Ae&#Vwt9e#ITx?-JEvnau1U9zkHcII3H||(PJzvT#Tt;{2;WS z#q~vjQaX9j3L;rtPE)PS5CpqEUIa8zscSI<9ZtbujeLcPYAv?O z(Q-SaBRte^us`?^1n3Yvo!D_~HIaTAF*<0<&%|rZe9c*3g!C3!)0pM8-w@_{$9S73 zB2$ps9B;mBbCFVfn|yUATjN5%VZBC>JRTVW(;OempWxDf&x+^s8J~9{ZGdImM=S)( za*_r%NNdtK0!xkdaU61Er6NoGT$P_yAD@IF2`$Fj)qdtX1rJGaXgZ_GnyBz_ssjgd z<`EUff*9yt0u!8P;TsI_y-Df6=|D=?hr5e3fa8N+0JflJ`)3(vj_x}`y}ULM)({z0 z=>2zxBeV{%UEO{Oi4zZxop;M>q?d3pvwT_Xmp_+7SfPeysrZOZYk zY;JCrv15{L}I^ z#USHwTVqCIc+OHbbqVUAZn}wbr32U_Jpsx~cWc3I&URu6wAdX1xa&FfVW0^91fCF1 z*)N?u#S1_OsaF3H>*(m{y%8j{I5_TbUog!MlDc%0N$GXh+157cF(1^O6eUObrFo<$ zk&6WpmfsKCKE8yG-Bz2DPa+;ihtefu$W%73%~Pfy^M5gh`%;bnVGRBAPew@7u)Y_K zS1AzU(^TvrxJ=ZK(@ONf&Bb$9)VT87^0*<`vB~q|`>?Rv7%%S}-0NE%*#PszU&Y_* zBbfEK>7sDQq@G|{lh=DFI-D|rs$)=bt$Y%<{7eGaA&|kD9d$&LXs{y}7GGebAqytS*HsmhD_F;4% zt=IY2i`viK!>qz@z7}38yjRIwdDC=B)xVll@J<7>JGhy^wRE*IxB}uu|1Oir6ksr^9L6r6z7{3h zK$d4ohM1m``we{-v0Xdx93D}hLr_M6{rM~D6M_{&0N+Kf38@N1GJ+jLes3^8K)9}e zqvhiEe%!v`Yunfl0$$7@W;=4Dq*5UDI@^@OF^DLD@qwY;qdifzm%v>5`Nk8ev*i!H zSH;QVFvGh}?&@E&o=)p$DYz0T zkLH1U?{Q>=u50Y4I)m1BNM4ymC>{*vBY-`Z*KQ(@dUUXlBr z&}CgC(z`;A5rh1fW=iaN6_CRJt(lsLS*w#leWG{sj=CU!hggU$?ZpeH-n>+NbM&do z^L6{<&IyC3xmn^uIj!qX7siTDux}KL3t9N%$4;ULcAU9+aPP9rgnh`v95!JJE#XFM zP<+S%0_wA@G#35W&v(oZO^24`qCNXg*57`*uV{XVN}#c7w&0NDXb5Q z=wcE{k7uV@>@~F=Rpw+cZ>=KJmn>f1Kq@{y9bP;W$ud2rJR-j=>4qGAAGuU@WyYX( zcdF^Ghf>Y%m8!Q;_vS}t>b}@rRBJj%>ua(><7=d*EuIuBAv)=QwV=Q7yq>&q|-@vn^%j zJ=u{iI+#Sxb0CSfF0_2aIv`#x)CND`aGtGdNVPEJAe#qK$rjk3(Sau)g zw!AGzxkP~3qI|@zP#;Zq)smUI;rkl4 zVv~(IUC!g%CZ3A1Jl4s}$JBcbwZu-x&SM4o8MO+od2S1BC1WrI2Ast{3fGlKQ25iy z7Gc3(^q-+d4Xg+Ayy+BQp4HCM3QVIF_q=p@B-M^rHX+c$gI({(1Hr5BVFwQZYH8&Q zWiMgh?(D_8#;e!{eR*ws!agMt-6_Kiizy(#q2PAX7^$P5x-X!cbUOq6gwGDI78A_^R!O*C++ zPmQUd7EmoX4^~lZa`bnBx=U7{DKXQYm)NNU5BK|AZm-oSeS9C!T;!U=i?y#uD=bGJ zD0g6Va(QmHaB@FKe7We8vi@D`>8CcX5|(yc#^rrF4vDRBx5N9I?G$F@oDn`?UqcsZ zx_h;skc#Ww^P%!8E4)Vx1j=z>_s#RT$GUnEao;&l(7wZ*JQAs($1Hp?ht@!&JWA`k zv_}%yIwMFmxiB=NtSBO^ymBf)n$h}QNLM99&Fc?$?TJA(`19S1`x6Biv+HF!4UJNy z0{#I$C9dPCY1rgc=2c_1w;ZpOBMa~CnPvPx&Z7lOCD+94Dg<=mlu3!2q`(*|o$c6$-SrJhz6)kzW9@AmJog6OlgM3{fH2|jX zz?}dzCRt!qi`Zv^L#g{!K>U&NRJ&kCShRO>0BQ~DM1e73s|)~RLI^#hKS^#PFfeI z+#n+Iq&Iw?z5wJsin8_s4|V>9993)CjZm~DVh1UrUr2_UJ@$J;c`AhPAoH6IaU%!w zb^({hBjyfG$AW~W31*ZQ`nkUFU4>Wh&FVx^90jZbSUR&A8nlLq`yNPqTL=WKYfTpzI*Al9g1E3JVIvVc)D9)jy>t1n-`4QLznG0akZxOBC{c%OYqa{q^?iyvP8rSi|+82YX9kNP5u z8%$?7H{?QpZma^+N8h2=GiG4?I9U!hO+~ULQRuqy$)`{es6Xc(nLSbv0SgQ$b;UF^ z;$2z#>V$mc0@8Zn;`b$QWHef0=Om4kR)TNd8J(P7HdN?Y)H>GGT}ALJn6q8Yls#~V z#3q04U}Tm+d`a6hnl902|{Jo^;nfs@dG!T(VR&? z^~V&f=EW_&fw*Wd&&1FEyt1mey#W$Ik%zeM#QK~yeg2W$MP0Nq#v=MHeL~f%c3#B6 z;<_pa_hWKfs5q%5ebT6{x2J14-V_8rN8&3m?RGhe^4lMFTq@i~Ev=U)x6f^lFZ+SG zEUuWSnr19OV(Hqq{b;*{-~EOwJTNP_z{=C(Z6PK3w?-uP&!aT%3E%p~;p&j`G>j6d zq(sT$kmQY!#4Qi^&aGB}Ol+3q++R@ViAId^GWz_LBa=4yqe3{o#{wpywn9iv$^wKXu_K(RQig?p#B}N(6ro_` z)65y;iKSU(sSDm#;Oc3ZUcx0}UuW7o1~6|%-!L;@#(H*ozIIJRy}czmlto+l@##7X z{^Owd0|#=3o%zRk5&bD8g{Ub1s^hF5XIXhgik&|^jyg;O!rg16EX8QJ5Ohvyf6&*+ z%#|-#`lYsO$5-Q*kg8F1JIvw#5O}0$vQed+;D*2xdlDLzadhV^;&6D=$UNWH_ZpNb zc6pBvc9L?_CiWw$1Jo1NFrn}IPmo-YY>9UvFyI8;Qn2TnY^rG6ySsu4osc1zw?lbt zt+)v#LXqp{jed1pc<~t6%1Ke@trwl`<*DFRC*^(jd{Se@j=u`)~9R-4lww?Y|u| zK=%XyMA1!+H^KkZ-C!>QXRZ$@@-&+BWdB}W2gvczi#qvaTxuZZqEBff{h#Z!1UCU~MKLhe-*TAD5u8k%4NiR(dW!K{ zQnDEYo&X^^(<10@2E5p~AJwc5=W3mH&tvn~_3f73MHz4zR|x#&1Q^=>;{@n4n=I(9 z?p%$FjQJ3Z5GAm&{xV5J>~jSVBc>-G6(Fi1*e|*a4A2FZac#SByab;)wl>)HRmcvT z+isV@(C9E~rE|o56_13(^de((I3v)$*k&u8Vj$CY>+$LZ(`;Taaj_niM*rB{Xdgo- zKXFLR9pSCyamt8wOy}KGR_yZ?h7&)YxK_)J&&qWF4RB1s^}bQIafLP1kMI!-&8Xxn zAGZVbV@q7ZI*ZrD)PVGKhJkal$lSL;ze?W}q8U%FiK zawMkVu;fF)Z9x{r#1&VXAk1kt`!%2>KUgMFEu-hTx8);g8NN(s+t#Z_Z3AxsPq4F0fua7)!%RH9_T@Ni#l@bGTz&o{54t(Ng_!MgD*4okQb92# zAlm2KxZJ*w`S3@gzk0_xf3?cXH9~L5S!dnj97010Q!ND^PDsP$iIM!JNi!ETd?x1l zV2f{WaJ?}Z1L#u<;EJl21Ly}qSda^daWs-?076$tJ6xYK@3fO|bXCW4zB7Ric-|7O zNx)TW1Ao=W+WA*&tEqvEEg%4h*?wQcIeq~IUX9rVbODaoA7r_7MVXFdqRqQ6+%|JT z#$Z9lc%BynWyV1!o7_&v=HXB<%gKU&NPq_^%x!Hs7#D#9lA7N{y=VsMzrJ8BQZLZ? zuMfm>JOUF4ND>{_2?fhX435lIfH0>B3_m*EpCbn8!;K^Ozjn;g5TTtP3=+G|E&ILq zg*lwLw*v5?W(AiTR+=bDp&m&%ym*|Wk6q)H8o^ z+uWV4#y+S!c?@wygdSyXcY|R6JXR~9hWHW{4D?g#zH(EKpYj>z6G0%WPD4i;5cc3o z)Y$;?m;J!lAYu?=EnaChXA*AWCDj4IFMr^Z`_k;+>kNR`{7_3k{tsozkyhTYxgU2_ zT}p0r{37;iB-YV@w-$+ka$$oqJVV03{ABhq%Pc72148UP+Ab^CtALB(ViRl+GwFiE zj>#R~WL?b-S|anH_MfqF1=TF?8&Ma-tL=?>xWol0(+)eTb_@*sMt|bR#O+G~n}z2r z!OZOlEopCXaX&|*Km^czaX0FI zd$c&#O4^NIcUccob;T^Fy!JpMv2p(_}y?%zNCQ_He2$|HKTCOQW*Xk-LZ z?>9S3Y(d-%TQphx2ljO2kZN{VMMX_;`w5THaR@P`-a1JW2b%|>7l}`M?FY}-i)}0i zPcp>FCXl1GQUEkX4Xp;Pt*s52v&8G4h4nW5HKugJOI(*>Ict6&MlnFH3xj*hZxPng zioMV@8cz4;I8zW#CLb)zLDpw`70%Cq1?H?(2EgW{^r|m0E*v|Qb48wai4vG ziz0~68}{RKa`d&GrDk=&3_QN>A4~`OGl+~vTE`-(5L3i@v!sa;S%16%?MkNT)i4b# zCOip!>(TR&M_8m~Rj)3{w3gr!-K}pWQ(6)=qEfJ${22c+MNrQiMp+Tw?isvjdwK$u z`I$@3NPs!*1{>G+UeM`yB`EUQy)*DPCch%4K5w@FVImOG1u;cP`3r?$S=_g*8sdM} zfV+R{`31fYtr^MO+sp)}Z^z{wr_1Fb`w5@9+lx1B_h6czQBsrT_k($yt!vRA&tq@E z%8||WF4B}eb-&)C21xubI_$>#YkJ@66{-bnCJs~9cjFK8H`wk*O^=rB5ZhD2I&xQ0UNG&sC?Z<>MY^?nXS8rw&fq!gbB{eerEoN86OdB)R z1A&VuxL3v0{$V?-EUhTPF4=^wbFAwI3SzTZKPV_ZEtykz?=T8LQ?Y+QQw|7ds?3Rp z3Fw|@(1H&?j}J{o%aI%~lY20zPi@|&8e{t$p#UI+8zjJ&4F{zq#|=YNoj@@%9lMB) zg|&MG7#bOOHIh2=yXxZC7R478A98gj41W)FuiS$r#_Ut2_oLmOe{6Jsj_T!`R%$O7V}^+ zf_N_89P)B_ye$r?&CY5+rB$>3e1U-S!}{7`pvv2+uxvHzJS*=|rSnH`fYb+WWk*jGPb#5OY8);8->!@G#MjD-ke|(yjP%F)Qq0etqUTK`74(M!c#Sf#=#&x0JzNM(f6H%gu5`+Imyt(IOHc8%7V3cv(JgS)a+kStSU0BWw4qD#!0^q&*gihKL{cHmm$z>A$)l zm))&?g`}*P+E{pzo=_lG$C79r5)R^EcO5Z^uGe#re}Xm`2O z9I{JbNr}5EIBTvfY&;PS4tT?i0(Z0e^X(MlY2=w}3EpdJ7E0&_7~F-XdZC@OE5^a^ zvelEU_dV8*;{Ty)RE2@)!Ju6ar7%35cMz;Ur&x|>bu@-$|L73Nzqtbw#jnN*DwBaA zJgDn+sXwj8@BRZEF0`0eCpDXS|Ng5e*#Zy{gqbr1%f6^ZYU}KrQHpk%(IGnrMt5zk zS=*N?pIrBy5Tv`H~(;x@_Q6rcf38 z)&yZRevU2ZCgpMfP8ba`5^3%^`6Szhlr*R|(JH2m*1~(|kwRVVS!697M@_z+IY+J3 zo*gXa#PEkjSWytGDPsTY&JsP|8%Pa^EF2R;n_aJ)S1T4s_a?kgOYQ9z`_O{60S?Vu z1ijLHprOfN!0)k>0Q4wuC#COAZ;wUD=#RR;`#zP=jz~e<9`CL@Ys5e*_^H2sajT~z zkvrxK(utNQl7YSX`(4E}(rM*dq6ybk_d=9$Nf~}iHRn!Q+9yL{`g=B{DUlTIiC$Co zuRy;CdcRppX6p>LznVXwE5*X3V8$IFfhpDRYXLfBecqkx{T(i|&)XcC1gdQY3%{*iinRNw zfyHL8V#gExGEgy>QhgCPA1r%(dD5f?gmR(yObK4^eCR*94>Q(1w6?0$ti6e#S3`r1 z<@dvjWGXL^BQ0=%S-3aPQ$lbWZ6vNAa9<2^4NxBw9?;Y!E7r9==@cTv91DF6Jq;6+e329Nd@Gj(fJxrz1%BPdkh;4e`vJ zpYhX6bgFn6BswC1VQJ#P0S!^Y16~#$2vlp8upkp&A>~Z&`}`K53(<~5+TS|!d| z742FpAx41!fYGV1`4f*bXf@34OYxh{R!K*G?Y({gRj`R>;s8YD3bdxrW(c?nXGxee zUJ}aA#r1B@}0>hCR^24!x|WXEB|daaaq`<8~uQ zzHptJRv{~p=xWio-$tcyTcCR}c}hE9v(42ej@Mjd&L}z}g`Z~B9%vyE$0grr!keB> zr89XNa%p#`qA|0HNkP2R8Y1Gpm`|^~RH^ysXdb;4u*yx!Q{>+?;rh#-8Q7hr*+JGA z89f{hb{MYH&$#DshDOO8pMaFO3Jk4e27^akfFZ!h6$I*c?eoo*87Y{|Rt@co$~X@b3(RKE_ z+dANpwiP%Cl@wp%*4L(V`gmEHP<*f{+q-b;&)~*vg#IB>ut|j?Ry{-SAZ*X5kG*h| zq+O>c5;gX_YYsjiw=CU;{?R`RmY}^`h za&kC@t|9VRyddC1NV7kfPZn#&-RBp!6r6Lp2sf?zuKMs|qvo~VXw-~sQ4;*~_@lvk znzPZLQq)0@!iWzR&z+Z7EuClk3ofRl_L7bhruP@Ucg4YRV zErxxLkJ{#iFWZ!#e;+&)cmTCRr9n%yx9dcr+T<@d6d=b)==#T?E_4aegS!;EHjp@i zb!$=wn~$nUKtNLu4AasB_8{yFh=MR7u5}=BPkNt-2SUip`g4r!VGsWmCs?cN>cE(gQ!_QVHwFfI7%jXfU2#_ug zrVdl87;EF66C1Y_w5k{BEfAIExu{O(ShTpyN)zTyRWmabrXg2-s5M6Zt`L%{Vndtu zHNGgyhv8x(x)%Z4y2-c^K3XyxqzM*2bC=Fx3bK7ph?X6v(L3dB=ep*5zW(9uh=g38DXi zMz*Wy0StYZMKHBCQt8mr&FY;!gr+KV8Is+67}{r;vDqNvA`ttCiQu9JFP0jxQZZgQ zc^jJ%7lRvJ&`hGmE-GBgI)Z!l;K^#ak;$6?-Iv(D6Nu=Q%UC~l!!`P149B#8&-0lA zvB$X2^De|A`A&3UdrAb;i<_!Cw8R3HJi8h@*@tDg+@!?9Cx4qQJT0DKe2VJsO(ZF3 z@rmxZszk2p!9^fq|CMzYWNeX->1oref>QJ9D*W!|!mAitW1Rz|E8Bg&G#>=j!4FHz z>tThMh;&V&*tJs}1o(ySiG*!p&8u?uQGVJ&STnzx7(q$uPJU<`jgEX#fo2B?<1C3nv2iPfV%(Ba`HSRPmcb2 zSTj$Q|F?GKw~~uhY4P-pm9wJL9O^8*?&E5qGX0((ZdBx z + +## In case you find a bug/suggested improvement for Spring Petclinic +Our issue tracker is available here: https://github.com/spring-petclinic/spring-petclinic-rest/issues + + +## Database configuration + +In its default configuration, Petclinic uses an in-memory database (HSQLDB) which gets populated at startup with data. + +A similar setup is provided for MySQL and PostgreSQL if a persistent database configuration is needed. + +Note that whenever the database type changes, the app needs to run with a different profile: `spring.profiles.active=mysql` for MySQL or `spring.profiles.active=postgres` for PostgreSQL. +See the [Spring Boot documentation](https://docs.spring.io/spring-boot/how-to/properties-and-configuration.html#howto.properties-and-configuration.set-active-spring-profiles) for more detail on how to set the active profile. +You can also change profile defined in the `application.properties` file. +For MySQL database, it is needed to change param `hsqldb` to `mysql` in the following line of `application.properies` file: +```properties +spring.profiles.active=hsqldb,spring-data-jpa +``` + +You can start MySQL or PostgreSQL locally with whatever installer works for your OS or use docker: + +```bash +docker run -e MYSQL_USER=petclinic -e MYSQL_PASSWORD=petclinic -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=petclinic -p 3306:3306 mysql:8.4 +``` + +or + +```bash +docker run -e POSTGRES_USER=petclinic -e POSTGRES_PASSWORD=petclinic -e POSTGRES_DB=petclinic -p 5432:5432 postgres:16.3 +``` + +Further documentation is provided for [MySQL](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt) +and [PostgreSQL](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/postgres/petclinic_db_setup_postgres.txt). + +Instead of vanilla `docker` you can also use the provided `docker-compose.yml` file to start the database containers. Each one has a profile just like the Spring profile: + +```bash +docker-compose --profile mysql up +``` + +or + +```bash +docker-compose --profile postgres up +``` + + +## API First Approach + +This API is built following some [API First approach principles](https://swagger.io/resources/articles/adopting-an-api-first-approach/). + +It is specified through the [OpenAPI](https://oai.github.io/Documentation/). +It is specified in this [file](./src/main/resources/openapi.yml). + +Some of the required classes are generated during the build time. +Here are the generated file types: +* DTOs +* API template interfaces specifying methods to override in the controllers + +To see how to get them generated you can read the next chapter. + +## Generated code + +Some of the required classes are generated during the build time using maven or any IDE (e.g., IntelliJ Idea or Eclipse). + +All of these classes are generated into the ``target/generated-sources`` folder. + +Here is a list of the generated packages and the corresponding tooling: + +| Package name | Tool | +|------------------------------------------------|------------------| +| org.springframework.samples.petclinic.mapper | [MapStruct](https://mapstruct.org/) | +| org.springframework.samples.petclinic.rest.dto | [OpenAPI Generator maven plugin](https://github.com/OpenAPITools/openapi-generator/) | + + +To get both, you have to run the following command: + +```jshelllanguage +mvn clean install +``` + +## Security configuration +In its default configuration, Petclinic doesn't have authentication and authorization enabled. + +### Basic Authentication +In order to use the basic authentication functionality, turn in on from the `application.properties` file +```properties +petclinic.security.enable=true +``` +This will secure all APIs and in order to access them, basic authentication is required. +Apart from authentication, APIs also require authorization. This is done via roles that a user can have. +The existing roles are listed below with the corresponding permissions + +* `OWNER_ADMIN` -> `OwnerController`, `PetController`, `PetTypeController` (`getAllPetTypes` and `getPetType`), `VisitController` +* `VET_ADMIN` -> `PetTypeController`, `SpecialityController`, `VetController` +* `ADMIN` -> `UserController` + +There is an existing user with the username `admin` and password `admin` that has access to all APIs. + In order to add a new user, please make `POST /api/users` request with the following payload: + +```json +{ + "username": "secondAdmin", + "password": "password", + "enabled": true, + "roles": [ + { "name" : "OWNER_ADMIN" } + ] +} +``` + +## Working with Petclinic in Eclipse/STS + +### prerequisites +The following items should be installed in your system: +* Maven 3 (https://maven.apache.org/install.html) +* git command line tool (https://help.github.com/articles/set-up-git) +* Eclipse with the m2e plugin (m2e is installed by default when using the STS (http://www.springsource.org/sts) distribution of Eclipse) + +Note: when m2e is available, there is an m2 icon in Help -> About dialog. +If m2e is not there, just follow the install process here: http://eclipse.org/m2e/download/ +* Eclipse with the [mapstruct plugin](https://mapstruct.org/documentation/ide-support/) installed. + +### Steps: + +1) In the command line +```sh +git clone https://github.com/spring-petclinic/spring-petclinic-rest.git +``` +2) Inside Eclipse +``` +File -> Import -> Maven -> Existing Maven project +``` + + +## Looking for something in particular? + +| Layer | Source | +|--|--| +| REST API controllers | [REST folder](src/main/java/org/springframework/samples/petclinic/rest) | +| Service | [ClinicServiceImpl.java](src/main/java/org/springframework/samples/petclinic/service/ClinicServiceImpl.java) | +| JDBC | [jdbc folder](src/main/java/org/springframework/samples/petclinic/repository/jdbc) | +| JPA | [jpa folder](src/main/java/org/springframework/samples/petclinic/repository/jpa) | +| Spring Data JPA | [springdatajpa folder](src/main/java/org/springframework/samples/petclinic/repository/springdatajpa) | +| Tests | [AbstractClinicServiceTests.java](src/test/java/org/springframework/samples/petclinic/service/clinicService/AbstractClinicServiceTests.java) | + + +## Publishing a Docker image + +This application uses [Google Jib]([https://github.com/GoogleContainerTools/jib) to build an optimized Docker image into the [Docker Hub](https://cloud.docker.com/u/springcommunity/repository/docker/springcommunity/spring-petclinic-rest/) repository. +The [pom.xml](pom.xml) has been configured to publish the image with a the `springcommunity/spring-petclinic-rest`image name. + +Command line to run: +```sh +mvn compile jib:build -X -DjibSerialize=true -Djib.to.auth.username=xxx -Djib.to.auth.password=xxxxx +``` + +## Interesting Spring Petclinic forks + +The Spring Petclinic master branch in the main [spring-projects](https://github.com/spring-projects/spring-petclinic) +GitHub org is the "canonical" implementation, currently based on Spring Boot and Thymeleaf. + +This [spring-petclinic-rest](https://github.com/spring-petclinic/spring-petclinic-rest/) project is one of the [several forks](https://spring-petclinic.github.io/docs/forks.html) +hosted in a special GitHub org: [spring-petclinic](https://github.com/spring-petclinic). +If you have a special interest in a different technology stack +that could be used to implement the Pet Clinic then please join the community there. + + +# Contributing + +The [issue tracker](https://github.com/spring-petclinic/spring-petclinic-rest/issues) is the preferred channel for bug reports, features requests and submitting pull requests. + +For pull requests, editor preferences are available in the [editor config](https://github.com/spring-petclinic/spring-petclinic-rest/blob/master/.editorconfig) for easy use in common text editors. Read more and download plugins at . + + + diff --git a/backend/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java b/backend/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java new file mode 100644 index 0000000..b41f2f4 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java @@ -0,0 +1,14 @@ +package org.springframework.samples.petclinic; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.builder.SpringApplicationBuilder; +import org.springframework.boot.web.servlet.support.SpringBootServletInitializer; + +@SpringBootApplication +public class PetClinicApplication extends SpringBootServletInitializer { + + public static void main(String[] args) { + SpringApplication.run(PetClinicApplication.class, args); + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/config/SwaggerConfig.java b/backend/src/main/java/org/springframework/samples/petclinic/config/SwaggerConfig.java new file mode 100755 index 0000000..44fdae4 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/config/SwaggerConfig.java @@ -0,0 +1,66 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.config; + + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.Collections; + +import io.swagger.v3.oas.models.Components; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Contact; +import io.swagger.v3.oas.models.info.Info; +import io.swagger.v3.oas.models.info.License; + +/** + * Java config for Springfox swagger documentation plugin + * + * @author Vitaliy Fedoriv + */ +@Configuration +public class SwaggerConfig { + + @Bean + OpenAPI customOpenAPI() { + + return new OpenAPI().components(new Components()).info(new Info() + .title("REST Petclinic backend Api Documentation").version("1.0") + .termsOfService("Petclinic backend terms of service") + .description( + "This is REST API documentation of the Spring Petclinic backend. If authentication is enabled, when calling the APIs use admin/admin") + .license(swaggerLicense()).contact(swaggerContact())); + } + + private Contact swaggerContact() { + Contact petclinicContact = new Contact(); + petclinicContact.setName("Vitaliy Fedoriv"); + petclinicContact.setEmail("vitaliy.fedoriv@gmail.com"); + petclinicContact.setUrl("https://github.com/spring-petclinic/spring-petclinic-rest"); + return petclinicContact; + } + + private License swaggerLicense() { + License petClinicLicense = new License(); + petClinicLicense.setName("Apache 2.0"); + petClinicLicense.setUrl("http://www.apache.org/licenses/LICENSE-2.0"); + petClinicLicense.setExtensions(Collections.emptyMap()); + return petClinicLicense; + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/mapper/OwnerMapper.java b/backend/src/main/java/org/springframework/samples/petclinic/mapper/OwnerMapper.java new file mode 100755 index 0000000..5acb8a1 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/mapper/OwnerMapper.java @@ -0,0 +1,29 @@ +package org.springframework.samples.petclinic.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.springframework.samples.petclinic.model.Owner; +import org.springframework.samples.petclinic.rest.dto.OwnerDto; +import org.springframework.samples.petclinic.rest.dto.OwnerFieldsDto; + +import java.util.Collection; +import java.util.List; + +/** + * Maps Owner & OwnerDto using Mapstruct + */ +@Mapper(uses = PetMapper.class) +public interface OwnerMapper { + + OwnerDto toOwnerDto(Owner owner); + + Owner toOwner(OwnerDto ownerDto); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "pets", ignore = true) + Owner toOwner(OwnerFieldsDto ownerDto); + + List toOwnerDtoCollection(Collection ownerCollection); + + Collection toOwners(Collection ownerDtos); +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/mapper/PetMapper.java b/backend/src/main/java/org/springframework/samples/petclinic/mapper/PetMapper.java new file mode 100755 index 0000000..ae16100 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/mapper/PetMapper.java @@ -0,0 +1,39 @@ +package org.springframework.samples.petclinic.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.rest.dto.PetDto; +import org.springframework.samples.petclinic.rest.dto.PetFieldsDto; +import org.springframework.samples.petclinic.rest.dto.PetTypeDto; + +import java.util.Collection; + +/** + * Map Pet & PetDto using mapstruct + */ +@Mapper(uses = VisitMapper.class) +public interface PetMapper { + + @Mapping(source = "owner.id", target = "ownerId") + PetDto toPetDto(Pet pet); + + Collection toPetsDto(Collection pets); + + Collection toPets(Collection pets); + + @Mapping(source = "ownerId", target = "owner.id") + Pet toPet(PetDto petDto); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "owner", ignore = true) + @Mapping(target = "visits", ignore = true) + Pet toPet(PetFieldsDto petFieldsDto); + + PetTypeDto toPetTypeDto(PetType petType); + + PetType toPetType(PetTypeDto petTypeDto); + + Collection toPetTypeDtos(Collection petTypes); +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/mapper/PetTypeMapper.java b/backend/src/main/java/org/springframework/samples/petclinic/mapper/PetTypeMapper.java new file mode 100644 index 0000000..efb6d60 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/mapper/PetTypeMapper.java @@ -0,0 +1,27 @@ +package org.springframework.samples.petclinic.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.rest.dto.PetTypeDto; +import org.springframework.samples.petclinic.rest.dto.PetTypeFieldsDto; + +import java.util.Collection; +import java.util.List; + +/** + * Map PetType & PetTypeDto using mapstruct + */ +@Mapper +public interface PetTypeMapper { + + PetType toPetType(PetTypeDto petTypeDto); + + @Mapping(target = "id", ignore = true) + PetType toPetType(PetTypeFieldsDto petTypeFieldsDto); + + PetTypeDto toPetTypeDto(PetType petType); + PetTypeFieldsDto toPetTypeFieldsDto(PetType petType); + + List toPetTypeDtos(Collection petTypes); +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/mapper/SpecialtyMapper.java b/backend/src/main/java/org/springframework/samples/petclinic/mapper/SpecialtyMapper.java new file mode 100644 index 0000000..c89334d --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/mapper/SpecialtyMapper.java @@ -0,0 +1,22 @@ +package org.springframework.samples.petclinic.mapper; + +import org.mapstruct.Mapper; +import org.springframework.samples.petclinic.rest.dto.SpecialtyDto; +import org.springframework.samples.petclinic.model.Specialty; + +import java.util.Collection; + +/** + * Map Specialty & SpecialtyDto using mapstruct + */ +@Mapper +public interface SpecialtyMapper { + Specialty toSpecialty(SpecialtyDto specialtyDto); + + SpecialtyDto toSpecialtyDto(Specialty specialty); + + Collection toSpecialtyDtos(Collection specialties); + + Collection toSpecialtys(Collection specialties); + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/mapper/UserMapper.java b/backend/src/main/java/org/springframework/samples/petclinic/mapper/UserMapper.java new file mode 100644 index 0000000..2b5aea0 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/mapper/UserMapper.java @@ -0,0 +1,32 @@ +package org.springframework.samples.petclinic.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.springframework.samples.petclinic.model.Role; +import org.springframework.samples.petclinic.model.User; +import org.springframework.samples.petclinic.rest.dto.RoleDto; +import org.springframework.samples.petclinic.rest.dto.UserDto; + +import java.util.Collection; + +/** + * Map User/Role & UserDto/RoleDto using mapstruct + */ +@Mapper +public interface UserMapper { + + @Mapping(target = "id", ignore = true) + @Mapping(target = "user", ignore = true) + Role toRole(RoleDto roleDto); + + RoleDto toRoleDto(Role role); + + Collection toRoleDtos(Collection roles); + + User toUser(UserDto userDto); + + UserDto toUserDto(User user); + + Collection toRoles(Collection roleDtos); + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/mapper/VetMapper.java b/backend/src/main/java/org/springframework/samples/petclinic/mapper/VetMapper.java new file mode 100644 index 0000000..fdcab6c --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/mapper/VetMapper.java @@ -0,0 +1,24 @@ +package org.springframework.samples.petclinic.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.springframework.samples.petclinic.model.Vet; +import org.springframework.samples.petclinic.rest.dto.VetDto; +import org.springframework.samples.petclinic.rest.dto.VetFieldsDto; + +import java.util.Collection; + +/** + * Map Vet & VetoDto using mapstruct + */ +@Mapper(uses = SpecialtyMapper.class) +public interface VetMapper { + Vet toVet(VetDto vetDto); + + @Mapping(target = "id", ignore = true) + Vet toVet(VetFieldsDto vetFieldsDto); + + VetDto toVetDto(Vet vet); + + Collection toVetDtos(Collection vets); +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/mapper/VisitMapper.java b/backend/src/main/java/org/springframework/samples/petclinic/mapper/VisitMapper.java new file mode 100644 index 0000000..98dc2db --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/mapper/VisitMapper.java @@ -0,0 +1,28 @@ +package org.springframework.samples.petclinic.mapper; + +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.springframework.samples.petclinic.model.Visit; +import org.springframework.samples.petclinic.rest.dto.VisitDto; +import org.springframework.samples.petclinic.rest.dto.VisitFieldsDto; + +import java.util.Collection; + +/** + * Map Visit & VisitDto using mapstruct + */ +@Mapper(uses = PetMapper.class) +public interface VisitMapper { + @Mapping(source = "petId", target = "pet.id") + Visit toVisit(VisitDto visitDto); + + @Mapping(target = "id", ignore = true) + @Mapping(target = "pet", ignore = true) + Visit toVisit(VisitFieldsDto visitFieldsDto); + + @Mapping(source = "pet.id", target = "petId") + VisitDto toVisitDto(Visit visit); + + Collection toVisitsDto(Collection visits); + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/model/BaseEntity.java b/backend/src/main/java/org/springframework/samples/petclinic/model/BaseEntity.java new file mode 100644 index 0000000..80bf755 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/model/BaseEntity.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.model; + +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +/** + * Simple JavaBean domain object with an id property. Used as a base class for objects needing this property. + * + * @author Ken Krebs + * @author Juergen Hoeller + */ +@MappedSuperclass +public class BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + protected Integer id; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + @JsonIgnore + public boolean isNew() { + return this.id == null; + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java b/backend/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java new file mode 100644 index 0000000..0ae1953 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java @@ -0,0 +1,51 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.model; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; + +import jakarta.validation.constraints.NotEmpty; + + +/** + * Simple JavaBean domain object adds a name property to BaseEntity. Used as a base class for objects + * needing these properties. + * + * @author Ken Krebs + * @author Juergen Hoeller + */ +@MappedSuperclass +public class NamedEntity extends BaseEntity { + + @Column(name = "name") + @NotEmpty + private String name; + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return this.getName(); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/model/Owner.java b/backend/src/main/java/org/springframework/samples/petclinic/model/Owner.java new file mode 100644 index 0000000..8cf3a6d --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/model/Owner.java @@ -0,0 +1,153 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.model; + +import org.springframework.beans.support.MutableSortDefinition; +import org.springframework.beans.support.PropertyComparator; +import org.springframework.core.style.ToStringCreator; + +import jakarta.persistence.*; +import jakarta.validation.constraints.Digits; +import jakarta.validation.constraints.NotEmpty; +import java.util.*; + + +/** + * Simple JavaBean domain object representing an owner. + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Sam Brannen + * @author Michael Isvy + */ +@Entity +@Table(name = "owners") +public class Owner extends Person { + @Column(name = "address") + @NotEmpty + private String address; + + @Column(name = "city") + @NotEmpty + private String city; + + @Column(name = "telephone") + @NotEmpty + @Digits(fraction = 0, integer = 10) + private String telephone; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner", fetch = FetchType.EAGER) + private Set pets; + + + public String getAddress() { + return this.address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getCity() { + return this.city; + } + + public void setCity(String city) { + this.city = city; + } + + public String getTelephone() { + return this.telephone; + } + + public void setTelephone(String telephone) { + this.telephone = telephone; + } + + protected Set getPetsInternal() { + if (this.pets == null) { + this.pets = new HashSet<>(); + } + return this.pets; + } + + protected void setPetsInternal(Set pets) { + this.pets = pets; + } + + public List getPets() { + List sortedPets = new ArrayList<>(getPetsInternal()); + PropertyComparator.sort(sortedPets, new MutableSortDefinition("name", true, true)); + return Collections.unmodifiableList(sortedPets); + } + + public void setPets(List pets) { + this.pets = new HashSet<>(pets); + } + + public void addPet(Pet pet) { + getPetsInternal().add(pet); + pet.setOwner(this); + } + + /** + * Return the Pet with the given name, or null if none found for this Owner. + * + * @param name to test + * @return true if pet name is already in use + */ + public Pet getPet(String name) { + return getPet(name, false); + } + + /** + * Return the Pet with the given name, or null if none found for this Owner. + * + * @param name to test + * @return true if pet name is already in use + */ + public Pet getPet(String name, boolean ignoreNew) { + name = name.toLowerCase(); + for (Pet pet : getPetsInternal()) { + if (!ignoreNew || !pet.isNew()) { + String compName = pet.getName(); + compName = compName.toLowerCase(); + if (compName.equals(name)) { + return pet; + } + } + } + return null; + } + + public Pet getPet(Integer petId) { + return getPetsInternal().stream().filter(p -> p.getId().equals(petId)).findFirst().orElse(null); + } + + @Override + public String toString() { + return new ToStringCreator(this) + + .append("id", this.getId()) + .append("new", this.isNew()) + .append("lastName", this.getLastName()) + .append("firstName", this.getFirstName()) + .append("address", this.address) + .append("city", this.city) + .append("telephone", this.telephone) + .toString(); + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/model/Person.java b/backend/src/main/java/org/springframework/samples/petclinic/model/Person.java new file mode 100644 index 0000000..6257f2e --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/model/Person.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.model; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; + +import jakarta.validation.constraints.NotEmpty; + +/** + * Simple JavaBean domain object representing an person. + * + * @author Ken Krebs + */ +@MappedSuperclass +public class Person extends BaseEntity { + + @Column(name = "first_name") + @NotEmpty + protected String firstName; + + @Column(name = "last_name") + @NotEmpty + protected String lastName; + + public String getFirstName() { + return this.firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return this.lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/model/Pet.java b/backend/src/main/java/org/springframework/samples/petclinic/model/Pet.java new file mode 100644 index 0000000..f5d3402 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/model/Pet.java @@ -0,0 +1,102 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.model; + +import org.springframework.beans.support.MutableSortDefinition; +import org.springframework.beans.support.PropertyComparator; +import org.springframework.format.annotation.DateTimeFormat; + +import jakarta.persistence.*; +import java.time.LocalDate; +import java.util.*; + + +/** + * Simple business object representing a pet. + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Sam Brannen + */ +@Entity +@Table(name = "pets") +public class Pet extends NamedEntity { + + @Column(name = "birth_date", columnDefinition = "DATE") + private LocalDate birthDate; + + @ManyToOne + @JoinColumn(name = "type_id") + private PetType type; + + @ManyToOne + @JoinColumn(name = "owner_id") + private Owner owner; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "pet", fetch = FetchType.EAGER) + private Set visits; + + public LocalDate getBirthDate() { + return this.birthDate; + } + + public void setBirthDate(LocalDate birthDate) { + this.birthDate = birthDate; + } + + public PetType getType() { + return this.type; + } + + public void setType(PetType type) { + this.type = type; + } + + public Owner getOwner() { + return this.owner; + } + + public void setOwner(Owner owner) { + this.owner = owner; + } + + protected Set getVisitsInternal() { + if (this.visits == null) { + this.visits = new HashSet<>(); + } + return this.visits; + } + + protected void setVisitsInternal(Set visits) { + this.visits = visits; + } + + public List getVisits() { + List sortedVisits = new ArrayList<>(getVisitsInternal()); + PropertyComparator.sort(sortedVisits, new MutableSortDefinition("date", false, false)); + return Collections.unmodifiableList(sortedVisits); + } + + public void setVisits(List visits) { + this.visits = new HashSet<>(visits); + } + + public void addVisit(Visit visit) { + getVisitsInternal().add(visit); + visit.setPet(this); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/model/PetType.java b/backend/src/main/java/org/springframework/samples/petclinic/model/PetType.java new file mode 100644 index 0000000..beecdec --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/model/PetType.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +/** + * @author Juergen Hoeller + * Can be Cat, Dog, Hamster... + */ +@Entity +@Table(name = "types") +public class PetType extends NamedEntity { + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/model/Role.java b/backend/src/main/java/org/springframework/samples/petclinic/model/Role.java new file mode 100644 index 0000000..da35c8e --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/model/Role.java @@ -0,0 +1,39 @@ +package org.springframework.samples.petclinic.model; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.UniqueConstraint; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +@Entity +@Table(name = "roles" ,uniqueConstraints = @UniqueConstraint(columnNames = {"username", "role"})) +public class Role extends BaseEntity { + + @ManyToOne + @JoinColumn(name = "username") + @JsonIgnore + private User user; + + @Column( name = "role") + private String name; + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/model/Specialty.java b/backend/src/main/java/org/springframework/samples/petclinic/model/Specialty.java new file mode 100644 index 0000000..8eefd31 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/model/Specialty.java @@ -0,0 +1,30 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.model; + +import jakarta.persistence.Entity; +import jakarta.persistence.Table; + +/** + * Models a {@link Vet Vet's} specialty (for example, dentistry). + * + * @author Juergen Hoeller + */ +@Entity +@Table(name = "specialties") +public class Specialty extends NamedEntity { + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/model/User.java b/backend/src/main/java/org/springframework/samples/petclinic/model/User.java new file mode 100644 index 0000000..62d412f --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/model/User.java @@ -0,0 +1,74 @@ +package org.springframework.samples.petclinic.model; + +import java.util.HashSet; +import java.util.Set; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +@Entity +@Table(name = "users") +public class User { + + @Id + @Column(name = "username") + private String username; + + @Column(name = "password") + private String password; + + @Column(name = "enabled") + private Boolean enabled; + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "user", fetch = FetchType.EAGER) + private Set roles; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public Set getRoles() { + return roles; + } + + public void setRoles(Set roles) { + this.roles = roles; + } + + @JsonIgnore + public void addRole(String roleName) { + if(this.roles == null) { + this.roles = new HashSet<>(); + } + Role role = new Role(); + role.setName(roleName); + this.roles.add(role); + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/model/Vet.java b/backend/src/main/java/org/springframework/samples/petclinic/model/Vet.java new file mode 100644 index 0000000..3add8c8 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/model/Vet.java @@ -0,0 +1,79 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.model; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import org.springframework.beans.support.MutableSortDefinition; +import org.springframework.beans.support.PropertyComparator; + +import jakarta.persistence.*; +import jakarta.xml.bind.annotation.XmlElement; +import java.util.*; + +/** + * Simple JavaBean domain object representing a veterinarian. + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Sam Brannen + * @author Arjen Poutsma + */ +@Entity +@Table(name = "vets") +public class Vet extends Person { + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "vet_specialties", joinColumns = @JoinColumn(name = "vet_id"), + inverseJoinColumns = @JoinColumn(name = "specialty_id")) + private Set specialties; + + @JsonIgnore + protected Set getSpecialtiesInternal() { + if (this.specialties == null) { + this.specialties = new HashSet<>(); + } + return this.specialties; + } + + protected void setSpecialtiesInternal(Set specialties) { + this.specialties = specialties; + } + + @XmlElement + public List getSpecialties() { + List sortedSpecs = new ArrayList<>(getSpecialtiesInternal()); + PropertyComparator.sort(sortedSpecs, new MutableSortDefinition("name", true, true)); + return Collections.unmodifiableList(sortedSpecs); + } + + public void setSpecialties(List specialties) { + this.specialties = new HashSet<>(specialties); + } + + @JsonIgnore + public int getNrOfSpecialties() { + return getSpecialtiesInternal().size(); + } + + public void addSpecialty(Specialty specialty) { + getSpecialtiesInternal().add(specialty); + } + + public void clearSpecialties() { + getSpecialtiesInternal().clear(); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/model/Visit.java b/backend/src/main/java/org/springframework/samples/petclinic/model/Visit.java new file mode 100644 index 0000000..f98cb45 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/model/Visit.java @@ -0,0 +1,116 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.model; + +import org.springframework.format.annotation.DateTimeFormat; + +import jakarta.persistence.*; +import jakarta.validation.constraints.NotEmpty; +import java.time.LocalDate; + +/** + * Simple JavaBean domain object representing a visit. + * + * @author Ken Krebs + */ +@Entity +@Table(name = "visits") +public class Visit extends BaseEntity { + + /** + * Holds value of property date. + */ + @Column(name = "visit_date", columnDefinition = "DATE") + private LocalDate date; + + /** + * Holds value of property description. + */ + @NotEmpty + @Column(name = "description") + private String description; + + /** + * Holds value of property pet. + */ + @ManyToOne + @JoinColumn(name = "pet_id") + private Pet pet; + + + /** + * Creates a new instance of Visit for the current date + */ + public Visit() { + this.date = LocalDate.now(); + } + + + /** + * Getter for property date. + * + * @return Value of property date. + */ + public LocalDate getDate() { + return this.date; + } + + /** + * Setter for property date. + * + * @param date New value of property date. + */ + public void setDate(LocalDate date) { + this.date = date; + } + + /** + * Getter for property description. + * + * @return Value of property description. + */ + public String getDescription() { + return this.description; + } + + /** + * Setter for property description. + * + * @param description New value of property description. + */ + public void setDescription(String description) { + this.description = description; + } + + /** + * Getter for property pet. + * + * @return Value of property pet. + */ + public Pet getPet() { + return this.pet; + } + + /** + * Setter for property pet. + * + * @param pet New value of property pet. + */ + public void setPet(Pet pet) { + this.pet = pet; + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/model/package-info.java b/backend/src/main/java/org/springframework/samples/petclinic/model/package-info.java new file mode 100644 index 0000000..c5d9a7b --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/model/package-info.java @@ -0,0 +1,5 @@ +/** + * The classes in this package represent PetClinic's business layer. + */ +package org.springframework.samples.petclinic.model; + diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/OwnerRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/OwnerRepository.java new file mode 100644 index 0000000..feefee8 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/OwnerRepository.java @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository; + +import java.util.Collection; + +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.BaseEntity; +import org.springframework.samples.petclinic.model.Owner; + +/** + * Repository class for Owner domain objects All method names are compliant with Spring Data naming + * conventions so this interface can easily be extended for Spring Data See here: http://static.springsource.org/spring-data/jpa/docs/current/reference/html/jpa.repositories.html#jpa.query-methods.query-creation + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Sam Brannen + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +public interface OwnerRepository { + + /** + * Retrieve Owners from the data store by last name, returning all owners whose last name starts + * with the given name. + * + * @param lastName Value to search for + * @return a Collection of matching Owners (or an empty Collection if none + * found) + */ + Collection findByLastName(String lastName) throws DataAccessException; + + /** + * Retrieve an Owner from the data store by id. + * + * @param id the id to search for + * @return the Owner if found + * @throws org.springframework.dao.DataRetrievalFailureException if not found + */ + Owner findById(int id) throws DataAccessException; + + + /** + * Save an Owner to the data store, either inserting or updating it. + * + * @param owner the Owner to save + * @see BaseEntity#isNew + */ + void save(Owner owner) throws DataAccessException; + + /** + * Retrieve Owners from the data store, returning all owners + * + * @return a Collection of Owners (or an empty Collection if none + * found) + */ + Collection findAll() throws DataAccessException; + + /** + * Delete an Owner to the data store by Owner. + * + * @param owner the Owner to delete + * + */ + void delete(Owner owner) throws DataAccessException; + + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/PetRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/PetRepository.java new file mode 100644 index 0000000..f531490 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/PetRepository.java @@ -0,0 +1,78 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository; + +import java.util.Collection; +import java.util.List; + +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.BaseEntity; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.model.PetType; + +/** + * Repository class for Pet domain objects All method names are compliant with Spring Data naming + * conventions so this interface can easily be extended for Spring Data See here: http://static.springsource.org/spring-data/jpa/docs/current/reference/html/jpa.repositories.html#jpa.query-methods.query-creation + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Sam Brannen + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +public interface PetRepository { + + /** + * Retrieve all PetTypes from the data store. + * + * @return a Collection of PetTypes + */ + List findPetTypes() throws DataAccessException; + + /** + * Retrieve a Pet from the data store by id. + * + * @param id the id to search for + * @return the Pet if found + * @throws org.springframework.dao.DataRetrievalFailureException if not found + */ + Pet findById(int id) throws DataAccessException; + + /** + * Save a Pet to the data store, either inserting or updating it. + * + * @param pet the Pet to save + * @see BaseEntity#isNew + */ + void save(Pet pet) throws DataAccessException; + + /** + * Retrieve Pets from the data store, returning all owners + * + * @return a Collection of Pets (or an empty Collection if none + * found) + */ + Collection findAll() throws DataAccessException; + + /** + * Delete an Pet to the data store by Pet. + * + * @param pet the Pet to delete + * + */ + void delete(Pet pet) throws DataAccessException; + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/PetTypeRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/PetTypeRepository.java new file mode 100644 index 0000000..d1c05e9 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/PetTypeRepository.java @@ -0,0 +1,41 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository; + +import java.util.Collection; + +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.PetType; + +/** + * @author Vitaliy Fedoriv + * + */ + +public interface PetTypeRepository { + + PetType findById(int id) throws DataAccessException; + + PetType findByName(String name) throws DataAccessException; + + Collection findAll() throws DataAccessException; + + void save(PetType petType) throws DataAccessException; + + void delete(PetType petType) throws DataAccessException; + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/SpecialtyRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/SpecialtyRepository.java new file mode 100644 index 0000000..256163c --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/SpecialtyRepository.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.Specialty; + +/** + * @author Vitaliy Fedoriv + * + */ + +public interface SpecialtyRepository { + + Specialty findById(int id) throws DataAccessException; + + List findSpecialtiesByNameIn(Set names); + + Collection findAll() throws DataAccessException; + + void save(Specialty specialty) throws DataAccessException; + + void delete(Specialty specialty) throws DataAccessException; + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/UserRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/UserRepository.java new file mode 100644 index 0000000..3e9b45b --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/UserRepository.java @@ -0,0 +1,9 @@ +package org.springframework.samples.petclinic.repository; + +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.User; + +public interface UserRepository { + + void save(User user) throws DataAccessException; +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/VetRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/VetRepository.java new file mode 100644 index 0000000..0f3a3d1 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/VetRepository.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository; + +import java.util.Collection; + +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.Vet; + +/** + * Repository class for Vet domain objects All method names are compliant with Spring Data naming + * conventions so this interface can easily be extended for Spring Data See here: http://static.springsource.org/spring-data/jpa/docs/current/reference/html/jpa.repositories.html#jpa.query-methods.query-creation + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Sam Brannen + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +public interface VetRepository { + + /** + * Retrieve all Vets from the data store. + * + * @return a Collection of Vets + */ + Collection findAll() throws DataAccessException; + + Vet findById(int id) throws DataAccessException; + + void save(Vet vet) throws DataAccessException; + + void delete(Vet vet) throws DataAccessException; + + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/VisitRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/VisitRepository.java new file mode 100644 index 0000000..fac5659 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/VisitRepository.java @@ -0,0 +1,53 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository; + +import java.util.Collection; +import java.util.List; + +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.BaseEntity; +import org.springframework.samples.petclinic.model.Visit; + +/** + * Repository class for Visit domain objects All method names are compliant with Spring Data naming + * conventions so this interface can easily be extended for Spring Data See here: http://static.springsource.org/spring-data/jpa/docs/current/reference/html/jpa.repositories.html#jpa.query-methods.query-creation + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Sam Brannen + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +public interface VisitRepository { + + /** + * Save a Visit to the data store, either inserting or updating it. + * + * @param visit the Visit to save + * @see BaseEntity#isNew + */ + void save(Visit visit) throws DataAccessException; + + List findByPetId(Integer petId); + + Visit findById(int id) throws DataAccessException; + + Collection findAll() throws DataAccessException; + + void delete(Visit visit) throws DataAccessException; + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcOwnerRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcOwnerRepositoryImpl.java new file mode 100644 index 0000000..406286b --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcOwnerRepositoryImpl.java @@ -0,0 +1,196 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.jdbc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.orm.ObjectRetrievalFailureException; +import org.springframework.samples.petclinic.model.Owner; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.model.Visit; +import org.springframework.samples.petclinic.repository.OwnerRepository; +import org.springframework.samples.petclinic.util.EntityUtils; +import org.springframework.stereotype.Repository; + +import javax.sql.DataSource; +import jakarta.transaction.Transactional; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * A simple JDBC-based implementation of the {@link OwnerRepository} interface. + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Rob Harrop + * @author Sam Brannen + * @author Thomas Risberg + * @author Mark Fisher + * @author Antoine Rey + * @author Vitaliy Fedoriv + */ +@Repository +@Profile("jdbc") +public class JdbcOwnerRepositoryImpl implements OwnerRepository { + + private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private SimpleJdbcInsert insertOwner; + + @Autowired + public JdbcOwnerRepositoryImpl(DataSource dataSource) { + + this.insertOwner = new SimpleJdbcInsert(dataSource) + .withTableName("owners") + .usingGeneratedKeyColumns("id"); + + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); + + } + + + /** + * Loads {@link Owner Owners} from the data store by last name, returning all owners whose last name starts with + * the given name; also loads the {@link Pet Pets} and {@link Visit Visits} for the corresponding owners, if not + * already loaded. + */ + @Override + public Collection findByLastName(String lastName) throws DataAccessException { + Map params = new HashMap<>(); + params.put("lastName", lastName + "%"); + List owners = this.namedParameterJdbcTemplate.query( + "SELECT id, first_name, last_name, address, city, telephone FROM owners WHERE last_name like :lastName", + params, + BeanPropertyRowMapper.newInstance(Owner.class) + ); + loadOwnersPetsAndVisits(owners); + return owners; + } + + /** + * Loads the {@link Owner} with the supplied id; also loads the {@link Pet Pets} and {@link Visit Visits} + * for the corresponding owner, if not already loaded. + */ + @Override + public Owner findById(int id) throws DataAccessException { + Owner owner; + try { + Map params = new HashMap<>(); + params.put("id", id); + owner = this.namedParameterJdbcTemplate.queryForObject( + "SELECT id, first_name, last_name, address, city, telephone FROM owners WHERE id= :id", + params, + BeanPropertyRowMapper.newInstance(Owner.class) + ); + } catch (EmptyResultDataAccessException ex) { + throw new ObjectRetrievalFailureException(Owner.class, id); + } + loadPetsAndVisits(owner); + return owner; + } + + public void loadPetsAndVisits(final Owner owner) { + Map params = new HashMap<>(); + params.put("id", owner.getId()); + final List pets = this.namedParameterJdbcTemplate.query( + "SELECT pets.id as pets_id, name, birth_date, type_id, owner_id, visits.id as visit_id, visit_date, description, visits.pet_id as visits_pet_id FROM pets LEFT OUTER JOIN visits ON pets.id = visits.pet_id WHERE owner_id=:id ORDER BY pets.id", + params, + new JdbcPetVisitExtractor() + ); + Collection petTypes = getPetTypes(); + for (JdbcPet pet : pets) { + pet.setType(EntityUtils.getById(petTypes, PetType.class, pet.getTypeId())); + owner.addPet(pet); + } + } + + @Override + public void save(Owner owner) throws DataAccessException { + BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(owner); + if (owner.isNew()) { + Number newKey = this.insertOwner.executeAndReturnKey(parameterSource); + owner.setId(newKey.intValue()); + } else { + this.namedParameterJdbcTemplate.update( + "UPDATE owners SET first_name=:firstName, last_name=:lastName, address=:address, " + + "city=:city, telephone=:telephone WHERE id=:id", + parameterSource); + } + } + + public Collection getPetTypes() throws DataAccessException { + return this.namedParameterJdbcTemplate.query( + "SELECT id, name FROM types ORDER BY name", new HashMap(), + BeanPropertyRowMapper.newInstance(PetType.class)); + } + + /** + * Loads the {@link Pet} and {@link Visit} data for the supplied {@link List} of {@link Owner Owners}. + * + * @param owners the list of owners for whom the pet and visit data should be loaded + * @see #loadPetsAndVisits(Owner) + */ + private void loadOwnersPetsAndVisits(List owners) { + for (Owner owner : owners) { + loadPetsAndVisits(owner); + } + } + + @Override + public Collection findAll() throws DataAccessException { + List owners = this.namedParameterJdbcTemplate.query( + "SELECT id, first_name, last_name, address, city, telephone FROM owners", + new HashMap(), + BeanPropertyRowMapper.newInstance(Owner.class)); + for (Owner owner : owners) { + loadPetsAndVisits(owner); + } + return owners; + } + + @Override + @Transactional + public void delete(Owner owner) throws DataAccessException { + Map owner_params = new HashMap<>(); + owner_params.put("id", owner.getId()); + List pets = owner.getPets(); + // cascade delete pets + for (Pet pet : pets){ + Map pet_params = new HashMap<>(); + pet_params.put("id", pet.getId()); + // cascade delete visits + List visits = pet.getVisits(); + for (Visit visit : visits){ + Map visit_params = new HashMap<>(); + visit_params.put("id", visit.getId()); + this.namedParameterJdbcTemplate.update("DELETE FROM visits WHERE id=:id", visit_params); + } + this.namedParameterJdbcTemplate.update("DELETE FROM pets WHERE id=:id", pet_params); + } + this.namedParameterJdbcTemplate.update("DELETE FROM owners WHERE id=:id", owner_params); + } + + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPet.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPet.java new file mode 100644 index 0000000..05d048d --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPet.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.jdbc; + +import org.springframework.samples.petclinic.model.Pet; + +/** + * Subclass of Pet that carries temporary id properties which are only relevant for a JDBC implementation of the + * PetRepository. + * + * @author Juergen Hoeller + */ +public class JdbcPet extends Pet { + + private int typeId; + + private int ownerId; + + public int getTypeId() { + return this.typeId; + } + + public void setTypeId(int typeId) { + this.typeId = typeId; + } + + public int getOwnerId() { + return this.ownerId; + } + + public void setOwnerId(int ownerId) { + this.ownerId = ownerId; + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetRepositoryImpl.java new file mode 100644 index 0000000..9911ff5 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetRepositoryImpl.java @@ -0,0 +1,169 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.jdbc; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.orm.ObjectRetrievalFailureException; +import org.springframework.samples.petclinic.model.Owner; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.model.Visit; +import org.springframework.samples.petclinic.repository.OwnerRepository; +import org.springframework.samples.petclinic.repository.PetRepository; +import org.springframework.samples.petclinic.repository.VisitRepository; +import org.springframework.samples.petclinic.util.EntityUtils; +import org.springframework.stereotype.Repository; + +/** + * @author Ken Krebs + * @author Juergen Hoeller + * @author Rob Harrop + * @author Sam Brannen + * @author Thomas Risberg + * @author Mark Fisher + * @author Vitaliy Fedoriv + */ +@Repository +@Profile("jdbc") +public class JdbcPetRepositoryImpl implements PetRepository { + + private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private SimpleJdbcInsert insertPet; + + private OwnerRepository ownerRepository; + + private VisitRepository visitRepository; + + + @Autowired + public JdbcPetRepositoryImpl(DataSource dataSource, + OwnerRepository ownerRepository, + VisitRepository visitRepository) { + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); + + this.insertPet = new SimpleJdbcInsert(dataSource) + .withTableName("pets") + .usingGeneratedKeyColumns("id"); + + this.ownerRepository = ownerRepository; + this.visitRepository = visitRepository; + } + + @Override + public List findPetTypes() throws DataAccessException { + Map params = new HashMap<>(); + return this.namedParameterJdbcTemplate.query( + "SELECT id, name FROM types ORDER BY name", + params, + BeanPropertyRowMapper.newInstance(PetType.class)); + } + + @Override + public Pet findById(int id) throws DataAccessException { + Integer ownerId; + try { + Map params = new HashMap<>(); + params.put("id", id); + ownerId = this.namedParameterJdbcTemplate.queryForObject("SELECT owner_id FROM pets WHERE id=:id", params, Integer.class); + } catch (EmptyResultDataAccessException ex) { + throw new ObjectRetrievalFailureException(Pet.class, id); + } + Owner owner = this.ownerRepository.findById(ownerId); + return EntityUtils.getById(owner.getPets(), Pet.class, id); + } + + @Override + public void save(Pet pet) throws DataAccessException { + if (pet.isNew()) { + Number newKey = this.insertPet.executeAndReturnKey( + createPetParameterSource(pet)); + pet.setId(newKey.intValue()); + } else { + this.namedParameterJdbcTemplate.update( + "UPDATE pets SET name=:name, birth_date=:birth_date, type_id=:type_id, " + + "owner_id=:owner_id WHERE id=:id", + createPetParameterSource(pet)); + } + } + + /** + * Creates a {@link MapSqlParameterSource} based on data values from the supplied {@link Pet} instance. + */ + private MapSqlParameterSource createPetParameterSource(Pet pet) { + return new MapSqlParameterSource() + .addValue("id", pet.getId()) + .addValue("name", pet.getName()) + .addValue("birth_date", pet.getBirthDate()) + .addValue("type_id", pet.getType().getId()) + .addValue("owner_id", pet.getOwner().getId()); + } + + @Override + public Collection findAll() throws DataAccessException { + Map params = new HashMap<>(); + Collection pets = new ArrayList(); + Collection jdbcPets = new ArrayList(); + jdbcPets = this.namedParameterJdbcTemplate + .query("SELECT pets.id as pets_id, name, birth_date, type_id, owner_id FROM pets", + params, + new JdbcPetRowMapper()); + Collection petTypes = this.namedParameterJdbcTemplate.query("SELECT id, name FROM types ORDER BY name", + new HashMap(), BeanPropertyRowMapper.newInstance(PetType.class)); + Collection owners = this.namedParameterJdbcTemplate.query( + "SELECT id, first_name, last_name, address, city, telephone FROM owners ORDER BY last_name", + new HashMap(), + BeanPropertyRowMapper.newInstance(Owner.class)); + for (JdbcPet jdbcPet : jdbcPets) { + jdbcPet.setType(EntityUtils.getById(petTypes, PetType.class, jdbcPet.getTypeId())); + jdbcPet.setOwner(EntityUtils.getById(owners, Owner.class, jdbcPet.getOwnerId())); + // TODO add visits + pets.add(jdbcPet); + } + return pets; + } + + @Override + public void delete(Pet pet) throws DataAccessException { + Map pet_params = new HashMap<>(); + pet_params.put("id", pet.getId()); + List visits = pet.getVisits(); + // cascade delete visits + for (Visit visit : visits) { + Map visit_params = new HashMap<>(); + visit_params.put("id", visit.getId()); + this.namedParameterJdbcTemplate.update("DELETE FROM visits WHERE id=:id", visit_params); + } + this.namedParameterJdbcTemplate.update("DELETE FROM pets WHERE id=:id", pet_params); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetRowMapper.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetRowMapper.java new file mode 100644 index 0000000..0305263 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetRowMapper.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.jdbc; + +import org.springframework.jdbc.core.RowMapper; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.Date; + +/** + * {@link RowMapper} implementation mapping data from a {@link ResultSet} to the corresponding properties + * of the {@link JdbcPet} class. + */ +public class JdbcPetRowMapper implements RowMapper { + + @Override + public JdbcPet mapRow(ResultSet rs, int rownum) throws SQLException { + JdbcPet pet = new JdbcPet(); + pet.setId(rs.getInt("pets_id")); + pet.setName(rs.getString("name")); + pet.setBirthDate(rs.getObject("birth_date", LocalDate.class)); + pet.setTypeId(rs.getInt("type_id")); + pet.setOwnerId(rs.getInt("owner_id")); + return pet; + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetTypeRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetTypeRepositoryImpl.java new file mode 100644 index 0000000..1835cb7 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetTypeRepositoryImpl.java @@ -0,0 +1,145 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.jdbc; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.orm.ObjectRetrievalFailureException; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.model.Visit; +import org.springframework.samples.petclinic.repository.PetTypeRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Repository +@Profile("jdbc") +public class JdbcPetTypeRepositoryImpl implements PetTypeRepository { + + private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private SimpleJdbcInsert insertPetType; + + @Autowired + public JdbcPetTypeRepositoryImpl(DataSource dataSource) { + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); + this.insertPetType = new SimpleJdbcInsert(dataSource) + .withTableName("types") + .usingGeneratedKeyColumns("id"); + } + + @Override + public PetType findById(int id) { + PetType petType; + try { + Map params = new HashMap<>(); + params.put("id", id); + petType = this.namedParameterJdbcTemplate.queryForObject( + "SELECT id, name FROM types WHERE id= :id", + params, + BeanPropertyRowMapper.newInstance(PetType.class)); + } catch (EmptyResultDataAccessException ex) { + throw new ObjectRetrievalFailureException(PetType.class, id); + } + return petType; + } + + @Override + public PetType findByName(String name) throws DataAccessException { + PetType petType; + try { + Map params = new HashMap<>(); + params.put("name", name); + petType = this.namedParameterJdbcTemplate.queryForObject( + "SELECT id, name FROM types WHERE name= :name", + params, + BeanPropertyRowMapper.newInstance(PetType.class)); + } catch (EmptyResultDataAccessException ex) { + throw new ObjectRetrievalFailureException(PetType.class, name); + } + return petType; + } + + @Override + public Collection findAll() throws DataAccessException { + Map params = new HashMap<>(); + return this.namedParameterJdbcTemplate.query( + "SELECT id, name FROM types", + params, + BeanPropertyRowMapper.newInstance(PetType.class)); + } + + @Override + public void save(PetType petType) throws DataAccessException { + BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(petType); + if (petType.isNew()) { + Number newKey = this.insertPetType.executeAndReturnKey(parameterSource); + petType.setId(newKey.intValue()); + } else { + this.namedParameterJdbcTemplate.update("UPDATE types SET name=:name WHERE id=:id", + parameterSource); + } + } + + @Override + public void delete(PetType petType) throws DataAccessException { + Map pettype_params = new HashMap<>(); + pettype_params.put("id", petType.getId()); + List pets = new ArrayList(); + pets = this.namedParameterJdbcTemplate. + query("SELECT pets.id, name, birth_date, type_id, owner_id FROM pets WHERE type_id=:id", + pettype_params, + BeanPropertyRowMapper.newInstance(Pet.class)); + // cascade delete pets + for (Pet pet : pets){ + Map pet_params = new HashMap<>(); + pet_params.put("id", pet.getId()); + List visits = new ArrayList(); + visits = this.namedParameterJdbcTemplate.query( + "SELECT id, pet_id, visit_date, description FROM visits WHERE pet_id = :id", + pet_params, + BeanPropertyRowMapper.newInstance(Visit.class)); + // cascade delete visits + for (Visit visit : visits){ + Map visit_params = new HashMap<>(); + visit_params.put("id", visit.getId()); + this.namedParameterJdbcTemplate.update("DELETE FROM visits WHERE id=:id", visit_params); + } + this.namedParameterJdbcTemplate.update("DELETE FROM pets WHERE id=:id", pet_params); + } + this.namedParameterJdbcTemplate.update("DELETE FROM types WHERE id=:id", pettype_params); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetVisitExtractor.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetVisitExtractor.java new file mode 100644 index 0000000..f6483e3 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcPetVisitExtractor.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.jdbc; + +import org.springframework.data.jdbc.core.OneToManyResultSetExtractor; +import org.springframework.jdbc.core.ResultSetExtractor; +import org.springframework.samples.petclinic.model.Visit; + +import java.sql.ResultSet; +import java.sql.SQLException; + +/** + * {@link ResultSetExtractor} implementation by using the + * {@link OneToManyResultSetExtractor} of Spring Data Core JDBC Extensions. + */ +public class JdbcPetVisitExtractor extends + OneToManyResultSetExtractor { + + public JdbcPetVisitExtractor() { + super(new JdbcPetRowMapper(), new JdbcVisitRowMapper()); + } + + @Override + protected Integer mapPrimaryKey(ResultSet rs) throws SQLException { + return rs.getInt("pets_id"); + } + + @Override + protected Integer mapForeignKey(ResultSet rs) throws SQLException { + if (rs.getObject("visits_pet_id") == null) { + return null; + } else { + return rs.getInt("visits_pet_id"); + } + } + + @Override + protected void addChild(JdbcPet root, Visit child) { + root.addVisit(child); + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcSpecialtyRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcSpecialtyRepositoryImpl.java new file mode 100644 index 0000000..3f125d9 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcSpecialtyRepositoryImpl.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.jdbc; + +import java.util.*; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.orm.ObjectRetrievalFailureException; +import org.springframework.samples.petclinic.model.Specialty; +import org.springframework.samples.petclinic.repository.SpecialtyRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Repository +@Profile("jdbc") +public class JdbcSpecialtyRepositoryImpl implements SpecialtyRepository { + + private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + private SimpleJdbcInsert insertSpecialty; + + @Autowired + public JdbcSpecialtyRepositoryImpl(DataSource dataSource) { + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); + this.insertSpecialty = new SimpleJdbcInsert(dataSource) + .withTableName("specialties") + .usingGeneratedKeyColumns("id"); + } + + @Override + public Specialty findById(int id) { + Specialty specialty; + try { + Map params = new HashMap<>(); + params.put("id", id); + specialty = this.namedParameterJdbcTemplate.queryForObject( + "SELECT id, name FROM specialties WHERE id= :id", + params, + BeanPropertyRowMapper.newInstance(Specialty.class)); + } catch (EmptyResultDataAccessException ex) { + throw new ObjectRetrievalFailureException(Specialty.class, id); + } + return specialty; + } + + @Override + public List findSpecialtiesByNameIn(Set names) { + List specialties; + try{ + String sql = "SELECT id, name FROM specialties WHERE specialties.name IN (:names)"; + Map params = new HashMap<>(); + params.put("names", names); + specialties = this.namedParameterJdbcTemplate.query( + sql, + params, + new BeanPropertyRowMapper<>(Specialty.class)); + } catch (EmptyResultDataAccessException ex){ + throw new ObjectRetrievalFailureException(Specialty.class, names); + } + + return specialties; + } + + @Override + public Collection findAll() throws DataAccessException { + Map params = new HashMap<>(); + return this.namedParameterJdbcTemplate.query( + "SELECT id, name FROM specialties", + params, + BeanPropertyRowMapper.newInstance(Specialty.class)); + } + + @Override + public void save(Specialty specialty) throws DataAccessException { + BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(specialty); + if (specialty.isNew()) { + Number newKey = this.insertSpecialty.executeAndReturnKey(parameterSource); + specialty.setId(newKey.intValue()); + } else { + this.namedParameterJdbcTemplate.update("UPDATE specialties SET name=:name WHERE id=:id", + parameterSource); + } + + } + + @Override + public void delete(Specialty specialty) throws DataAccessException { + Map params = new HashMap<>(); + params.put("id", specialty.getId()); + this.namedParameterJdbcTemplate.update("DELETE FROM vet_specialties WHERE specialty_id=:id", params); + this.namedParameterJdbcTemplate.update("DELETE FROM specialties WHERE id=:id", params); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcUserRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcUserRepositoryImpl.java new file mode 100644 index 0000000..7fc51ec --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcUserRepositoryImpl.java @@ -0,0 +1,68 @@ +package org.springframework.samples.petclinic.repository.jdbc; + +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.samples.petclinic.model.Role; +import org.springframework.samples.petclinic.model.User; +import org.springframework.samples.petclinic.repository.UserRepository; +import org.springframework.stereotype.Repository; + +@Repository +@Profile("jdbc") +public class JdbcUserRepositoryImpl implements UserRepository { + + private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private SimpleJdbcInsert insertUser; + + @Autowired + public JdbcUserRepositoryImpl(DataSource dataSource) { + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); + this.insertUser = new SimpleJdbcInsert(dataSource).withTableName("users"); + } + + @Override + public void save(User user) throws DataAccessException { + + BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(user); + + try { + getByUsername(user.getUsername()); + this.namedParameterJdbcTemplate.update("UPDATE users SET password=:password, enabled=:enabled WHERE username=:username", parameterSource); + } catch (EmptyResultDataAccessException e) { + this.insertUser.execute(parameterSource); + } finally { + updateUserRoles(user); + } + } + + private User getByUsername(String username) { + + Map params = new HashMap<>(); + params.put("username", username); + return this.namedParameterJdbcTemplate.queryForObject("SELECT * FROM users WHERE username=:username", + params, BeanPropertyRowMapper.newInstance(User.class)); + } + + private void updateUserRoles(User user) { + Map params = new HashMap<>(); + params.put("username", user.getUsername()); + this.namedParameterJdbcTemplate.update("DELETE FROM roles WHERE username=:username", params); + for (Role role : user.getRoles()) { + params.put("role", role.getName()); + if (role.getName() != null) { + this.namedParameterJdbcTemplate.update("INSERT INTO roles(username, role) VALUES (:username, :role)", params); + } + } + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcVetRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcVetRepositoryImpl.java new file mode 100644 index 0000000..c5bc285 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcVetRepositoryImpl.java @@ -0,0 +1,174 @@ +/* + * Copyright 2002-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.jdbc; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.sql.DataSource; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.orm.ObjectRetrievalFailureException; +import org.springframework.samples.petclinic.model.Specialty; +import org.springframework.samples.petclinic.model.Vet; +import org.springframework.samples.petclinic.repository.VetRepository; +import org.springframework.samples.petclinic.util.EntityUtils; +import org.springframework.stereotype.Repository; + +/** + * A simple JDBC-based implementation of the {@link VetRepository} interface. + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Rob Harrop + * @author Sam Brannen + * @author Thomas Risberg + * @author Mark Fisher + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +@Repository +@Profile("jdbc") +public class JdbcVetRepositoryImpl implements VetRepository { + + private JdbcTemplate jdbcTemplate; + private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private SimpleJdbcInsert insertVet; + + @Autowired + public JdbcVetRepositoryImpl(DataSource dataSource, JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + this.insertVet = new SimpleJdbcInsert(dataSource).withTableName("vets").usingGeneratedKeyColumns("id"); + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); + } + + /** + * Refresh the cache of Vets that the ClinicService is holding. + */ + @Override + public Collection findAll() throws DataAccessException { + List vets = new ArrayList<>(); + // Retrieve the list of all vets. + vets.addAll(this.jdbcTemplate.query( + "SELECT id, first_name, last_name FROM vets ORDER BY last_name,first_name", + BeanPropertyRowMapper.newInstance(Vet.class))); + + // Retrieve the list of all possible specialties. + final List specialties = this.jdbcTemplate.query( + "SELECT id, name FROM specialties", + BeanPropertyRowMapper.newInstance(Specialty.class)); + + // Build each vet's list of specialties. + for (Vet vet : vets) { + final List vetSpecialtiesIds = this.jdbcTemplate.query( + "SELECT specialty_id FROM vet_specialties WHERE vet_id=?", + new BeanPropertyRowMapper() { + @Override + public Integer mapRow(ResultSet rs, int row) throws SQLException { + return rs.getInt(1); + } + }, + vet.getId()); + for (int specialtyId : vetSpecialtiesIds) { + Specialty specialty = EntityUtils.getById(specialties, Specialty.class, specialtyId); + vet.addSpecialty(specialty); + } + } + return vets; + } + + @Override + public Vet findById(int id) throws DataAccessException { + Vet vet; + try { + Map vet_params = new HashMap<>(); + vet_params.put("id", id); + vet = this.namedParameterJdbcTemplate.queryForObject( + "SELECT id, first_name, last_name FROM vets WHERE id= :id", + vet_params, + BeanPropertyRowMapper.newInstance(Vet.class)); + + final List specialties = this.namedParameterJdbcTemplate.query( + "SELECT id, name FROM specialties", vet_params, BeanPropertyRowMapper.newInstance(Specialty.class)); + + final List vetSpecialtiesIds = this.namedParameterJdbcTemplate.query( + "SELECT specialty_id FROM vet_specialties WHERE vet_id=:id", + vet_params, + new BeanPropertyRowMapper() { + @Override + public Integer mapRow(ResultSet rs, int row) throws SQLException { + return rs.getInt(1); + } + }); + for (int specialtyId : vetSpecialtiesIds) { + Specialty specialty = EntityUtils.getById(specialties, Specialty.class, specialtyId); + vet.addSpecialty(specialty); + } + + } catch (EmptyResultDataAccessException ex) { + throw new ObjectRetrievalFailureException(Vet.class, id); + } + return vet; + } + + @Override + public void save(Vet vet) throws DataAccessException { + BeanPropertySqlParameterSource parameterSource = new BeanPropertySqlParameterSource(vet); + if (vet.isNew()) { + Number newKey = this.insertVet.executeAndReturnKey(parameterSource); + vet.setId(newKey.intValue()); + updateVetSpecialties(vet); + } else { + this.namedParameterJdbcTemplate + .update("UPDATE vets SET first_name=:firstName, last_name=:lastName WHERE id=:id", parameterSource); + updateVetSpecialties(vet); + } + } + + @Override + public void delete(Vet vet) throws DataAccessException { + Map params = new HashMap<>(); + params.put("id", vet.getId()); + this.namedParameterJdbcTemplate.update("DELETE FROM vet_specialties WHERE vet_id=:id", params); + this.namedParameterJdbcTemplate.update("DELETE FROM vets WHERE id=:id", params); + } + + private void updateVetSpecialties(Vet vet) throws DataAccessException { + Map params = new HashMap<>(); + params.put("id", vet.getId()); + this.namedParameterJdbcTemplate.update("DELETE FROM vet_specialties WHERE vet_id=:id", params); + for (Specialty spec : vet.getSpecialties()) { + params.put("spec_id", spec.getId()); + if(!(spec.getId() == null)) { + this.namedParameterJdbcTemplate.update("INSERT INTO vet_specialties VALUES (:id, :spec_id)", params); + } + } + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcVisitRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcVisitRepositoryImpl.java new file mode 100644 index 0000000..b8e6ea9 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcVisitRepositoryImpl.java @@ -0,0 +1,177 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.jdbc; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.BeanPropertyRowMapper; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.jdbc.core.simple.SimpleJdbcInsert; +import org.springframework.orm.ObjectRetrievalFailureException; +import org.springframework.samples.petclinic.model.Owner; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.model.Visit; +import org.springframework.samples.petclinic.repository.VisitRepository; +import org.springframework.stereotype.Repository; + +import javax.sql.DataSource; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.*; + +/** + * A simple JDBC-based implementation of the {@link VisitRepository} interface. + * + * @author Ken Krebs + * @author Juergen Hoeller + * @author Rob Harrop + * @author Sam Brannen + * @author Thomas Risberg + * @author Mark Fisher + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +@Repository +@Profile("jdbc") +public class JdbcVisitRepositoryImpl implements VisitRepository { + + protected SimpleJdbcInsert insertVisit; + private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + + @Autowired + public JdbcVisitRepositoryImpl(DataSource dataSource) { + this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource); + + this.insertVisit = new SimpleJdbcInsert(dataSource) + .withTableName("visits") + .usingGeneratedKeyColumns("id"); + } + + + /** + * Creates a {@link MapSqlParameterSource} based on data values from the supplied {@link Visit} instance. + */ + protected MapSqlParameterSource createVisitParameterSource(Visit visit) { + return new MapSqlParameterSource() + .addValue("id", visit.getId()) + .addValue("visit_date", visit.getDate()) + .addValue("description", visit.getDescription()) + .addValue("pet_id", visit.getPet().getId()); + } + + @Override + public List findByPetId(Integer petId) { + Map params = new HashMap<>(); + params.put("id", petId); + JdbcPet pet = this.namedParameterJdbcTemplate.queryForObject( + "SELECT id as pets_id, name, birth_date, type_id, owner_id FROM pets WHERE id=:id", + params, + new JdbcPetRowMapper()); + + List visits = this.namedParameterJdbcTemplate.query( + "SELECT id as visit_id, visit_date, description FROM visits WHERE pet_id=:id", + params, new JdbcVisitRowMapper()); + + for (Visit visit : visits) { + visit.setPet(pet); + } + + return visits; + } + + @Override + public Visit findById(int id) throws DataAccessException { + Visit visit; + try { + Map params = new HashMap<>(); + params.put("id", id); + visit = this.namedParameterJdbcTemplate.queryForObject( + "SELECT id as visit_id, visits.pet_id as pets_id, visit_date, description FROM visits WHERE id= :id", + params, + new JdbcVisitRowMapperExt()); + } catch (EmptyResultDataAccessException ex) { + throw new ObjectRetrievalFailureException(Visit.class, id); + } + return visit; + } + + @Override + public Collection findAll() throws DataAccessException { + Map params = new HashMap<>(); + return this.namedParameterJdbcTemplate.query( + "SELECT id as visit_id, pets.id as pets_id, visit_date, description FROM visits LEFT JOIN pets ON visits.pet_id = pets.id", + params, new JdbcVisitRowMapperExt()); + } + + @Override + public void save(Visit visit) throws DataAccessException { + if (visit.isNew()) { + Number newKey = this.insertVisit.executeAndReturnKey(createVisitParameterSource(visit)); + visit.setId(newKey.intValue()); + } else { + this.namedParameterJdbcTemplate.update( + "UPDATE visits SET visit_date=:visit_date, description=:description, pet_id=:pet_id WHERE id=:id ", + createVisitParameterSource(visit)); + } + } + + @Override + public void delete(Visit visit) throws DataAccessException { + Map params = new HashMap<>(); + params.put("id", visit.getId()); + this.namedParameterJdbcTemplate.update("DELETE FROM visits WHERE id=:id", params); + } + + protected class JdbcVisitRowMapperExt implements RowMapper { + + @Override + public Visit mapRow(ResultSet rs, int rowNum) throws SQLException { + Visit visit = new Visit(); + JdbcPet pet = new JdbcPet(); + PetType petType = new PetType(); + Owner owner = new Owner(); + visit.setId(rs.getInt("visit_id")); + Date visitDate = rs.getDate("visit_date"); + visit.setDate(new java.sql.Date(visitDate.getTime()).toLocalDate()); + visit.setDescription(rs.getString("description")); + Map params = new HashMap<>(); + params.put("id", rs.getInt("pets_id")); + pet = JdbcVisitRepositoryImpl.this.namedParameterJdbcTemplate.queryForObject( + "SELECT pets.id as pets_id, name, birth_date, type_id, owner_id FROM pets WHERE pets.id=:id", + params, + new JdbcPetRowMapper()); + params.put("type_id", pet.getTypeId()); + petType = JdbcVisitRepositoryImpl.this.namedParameterJdbcTemplate.queryForObject( + "SELECT id, name FROM types WHERE id= :type_id", + params, + BeanPropertyRowMapper.newInstance(PetType.class)); + pet.setType(petType); + params.put("owner_id", pet.getOwnerId()); + owner = JdbcVisitRepositoryImpl.this.namedParameterJdbcTemplate.queryForObject( + "SELECT id, first_name, last_name, address, city, telephone FROM owners WHERE id= :owner_id", + params, + BeanPropertyRowMapper.newInstance(Owner.class)); + pet.setOwner(owner); + visit.setPet(pet); + return visit; + } + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcVisitRowMapper.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcVisitRowMapper.java new file mode 100644 index 0000000..9cab6c5 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/JdbcVisitRowMapper.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.jdbc; + + +import org.springframework.jdbc.core.RowMapper; +import org.springframework.samples.petclinic.model.Visit; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.time.LocalDate; +import java.util.Date; + +/** + * {@link RowMapper} implementation mapping data from a {@link ResultSet} to the corresponding properties + * of the {@link Visit} class. + */ +class JdbcVisitRowMapper implements RowMapper { + + @Override + public Visit mapRow(ResultSet rs, int row) throws SQLException { + Visit visit = new Visit(); + visit.setId(rs.getInt("visit_id")); + visit.setDate(rs.getObject("visit_date", LocalDate.class)); + visit.setDescription(rs.getString("description")); + return visit; + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/package-info.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/package-info.java new file mode 100644 index 0000000..d093503 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jdbc/package-info.java @@ -0,0 +1,6 @@ +/** + * The classes in this package represent the JDBC implementation + * of PetClinic's persistence layer. + */ +package org.springframework.samples.petclinic.repository.jdbc; + diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaOwnerRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaOwnerRepositoryImpl.java new file mode 100644 index 0000000..3240d07 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaOwnerRepositoryImpl.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.jpa; + +import java.util.Collection; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; + +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.orm.hibernate5.support.OpenSessionInViewFilter; +import org.springframework.samples.petclinic.model.Owner; +import org.springframework.samples.petclinic.repository.OwnerRepository; +import org.springframework.stereotype.Repository; + +/** + * JPA implementation of the {@link OwnerRepository} interface. + * + * @author Mike Keith + * @author Rod Johnson + * @author Sam Brannen + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +@Repository +@Profile("jpa") +public class JpaOwnerRepositoryImpl implements OwnerRepository { + + @PersistenceContext + private EntityManager em; + + + /** + * Important: in the current version of this method, we load Owners with all their Pets and Visits while + * we do not need Visits at all and we only need one property from the Pet objects (the 'name' property). + * There are some ways to improve it such as: + * - creating a Ligtweight class (example here: https://community.jboss.org/wiki/LightweightClass) + * - Turning on lazy-loading and using {@link OpenSessionInViewFilter} + */ + @SuppressWarnings("unchecked") + public Collection findByLastName(String lastName) { + // using 'join fetch' because a single query should load both owners and pets + // using 'left join fetch' because it might happen that an owner does not have pets yet + Query query = this.em.createQuery("SELECT DISTINCT owner FROM Owner owner left join fetch owner.pets WHERE owner.lastName LIKE :lastName"); + query.setParameter("lastName", lastName + "%"); + return query.getResultList(); + } + + @Override + public Owner findById(int id) { + // using 'join fetch' because a single query should load both owners and pets + // using 'left join fetch' because it might happen that an owner does not have pets yet + Query query = this.em.createQuery("SELECT owner FROM Owner owner left join fetch owner.pets WHERE owner.id =:id"); + query.setParameter("id", id); + return (Owner) query.getSingleResult(); + } + + + @Override + public void save(Owner owner) { + if (owner.getId() == null) { + this.em.persist(owner); + } else { + this.em.merge(owner); + } + + } + + @SuppressWarnings("unchecked") + @Override + public Collection findAll() throws DataAccessException { + Query query = this.em.createQuery("SELECT owner FROM Owner owner"); + return query.getResultList(); + } + + @Override + public void delete(Owner owner) throws DataAccessException { + this.em.remove(this.em.contains(owner) ? owner : this.em.merge(owner)); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaPetRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaPetRepositoryImpl.java new file mode 100644 index 0000000..8b04981 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaPetRepositoryImpl.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.jpa; + +import java.util.Collection; +import java.util.List; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.repository.PetRepository; +import org.springframework.stereotype.Repository; + +/** + * JPA implementation of the {@link PetRepository} interface. + * + * @author Mike Keith + * @author Rod Johnson + * @author Sam Brannen + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +@Repository +@Profile("jpa") +public class JpaPetRepositoryImpl implements PetRepository { + + @PersistenceContext + private EntityManager em; + + @Override + @SuppressWarnings("unchecked") + public List findPetTypes() { + return this.em.createQuery("SELECT ptype FROM PetType ptype ORDER BY ptype.name").getResultList(); + } + + @Override + public Pet findById(int id) { + return this.em.find(Pet.class, id); + } + + @Override + public void save(Pet pet) { + if (pet.getId() == null) { + this.em.persist(pet); + } else { + this.em.merge(pet); + } + } + + @SuppressWarnings("unchecked") + @Override + public Collection findAll() throws DataAccessException { + return this.em.createQuery("SELECT pet FROM Pet pet").getResultList(); + } + + @Override + public void delete(Pet pet) throws DataAccessException { + //this.em.remove(this.em.contains(pet) ? pet : this.em.merge(pet)); + String petId = pet.getId().toString(); + this.em.createQuery("DELETE FROM Visit visit WHERE pet.id=" + petId).executeUpdate(); + this.em.createQuery("DELETE FROM Pet pet WHERE id=" + petId).executeUpdate(); + if (em.contains(pet)) { + em.remove(pet); + } + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaPetTypeRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaPetTypeRepositoryImpl.java new file mode 100644 index 0000000..184aa38 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaPetTypeRepositoryImpl.java @@ -0,0 +1,92 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.jpa; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.model.Visit; +import org.springframework.samples.petclinic.repository.PetTypeRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Repository +@Profile("jpa") +public class JpaPetTypeRepositoryImpl implements PetTypeRepository { + + @PersistenceContext + private EntityManager em; + + @Override + public PetType findById(int id) { + return this.em.find(PetType.class, id); + } + + @Override + public PetType findByName(String name) throws DataAccessException { + return this.em.createQuery("SELECT p FROM PetType p WHERE p.name = :name", PetType.class) + .setParameter("name", name) + .getSingleResult(); + } + + + @SuppressWarnings("unchecked") + @Override + public Collection findAll() throws DataAccessException { + return this.em.createQuery("SELECT ptype FROM PetType ptype").getResultList(); + } + + @Override + public void save(PetType petType) throws DataAccessException { + if (petType.getId() == null) { + this.em.persist(petType); + } else { + this.em.merge(petType); + } + + } + + @SuppressWarnings("unchecked") + @Override + public void delete(PetType petType) throws DataAccessException { + this.em.remove(this.em.contains(petType) ? petType : this.em.merge(petType)); + Integer petTypeId = petType.getId(); + + List pets = this.em.createQuery("SELECT pet FROM Pet pet WHERE type.id=" + petTypeId).getResultList(); + for (Pet pet : pets){ + List visits = pet.getVisits(); + for (Visit visit : visits){ + this.em.createQuery("DELETE FROM Visit visit WHERE id=" + visit.getId()).executeUpdate(); + } + this.em.createQuery("DELETE FROM Pet pet WHERE id=" + pet.getId()).executeUpdate(); + } + this.em.createQuery("DELETE FROM PetType pettype WHERE id=" + petTypeId).executeUpdate(); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaSpecialtyRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaSpecialtyRepositoryImpl.java new file mode 100644 index 0000000..3c95587 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaSpecialtyRepositoryImpl.java @@ -0,0 +1,80 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.jpa; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.Specialty; +import org.springframework.samples.petclinic.repository.SpecialtyRepository; +import org.springframework.stereotype.Repository; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Repository +@Profile("jpa") +public class JpaSpecialtyRepositoryImpl implements SpecialtyRepository { + + @PersistenceContext + private EntityManager em; + + @Override + public Specialty findById(int id) { + return this.em.find(Specialty.class, id); + } + + @Override + public List findSpecialtiesByNameIn(Set names) { + final String jpql = "SELECT s FROM Specialty s WHERE s.name IN :names"; + return em.createQuery(jpql, Specialty.class) + .setParameter("names", names) + .getResultList(); + } + + @SuppressWarnings("unchecked") + @Override + public Collection findAll() throws DataAccessException { + return this.em.createQuery("SELECT s FROM Specialty s").getResultList(); + } + + @Override + public void save(Specialty specialty) throws DataAccessException { + if (specialty.getId() == null) { + this.em.persist(specialty); + } else { + this.em.merge(specialty); + } + } + + @Override + public void delete(Specialty specialty) throws DataAccessException { + this.em.remove(this.em.contains(specialty) ? specialty : this.em.merge(specialty)); + Integer specId = specialty.getId(); + this.em.createNativeQuery("DELETE FROM vet_specialties WHERE specialty_id=" + specId).executeUpdate(); + this.em.createQuery("DELETE FROM Specialty specialty WHERE id=" + specId).executeUpdate(); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaUserRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaUserRepositoryImpl.java new file mode 100644 index 0000000..de3fd4e --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaUserRepositoryImpl.java @@ -0,0 +1,27 @@ +package org.springframework.samples.petclinic.repository.jpa; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.User; +import org.springframework.samples.petclinic.repository.UserRepository; +import org.springframework.stereotype.Repository; + +@Repository +@Profile("jpa") +public class JpaUserRepositoryImpl implements UserRepository { + + @PersistenceContext + private EntityManager em; + + @Override + public void save(User user) throws DataAccessException { + if (this.em.find(User.class, user.getUsername()) == null) { + this.em.persist(user); + } else { + this.em.merge(user); + } + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaVetRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaVetRepositoryImpl.java new file mode 100644 index 0000000..eed4dbb --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaVetRepositoryImpl.java @@ -0,0 +1,71 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.jpa; + +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.Vet; +import org.springframework.samples.petclinic.repository.VetRepository; +import org.springframework.stereotype.Repository; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.Collection; + +/** + * JPA implementation of the {@link VetRepository} interface. + * + * @author Mike Keith + * @author Rod Johnson + * @author Sam Brannen + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +@Repository +@Profile("jpa") +public class JpaVetRepositoryImpl implements VetRepository { + + @PersistenceContext + private EntityManager em; + + + @Override + public Vet findById(int id) throws DataAccessException { + return this.em.find(Vet.class, id); + } + + @SuppressWarnings("unchecked") + @Override + public Collection findAll() throws DataAccessException { + return this.em.createQuery("SELECT vet FROM Vet vet").getResultList(); + } + + @Override + public void save(Vet vet) throws DataAccessException { + if (vet.getId() == null) { + this.em.persist(vet); + } else { + this.em.merge(vet); + } + } + + @Override + public void delete(Vet vet) throws DataAccessException { + this.em.remove(this.em.contains(vet) ? vet : this.em.merge(vet)); + } + + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaVisitRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaVisitRepositoryImpl.java new file mode 100644 index 0000000..79d0325 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/JpaVisitRepositoryImpl.java @@ -0,0 +1,84 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.jpa; + +import java.util.Collection; +import java.util.List; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import jakarta.persistence.Query; + +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.Visit; +import org.springframework.samples.petclinic.repository.VisitRepository; +import org.springframework.stereotype.Repository; + +/** + * JPA implementation of the ClinicService interface using EntityManager. + *

+ *

The mappings are defined in "orm.xml" located in the META-INF directory. + * + * @author Mike Keith + * @author Rod Johnson + * @author Sam Brannen + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +@Repository +@Profile("jpa") +public class JpaVisitRepositoryImpl implements VisitRepository { + + @PersistenceContext + private EntityManager em; + + + @Override + public void save(Visit visit) { + if (visit.getId() == null) { + this.em.persist(visit); + } else { + this.em.merge(visit); + } + } + + + @Override + @SuppressWarnings("unchecked") + public List findByPetId(Integer petId) { + Query query = this.em.createQuery("SELECT v FROM Visit v where v.pet.id= :id"); + query.setParameter("id", petId); + return query.getResultList(); + } + + @Override + public Visit findById(int id) throws DataAccessException { + return this.em.find(Visit.class, id); + } + + @SuppressWarnings("unchecked") + @Override + public Collection findAll() throws DataAccessException { + return this.em.createQuery("SELECT v FROM Visit v").getResultList(); + } + + @Override + public void delete(Visit visit) throws DataAccessException { + this.em.remove(this.em.contains(visit) ? visit : this.em.merge(visit)); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/package-info.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/package-info.java new file mode 100644 index 0000000..04fd89b --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/jpa/package-info.java @@ -0,0 +1,6 @@ +/** + * The classes in this package represent the JPA implementation + * of PetClinic's persistence layer. + */ +package org.springframework.samples.petclinic.repository.jpa; + diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/PetRepositoryOverride.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/PetRepositoryOverride.java new file mode 100644 index 0000000..78c02b0 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/PetRepositoryOverride.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.springdatajpa; + +import org.springframework.context.annotation.Profile; +import org.springframework.samples.petclinic.model.Pet; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Profile("spring-data-jpa") +public interface PetRepositoryOverride { + + void delete(Pet pet); + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/PetTypeRepositoryOverride.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/PetTypeRepositoryOverride.java new file mode 100644 index 0000000..a3b19e9 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/PetTypeRepositoryOverride.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.springdatajpa; + +import org.springframework.context.annotation.Profile; +import org.springframework.samples.petclinic.model.PetType; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Profile("spring-data-jpa") +public interface PetTypeRepositoryOverride { + + void delete(PetType petType); + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpecialtyRepositoryOverride.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpecialtyRepositoryOverride.java new file mode 100644 index 0000000..656d8ef --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpecialtyRepositoryOverride.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.springdatajpa; + +import org.springframework.context.annotation.Profile; +import org.springframework.samples.petclinic.model.Specialty; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Profile("spring-data-jpa") +public interface SpecialtyRepositoryOverride { + + void delete(Specialty specialty); + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataOwnerRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataOwnerRepository.java new file mode 100644 index 0000000..78c0093 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataOwnerRepository.java @@ -0,0 +1,44 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.springdatajpa; + +import java.util.Collection; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; +import org.springframework.samples.petclinic.model.Owner; +import org.springframework.samples.petclinic.repository.OwnerRepository; + +/** + * Spring Data JPA specialization of the {@link OwnerRepository} interface + * + * @author Michael Isvy + * @since 15.1.2013 + */ + +@Profile("spring-data-jpa") +public interface SpringDataOwnerRepository extends OwnerRepository, Repository { + + @Override + @Query("SELECT DISTINCT owner FROM Owner owner left join fetch owner.pets WHERE owner.lastName LIKE :lastName%") + Collection findByLastName(@Param("lastName") String lastName); + + @Override + @Query("SELECT owner FROM Owner owner left join fetch owner.pets WHERE owner.id =:id") + Owner findById(@Param("id") int id); +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetRepository.java new file mode 100644 index 0000000..501d398 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetRepository.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.springdatajpa; + +import java.util.List; + +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.repository.PetRepository; + +/** + * Spring Data JPA specialization of the {@link PetRepository} interface + * + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ + +@Profile("spring-data-jpa") +public interface SpringDataPetRepository extends PetRepository, Repository, PetRepositoryOverride { + + @Override + @Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name") + List findPetTypes() throws DataAccessException; +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetRepositoryImpl.java new file mode 100644 index 0000000..4299505 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetRepositoryImpl.java @@ -0,0 +1,46 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.springdatajpa; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import org.springframework.context.annotation.Profile; +import org.springframework.samples.petclinic.model.Pet; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Profile("spring-data-jpa") +public class SpringDataPetRepositoryImpl implements PetRepositoryOverride { + + @PersistenceContext + private EntityManager em; + + @Override + public void delete(Pet pet) { + String petId = pet.getId().toString(); + this.em.createQuery("DELETE FROM Visit visit WHERE pet.id=" + petId).executeUpdate(); + this.em.createQuery("DELETE FROM Pet pet WHERE id=" + petId).executeUpdate(); + if (em.contains(pet)) { + em.remove(pet); + } + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetTypeRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetTypeRepository.java new file mode 100644 index 0000000..18a6979 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetTypeRepository.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.springdatajpa; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.repository.Repository; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.repository.PetTypeRepository; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Profile("spring-data-jpa") +public interface SpringDataPetTypeRepository extends PetTypeRepository, Repository, PetTypeRepositoryOverride { + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetTypeRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetTypeRepositoryImpl.java new file mode 100644 index 0000000..5350c4e --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataPetTypeRepositoryImpl.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.springdatajpa; + +import org.springframework.context.annotation.Profile; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.model.Visit; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; +import java.util.List; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Profile("spring-data-jpa") +public class SpringDataPetTypeRepositoryImpl implements PetTypeRepositoryOverride { + + @PersistenceContext + private EntityManager em; + + @SuppressWarnings("unchecked") + @Override + public void delete(PetType petType) { + this.em.remove(this.em.contains(petType) ? petType : this.em.merge(petType)); + Integer petTypeId = petType.getId(); + + List pets = this.em.createQuery("SELECT pet FROM Pet pet WHERE type.id=" + petTypeId).getResultList(); + for (Pet pet : pets){ + List visits = pet.getVisits(); + for (Visit visit : visits){ + this.em.createQuery("DELETE FROM Visit visit WHERE id=" + visit.getId()).executeUpdate(); + } + this.em.createQuery("DELETE FROM Pet pet WHERE id=" + pet.getId()).executeUpdate(); + } + this.em.createQuery("DELETE FROM PetType pettype WHERE id=" + petTypeId).executeUpdate(); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataSpecialtyRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataSpecialtyRepository.java new file mode 100644 index 0000000..5ec7364 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataSpecialtyRepository.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.springdatajpa; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.repository.Repository; +import org.springframework.samples.petclinic.model.Specialty; +import org.springframework.samples.petclinic.repository.SpecialtyRepository; + + +/** + * @author Vitaliy Fedoriv + * + */ + +@Profile("spring-data-jpa") +public interface SpringDataSpecialtyRepository extends SpecialtyRepository, Repository, SpecialtyRepositoryOverride { + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataSpecialtyRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataSpecialtyRepositoryImpl.java new file mode 100644 index 0000000..940625f --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataSpecialtyRepositoryImpl.java @@ -0,0 +1,44 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.springdatajpa; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import org.springframework.context.annotation.Profile; +import org.springframework.samples.petclinic.model.Specialty; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Profile("spring-data-jpa") +public class SpringDataSpecialtyRepositoryImpl implements SpecialtyRepositoryOverride { + + @PersistenceContext + private EntityManager em; + + @Override + public void delete(Specialty specialty) { + this.em.remove(this.em.contains(specialty) ? specialty : this.em.merge(specialty)); + Integer specId = specialty.getId(); + this.em.createNativeQuery("DELETE FROM vet_specialties WHERE specialty_id=" + specId).executeUpdate(); + this.em.createQuery("DELETE FROM Specialty specialty WHERE id=" + specId).executeUpdate(); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataUserRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataUserRepository.java new file mode 100644 index 0000000..a698c82 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataUserRepository.java @@ -0,0 +1,11 @@ +package org.springframework.samples.petclinic.repository.springdatajpa; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.repository.Repository; +import org.springframework.samples.petclinic.model.User; +import org.springframework.samples.petclinic.repository.UserRepository; + +@Profile("spring-data-jpa") +public interface SpringDataUserRepository extends UserRepository, Repository { + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataVetRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataVetRepository.java new file mode 100644 index 0000000..c52e440 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataVetRepository.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.springdatajpa; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.repository.Repository; +import org.springframework.samples.petclinic.model.Vet; +import org.springframework.samples.petclinic.repository.VetRepository; + +/** + * Spring Data JPA specialization of the {@link VetRepository} interface + * + * @author Michael Isvy + * @since 15.1.2013 + */ + +@Profile("spring-data-jpa") +public interface SpringDataVetRepository extends VetRepository, Repository { +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataVisitRepository.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataVisitRepository.java new file mode 100644 index 0000000..1b3c6c3 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataVisitRepository.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.repository.springdatajpa; + +import org.springframework.context.annotation.Profile; +import org.springframework.data.repository.Repository; +import org.springframework.samples.petclinic.model.Visit; +import org.springframework.samples.petclinic.repository.VisitRepository; + +/** + * Spring Data JPA specialization of the {@link VisitRepository} interface + * + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ + +@Profile("spring-data-jpa") +public interface SpringDataVisitRepository extends VisitRepository, Repository, VisitRepositoryOverride { +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataVisitRepositoryImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataVisitRepositoryImpl.java new file mode 100644 index 0000000..4c2fcc0 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/SpringDataVisitRepositoryImpl.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.springdatajpa; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.PersistenceContext; + +import org.springframework.context.annotation.Profile; +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.Visit; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Profile("spring-data-jpa") +public class SpringDataVisitRepositoryImpl implements VisitRepositoryOverride { + + @PersistenceContext + private EntityManager em; + + @Override + public void delete(Visit visit) throws DataAccessException { + String visitId = visit.getId().toString(); + this.em.createQuery("DELETE FROM Visit visit WHERE id=" + visitId).executeUpdate(); + if (em.contains(visit)) { + em.remove(visit); + } + } + + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/VisitRepositoryOverride.java b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/VisitRepositoryOverride.java new file mode 100644 index 0000000..7a622f3 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/repository/springdatajpa/VisitRepositoryOverride.java @@ -0,0 +1,32 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.repository.springdatajpa; + +import org.springframework.context.annotation.Profile; +import org.springframework.samples.petclinic.model.Visit; + +/** + * @author Vitaliy Fedoriv + * + */ + +@Profile("spring-data-jpa") +public interface VisitRepositoryOverride { + + void delete(Visit visit); + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/rest/advice/ExceptionControllerAdvice.java b/backend/src/main/java/org/springframework/samples/petclinic/rest/advice/ExceptionControllerAdvice.java new file mode 100644 index 0000000..a01b1eb --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/rest/advice/ExceptionControllerAdvice.java @@ -0,0 +1,109 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.advice; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.samples.petclinic.rest.controller.BindingErrorsResponse; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.WebRequest; + +import static org.springframework.http.HttpStatus.BAD_REQUEST; + +/** + * Global Exception handler for REST controllers. + *

+ * This class handles exceptions thrown by REST controllers and returns + * appropriate HTTP responses to the client. + * + * @author Vitaliy Fedoriv + * @author Alexander Dudkin + */ +@ControllerAdvice +public class ExceptionControllerAdvice { + + /** + * Record for storing error information. + *

+ * This record encapsulates the class name and message of the exception. + * + * @param className The name of the exception class + * @param exMessage The message of the exception + */ + private record ErrorInfo(String className, String exMessage) { + public ErrorInfo(Exception ex) { + this(ex.getClass().getName(), ex.getLocalizedMessage()); + } + } + + /** + * Handles all general exceptions by returning a 500 Internal Server Error status with error details. + * + * @param e The exception to be handled + * @return A {@link ResponseEntity} containing the error information and a 500 Internal Server Error status + */ + @ExceptionHandler(Exception.class) + public ResponseEntity handleGeneralException(Exception e) { + ErrorInfo info = new ErrorInfo(e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(info); + } + + /** + * Handles {@link DataIntegrityViolationException} which typically indicates database constraint violations. + * This method returns a 404 Not Found status if an entity does not exist. + * + * @param ex The {@link DataIntegrityViolationException} to be handled + * @return A {@link ResponseEntity} containing the error information and a 404 Not Found status + */ + @ExceptionHandler(DataIntegrityViolationException.class) + @ResponseStatus(code = HttpStatus.NOT_FOUND) + @ResponseBody + public ResponseEntity handleDataIntegrityViolationException(DataIntegrityViolationException ex) { + ErrorInfo errorInfo = new ErrorInfo(ex); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorInfo); + } + + /** + * Handles exception thrown by Bean Validation on controller methods parameters + * + * @param ex The thrown exception + * + * @return an empty response entity + */ + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(BAD_REQUEST) + @ResponseBody + public ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) { + BindingErrorsResponse errors = new BindingErrorsResponse(); + BindingResult bindingResult = ex.getBindingResult(); + if (bindingResult.hasErrors()) { + errors.addAllErrors(bindingResult); + return ResponseEntity.badRequest().body(new ErrorInfo("MethodArgumentNotValidException", "Validation failed")); + } + return ResponseEntity.badRequest().build(); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/BindingErrorsResponse.java b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/BindingErrorsResponse.java new file mode 100644 index 0000000..0c77284 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/BindingErrorsResponse.java @@ -0,0 +1,137 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.validation.BindingResult; +import org.springframework.validation.FieldError; + +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author Vitaliy Fedoriv + * + */ + +public class BindingErrorsResponse { + + public BindingErrorsResponse() { + this(null); + } + + public BindingErrorsResponse(Integer id) { + this(null, id); + } + + public BindingErrorsResponse(Integer pathId, Integer bodyId) { + boolean onlyBodyIdSpecified = pathId == null && bodyId != null; + if (onlyBodyIdSpecified) { + addBodyIdError(bodyId, "must not be specified"); + } + boolean bothIdsSpecified = pathId != null && bodyId != null; + if (bothIdsSpecified && !pathId.equals(bodyId)) { + addBodyIdError(bodyId, String.format("does not match pathId: %d", pathId)); + } + } + + private void addBodyIdError(Integer bodyId, String message) { + BindingError error = new BindingError(); + error.setObjectName("body"); + error.setFieldName("id"); + error.setFieldValue(bodyId.toString()); + error.setErrorMessage(message); + addError(error); + } + + private final List bindingErrors = new ArrayList(); + + public void addError(BindingError bindingError) { + this.bindingErrors.add(bindingError); + } + + public void addAllErrors(BindingResult bindingResult) { + for (FieldError fieldError : bindingResult.getFieldErrors()) { + BindingError error = new BindingError(); + error.setObjectName(fieldError.getObjectName()); + error.setFieldName(fieldError.getField()); + error.setFieldValue(String.valueOf(fieldError.getRejectedValue())); + error.setErrorMessage(fieldError.getDefaultMessage()); + addError(error); + } + } + + public String toJSON() { + ObjectMapper mapper = new ObjectMapper(); + mapper.setVisibility(PropertyAccessor.FIELD, Visibility.ANY); + String errorsAsJSON = ""; + try { + errorsAsJSON = mapper.writeValueAsString(bindingErrors); + } catch (JsonProcessingException e) { + e.printStackTrace(); + } + return errorsAsJSON; + } + + @Override + public String toString() { + return "BindingErrorsResponse [bindingErrors=" + bindingErrors + "]"; + } + + protected static class BindingError { + + private String objectName; + private String fieldName; + private String fieldValue; + private String errorMessage; + + public BindingError() { + this.objectName = ""; + this.fieldName = ""; + this.fieldValue = ""; + this.errorMessage = ""; + } + + protected void setObjectName(String objectName) { + this.objectName = objectName; + } + + protected void setFieldName(String fieldName) { + this.fieldName = fieldName; + } + + protected void setFieldValue(String fieldValue) { + this.fieldValue = fieldValue; + } + + protected void setErrorMessage(String error_message) { + this.errorMessage = error_message; + } + + @Override + public String toString() { + return "BindingError [objectName=" + objectName + ", fieldName=" + fieldName + ", fieldValue=" + fieldValue + + ", errorMessage=" + errorMessage + "]"; + } + + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/OwnerRestController.java b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/OwnerRestController.java new file mode 100644 index 0000000..906f8f5 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/OwnerRestController.java @@ -0,0 +1,177 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.samples.petclinic.mapper.OwnerMapper; +import org.springframework.samples.petclinic.mapper.PetMapper; +import org.springframework.samples.petclinic.mapper.VisitMapper; +import org.springframework.samples.petclinic.model.Owner; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.model.Visit; +import org.springframework.samples.petclinic.rest.api.OwnersApi; +import org.springframework.samples.petclinic.rest.dto.*; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; + +import jakarta.transaction.Transactional; + +import java.util.Collection; +import java.util.List; + +/** + * @author Vitaliy Fedoriv + */ + +@RestController +@CrossOrigin(exposedHeaders = "errors, content-type") +@RequestMapping("/api") +public class OwnerRestController implements OwnersApi { + + private final ClinicService clinicService; + + private final OwnerMapper ownerMapper; + + private final PetMapper petMapper; + + private final VisitMapper visitMapper; + + public OwnerRestController(ClinicService clinicService, + OwnerMapper ownerMapper, + PetMapper petMapper, + VisitMapper visitMapper) { + this.clinicService = clinicService; + this.ownerMapper = ownerMapper; + this.petMapper = petMapper; + this.visitMapper = visitMapper; + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity> listOwners(String lastName) { + Collection owners; + if (lastName != null) { + owners = this.clinicService.findOwnerByLastName(lastName); + } else { + owners = this.clinicService.findAllOwners(); + } + if (owners.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(ownerMapper.toOwnerDtoCollection(owners), HttpStatus.OK); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity getOwner(Integer ownerId) { + Owner owner = this.clinicService.findOwnerById(ownerId); + if (owner == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(ownerMapper.toOwnerDto(owner), HttpStatus.OK); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity addOwner(OwnerFieldsDto ownerFieldsDto) { + HttpHeaders headers = new HttpHeaders(); + Owner owner = ownerMapper.toOwner(ownerFieldsDto); + this.clinicService.saveOwner(owner); + OwnerDto ownerDto = ownerMapper.toOwnerDto(owner); + headers.setLocation(UriComponentsBuilder.newInstance() + .path("/api/owners/{id}").buildAndExpand(owner.getId()).toUri()); + return new ResponseEntity<>(ownerDto, headers, HttpStatus.CREATED); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity updateOwner(Integer ownerId, OwnerFieldsDto ownerFieldsDto) { + Owner currentOwner = this.clinicService.findOwnerById(ownerId); + if (currentOwner == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + currentOwner.setAddress(ownerFieldsDto.getAddress()); + currentOwner.setCity(ownerFieldsDto.getCity()); + currentOwner.setFirstName(ownerFieldsDto.getFirstName()); + currentOwner.setLastName(ownerFieldsDto.getLastName()); + currentOwner.setTelephone(ownerFieldsDto.getTelephone()); + this.clinicService.saveOwner(currentOwner); + return new ResponseEntity<>(ownerMapper.toOwnerDto(currentOwner), HttpStatus.NO_CONTENT); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Transactional + @Override + public ResponseEntity deleteOwner(Integer ownerId) { + Owner owner = this.clinicService.findOwnerById(ownerId); + if (owner == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + this.clinicService.deleteOwner(owner); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity addPetToOwner(Integer ownerId, PetFieldsDto petFieldsDto) { + HttpHeaders headers = new HttpHeaders(); + Pet pet = petMapper.toPet(petFieldsDto); + Owner owner = new Owner(); + owner.setId(ownerId); + pet.setOwner(owner); + this.clinicService.savePet(pet); + PetDto petDto = petMapper.toPetDto(pet); + headers.setLocation(UriComponentsBuilder.newInstance().path("/api/pets/{id}") + .buildAndExpand(pet.getId()).toUri()); + return new ResponseEntity<>(petDto, headers, HttpStatus.CREATED); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity addVisitToOwner(Integer ownerId, Integer petId, VisitFieldsDto visitFieldsDto) { + HttpHeaders headers = new HttpHeaders(); + Visit visit = visitMapper.toVisit(visitFieldsDto); + Pet pet = new Pet(); + pet.setId(petId); + visit.setPet(pet); + this.clinicService.saveVisit(visit); + VisitDto visitDto = visitMapper.toVisitDto(visit); + headers.setLocation(UriComponentsBuilder.newInstance().path("/api/visits/{id}") + .buildAndExpand(visit.getId()).toUri()); + return new ResponseEntity<>(visitDto, headers, HttpStatus.CREATED); + } + + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity getOwnersPet(Integer ownerId, Integer petId) { + Owner owner = this.clinicService.findOwnerById(ownerId); + if (owner != null) { + Pet pet = owner.getPet(petId); + if (pet != null) { + return new ResponseEntity<>(petMapper.toPetDto(pet), HttpStatus.OK); + } + } + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/PetRestController.java b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/PetRestController.java new file mode 100644 index 0000000..e5727fd --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/PetRestController.java @@ -0,0 +1,109 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.samples.petclinic.mapper.PetMapper; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.rest.api.PetsApi; +import org.springframework.samples.petclinic.rest.dto.PetDto; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.util.UriComponentsBuilder; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vitaliy Fedoriv + */ + +@RestController +@CrossOrigin(exposedHeaders = "errors, content-type") +@RequestMapping("api") +public class PetRestController implements PetsApi { + + private final ClinicService clinicService; + + private final PetMapper petMapper; + + public PetRestController(ClinicService clinicService, PetMapper petMapper) { + this.clinicService = clinicService; + this.petMapper = petMapper; + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity getPet(Integer petId) { + PetDto pet = petMapper.toPetDto(this.clinicService.findPetById(petId)); + if (pet == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(pet, HttpStatus.OK); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity> listPets() { + List pets = new ArrayList<>(petMapper.toPetsDto(this.clinicService.findAllPets())); + if (pets.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(pets, HttpStatus.OK); + } + + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity updatePet(Integer petId, PetDto petDto) { + Pet currentPet = this.clinicService.findPetById(petId); + if (currentPet == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + currentPet.setBirthDate(petDto.getBirthDate()); + currentPet.setName(petDto.getName()); + currentPet.setType(petMapper.toPetType(petDto.getType())); + this.clinicService.savePet(currentPet); + return new ResponseEntity<>(petMapper.toPetDto(currentPet), HttpStatus.NO_CONTENT); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity deletePet(Integer petId) { + Pet pet = this.clinicService.findPetById(petId); + if (pet == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + this.clinicService.deletePet(pet); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity addPet(PetDto petDto) { + HttpHeaders headers = new HttpHeaders(); + Pet pet = petMapper.toPet(petDto); + this.clinicService.savePet(pet); + headers.setLocation(UriComponentsBuilder.newInstance().path("/api/pets/{id}").buildAndExpand(pet.getId()).toUri()); + return new ResponseEntity<>(petMapper.toPetDto(pet), headers, HttpStatus.CREATED); + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/PetTypeRestController.java b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/PetTypeRestController.java new file mode 100644 index 0000000..3d0f423 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/PetTypeRestController.java @@ -0,0 +1,105 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.samples.petclinic.mapper.PetTypeMapper; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.rest.api.PettypesApi; +import org.springframework.samples.petclinic.rest.dto.PetTypeDto; +import org.springframework.samples.petclinic.rest.dto.PetTypeFieldsDto; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.util.UriComponentsBuilder; + +import jakarta.transaction.Transactional; + +import java.util.ArrayList; +import java.util.List; + +@RestController +@CrossOrigin(exposedHeaders = "errors, content-type") +@RequestMapping("api") +public class PetTypeRestController implements PettypesApi { + + private final ClinicService clinicService; + private final PetTypeMapper petTypeMapper; + + + public PetTypeRestController(ClinicService clinicService, PetTypeMapper petTypeMapper) { + this.clinicService = clinicService; + this.petTypeMapper = petTypeMapper; + } + + @PreAuthorize("hasAnyRole(@roles.OWNER_ADMIN, @roles.VET_ADMIN)") + @Override + public ResponseEntity> listPetTypes() { + List petTypes = new ArrayList<>(this.clinicService.findAllPetTypes()); + if (petTypes.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(petTypeMapper.toPetTypeDtos(petTypes), HttpStatus.OK); + } + + @PreAuthorize("hasAnyRole(@roles.OWNER_ADMIN, @roles.VET_ADMIN)") + @Override + public ResponseEntity getPetType(Integer petTypeId) { + PetType petType = this.clinicService.findPetTypeById(petTypeId); + if (petType == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(petTypeMapper.toPetTypeDto(petType), HttpStatus.OK); + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Override + public ResponseEntity addPetType(PetTypeFieldsDto petTypeFieldsDto) { + HttpHeaders headers = new HttpHeaders(); + final PetType type = petTypeMapper.toPetType(petTypeFieldsDto); + this.clinicService.savePetType(type); + headers.setLocation(UriComponentsBuilder.newInstance().path("/api/pettypes/{id}").buildAndExpand(type.getId()).toUri()); + return new ResponseEntity<>(petTypeMapper.toPetTypeDto(type), headers, HttpStatus.CREATED); + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Override + public ResponseEntity updatePetType(Integer petTypeId, PetTypeDto petTypeDto) { + PetType currentPetType = this.clinicService.findPetTypeById(petTypeId); + if (currentPetType == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + currentPetType.setName(petTypeDto.getName()); + this.clinicService.savePetType(currentPetType); + return new ResponseEntity<>(petTypeMapper.toPetTypeDto(currentPetType), HttpStatus.NO_CONTENT); + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Transactional + @Override + public ResponseEntity deletePetType(Integer petTypeId) { + PetType petType = this.clinicService.findPetTypeById(petTypeId); + if (petType == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + this.clinicService.deletePetType(petType); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/RootRestController.java b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/RootRestController.java new file mode 100644 index 0000000..7984f66 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/RootRestController.java @@ -0,0 +1,47 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import java.io.IOException; + +import jakarta.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * @author Vitaliy Fedoriv + * + */ + +@RestController +@CrossOrigin(exposedHeaders = "errors, content-type") +@RequestMapping("/") +public class RootRestController { + + @Value("#{servletContext.contextPath}") + private String servletContextPath; + + @RequestMapping(value = "/") + public void redirectToSwagger(HttpServletResponse response) throws IOException { + response.sendRedirect(this.servletContextPath + "/swagger-ui/index.html"); + } + +} + diff --git a/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/SpecialtyRestController.java b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/SpecialtyRestController.java new file mode 100644 index 0000000..0b82e81 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/SpecialtyRestController.java @@ -0,0 +1,108 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.samples.petclinic.mapper.SpecialtyMapper; +import org.springframework.samples.petclinic.model.Specialty; +import org.springframework.samples.petclinic.rest.api.SpecialtiesApi; +import org.springframework.samples.petclinic.rest.dto.SpecialtyDto; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.util.UriComponentsBuilder; + +import jakarta.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vitaliy Fedoriv + */ + +@RestController +@CrossOrigin(exposedHeaders = "errors, content-type") +@RequestMapping("api") +public class SpecialtyRestController implements SpecialtiesApi { + + private final ClinicService clinicService; + + private final SpecialtyMapper specialtyMapper; + + public SpecialtyRestController(ClinicService clinicService, SpecialtyMapper specialtyMapper) { + this.clinicService = clinicService; + this.specialtyMapper = specialtyMapper; + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Override + public ResponseEntity> listSpecialties() { + List specialties = new ArrayList<>(); + specialties.addAll(specialtyMapper.toSpecialtyDtos(this.clinicService.findAllSpecialties())); + if (specialties.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(specialties, HttpStatus.OK); + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Override + public ResponseEntity getSpecialty(Integer specialtyId) { + Specialty specialty = this.clinicService.findSpecialtyById(specialtyId); + if (specialty == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(specialtyMapper.toSpecialtyDto(specialty), HttpStatus.OK); + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Override + public ResponseEntity addSpecialty(SpecialtyDto specialtyDto) { + HttpHeaders headers = new HttpHeaders(); + Specialty specialty = specialtyMapper.toSpecialty(specialtyDto); + this.clinicService.saveSpecialty(specialty); + headers.setLocation(UriComponentsBuilder.newInstance().path("/api/specialties/{id}").buildAndExpand(specialty.getId()).toUri()); + return new ResponseEntity<>(specialtyMapper.toSpecialtyDto(specialty), headers, HttpStatus.CREATED); + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Override + public ResponseEntity updateSpecialty(Integer specialtyId, SpecialtyDto specialtyDto) { + Specialty currentSpecialty = this.clinicService.findSpecialtyById(specialtyId); + if (currentSpecialty == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + currentSpecialty.setName(specialtyDto.getName()); + this.clinicService.saveSpecialty(currentSpecialty); + return new ResponseEntity<>(specialtyMapper.toSpecialtyDto(currentSpecialty), HttpStatus.NO_CONTENT); + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Transactional + @Override + public ResponseEntity deleteSpecialty(Integer specialtyId) { + Specialty specialty = this.clinicService.findSpecialtyById(specialtyId); + if (specialty == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + this.clinicService.deleteSpecialty(specialty); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/UserRestController.java b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/UserRestController.java new file mode 100644 index 0000000..ce1d843 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/UserRestController.java @@ -0,0 +1,57 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.samples.petclinic.mapper.UserMapper; +import org.springframework.samples.petclinic.model.User; +import org.springframework.samples.petclinic.rest.api.UsersApi; +import org.springframework.samples.petclinic.rest.dto.UserDto; +import org.springframework.samples.petclinic.service.UserService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.CrossOrigin; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import jakarta.validation.Valid; + +@RestController +@CrossOrigin(exposedHeaders = "errors, content-type") +@RequestMapping("api") +public class UserRestController implements UsersApi { + + private final UserService userService; + private final UserMapper userMapper; + + public UserRestController(UserService userService, UserMapper userMapper) { + this.userService = userService; + this.userMapper = userMapper; + } + + + @PreAuthorize( "hasRole(@roles.ADMIN)" ) + @Override + public ResponseEntity addUser(UserDto userDto) { + HttpHeaders headers = new HttpHeaders(); + User user = userMapper.toUser(userDto); + this.userService.saveUser(user); + return new ResponseEntity<>(userMapper.toUserDto(user), headers, HttpStatus.CREATED); + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/VetRestController.java b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/VetRestController.java new file mode 100644 index 0000000..531dede --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/VetRestController.java @@ -0,0 +1,123 @@ +/* + * Copyright 2016-2018 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.rest.controller; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.samples.petclinic.mapper.SpecialtyMapper; +import org.springframework.samples.petclinic.mapper.VetMapper; +import org.springframework.samples.petclinic.model.Specialty; +import org.springframework.samples.petclinic.model.Vet; +import org.springframework.samples.petclinic.rest.api.VetsApi; +import org.springframework.samples.petclinic.rest.dto.VetDto; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.util.UriComponentsBuilder; + +import jakarta.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +/** + * @author Vitaliy Fedoriv + */ + +@RestController +@CrossOrigin(exposedHeaders = "errors, content-type") +@RequestMapping("api") +public class VetRestController implements VetsApi { + + private final ClinicService clinicService; + private final VetMapper vetMapper; + private final SpecialtyMapper specialtyMapper; + + public VetRestController(ClinicService clinicService, VetMapper vetMapper, SpecialtyMapper specialtyMapper) { + this.clinicService = clinicService; + this.vetMapper = vetMapper; + this.specialtyMapper = specialtyMapper; + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Override + public ResponseEntity> listVets() { + List vets = new ArrayList<>(); + vets.addAll(vetMapper.toVetDtos(this.clinicService.findAllVets())); + if (vets.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(vets, HttpStatus.OK); + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Override + public ResponseEntity getVet(Integer vetId) { + Vet vet = this.clinicService.findVetById(vetId); + if (vet == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(vetMapper.toVetDto(vet), HttpStatus.OK); + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Override + public ResponseEntity addVet(VetDto vetDto) { + HttpHeaders headers = new HttpHeaders(); + Vet vet = vetMapper.toVet(vetDto); + if(vet.getNrOfSpecialties() > 0){ + List vetSpecialities = this.clinicService.findSpecialtiesByNameIn(vet.getSpecialties().stream().map(Specialty::getName).collect(Collectors.toSet())); + vet.setSpecialties(vetSpecialities); + } + this.clinicService.saveVet(vet); + headers.setLocation(UriComponentsBuilder.newInstance().path("/api/vets/{id}").buildAndExpand(vet.getId()).toUri()); + return new ResponseEntity<>(vetMapper.toVetDto(vet), headers, HttpStatus.CREATED); + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Override + public ResponseEntity updateVet(Integer vetId,VetDto vetDto) { + Vet currentVet = this.clinicService.findVetById(vetId); + if (currentVet == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + currentVet.setFirstName(vetDto.getFirstName()); + currentVet.setLastName(vetDto.getLastName()); + currentVet.clearSpecialties(); + for (Specialty spec : specialtyMapper.toSpecialtys(vetDto.getSpecialties())) { + currentVet.addSpecialty(spec); + } + if(currentVet.getNrOfSpecialties() > 0){ + List vetSpecialities = this.clinicService.findSpecialtiesByNameIn(currentVet.getSpecialties().stream().map(Specialty::getName).collect(Collectors.toSet())); + currentVet.setSpecialties(vetSpecialities); + } + this.clinicService.saveVet(currentVet); + return new ResponseEntity<>(vetMapper.toVetDto(currentVet), HttpStatus.NO_CONTENT); + } + + @PreAuthorize("hasRole(@roles.VET_ADMIN)") + @Transactional + @Override + public ResponseEntity deleteVet(Integer vetId) { + Vet vet = this.clinicService.findVetById(vetId); + if (vet == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + this.clinicService.deleteVet(vet); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/VisitRestController.java b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/VisitRestController.java new file mode 100644 index 0000000..138f040 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/rest/controller/VisitRestController.java @@ -0,0 +1,110 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.samples.petclinic.mapper.VisitMapper; +import org.springframework.samples.petclinic.model.Visit; +import org.springframework.samples.petclinic.rest.api.VisitsApi; +import org.springframework.samples.petclinic.rest.dto.VisitDto; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.*; +import org.springframework.web.util.UriComponentsBuilder; + +import jakarta.transaction.Transactional; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Vitaliy Fedoriv + */ + +@RestController +@CrossOrigin(exposedHeaders = "errors, content-type") +@RequestMapping("api") +public class VisitRestController implements VisitsApi { + + private final ClinicService clinicService; + + private final VisitMapper visitMapper; + + public VisitRestController(ClinicService clinicService, VisitMapper visitMapper) { + this.clinicService = clinicService; + this.visitMapper = visitMapper; + } + + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity> listVisits() { + List visits = new ArrayList<>(this.clinicService.findAllVisits()); + if (visits.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(new ArrayList<>(visitMapper.toVisitsDto(visits)), HttpStatus.OK); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity getVisit( Integer visitId) { + Visit visit = this.clinicService.findVisitById(visitId); + if (visit == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + return new ResponseEntity<>(visitMapper.toVisitDto(visit), HttpStatus.OK); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity addVisit(VisitDto visitDto) { + HttpHeaders headers = new HttpHeaders(); + Visit visit = visitMapper.toVisit(visitDto); + this.clinicService.saveVisit(visit); + visitDto = visitMapper.toVisitDto(visit); + headers.setLocation(UriComponentsBuilder.newInstance().path("/api/visits/{id}").buildAndExpand(visit.getId()).toUri()); + return new ResponseEntity<>(visitDto, headers, HttpStatus.CREATED); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Override + public ResponseEntity updateVisit(Integer visitId, VisitDto visitDto) { + Visit currentVisit = this.clinicService.findVisitById(visitId); + if (currentVisit == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + currentVisit.setDate(visitDto.getDate()); + currentVisit.setDescription(visitDto.getDescription()); + this.clinicService.saveVisit(currentVisit); + return new ResponseEntity<>(visitMapper.toVisitDto(currentVisit), HttpStatus.NO_CONTENT); + } + + @PreAuthorize("hasRole(@roles.OWNER_ADMIN)") + @Transactional + @Override + public ResponseEntity deleteVisit(Integer visitId) { + Visit visit = this.clinicService.findVisitById(visitId); + if (visit == null) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + this.clinicService.deleteVisit(visit); + return new ResponseEntity<>(HttpStatus.NO_CONTENT); + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/rest/package-info.java b/backend/src/main/java/org/springframework/samples/petclinic/rest/package-info.java new file mode 100644 index 0000000..dfae7ea --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/rest/package-info.java @@ -0,0 +1,5 @@ +/** + * The classes in this package represent PetClinic's REST API. + */ + +package org.springframework.samples.petclinic.rest; \ No newline at end of file diff --git a/backend/src/main/java/org/springframework/samples/petclinic/security/BasicAuthenticationConfig.java b/backend/src/main/java/org/springframework/samples/petclinic/security/BasicAuthenticationConfig.java new file mode 100644 index 0000000..b91e1e4 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/security/BasicAuthenticationConfig.java @@ -0,0 +1,58 @@ +package org.springframework.samples.petclinic.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.Customizer; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.crypto.password.DelegatingPasswordEncoder; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +import javax.sql.DataSource; +import java.util.Map; + +@Configuration +@EnableMethodSecurity(prePostEnabled = true) // Enable @PreAuthorize method-level security +@ConditionalOnProperty(name = "petclinic.security.enable", havingValue = "true") +public class BasicAuthenticationConfig { + + @Autowired + private DataSource dataSource; + + @Bean + public PasswordEncoder passwordEncoder() { + var encoders = Map.of("noop", NoOpPasswordEncoder.getInstance()); + var passwordEncoder = new DelegatingPasswordEncoder("noop", encoders); + passwordEncoder.setDefaultPasswordEncoderForMatches(NoOpPasswordEncoder.getInstance()); + return passwordEncoder; + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests((authz) -> authz + .anyRequest().authenticated()) + .httpBasic(Customizer.withDefaults()); + // @formatter:on + return http.build(); + } + + @Autowired + public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { + // @formatter:off + auth + .jdbcAuthentication() + .dataSource(dataSource) + .usersByUsernameQuery("select username,password,enabled from users where username=?") + .authoritiesByUsernameQuery("select username,role from roles where username=?"); + // @formatter:on + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/security/DisableSecurityConfig.java b/backend/src/main/java/org/springframework/samples/petclinic/security/DisableSecurityConfig.java new file mode 100644 index 0000000..2075fcb --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/security/DisableSecurityConfig.java @@ -0,0 +1,29 @@ +package org.springframework.samples.petclinic.security; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.web.SecurityFilterChain; + +/** + * Starting from Spring Boot 2, if Spring Security is present, endpoints are secured by default + * using Spring Security’s content-negotiation strategy. + */ +@Configuration +@ConditionalOnProperty(name = "petclinic.security.enable", havingValue = "false") +public class DisableSecurityConfig { + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + // @formatter:off + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests((authz) -> authz + .anyRequest().permitAll() + ); + // @formatter:on + return http.build(); + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/security/Roles.java b/backend/src/main/java/org/springframework/samples/petclinic/security/Roles.java new file mode 100644 index 0000000..d60f049 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/security/Roles.java @@ -0,0 +1,11 @@ +package org.springframework.samples.petclinic.security; + +import org.springframework.stereotype.Component; + +@Component +public class Roles { + + public final String OWNER_ADMIN = "ROLE_OWNER_ADMIN"; + public final String VET_ADMIN = "ROLE_VET_ADMIN"; + public final String ADMIN = "ROLE_ADMIN"; +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/service/ClinicService.java b/backend/src/main/java/org/springframework/samples/petclinic/service/ClinicService.java new file mode 100644 index 0000000..32d0975 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/service/ClinicService.java @@ -0,0 +1,70 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.service; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.springframework.dao.DataAccessException; +import org.springframework.samples.petclinic.model.Owner; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.model.Specialty; +import org.springframework.samples.petclinic.model.Vet; +import org.springframework.samples.petclinic.model.Visit; + +/** + * Mostly used as a facade so all controllers have a single point of entry + * + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +public interface ClinicService { + + Pet findPetById(int id) throws DataAccessException; + Collection findAllPets() throws DataAccessException; + void savePet(Pet pet) throws DataAccessException; + void deletePet(Pet pet) throws DataAccessException; + + Collection findVisitsByPetId(int petId); + Visit findVisitById(int visitId) throws DataAccessException; + Collection findAllVisits() throws DataAccessException; + void saveVisit(Visit visit) throws DataAccessException; + void deleteVisit(Visit visit) throws DataAccessException; + Vet findVetById(int id) throws DataAccessException; + Collection findVets() throws DataAccessException; + Collection findAllVets() throws DataAccessException; + void saveVet(Vet vet) throws DataAccessException; + void deleteVet(Vet vet) throws DataAccessException; + Owner findOwnerById(int id) throws DataAccessException; + Collection findAllOwners() throws DataAccessException; + void saveOwner(Owner owner) throws DataAccessException; + void deleteOwner(Owner owner) throws DataAccessException; + Collection findOwnerByLastName(String lastName) throws DataAccessException; + + PetType findPetTypeById(int petTypeId); + Collection findAllPetTypes() throws DataAccessException; + Collection findPetTypes() throws DataAccessException; + void savePetType(PetType petType) throws DataAccessException; + void deletePetType(PetType petType) throws DataAccessException; + Specialty findSpecialtyById(int specialtyId); + Collection findAllSpecialties() throws DataAccessException; + void saveSpecialty(Specialty specialty) throws DataAccessException; + void deleteSpecialty(Specialty specialty) throws DataAccessException; + + List findSpecialtiesByNameIn(Set names) throws DataAccessException; +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/service/ClinicServiceImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/service/ClinicServiceImpl.java new file mode 100644 index 0000000..baf8118 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/service/ClinicServiceImpl.java @@ -0,0 +1,250 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.orm.ObjectRetrievalFailureException; +import org.springframework.samples.petclinic.model.*; +import org.springframework.samples.petclinic.repository.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +/** + * Mostly used as a facade for all Petclinic controllers + * Also a placeholder for @Transactional and @Cacheable annotations + * + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +@Service +public class ClinicServiceImpl implements ClinicService { + + private final PetRepository petRepository; + private final VetRepository vetRepository; + private final OwnerRepository ownerRepository; + private final VisitRepository visitRepository; + private final SpecialtyRepository specialtyRepository; + private final PetTypeRepository petTypeRepository; + + @Autowired + public ClinicServiceImpl( + PetRepository petRepository, + VetRepository vetRepository, + OwnerRepository ownerRepository, + VisitRepository visitRepository, + SpecialtyRepository specialtyRepository, + PetTypeRepository petTypeRepository) { + this.petRepository = petRepository; + this.vetRepository = vetRepository; + this.ownerRepository = ownerRepository; + this.visitRepository = visitRepository; + this.specialtyRepository = specialtyRepository; + this.petTypeRepository = petTypeRepository; + } + + @Override + @Transactional(readOnly = true) + public Collection findAllPets() throws DataAccessException { + return petRepository.findAll(); + } + + @Override + @Transactional + public void deletePet(Pet pet) throws DataAccessException { + petRepository.delete(pet); + } + + @Override + @Transactional(readOnly = true) + public Visit findVisitById(int visitId) throws DataAccessException { + return findEntityById(() -> visitRepository.findById(visitId)); + } + + @Override + @Transactional(readOnly = true) + public Collection findAllVisits() throws DataAccessException { + return visitRepository.findAll(); + } + + @Override + @Transactional + public void deleteVisit(Visit visit) throws DataAccessException { + visitRepository.delete(visit); + } + + @Override + @Transactional(readOnly = true) + public Vet findVetById(int id) throws DataAccessException { + return findEntityById(() -> vetRepository.findById(id)); + } + + @Override + @Transactional(readOnly = true) + public Collection findAllVets() throws DataAccessException { + return vetRepository.findAll(); + } + + @Override + @Transactional + public void saveVet(Vet vet) throws DataAccessException { + vetRepository.save(vet); + } + + @Override + @Transactional + public void deleteVet(Vet vet) throws DataAccessException { + vetRepository.delete(vet); + } + + @Override + @Transactional(readOnly = true) + public Collection findAllOwners() throws DataAccessException { + return ownerRepository.findAll(); + } + + @Override + @Transactional + public void deleteOwner(Owner owner) throws DataAccessException { + ownerRepository.delete(owner); + } + + @Override + @Transactional(readOnly = true) + public PetType findPetTypeById(int petTypeId) { + return findEntityById(() -> petTypeRepository.findById(petTypeId)); + } + + @Override + @Transactional(readOnly = true) + public Collection findAllPetTypes() throws DataAccessException { + return petTypeRepository.findAll(); + } + + @Override + @Transactional + public void savePetType(PetType petType) throws DataAccessException { + petTypeRepository.save(petType); + } + + @Override + @Transactional + public void deletePetType(PetType petType) throws DataAccessException { + petTypeRepository.delete(petType); + } + + @Override + @Transactional(readOnly = true) + public Specialty findSpecialtyById(int specialtyId) { + return findEntityById(() -> specialtyRepository.findById(specialtyId)); + } + + @Override + @Transactional(readOnly = true) + public Collection findAllSpecialties() throws DataAccessException { + return specialtyRepository.findAll(); + } + + @Override + @Transactional + public void saveSpecialty(Specialty specialty) throws DataAccessException { + specialtyRepository.save(specialty); + } + + @Override + @Transactional + public void deleteSpecialty(Specialty specialty) throws DataAccessException { + specialtyRepository.delete(specialty); + } + + @Override + @Transactional(readOnly = true) + public Collection findPetTypes() throws DataAccessException { + return petRepository.findPetTypes(); + } + + @Override + @Transactional(readOnly = true) + public Owner findOwnerById(int id) throws DataAccessException { + return findEntityById(() -> ownerRepository.findById(id)); + } + + @Override + @Transactional(readOnly = true) + public Pet findPetById(int id) throws DataAccessException { + return findEntityById(() -> petRepository.findById(id)); + } + + @Override + @Transactional + public void savePet(Pet pet) throws DataAccessException { + petRepository.save(pet); + } + + @Override + @Transactional + public void saveVisit(Visit visit) throws DataAccessException { + visitRepository.save(visit); + + } + + @Override + @Transactional(readOnly = true) + public Collection findVets() throws DataAccessException { + return vetRepository.findAll(); + } + + @Override + @Transactional + public void saveOwner(Owner owner) throws DataAccessException { + ownerRepository.save(owner); + + } + + @Override + @Transactional(readOnly = true) + public Collection findOwnerByLastName(String lastName) throws DataAccessException { + return ownerRepository.findByLastName(lastName); + } + + @Override + @Transactional(readOnly = true) + public Collection findVisitsByPetId(int petId) { + return visitRepository.findByPetId(petId); + } + + @Override + @Transactional(readOnly = true) + public List findSpecialtiesByNameIn(Set names) { + return findEntityById(() -> specialtyRepository.findSpecialtiesByNameIn(names)); + } + + private T findEntityById(Supplier supplier) { + try { + return supplier.get(); + } catch (ObjectRetrievalFailureException | EmptyResultDataAccessException e) { + // Just ignore not found exceptions for Jdbc/Jpa realization + return null; + } + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/service/UserService.java b/backend/src/main/java/org/springframework/samples/petclinic/service/UserService.java new file mode 100644 index 0000000..352605a --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/service/UserService.java @@ -0,0 +1,8 @@ +package org.springframework.samples.petclinic.service; + +import org.springframework.samples.petclinic.model.User; + +public interface UserService { + + void saveUser(User user) ; +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/service/UserServiceImpl.java b/backend/src/main/java/org/springframework/samples/petclinic/service/UserServiceImpl.java new file mode 100644 index 0000000..fb099e1 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/service/UserServiceImpl.java @@ -0,0 +1,36 @@ +package org.springframework.samples.petclinic.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.samples.petclinic.model.User; +import org.springframework.samples.petclinic.model.Role; +import org.springframework.samples.petclinic.repository.UserRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class UserServiceImpl implements UserService { + + @Autowired + private UserRepository userRepository; + + @Override + @Transactional + public void saveUser(User user) { + + if(user.getRoles() == null || user.getRoles().isEmpty()) { + throw new IllegalArgumentException("User must have at least a role set!"); + } + + for (Role role : user.getRoles()) { + if(!role.getName().startsWith("ROLE_")) { + role.setName("ROLE_" + role.getName()); + } + + if(role.getUser() == null) { + role.setUser(user); + } + } + + userRepository.save(user); + } +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/util/CallMonitoringAspect.java b/backend/src/main/java/org/springframework/samples/petclinic/util/CallMonitoringAspect.java new file mode 100644 index 0000000..fddf17d --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/util/CallMonitoringAspect.java @@ -0,0 +1,97 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.util; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.springframework.jmx.export.annotation.ManagedAttribute; +import org.springframework.jmx.export.annotation.ManagedOperation; +import org.springframework.jmx.export.annotation.ManagedResource; +import org.springframework.util.StopWatch; + +/** + * Simple aspect that monitors call count and call invocation time. It uses JMX annotations and therefore can be + * monitored using any JMX console such as the jConsole + *

+ * This is only useful if you use JPA or JDBC. Spring-data-jpa doesn't have any correctly annotated classes to join on + * + * @author Rob Harrop + * @author Juergen Hoeller + * @author Michael Isvy + * @since 2.5 + */ +@ManagedResource("petclinic:type=CallMonitor") +@Aspect +public class CallMonitoringAspect { + + private boolean enabled = true; + + private int callCount = 0; + + private long accumulatedCallTime = 0; + + @ManagedAttribute + public boolean isEnabled() { + return enabled; + } + + @ManagedAttribute + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + @ManagedOperation + public void reset() { + this.callCount = 0; + this.accumulatedCallTime = 0; + } + + @ManagedAttribute + public int getCallCount() { + return callCount; + } + + @ManagedAttribute + public long getCallTime() { + if (this.callCount > 0) + return this.accumulatedCallTime / this.callCount; + else + return 0; + } + + + @Around("within(@org.springframework.stereotype.Repository *)") + public Object invoke(ProceedingJoinPoint joinPoint) throws Throwable { + if (this.enabled) { + StopWatch sw = new StopWatch(joinPoint.toShortString()); + + sw.start("invoke"); + try { + return joinPoint.proceed(); + } finally { + sw.stop(); + synchronized (this) { + this.callCount++; + this.accumulatedCallTime += sw.getTotalTimeMillis(); + } + } + } else { + return joinPoint.proceed(); + } + } + +} diff --git a/backend/src/main/java/org/springframework/samples/petclinic/util/EntityUtils.java b/backend/src/main/java/org/springframework/samples/petclinic/util/EntityUtils.java new file mode 100644 index 0000000..eee3906 --- /dev/null +++ b/backend/src/main/java/org/springframework/samples/petclinic/util/EntityUtils.java @@ -0,0 +1,54 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.util; + +import java.util.Collection; + +import org.springframework.orm.ObjectRetrievalFailureException; +import org.springframework.samples.petclinic.model.BaseEntity; + +/** + * Utility methods for handling entities. Separate from the BaseEntity class mainly because of dependency on the + * ORM-associated ObjectRetrievalFailureException. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @see org.springframework.samples.petclinic.model.BaseEntity + * @since 29.10.2003 + */ +public abstract class EntityUtils { + + /** + * Look up the entity of the given class with the given id in the given collection. + * + * @param entities the collection to search + * @param entityClass the entity class to look up + * @param entityId the entity id to look up + * @return the found entity + * @throws ObjectRetrievalFailureException if the entity was not found + */ + public static T getById(Collection entities, Class entityClass, int entityId) + throws ObjectRetrievalFailureException { + for (T entity : entities) { + if (entity.getId() == entityId && entityClass.isInstance(entity)) { + return entity; + } + } + throw new ObjectRetrievalFailureException(entityClass, entityId); + } + +} diff --git a/backend/src/main/resources/application-hsqldb.properties b/backend/src/main/resources/application-hsqldb.properties new file mode 100644 index 0000000..d65ff9f --- /dev/null +++ b/backend/src/main/resources/application-hsqldb.properties @@ -0,0 +1,8 @@ +# HSQLDB config start +#---------------------------------------------------------------- +spring.datasource.url=jdbc:hsqldb:mem:petclinic +spring.datasource.username=sa +spring.datasource.password= +spring.jpa.hibernate.ddl-auto=none +#---------------------------------------------------------------- +# HSQLDB config end diff --git a/backend/src/main/resources/application-mysql.properties b/backend/src/main/resources/application-mysql.properties new file mode 100644 index 0000000..e23dfa6 --- /dev/null +++ b/backend/src/main/resources/application-mysql.properties @@ -0,0 +1,7 @@ +# database init, supports mysql too +database=mysql +spring.datasource.url=${MYSQL_URL:jdbc:mysql://localhost/petclinic} +spring.datasource.username=${MYSQL_USER:petclinic} +spring.datasource.password=${MYSQL_PASS:petclinic} +# SQL is written to be idempotent so this is safe +spring.sql.init.mode=always diff --git a/backend/src/main/resources/application-postgres.properties b/backend/src/main/resources/application-postgres.properties new file mode 100644 index 0000000..60889b4 --- /dev/null +++ b/backend/src/main/resources/application-postgres.properties @@ -0,0 +1,6 @@ +database=postgres +spring.datasource.url=${POSTGRES_URL:jdbc:postgresql://localhost/petclinic} +spring.datasource.username=${POSTGRES_USER:petclinic} +spring.datasource.password=${POSTGRES_PASS:petclinic} +# SQL is written to be idempotent so this is safe +spring.sql.init.mode=always diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties new file mode 100644 index 0000000..460f991 --- /dev/null +++ b/backend/src/main/resources/application.properties @@ -0,0 +1,44 @@ +# active profiles config +# +# application use two active profiles +# +# one - for select database +# ------------------------------------------------ +# When using HSQL, use: hsqldb +# When using MySQL, use: mysql +# When using PostgeSQL, use: postgres +# ------------------------------------------------ +# +# one for select repository layer +# ------------------------------------------------ +# When using Spring jpa, use: jpa +# When using Spring JDBC, use: jdbc +# When using Spring Data JPA, use: spring-data-jpa +# ------------------------------------------------ + +spring.profiles.active=hsqldb,spring-data-jpa + +# ------------------------------------------------ + +server.port=9966 +server.servlet.context-path=/petclinic/ + +# database init, supports mysql and postgres too +database=hsqldb +spring.sql.init.schema-locations=classpath*:db/${database}/schema.sql +spring.sql.init.data-locations=classpath*:db/${database}/data.sql + + +spring.messages.basename=messages/messages +spring.jpa.open-in-view=false + +logging.level.org.springframework=INFO +#logging.level.org.springframework=DEBUG + +#logging.level.org.hibernate.SQL=DEBUG +#logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE + +# enable the desired authentication type +# by default the authentication is disabled +petclinic.security.enable=false + diff --git a/backend/src/main/resources/db/hsqldb/data.sql b/backend/src/main/resources/db/hsqldb/data.sql new file mode 100644 index 0000000..e5ee7b9 --- /dev/null +++ b/backend/src/main/resources/db/hsqldb/data.sql @@ -0,0 +1,59 @@ +INSERT INTO vets VALUES (1, 'James', 'Carter'); +INSERT INTO vets VALUES (2, 'Helen', 'Leary'); +INSERT INTO vets VALUES (3, 'Linda', 'Douglas'); +INSERT INTO vets VALUES (4, 'Rafael', 'Ortega'); +INSERT INTO vets VALUES (5, 'Henry', 'Stevens'); +INSERT INTO vets VALUES (6, 'Sharon', 'Jenkins'); + +INSERT INTO specialties VALUES (1, 'radiology'); +INSERT INTO specialties VALUES (2, 'surgery'); +INSERT INTO specialties VALUES (3, 'dentistry'); + +INSERT INTO vet_specialties VALUES (2, 1); +INSERT INTO vet_specialties VALUES (3, 2); +INSERT INTO vet_specialties VALUES (3, 3); +INSERT INTO vet_specialties VALUES (4, 2); +INSERT INTO vet_specialties VALUES (5, 1); + +INSERT INTO types VALUES (1, 'cat'); +INSERT INTO types VALUES (2, 'dog'); +INSERT INTO types VALUES (3, 'lizard'); +INSERT INTO types VALUES (4, 'snake'); +INSERT INTO types VALUES (5, 'bird'); +INSERT INTO types VALUES (6, 'hamster'); + +INSERT INTO owners VALUES (1, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023'); +INSERT INTO owners VALUES (2, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749'); +INSERT INTO owners VALUES (3, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763'); +INSERT INTO owners VALUES (4, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198'); +INSERT INTO owners VALUES (5, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765'); +INSERT INTO owners VALUES (6, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654'); +INSERT INTO owners VALUES (7, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387'); +INSERT INTO owners VALUES (8, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683'); +INSERT INTO owners VALUES (9, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435'); +INSERT INTO owners VALUES (10, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487'); + +INSERT INTO pets VALUES (1, 'Leo', '2010-09-07', 1, 1); +INSERT INTO pets VALUES (2, 'Basil', '2012-08-06', 6, 2); +INSERT INTO pets VALUES (3, 'Rosy', '2011-04-17', 2, 3); +INSERT INTO pets VALUES (4, 'Jewel', '2010-03-07', 2, 3); +INSERT INTO pets VALUES (5, 'Iggy', '2010-11-30', 3, 4); +INSERT INTO pets VALUES (6, 'George', '2010-01-20', 4, 5); +INSERT INTO pets VALUES (7, 'Samantha', '2012-09-04', 1, 6); +INSERT INTO pets VALUES (8, 'Max', '2012-09-04', 1, 6); +INSERT INTO pets VALUES (9, 'Lucky', '2011-08-06', 5, 7); +INSERT INTO pets VALUES (10, 'Mulligan', '2007-02-24', 2, 8); +INSERT INTO pets VALUES (11, 'Freddy', '2010-03-09', 5, 9); +INSERT INTO pets VALUES (12, 'Lucky', '2010-06-24', 2, 10); +INSERT INTO pets VALUES (13, 'Sly', '2012-06-08', 1, 10); + +INSERT INTO visits VALUES (1, 7, '2013-01-01', 'rabies shot'); +INSERT INTO visits VALUES (2, 8, '2013-01-02', 'rabies shot'); +INSERT INTO visits VALUES (3, 8, '2013-01-03', 'neutered'); +INSERT INTO visits VALUES (4, 7, '2013-01-04', 'spayed'); + +INSERT INTO users(username,password,enabled) VALUES ('admin','{noop}admin', true); + +INSERT INTO roles (username, role) VALUES ('admin', 'ROLE_OWNER_ADMIN'); +INSERT INTO roles (username, role) VALUES ('admin', 'ROLE_VET_ADMIN'); +INSERT INTO roles (username, role) VALUES ('admin', 'ROLE_ADMIN'); diff --git a/backend/src/main/resources/db/hsqldb/schema.sql b/backend/src/main/resources/db/hsqldb/schema.sql new file mode 100644 index 0000000..d14ecf3 --- /dev/null +++ b/backend/src/main/resources/db/hsqldb/schema.sql @@ -0,0 +1,82 @@ +DROP TABLE vet_specialties IF EXISTS; +DROP TABLE vets IF EXISTS; +DROP TABLE specialties IF EXISTS; +DROP TABLE visits IF EXISTS; +DROP TABLE pets IF EXISTS; +DROP TABLE types IF EXISTS; +DROP TABLE owners IF EXISTS; +DROP TABLE roles IF EXISTS; +DROP TABLE users IF EXISTS; + + +CREATE TABLE vets ( + id INTEGER IDENTITY PRIMARY KEY, + first_name VARCHAR(30), + last_name VARCHAR(30) +); +CREATE INDEX vets_last_name ON vets (last_name); + +CREATE TABLE specialties ( + id INTEGER IDENTITY PRIMARY KEY, + name VARCHAR(80) +); +CREATE INDEX specialties_name ON specialties (name); + +CREATE TABLE vet_specialties ( + vet_id INTEGER NOT NULL, + specialty_id INTEGER NOT NULL +); +ALTER TABLE vet_specialties ADD CONSTRAINT fk_vet_specialties_vets FOREIGN KEY (vet_id) REFERENCES vets (id); +ALTER TABLE vet_specialties ADD CONSTRAINT fk_vet_specialties_specialties FOREIGN KEY (specialty_id) REFERENCES specialties (id); + +CREATE TABLE types ( + id INTEGER IDENTITY PRIMARY KEY, + name VARCHAR(80) +); +CREATE INDEX types_name ON types (name); + +CREATE TABLE owners ( + id INTEGER IDENTITY PRIMARY KEY, + first_name VARCHAR(30), + last_name VARCHAR_IGNORECASE(30), + address VARCHAR(255), + city VARCHAR(80), + telephone VARCHAR(20) +); +CREATE INDEX owners_last_name ON owners (last_name); + +CREATE TABLE pets ( + id INTEGER IDENTITY PRIMARY KEY, + name VARCHAR(30), + birth_date DATE, + type_id INTEGER NOT NULL, + owner_id INTEGER NOT NULL +); +ALTER TABLE pets ADD CONSTRAINT fk_pets_owners FOREIGN KEY (owner_id) REFERENCES owners (id); +ALTER TABLE pets ADD CONSTRAINT fk_pets_types FOREIGN KEY (type_id) REFERENCES types (id); +CREATE INDEX pets_name ON pets (name); + +CREATE TABLE visits ( + id INTEGER IDENTITY PRIMARY KEY, + pet_id INTEGER NOT NULL, + visit_date DATE, + description VARCHAR(255) +); +ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id); +CREATE INDEX visits_pet_id ON visits (pet_id); + +CREATE TABLE users ( + username VARCHAR(20) NOT NULL , + password VARCHAR(20) NOT NULL , + enabled BOOLEAN DEFAULT TRUE NOT NULL , + PRIMARY KEY (username) +); + +CREATE TABLE roles ( + id INTEGER IDENTITY PRIMARY KEY, + username VARCHAR(20) NOT NULL, + role VARCHAR(20) NOT NULL +); +ALTER TABLE roles ADD CONSTRAINT fk_username FOREIGN KEY (username) REFERENCES users (username); +CREATE INDEX fk_username_idx ON roles (username); + diff --git a/backend/src/main/resources/db/mysql/data.sql b/backend/src/main/resources/db/mysql/data.sql new file mode 100644 index 0000000..0bbcf4a --- /dev/null +++ b/backend/src/main/resources/db/mysql/data.sql @@ -0,0 +1,59 @@ +INSERT IGNORE INTO vets VALUES (1, 'James', 'Carter'); +INSERT IGNORE INTO vets VALUES (2, 'Helen', 'Leary'); +INSERT IGNORE INTO vets VALUES (3, 'Linda', 'Douglas'); +INSERT IGNORE INTO vets VALUES (4, 'Rafael', 'Ortega'); +INSERT IGNORE INTO vets VALUES (5, 'Henry', 'Stevens'); +INSERT IGNORE INTO vets VALUES (6, 'Sharon', 'Jenkins'); + +INSERT IGNORE INTO specialties VALUES (1, 'radiology'); +INSERT IGNORE INTO specialties VALUES (2, 'surgery'); +INSERT IGNORE INTO specialties VALUES (3, 'dentistry'); + +INSERT IGNORE INTO vet_specialties VALUES (2, 1); +INSERT IGNORE INTO vet_specialties VALUES (3, 2); +INSERT IGNORE INTO vet_specialties VALUES (3, 3); +INSERT IGNORE INTO vet_specialties VALUES (4, 2); +INSERT IGNORE INTO vet_specialties VALUES (5, 1); + +INSERT IGNORE INTO types VALUES (1, 'cat'); +INSERT IGNORE INTO types VALUES (2, 'dog'); +INSERT IGNORE INTO types VALUES (3, 'lizard'); +INSERT IGNORE INTO types VALUES (4, 'snake'); +INSERT IGNORE INTO types VALUES (5, 'bird'); +INSERT IGNORE INTO types VALUES (6, 'hamster'); + +INSERT IGNORE INTO owners VALUES (1, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023'); +INSERT IGNORE INTO owners VALUES (2, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749'); +INSERT IGNORE INTO owners VALUES (3, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763'); +INSERT IGNORE INTO owners VALUES (4, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198'); +INSERT IGNORE INTO owners VALUES (5, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765'); +INSERT IGNORE INTO owners VALUES (6, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654'); +INSERT IGNORE INTO owners VALUES (7, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387'); +INSERT IGNORE INTO owners VALUES (8, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683'); +INSERT IGNORE INTO owners VALUES (9, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435'); +INSERT IGNORE INTO owners VALUES (10, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487'); + +INSERT IGNORE INTO pets VALUES (1, 'Leo', '2000-09-07', 1, 1); +INSERT IGNORE INTO pets VALUES (2, 'Basil', '2002-08-06', 6, 2); +INSERT IGNORE INTO pets VALUES (3, 'Rosy', '2001-04-17', 2, 3); +INSERT IGNORE INTO pets VALUES (4, 'Jewel', '2000-03-07', 2, 3); +INSERT IGNORE INTO pets VALUES (5, 'Iggy', '2000-11-30', 3, 4); +INSERT IGNORE INTO pets VALUES (6, 'George', '2000-01-20', 4, 5); +INSERT IGNORE INTO pets VALUES (7, 'Samantha', '1995-09-04', 1, 6); +INSERT IGNORE INTO pets VALUES (8, 'Max', '1995-09-04', 1, 6); +INSERT IGNORE INTO pets VALUES (9, 'Lucky', '1999-08-06', 5, 7); +INSERT IGNORE INTO pets VALUES (10, 'Mulligan', '1997-02-24', 2, 8); +INSERT IGNORE INTO pets VALUES (11, 'Freddy', '2000-03-09', 5, 9); +INSERT IGNORE INTO pets VALUES (12, 'Lucky', '2000-06-24', 2, 10); +INSERT IGNORE INTO pets VALUES (13, 'Sly', '2002-06-08', 1, 10); + +INSERT IGNORE INTO visits VALUES (1, 7, '2010-03-04', 'rabies shot'); +INSERT IGNORE INTO visits VALUES (2, 8, '2011-03-04', 'rabies shot'); +INSERT IGNORE INTO visits VALUES (3, 8, '2009-06-04', 'neutered'); +INSERT IGNORE INTO visits VALUES (4, 7, '2008-09-04', 'spayed'); + +INSERT IGNORE INTO users(username,password,enabled) VALUES ('admin','{noop}admin', true); + +INSERT IGNORE INTO roles (username, role) VALUES ('admin', 'ROLE_OWNER_ADMIN'); +INSERT IGNORE INTO roles (username, role) VALUES ('admin', 'ROLE_VET_ADMIN'); +INSERT IGNORE INTO roles (username, role) VALUES ('admin', 'ROLE_ADMIN'); diff --git a/backend/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt b/backend/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt new file mode 100644 index 0000000..21213b4 --- /dev/null +++ b/backend/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt @@ -0,0 +1,36 @@ +================================================================================ +=== Spring PetClinic sample application - MySQL Configuration === +================================================================================ + +@author Sam Brannen +@author Costin Leau +@author Dave Syer + +-------------------------------------------------------------------------------- + +1) Download and install the MySQL database (e.g., MySQL Community Server 5.1.x), + which can be found here: https://dev.mysql.com/downloads/. Or run the + "docker-compose.yml" from the root of the project (if you have docker installed + locally): + + $ docker-compose up + ... + mysql_1_eedb4818d817 | MySQL init process done. Ready for start up. + ... + +2) (Once only) create the PetClinic database and user by executing the "db/mysql/user.sql" + scripts. You can connect to the database running in the docker container using + `mysql -u root -h localhost --protocol tcp`, but you don't need to run the script there + because the petclinic user is already set up if you use the provided `docker-compose.yml`. + +3) Run the app with `spring.profiles.active=mysql` (e.g. as a System property via the command + line, but any way that sets that property in a Spring Boot app should work). For example use + + mvn spring-boot:run -Dspring-boot.run.profiles=mysql + + To activate the profile on the command line. + +N.B. the "petclinic" database has to exist for the app to work with the JDBC URL value +as it is configured by default. This condition is taken care of automatically by the +docker-compose configuration provided, or by the `user.sql` script if you run that as +root. diff --git a/backend/src/main/resources/db/mysql/schema.sql b/backend/src/main/resources/db/mysql/schema.sql new file mode 100644 index 0000000..109d566 --- /dev/null +++ b/backend/src/main/resources/db/mysql/schema.sql @@ -0,0 +1,72 @@ +CREATE TABLE IF NOT EXISTS vets ( + id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + first_name VARCHAR(30), + last_name VARCHAR(30), + INDEX(last_name) +) engine=InnoDB; + +CREATE TABLE IF NOT EXISTS specialties ( + id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80), + INDEX(name) +) engine=InnoDB; + +CREATE TABLE IF NOT EXISTS vet_specialties ( + vet_id INT(4) UNSIGNED NOT NULL, + specialty_id INT(4) UNSIGNED NOT NULL, + FOREIGN KEY (vet_id) REFERENCES vets(id), + FOREIGN KEY (specialty_id) REFERENCES specialties(id), + UNIQUE (vet_id,specialty_id) +) engine=InnoDB; + +CREATE TABLE IF NOT EXISTS types ( + id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(80), + INDEX(name) +) engine=InnoDB; + +CREATE TABLE IF NOT EXISTS owners ( + id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + first_name VARCHAR(30), + last_name VARCHAR(30), + address VARCHAR(255), + city VARCHAR(80), + telephone VARCHAR(20), + INDEX(last_name) +) engine=InnoDB; + +CREATE TABLE IF NOT EXISTS pets ( + id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(30), + birth_date DATE, + type_id INT(4) UNSIGNED NOT NULL, + owner_id INT(4) UNSIGNED NOT NULL, + INDEX(name), + FOREIGN KEY (owner_id) REFERENCES owners(id), + FOREIGN KEY (type_id) REFERENCES types(id) +) engine=InnoDB; + +CREATE TABLE IF NOT EXISTS visits ( + id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, + pet_id INT(4) UNSIGNED NOT NULL, + visit_date DATE, + description VARCHAR(255), + FOREIGN KEY (pet_id) REFERENCES pets(id) +) engine=InnoDB; + +CREATE TABLE IF NOT EXISTS users ( + username VARCHAR(20) NOT NULL , + password VARCHAR(20) NOT NULL , + enabled TINYINT NOT NULL DEFAULT 1 , + PRIMARY KEY (username) +) engine=InnoDB; + +CREATE TABLE IF NOT EXISTS roles ( + id int(11) NOT NULL AUTO_INCREMENT, + username varchar(20) NOT NULL, + role varchar(20) NOT NULL, + PRIMARY KEY (id), + UNIQUE KEY uni_username_role (role,username), + KEY fk_username_idx (username), + CONSTRAINT fk_username FOREIGN KEY (username) REFERENCES users (username) +) engine=InnoDB; diff --git a/backend/src/main/resources/db/postgres/data.sql b/backend/src/main/resources/db/postgres/data.sql new file mode 100644 index 0000000..b1dfda8 --- /dev/null +++ b/backend/src/main/resources/db/postgres/data.sql @@ -0,0 +1,59 @@ +INSERT INTO vets VALUES (1, 'James', 'Carter') ON CONFLICT DO NOTHING; +INSERT INTO vets VALUES (2, 'Helen', 'Leary') ON CONFLICT DO NOTHING; +INSERT INTO vets VALUES (3, 'Linda', 'Douglas') ON CONFLICT DO NOTHING; +INSERT INTO vets VALUES (4, 'Rafael', 'Ortega') ON CONFLICT DO NOTHING; +INSERT INTO vets VALUES (5, 'Henry', 'Stevens') ON CONFLICT DO NOTHING; +INSERT INTO vets VALUES (6, 'Sharon', 'Jenkins') ON CONFLICT DO NOTHING; + +INSERT INTO specialties VALUES (1, 'radiology') ON CONFLICT DO NOTHING; +INSERT INTO specialties VALUES (2, 'surgery') ON CONFLICT DO NOTHING; +INSERT INTO specialties VALUES (3, 'dentistry') ON CONFLICT DO NOTHING; + +INSERT INTO vet_specialties VALUES (2, 1) ON CONFLICT DO NOTHING; +INSERT INTO vet_specialties VALUES (3, 2) ON CONFLICT DO NOTHING; +INSERT INTO vet_specialties VALUES (3, 3) ON CONFLICT DO NOTHING; +INSERT INTO vet_specialties VALUES (4, 2) ON CONFLICT DO NOTHING; +INSERT INTO vet_specialties VALUES (5, 1) ON CONFLICT DO NOTHING; + +INSERT INTO types VALUES (1, 'cat') ON CONFLICT DO NOTHING; +INSERT INTO types VALUES (2, 'dog') ON CONFLICT DO NOTHING; +INSERT INTO types VALUES (3, 'lizard') ON CONFLICT DO NOTHING; +INSERT INTO types VALUES (4, 'snake') ON CONFLICT DO NOTHING; +INSERT INTO types VALUES (5, 'bird') ON CONFLICT DO NOTHING; +INSERT INTO types VALUES (6, 'hamster') ON CONFLICT DO NOTHING; + +INSERT INTO owners VALUES (1, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023') ON CONFLICT DO NOTHING; +INSERT INTO owners VALUES (2, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749') ON CONFLICT DO NOTHING; +INSERT INTO owners VALUES (3, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763') ON CONFLICT DO NOTHING; +INSERT INTO owners VALUES (4, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198') ON CONFLICT DO NOTHING; +INSERT INTO owners VALUES (5, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765') ON CONFLICT DO NOTHING; +INSERT INTO owners VALUES (6, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654') ON CONFLICT DO NOTHING; +INSERT INTO owners VALUES (7, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387') ON CONFLICT DO NOTHING; +INSERT INTO owners VALUES (8, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683') ON CONFLICT DO NOTHING; +INSERT INTO owners VALUES (9, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435') ON CONFLICT DO NOTHING; +INSERT INTO owners VALUES (10, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487') ON CONFLICT DO NOTHING; + +INSERT INTO pets VALUES (1, 'Leo', '2000-09-07', 1, 1) ON CONFLICT DO NOTHING; +INSERT INTO pets VALUES (2, 'Basil', '2002-08-06', 6, 2) ON CONFLICT DO NOTHING; +INSERT INTO pets VALUES (3, 'Rosy', '2001-04-17', 2, 3) ON CONFLICT DO NOTHING; +INSERT INTO pets VALUES (4, 'Jewel', '2000-03-07', 2, 3) ON CONFLICT DO NOTHING; +INSERT INTO pets VALUES (5, 'Iggy', '2000-11-30', 3, 4) ON CONFLICT DO NOTHING; +INSERT INTO pets VALUES (6, 'George', '2000-01-20', 4, 5) ON CONFLICT DO NOTHING; +INSERT INTO pets VALUES (7, 'Samantha', '1995-09-04', 1, 6) ON CONFLICT DO NOTHING; +INSERT INTO pets VALUES (8, 'Max', '1995-09-04', 1, 6) ON CONFLICT DO NOTHING; +INSERT INTO pets VALUES (9, 'Lucky', '1999-08-06', 5, 7) ON CONFLICT DO NOTHING; +INSERT INTO pets VALUES (10, 'Mulligan', '1997-02-24', 2, 8) ON CONFLICT DO NOTHING; +INSERT INTO pets VALUES (11, 'Freddy', '2000-03-09', 5, 9) ON CONFLICT DO NOTHING; +INSERT INTO pets VALUES (12, 'Lucky', '2000-06-24', 2, 10) ON CONFLICT DO NOTHING; +INSERT INTO pets VALUES (13, 'Sly', '2002-06-08', 1, 10) ON CONFLICT DO NOTHING; + +INSERT INTO visits VALUES (1, 7, '2010-03-04', 'rabies shot') ON CONFLICT DO NOTHING; +INSERT INTO visits VALUES (2, 8, '2011-03-04', 'rabies shot') ON CONFLICT DO NOTHING; +INSERT INTO visits VALUES (3, 8, '2009-06-04', 'neutered') ON CONFLICT DO NOTHING; +INSERT INTO visits VALUES (4, 7, '2008-09-04', 'spayed') ON CONFLICT DO NOTHING; + +INSERT INTO users(username,password,enabled) VALUES ('admin','{noop}admin', true) ON CONFLICT DO NOTHING; + +INSERT INTO roles (username, role) VALUES ('admin', 'ROLE_OWNER_ADMIN') ON CONFLICT DO NOTHING; +INSERT INTO roles (username, role) VALUES ('admin', 'ROLE_VET_ADMIN') ON CONFLICT DO NOTHING; +INSERT INTO roles (username, role) VALUES ('admin', 'ROLE_ADMIN') ON CONFLICT DO NOTHING; diff --git a/backend/src/main/resources/db/postgres/petclinic_db_setup_postgres.txt b/backend/src/main/resources/db/postgres/petclinic_db_setup_postgres.txt new file mode 100644 index 0000000..981e2b0 --- /dev/null +++ b/backend/src/main/resources/db/postgres/petclinic_db_setup_postgres.txt @@ -0,0 +1,22 @@ +================================================================================ +=== Spring PetClinic sample application - PostgreSQL Configuration === +================================================================================ + +@author Vitaliy Fedoriv +@autor Antoine Rey + +-------------------------------------------------------------------------------- + +1) Run the "docker-compose.yml" from the root of the project: + + $ docker-compose up + ... + spring-petclinic-postgres-1 | The files belonging to this database system will be owned by user "postgres". + ... + +2) Run the app with `spring.profiles.active=postgres` (e.g. as a System property via the command + line, but any way that sets that property in a Spring Boot app should work). For example use + + mvn spring-boot:run -Dspring-boot.run.profiles=postgres + + To activate the profile on the command line. diff --git a/backend/src/main/resources/db/postgres/schema.sql b/backend/src/main/resources/db/postgres/schema.sql new file mode 100644 index 0000000..49f997b --- /dev/null +++ b/backend/src/main/resources/db/postgres/schema.sql @@ -0,0 +1,102 @@ +CREATE TABLE IF NOT EXISTS vets ( + id SERIAL, + first_name VARCHAR(30), + last_name VARCHAR(30), + CONSTRAINT pk_vets PRIMARY KEY (id) +); + +CREATE INDEX IF NOT EXISTS idx_vets_last_name ON vets (last_name); + +ALTER SEQUENCE vets_id_seq RESTART WITH 100; + + +CREATE TABLE IF NOT EXISTS specialties ( + id SERIAL, + name VARCHAR(80), + CONSTRAINT pk_specialties PRIMARY KEY (id) +); + +CREATE INDEX IF NOT EXISTS idx_specialties_name ON specialties (name); + +ALTER SEQUENCE specialties_id_seq RESTART WITH 100; + + +CREATE TABLE IF NOT EXISTS vet_specialties ( + vet_id INT NOT NULL, + specialty_id INT NOT NULL, + FOREIGN KEY (vet_id) REFERENCES vets(id), + FOREIGN KEY (specialty_id) REFERENCES specialties(id), + CONSTRAINT unique_ids UNIQUE (vet_id,specialty_id) +); + + + +CREATE TABLE IF NOT EXISTS types ( + id SERIAL, + name VARCHAR(80), + CONSTRAINT pk_types PRIMARY KEY (id) +); + +CREATE INDEX IF NOT EXISTS idx_types_name ON types (name); + +ALTER SEQUENCE types_id_seq RESTART WITH 100; + +CREATE TABLE IF NOT EXISTS owners ( + id SERIAL, + first_name VARCHAR(30), + last_name VARCHAR(30), + address VARCHAR(255), + city VARCHAR(80), + telephone VARCHAR(20), + CONSTRAINT pk_owners PRIMARY KEY (id) +); + +CREATE INDEX IF NOT EXISTS idx_owners_last_name ON owners (last_name); + +ALTER SEQUENCE owners_id_seq RESTART WITH 100; + + +CREATE TABLE IF NOT EXISTS pets ( + id SERIAL, + name VARCHAR(30), + birth_date DATE, + type_id INT NOT NULL, + owner_id INT NOT NULL, + FOREIGN KEY (owner_id) REFERENCES owners(id), + FOREIGN KEY (type_id) REFERENCES types(id), + CONSTRAINT pk_pets PRIMARY KEY (id) +); + +CREATE INDEX IF NOT EXISTS idx_pets_name ON pets (name); + +ALTER SEQUENCE pets_id_seq RESTART WITH 100; + + +CREATE TABLE IF NOT EXISTS visits ( + id SERIAL, + pet_id INT NOT NULL, + visit_date DATE, + description VARCHAR(255), + FOREIGN KEY (pet_id) REFERENCES pets(id), + CONSTRAINT pk_visits PRIMARY KEY (id) +); + +ALTER SEQUENCE visits_id_seq RESTART WITH 100; + +CREATE TABLE IF NOT EXISTS users ( + username VARCHAR(20) NOT NULL , + password VARCHAR(20) NOT NULL , + enabled boolean NOT NULL DEFAULT true , + CONSTRAINT pk_users PRIMARY KEY (username) +); + +CREATE TABLE IF NOT EXISTS roles ( + id SERIAL, + username varchar(20) NOT NULL, + role varchar(20) NOT NULL, + CONSTRAINT pk_roles PRIMARY KEY (id), + FOREIGN KEY (username) REFERENCES users (username) +); + +ALTER TABLE roles ADD CONSTRAINT uni_username_role UNIQUE (role,username); +ALTER SEQUENCE roles_id_seq RESTART WITH 100; diff --git a/backend/src/main/resources/logback.xml b/backend/src/main/resources/logback.xml new file mode 100644 index 0000000..54bfd5f --- /dev/null +++ b/backend/src/main/resources/logback.xml @@ -0,0 +1,22 @@ + + + + + + + true + + + + + %-5level %logger{0} - %msg%n + + + + + + + + + + diff --git a/backend/src/main/resources/messages/messages.properties b/backend/src/main/resources/messages/messages.properties new file mode 100644 index 0000000..173417a --- /dev/null +++ b/backend/src/main/resources/messages/messages.properties @@ -0,0 +1,8 @@ +welcome=Welcome +required=is required +notFound=has not been found +duplicate=is already in use +nonNumeric=must be all numeric +duplicateFormSubmission=Duplicate form submission is not allowed +typeMismatch.date=invalid date +typeMismatch.birthDate=invalid date diff --git a/backend/src/main/resources/messages/messages_de.properties b/backend/src/main/resources/messages/messages_de.properties new file mode 100644 index 0000000..124bee4 --- /dev/null +++ b/backend/src/main/resources/messages/messages_de.properties @@ -0,0 +1,8 @@ +welcome=Willkommen +required=muss angegeben werden +notFound=wurde nicht gefunden +duplicate=ist bereits vergeben +nonNumeric=darf nur numerisch sein +duplicateFormSubmission=Wiederholtes Absenden des Formulars ist nicht erlaubt +typeMismatch.date=ungültiges Datum +typeMismatch.birthDate=ungültiges Datum diff --git a/backend/src/main/resources/messages/messages_en.properties b/backend/src/main/resources/messages/messages_en.properties new file mode 100644 index 0000000..05d519b --- /dev/null +++ b/backend/src/main/resources/messages/messages_en.properties @@ -0,0 +1 @@ +# This file is intentionally empty. Message look-ups will fall back to the default "messages.properties" file. \ No newline at end of file diff --git a/backend/src/main/resources/openapi.yml b/backend/src/main/resources/openapi.yml new file mode 100755 index 0000000..792cfa0 --- /dev/null +++ b/backend/src/main/resources/openapi.yml @@ -0,0 +1,2220 @@ +openapi: 3.0.1 +info: + title: Spring PetClinic + description: Spring PetClinic Sample Application. + license: + name: Apache 2.0 + url: http://www.apache.org/licenses/LICENSE-2.0 + version: '1.0' +servers: + - url: http://localhost:9966/petclinic/api +tags: + - name: failing + description: Endpoint which always returns an error. + - name: owner + description: Endpoints related to pet owners. + - name: user + description: Endpoints related to users. + - name: pet + description: Endpoints related to pets. + - name: vet + description: Endpoints related to vets. + - name: visit + description: Endpoints related to vet visits. + - name: pettypes + description: Endpoints related to pet types. + - name: specialty + description: Endpoints related to vet specialties. +paths: + /oops: + get: + tags: + - failing + operationId: failingRequest + summary: Always fails + description: Produces sample error response. + responses: + 200: + description: Never returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + text/plain: + schema: + type: string + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /owners: + post: + tags: + - owner + operationId: addOwner + summary: Adds a pet owner + description: Records the details of a new pet owner. + requestBody: + description: The pet owner + content: + application/json: + schema: + $ref: '#/components/schemas/OwnerFields' + required: true + responses: + 201: + description: The pet owner was successfully added. + content: + application/json: + schema: + $ref: '#/components/schemas/Owner' + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + get: + tags: + - owner + operationId: listOwners + summary: Lists pet owners + description: Returns an array of pet owners. + parameters: + - name: lastName + in: query + description: Last name. + required: false + schema: + type: string + example: Davis + responses: + 200: + description: Owner details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Owner' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /owners/{ownerId}: + get: + tags: + - owner + operationId: getOwner + summary: Get a pet owner by ID + description: Returns the pet owner or a 404 error. + parameters: + - name: ownerId + in: path + description: The ID of the pet owner. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Owner details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Owner' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Owner not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + put: + tags: + - owner + operationId: updateOwner + summary: Update a pet owner's details + description: Updates the pet owner record with the specified details. + parameters: + - name: ownerId + in: path + description: The ID of the pet owner. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + requestBody: + description: The pet owner details to use for the update. + content: + application/json: + schema: + $ref: '#/components/schemas/OwnerFields' + required: true + responses: + 200: + description: Update successful. + content: + application/json: + schema: + $ref: '#/components/schemas/Owner' + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Owner not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + + delete: + tags: + - owner + operationId: deleteOwner + summary: Delete an owner by ID + description: Returns the owner or a 404 error. + parameters: + - name: ownerId + in: path + description: The ID of the owner. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Owner details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Owner' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Owner not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /owners/{ownerId}/pets: + post: + tags: + - pet + operationId: addPetToOwner + summary: Adds a pet to an owner + description: Records the details of a new pet. + parameters: + - name: ownerId + in: path + description: The ID of the pet owner. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + requestBody: + description: The details of the new pet. + content: + application/json: + schema: + $ref: '#/components/schemas/PetFields' + required: true + responses: + 201: + description: The pet was successfully added. + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Pet or Owner not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /owners/{ownerId}/pets/{petId}: + get: + tags: + - pet + operationId: getOwnersPet + summary: Get a pet by ID + description: Returns the pet or a 404 error. + parameters: + - name: ownerId + in: path + description: The ID of the pet owner. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + - name: petId + in: path + description: The ID of the pet. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Pet details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Owner or pet not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + put: + tags: + - pet + operationId: updateOwnersPet + + summary: Update a pet's details + description: Updates the pet record with the specified details. + parameters: + - name: ownerId + in: path + description: The ID of the pet owner. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + - name: petId + in: path + description: The ID of the pet. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + requestBody: + description: The pet details to use for the update. + content: + application/json: + schema: + $ref: '#/components/schemas/PetFields' + required: true + responses: + 204: + description: Update successful. + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Pet not found for this owner. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /owners/{ownerId}/pets/{petId}/visits: + post: + tags: + - visit + operationId: addVisitToOwner + summary: Adds a vet visit + description: Records the details of a new vet visit. + parameters: + - name: ownerId + in: path + description: The ID of the pet owner. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + - name: petId + in: path + description: The ID of the pet. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + requestBody: + description: The details of the new vet visit. + content: + application/json: + schema: + $ref: '#/components/schemas/VisitFields' + required: true + responses: + 201: + description: The vet visit was successfully added. + content: + application/json: + schema: + $ref: '#/components/schemas/Visit' + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Pet not found for this owner. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /pettypes: + get: + tags: + - pettypes + operationId: listPetTypes + summary: Lists pet types + description: Returns an array of pet types. + responses: + 200: + description: Pet types found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PetType' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + post: + tags: + - pettypes + operationId: addPetType + summary: Create a pet type + description: Creates a pet type . + requestBody: + description: The pet type + content: + application/json: + schema: + $ref: '#/components/schemas/PetTypeFields' + required: true + responses: + 200: + description: Pet type created successfully. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/PetType' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Pet Type not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /pettypes/{petTypeId}: + get: + tags: + - pettypes + operationId: getPetType + summary: Get a pet type by ID + description: Returns the pet type or a 404 error. + parameters: + - name: petTypeId + in: path + description: The ID of the pet type. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Pet type details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/PetType' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Pet Type not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + put: + tags: + - pettypes + operationId: updatePetType + summary: Update a pet type by ID + description: Returns the pet type or a 404 error. + parameters: + - name: petTypeId + in: path + description: The ID of the pet type. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + requestBody: + description: The pet type + content: + application/json: + schema: + $ref: '#/components/schemas/PetType' + required: true + responses: + 200: + description: Pet type details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/PetType' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Pet Type not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + delete: + tags: + - pettypes + operationId: deletePetType + summary: Delete a pet type by ID + description: Returns the pet type or a 404 error. + parameters: + - name: petTypeId + in: path + description: The ID of the pet type. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Pet type details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/PetType' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Pet type not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + + /pets: + get: + tags: + - pet + operationId: listPets + summary: Lists pet + description: Returns an array of pet . + responses: + 200: + description: Pet types found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Pet' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + post: + tags: + - pet + operationId: addPet + summary: Create a pet + description: Creates a pet . + requestBody: + description: The pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + 201: + description: Pet type created successfully. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Pet not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /pets/{petId}: + get: + tags: + - pet + operationId: getPet + summary: Get a pet by ID + description: Returns the pet or a 404 error. + parameters: + - name: petId + in: path + description: The ID of the pet. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Pet details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Pet not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + put: + tags: + - pet + operationId: updatePet + summary: Update a pet by ID + description: Returns the pet or a 404 error. + parameters: + - name: petId + in: path + description: The ID of the pet. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + requestBody: + description: The pet + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + required: true + responses: + 200: + description: Pet details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Pet not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + delete: + tags: + - pet + operationId: deletePet + summary: Delete a pet by ID + description: Returns the pet or a 404 error. + parameters: + - name: petId + in: path + description: The ID of the pet. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Pet details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Pet' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Pet not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /visits: + get: + tags: + - visit + operationId: listVisits + summary: Lists visits + description: Returns an array of visit . + responses: + 200: + description: visits found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Visit' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + post: + tags: + - visit + operationId: addVisit + summary: Create a visit + description: Creates a visit. + requestBody: + description: The visit + content: + application/json: + schema: + $ref: '#/components/schemas/Visit' + required: true + responses: + 200: + description: visit created successfully. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Visit' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Visit not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /visits/{visitId}: + get: + tags: + - visit + operationId: getVisit + summary: Get a visit by ID + description: Returns the visit or a 404 error. + parameters: + - name: visitId + in: path + description: The ID of the visit. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Visit details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Visit' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Visit not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + put: + tags: + - visit + operationId: updateVisit + summary: Update a visit by ID + description: Returns the visit or a 404 error. + parameters: + - name: visitId + in: path + description: The ID of the visit. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + requestBody: + description: The visit + content: + application/json: + schema: + $ref: '#/components/schemas/Visit' + required: true + responses: + 200: + description: Visit details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Visit' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Visit not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + delete: + tags: + - visit + operationId: deleteVisit + summary: Delete a visit by ID + description: Returns the visit or a 404 error. + parameters: + - name: visitId + in: path + description: The ID of the visit. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Visit details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Visit' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Visit not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /specialties: + get: + tags: + - specialty + operationId: listSpecialties + summary: Lists specialties + description: Returns an array of specialty . + responses: + 200: + description: Specialties found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Specialty' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + post: + tags: + - specialty + operationId: addSpecialty + summary: Create a specialty + description: Creates a specialty . + requestBody: + description: The specialty + content: + application/json: + schema: + $ref: '#/components/schemas/Specialty' + required: true + responses: + 200: + description: Specialty created successfully. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Specialty' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Specialty not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /specialties/{specialtyId}: + get: + tags: + - specialty + operationId: getSpecialty + summary: Get a specialty by ID + description: Returns the specialty or a 404 error. + parameters: + - name: specialtyId + in: path + description: The ID of the speciality. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Specialty details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Specialty' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Specialty not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + put: + tags: + - specialty + operationId: updateSpecialty + summary: Update a specialty by ID + description: Returns the specialty or a 404 error. + parameters: + - name: specialtyId + in: path + description: The ID of the specialty. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + requestBody: + description: The pet + content: + application/json: + schema: + $ref: '#/components/schemas/Specialty' + required: true + responses: + 200: + description: Specialty details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Specialty' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Specialty not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + delete: + tags: + - specialty + operationId: deleteSpecialty + summary: Delete a specialty by ID + description: Returns the specialty or a 404 error. + parameters: + - name: specialtyId + in: path + description: The ID of the specialty. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Specialty details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Specialty' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Specialty not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /vets: + get: + tags: + - vet + operationId: listVets + summary: Lists vets + description: Returns an array of vets. + responses: + 200: + description: Vets found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Vet' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + + post: + tags: + - vet + operationId: addVet + summary: Create a Vet + description: Creates a vet . + requestBody: + description: The vet + content: + application/json: + schema: + $ref: '#/components/schemas/Vet' + required: true + responses: + 200: + description: Vet created successfully. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Vet' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Vet not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /vets/{vetId}: + get: + tags: + - vet + operationId: getVet + summary: Get a vet by ID + description: Returns the vet or a 404 error. + parameters: + - name: vetId + in: path + description: The ID of the vet. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Vet details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Vet' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Vet not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + put: + tags: + - vet + operationId: updateVet + summary: Update a vet by ID + description: Returns the vet or a 404 error. + parameters: + - name: vetId + in: path + description: The ID of the vet. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + requestBody: + description: The vet + content: + application/json: + schema: + $ref: '#/components/schemas/Vet' + required: true + responses: + 200: + description: Pet type details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Vet' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Vet not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + delete: + tags: + - vet + operationId: deleteVet + summary: Delete a vet by ID + description: Returns the vet or a 404 error. + parameters: + - name: vetId + in: path + description: The ID of the vet. + required: true + schema: + type: integer + format: int32 + minimum: 0 + example: 1 + responses: + 200: + description: Vet details found and returned. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/Vet' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: Vet not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + /users: + post: + tags: + - user + operationId: addUser + summary: Create a user + description: Creates a user. + requestBody: + description: The user + content: + application/json: + schema: + $ref: '#/components/schemas/User' + required: true + responses: + 200: + description: User created successfully. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + content: + application/json: + schema: + $ref: '#/components/schemas/User' + 304: + description: Not modified. + headers: + ETag: + description: An ID for this version of the response. + schema: + type: string + 400: + description: Bad request. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 404: + description: User not found. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' + 500: + description: Server error. + content: + application/json: + schema: + $ref: '#/components/schemas/RestError' +components: + schemas: + RestError: + title: REST Error + description: The schema for all error responses. + type: object + properties: + status: + title: Status + description: The HTTP status code. + type: integer + format: int32 + example: 400 + readOnly: true + error: + title: Error + description: The short error message. + type: string + example: Bad Request + readOnly: true + path: + title: Path + description: The path of the URL for this request. + type: string + format: uri + example: '/api/owners' + readOnly: true + timestamp: + title: Timestamp + description: The time the error occurred. + type: string + format: date-time + example: '2019-08-21T21:41:46.158+0000' + readOnly: true + message: + title: Message + description: The long error message. + type: string + example: 'Request failed schema validation' + readOnly: true + schemaValidationErrors: + title: Schema validation errors + description: Validation errors against the OpenAPI schema. + type: array + items: + $ref: '#/components/schemas/ValidationMessage' + trace: + title: Trace + description: The stacktrace for this error. + type: string + example: 'com.atlassian.oai.validator.springmvc.InvalidRequestException: ...' + readOnly: true + required: + - status + - error + - path + - timestamp + - message + - schemaValidationErrors + ValidationMessage: + title: Validation message + description: Messages describing a validation error. + type: object + properties: + message: + title: Message + description: The validation message. + type: string + example: "[Path '/lastName'] Instance type (null) does not match any allowed primitive type (allowed: ['string'])" + readOnly: true + required: + - message + additionalProperties: true + Specialty: + title: Specialty + description: Fields of specialty of vets. + type: object + properties: + id: + title: ID + description: The ID of the specialty. + type: integer + format: int32 + minimum: 0 + example: 1 + readOnly: true + name: + title: Name + description: The name of the specialty. + type: string + maxLength: 80 + minLength: 1 + example: radiology + required: + - id + - name + OwnerFields: + title: Owner fields + description: Editable fields of a pet owner. + type: object + properties: + firstName: + title: First name + description: The first name of the pet owner. + type: string + minLength: 1 + maxLength: 30 + pattern: '^[a-zA-Z]*$' + example: George + lastName: + title: Last name + description: The last name of the pet owner. + type: string + minLength: 1 + maxLength: 30 + pattern: '^[a-zA-Z]*$' + example: Franklin + address: + title: Address + description: The postal address of the pet owner. + type: string + minLength: 1 + maxLength: 255 + example: '110 W. Liberty St.' + city: + title: City + description: The city of the pet owner. + type: string + minLength: 1 + maxLength: 80 + example: Madison + telephone: + title: Telephone number + description: The telephone number of the pet owner. + type: string + minLength: 1 + maxLength: 20 + pattern: '^[0-9]*$' + example: '6085551023' + required: + - firstName + - lastName + - address + - city + - telephone + Owner: + title: Owner + description: A pet owner. + allOf: + - $ref: '#/components/schemas/OwnerFields' + - type: object + properties: + id: + title: ID + description: The ID of the pet owner. + type: integer + format: int32 + minimum: 0 + example: 1 + readOnly: true + pets: + title: Pets + description: The pets owned by this individual including any booked vet visits. + type: array + items: + $ref: '#/components/schemas/Pet' + readOnly: true + required: + - pets + PetFields: + title: Pet fields + description: Editable fields of a pet. + type: object + properties: + name: + title: Name + description: The name of the pet. + type: string + maxLength: 30 + example: Leo + birthDate: + title: Birth date + description: The date of birth of the pet. + type: string + format: date + example: '2010-09-07' + type: + $ref: '#/components/schemas/PetType' + required: + - name + - birthDate + - type + Pet: + title: Pet + description: A pet. + allOf: + - $ref: '#/components/schemas/PetFields' + - type: object + properties: + id: + title: ID + description: The ID of the pet. + type: integer + format: int32 + minimum: 0 + example: 1 + readOnly: true + ownerId: + title: Owner ID + description: The ID of the pet's owner. + type: integer + format: int32 + minimum: 0 + example: 1 + readOnly: true + visits: + title: Visits + description: Vet visit bookings for this pet. + type: array + items: + $ref: '#/components/schemas/Visit' + readOnly: true + required: + - id + - type + - visits + VetFields: + title: VetFields + description: Editable fields of a veterinarian. + type: object + properties: + firstName: + title: First name + description: The first name of the vet. + type: string + minLength: 1 + maxLength: 30 + pattern: '^[a-zA-Z]*$' + example: 'James' + lastName: + title: Last name + description: The last name of the vet. + type: string + minLength: 1 + maxLength: 30 + pattern: '^[a-zA-Z]*$' + example: 'Carter' + specialties: + title: Specialties + description: The specialties of the vet. + type: array + items: + $ref: '#/components/schemas/Specialty' + required: + - firstName + - lastName + - specialties + Vet: + title: Vet + description: A veterinarian. + allOf: + - $ref: '#/components/schemas/VetFields' + - type: object + properties: + id: + title: ID + description: The ID of the vet. + type: integer + format: int32 + minimum: 0 + example: 1 + readOnly: true + required: + - id + - firstName + - lastName + - specialties + VisitFields: + title: Visit fields + description: Editable fields of a vet visit. + type: object + properties: + date: + title: Date + description: The date of the visit. + type: string + format: date + example: '2013-01-01' + description: + title: Description + description: The description for the visit. + type: string + minLength: 1 + maxLength: 255 + example: 'rabies shot' + required: + - description + Visit: + title: Visit + description: A booking for a vet visit. + allOf: + - $ref: '#/components/schemas/VisitFields' + - type: object + properties: + id: + title: ID + description: The ID of the visit. + type: integer + format: int32 + minimum: 0 + example: 1 + readOnly: true + petId: + title: Pet ID + description: The ID of the pet. + type: integer + format: int32 + minimum: 0 + example: 1 + readOnly: true + required: + - id + PetTypeFields: + title: PetType fields + description: Editable fields of a pet type. + type: object + properties: + name: + title: Name + description: The name of the pet type. + type: string + maxLength: 80 + minLength: 1 + example: cat + required: + - name + PetType: + title: Pet type + description: A pet type. + allOf: + - $ref: '#/components/schemas/PetTypeFields' + - type: object + properties: + id: + title: ID + description: The ID of the pet type. + type: integer + format: int32 + minimum: 0 + example: 1 + required: + - id + User: + title: User + description: An user. + type: object + properties: + username: + title: username + description: The username + type: string + maxLength: 80 + minLength: 1 + example: john.doe + password: + title: Password + description: The password + type: string + maxLength: 80 + minLength: 1 + example: 1234abc + enabled: + title: enabled + description: Indicates if the user is enabled + type: boolean + example: true + roles: + title: Roles + description: The roles of an user + type: array + items: + $ref: '#/components/schemas/Role' + required: + - username + Role: + title: Role + description: A role. + type: object + properties: + name: + title: name + description: The role's name + type: string + maxLength: 80 + minLength: 1 + example: admin + required: + - name diff --git a/backend/src/test/java/org/springframework/samples/petclinic/SpringConfigTests.java b/backend/src/test/java/org/springframework/samples/petclinic/SpringConfigTests.java new file mode 100644 index 0000000..e33631f --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/SpringConfigTests.java @@ -0,0 +1,13 @@ +package org.springframework.samples.petclinic; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest(classes = PetClinicApplication.class) +class SpringConfigTests { + + @Test + void contextLoads() { + // Test the Spring configuration + } +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/model/ValidatorTests.java b/backend/src/test/java/org/springframework/samples/petclinic/model/ValidatorTests.java new file mode 100644 index 0000000..a441303 --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/model/ValidatorTests.java @@ -0,0 +1,45 @@ +package org.springframework.samples.petclinic.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Locale; +import java.util.Set; + +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validator; + +import org.junit.jupiter.api.Test; +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; + +/** + * @author Michael Isvy + * Simple test to make sure that Bean Validation is working + * (useful when upgrading to a new version of Hibernate Validator/ Bean Validation) + */ +class ValidatorTests { + + private Validator createValidator() { + LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean(); + localValidatorFactoryBean.afterPropertiesSet(); + return localValidatorFactoryBean; + } + + @Test + void shouldNotValidateWhenFirstNameEmpty() { + + LocaleContextHolder.setLocale(Locale.ENGLISH); + Person person = new Person(); + person.setFirstName(""); + person.setLastName("smith"); + + Validator validator = createValidator(); + Set> constraintViolations = validator.validate(person); + + assertThat(constraintViolations.size()).isEqualTo(1); + ConstraintViolation violation = constraintViolations.iterator().next(); + assertThat(violation.getPropertyPath().toString()).isEqualTo("firstName"); + assertThat(violation.getMessage()).isEqualTo("must not be empty"); + } + +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/OwnerRestControllerTests.java b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/OwnerRestControllerTests.java new file mode 100644 index 0000000..c456128 --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/OwnerRestControllerTests.java @@ -0,0 +1,433 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.samples.petclinic.mapper.OwnerMapper; +import org.springframework.samples.petclinic.mapper.PetMapper; +import org.springframework.samples.petclinic.mapper.VisitMapper; +import org.springframework.samples.petclinic.model.Owner; +import org.springframework.samples.petclinic.rest.advice.ExceptionControllerAdvice; +import org.springframework.samples.petclinic.rest.dto.OwnerDto; +import org.springframework.samples.petclinic.rest.dto.PetDto; +import org.springframework.samples.petclinic.rest.dto.PetTypeDto; +import org.springframework.samples.petclinic.rest.dto.VisitDto; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.samples.petclinic.service.clinicService.ApplicationTestConfig; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + + +/** + * Test class for {@link OwnerRestController} + * + * @author Vitaliy Fedoriv + */ +@SpringBootTest +@ContextConfiguration(classes = ApplicationTestConfig.class) +@WebAppConfiguration +class OwnerRestControllerTests { + + @Autowired + private OwnerRestController ownerRestController; + + @Autowired + private OwnerMapper ownerMapper; + + @Autowired + private PetMapper petMapper; + + @Autowired + private VisitMapper visitMapper; + + @MockBean + private ClinicService clinicService; + + private MockMvc mockMvc; + + private List owners; + + private List pets; + + private List visits; + + @BeforeEach + void initOwners() { + this.mockMvc = MockMvcBuilders.standaloneSetup(ownerRestController) + .setControllerAdvice(new ExceptionControllerAdvice()) + .build(); + owners = new ArrayList<>(); + + OwnerDto ownerWithPet = new OwnerDto(); + owners.add(ownerWithPet.id(1).firstName("George").lastName("Franklin").address("110 W. Liberty St.").city("Madison").telephone("6085551023").addPetsItem(getTestPetWithIdAndName(ownerWithPet, 1, "Rosy"))); + OwnerDto owner = new OwnerDto(); + owners.add(owner.id(2).firstName("Betty").lastName("Davis").address("638 Cardinal Ave.").city("Sun Prairie").telephone("6085551749")); + owner = new OwnerDto(); + owners.add(owner.id(3).firstName("Eduardo").lastName("Rodriquez").address("2693 Commerce St.").city("McFarland").telephone("6085558763")); + owner = new OwnerDto(); + owners.add(owner.id(4).firstName("Harold").lastName("Davis").address("563 Friendly St.").city("Windsor").telephone("6085553198")); + + PetTypeDto petType = new PetTypeDto(); + petType.id(2) + .name("dog"); + + pets = new ArrayList<>(); + PetDto pet = new PetDto(); + pets.add(pet.id(3) + .name("Rosy") + .birthDate(LocalDate.now()) + .type(petType)); + + pet = new PetDto(); + pets.add(pet.id(4) + .name("Jewel") + .birthDate(LocalDate.now()) + .type(petType)); + + visits = new ArrayList<>(); + VisitDto visit = new VisitDto(); + visit.setId(2); + visit.setPetId(pet.getId()); + visit.setDate(LocalDate.now()); + visit.setDescription("rabies shot"); + visits.add(visit); + + visit = new VisitDto(); + visit.setId(3); + visit.setPetId(pet.getId()); + visit.setDate(LocalDate.now()); + visit.setDescription("neutered"); + visits.add(visit); + } + + private PetDto getTestPetWithIdAndName(final OwnerDto owner, final int id, final String name) { + PetTypeDto petType = new PetTypeDto(); + PetDto pet = new PetDto(); + pet.id(id).name(name).birthDate(LocalDate.now()).type(petType.id(2).name("dog")).addVisitsItem(getTestVisitForPet(pet, 1)); + return pet; + } + + private VisitDto getTestVisitForPet(final PetDto pet, final int id) { + VisitDto visit = new VisitDto(); + return visit.id(id).date(LocalDate.now()).description("test" + id); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetOwnerSuccess() throws Exception { + given(this.clinicService.findOwnerById(1)).willReturn(ownerMapper.toOwner(owners.get(0))); + this.mockMvc.perform(get("/api/owners/1") + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.firstName").value("George")); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetOwnerNotFound() throws Exception { + given(this.clinicService.findOwnerById(2)).willReturn(null); + this.mockMvc.perform(get("/api/owners/2") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetOwnersListSuccess() throws Exception { + owners.remove(0); + owners.remove(1); + given(this.clinicService.findOwnerByLastName("Davis")).willReturn(ownerMapper.toOwners(owners)); + this.mockMvc.perform(get("/api/owners?lastName=Davis") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.[0].id").value(2)) + .andExpect(jsonPath("$.[0].firstName").value("Betty")) + .andExpect(jsonPath("$.[1].id").value(4)) + .andExpect(jsonPath("$.[1].firstName").value("Harold")); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetOwnersListNotFound() throws Exception { + owners.clear(); + given(this.clinicService.findOwnerByLastName("0")).willReturn(ownerMapper.toOwners(owners)); + this.mockMvc.perform(get("/api/owners/?lastName=0") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetAllOwnersSuccess() throws Exception { + owners.remove(0); + owners.remove(1); + given(this.clinicService.findAllOwners()).willReturn(ownerMapper.toOwners(owners)); + this.mockMvc.perform(get("/api/owners/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.[0].id").value(2)) + .andExpect(jsonPath("$.[0].firstName").value("Betty")) + .andExpect(jsonPath("$.[1].id").value(4)) + .andExpect(jsonPath("$.[1].firstName").value("Harold")); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetAllOwnersNotFound() throws Exception { + owners.clear(); + given(this.clinicService.findAllOwners()).willReturn(ownerMapper.toOwners(owners)); + this.mockMvc.perform(get("/api/owners/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testCreateOwnerSuccess() throws Exception { + OwnerDto newOwnerDto = owners.get(0); + newOwnerDto.setId(null); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + String newOwnerAsJSON = mapper.writeValueAsString(newOwnerDto); + this.mockMvc.perform(post("/api/owners/") + .content(newOwnerAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testCreateOwnerError() throws Exception { + OwnerDto newOwnerDto = owners.get(0); + newOwnerDto.setId(null); + newOwnerDto.setFirstName(null); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + String newOwnerAsJSON = mapper.writeValueAsString(newOwnerDto); + this.mockMvc.perform(post("/api/owners/") + .content(newOwnerAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testUpdateOwnerSuccess() throws Exception { + given(this.clinicService.findOwnerById(1)).willReturn(ownerMapper.toOwner(owners.get(0))); + int ownerId = owners.get(0).getId(); + OwnerDto updatedOwnerDto = new OwnerDto(); + // body.id = ownerId which is used in url path + updatedOwnerDto.setId(ownerId); + updatedOwnerDto.setFirstName("GeorgeI"); + updatedOwnerDto.setLastName("Franklin"); + updatedOwnerDto.setAddress("110 W. Liberty St."); + updatedOwnerDto.setCity("Madison"); + updatedOwnerDto.setTelephone("6085551023"); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + String newOwnerAsJSON = mapper.writeValueAsString(updatedOwnerDto); + this.mockMvc.perform(put("/api/owners/" + ownerId) + .content(newOwnerAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().contentType("application/json")) + .andExpect(status().isNoContent()); + + this.mockMvc.perform(get("/api/owners/" + ownerId) + .accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(ownerId)) + .andExpect(jsonPath("$.firstName").value("GeorgeI")); + + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testUpdateOwnerSuccessNoBodyId() throws Exception { + given(this.clinicService.findOwnerById(1)).willReturn(ownerMapper.toOwner(owners.get(0))); + int ownerId = owners.get(0).getId(); + OwnerDto updatedOwnerDto = new OwnerDto(); + updatedOwnerDto.setFirstName("GeorgeI"); + updatedOwnerDto.setLastName("Franklin"); + updatedOwnerDto.setAddress("110 W. Liberty St."); + updatedOwnerDto.setCity("Madison"); + + updatedOwnerDto.setTelephone("6085551023"); + ObjectMapper mapper = new ObjectMapper(); + String newOwnerAsJSON = mapper.writeValueAsString(updatedOwnerDto); + this.mockMvc.perform(put("/api/owners/" + ownerId) + .content(newOwnerAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().contentType("application/json")) + .andExpect(status().isNoContent()); + + this.mockMvc.perform(get("/api/owners/" + ownerId) + .accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(ownerId)) + .andExpect(jsonPath("$.firstName").value("GeorgeI")); + + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testUpdateOwnerError() throws Exception { + OwnerDto newOwnerDto = owners.get(0); + newOwnerDto.setFirstName(""); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + String newOwnerAsJSON = mapper.writeValueAsString(newOwnerDto); + this.mockMvc.perform(put("/api/owners/1") + .content(newOwnerAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testDeleteOwnerSuccess() throws Exception { + OwnerDto newOwnerDto = owners.get(0); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + String newOwnerAsJSON = mapper.writeValueAsString(newOwnerDto); + final Owner owner = ownerMapper.toOwner(owners.get(0)); + given(this.clinicService.findOwnerById(1)).willReturn(owner); + this.mockMvc.perform(delete("/api/owners/1") + .content(newOwnerAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNoContent()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testDeleteOwnerError() throws Exception { + OwnerDto newOwnerDto = owners.get(0); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + String newOwnerAsJSON = mapper.writeValueAsString(newOwnerDto); + given(this.clinicService.findOwnerById(999)).willReturn(null); + this.mockMvc.perform(delete("/api/owners/999") + .content(newOwnerAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testCreatePetSuccess() throws Exception { + PetDto newPet = pets.get(0); + newPet.setId(999); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd")); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + String newPetAsJSON = mapper.writeValueAsString(newPet); + System.err.println("--> newPetAsJSON=" + newPetAsJSON); + this.mockMvc.perform(post("/api/owners/1/pets/") + .content(newPetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testCreatePetError() throws Exception { + PetDto newPet = pets.get(0); + newPet.setId(null); + newPet.setName(null); + ObjectMapper mapper = new ObjectMapper(); + mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd")); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + mapper.registerModule(new JavaTimeModule()); + String newPetAsJSON = mapper.writeValueAsString(newPet); + this.mockMvc.perform(post("/api/owners/1/pets/") + .content(newPetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()).andDo(MockMvcResultHandlers.print()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testCreateVisitSuccess() throws Exception { + VisitDto newVisit = visits.get(0); + newVisit.setId(999); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + String newVisitAsJSON = mapper.writeValueAsString(visitMapper.toVisit(newVisit)); + System.out.println("newVisitAsJSON " + newVisitAsJSON); + this.mockMvc.perform(post("/api/owners/1/pets/1/visits") + .content(newVisitAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetOwnerPetSuccess() throws Exception { + var owner = ownerMapper.toOwner(owners.get(0)); + given(this.clinicService.findOwnerById(2)).willReturn(owner); + var pet = petMapper.toPet(pets.get(0)); + pet.setOwner(owner); + given(this.clinicService.findPetById(1)).willReturn(pet); + this.mockMvc.perform(get("/api/owners/2/pets/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetOwnersPetsWithOwnerNotFound() throws Exception { + owners.clear(); + given(this.clinicService.findAllOwners()).willReturn(ownerMapper.toOwners(owners)); + this.mockMvc.perform(get("/api/owners/1/pets/1") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetOwnersPetsWithPetNotFound() throws Exception { + var owner1 = ownerMapper.toOwner(owners.get(0)); + given(this.clinicService.findOwnerById(1)).willReturn(owner1); + this.mockMvc.perform(get("/api/owners/1/pets/2") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/PetRestControllerTests.java b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/PetRestControllerTests.java new file mode 100644 index 0000000..c6029fe --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/PetRestControllerTests.java @@ -0,0 +1,250 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.samples.petclinic.mapper.PetMapper; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.rest.advice.ExceptionControllerAdvice; +import org.springframework.samples.petclinic.rest.dto.OwnerDto; +import org.springframework.samples.petclinic.rest.dto.PetDto; +import org.springframework.samples.petclinic.rest.dto.PetTypeDto; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.samples.petclinic.service.clinicService.ApplicationTestConfig; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + + +/** + * Test class for {@link PetRestController} + * + * @author Vitaliy Fedoriv + */ + +@SpringBootTest +@ContextConfiguration(classes = ApplicationTestConfig.class) +@WebAppConfiguration +class PetRestControllerTests { + + @MockBean + protected ClinicService clinicService; + @Autowired + private PetRestController petRestController; + @Autowired + private PetMapper petMapper; + private MockMvc mockMvc; + + private List pets; + + @BeforeEach + void initPets() { + this.mockMvc = MockMvcBuilders.standaloneSetup(petRestController) + .setControllerAdvice(new ExceptionControllerAdvice()) + .build(); + pets = new ArrayList<>(); + + OwnerDto owner = new OwnerDto(); + owner.id(1).firstName("Eduardo") + .lastName("Rodriquez") + .address("2693 Commerce St.") + .city("McFarland") + .telephone("6085558763"); + + PetTypeDto petType = new PetTypeDto(); + petType.id(2) + .name("dog"); + + PetDto pet = new PetDto(); + pets.add(pet.id(3) + .name("Rosy") + .birthDate(LocalDate.now()) + .type(petType)); + + pet = new PetDto(); + pets.add(pet.id(4) + .name("Jewel") + .birthDate(LocalDate.now()) + .type(petType)); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetPetSuccess() throws Exception { + given(this.clinicService.findPetById(3)).willReturn(petMapper.toPet(pets.get(0))); + this.mockMvc.perform(get("/api/pets/3") + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(3)) + .andExpect(jsonPath("$.name").value("Rosy")); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetPetNotFound() throws Exception { + given(petMapper.toPetDto(this.clinicService.findPetById(-1))).willReturn(null); + this.mockMvc.perform(get("/api/pets/999") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetAllPetsSuccess() throws Exception { + final Collection pets = petMapper.toPets(this.pets); + System.err.println(pets); + when(this.clinicService.findAllPets()).thenReturn(pets); + //given(this.clinicService.findAllPets()).willReturn(petMapper.toPets(pets)); + this.mockMvc.perform(get("/api/pets/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.[0].id").value(3)) + .andExpect(jsonPath("$.[0].name").value("Rosy")) + .andExpect(jsonPath("$.[1].id").value(4)) + .andExpect(jsonPath("$.[1].name").value("Jewel")); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testGetAllPetsNotFound() throws Exception { + pets.clear(); + given(this.clinicService.findAllPets()).willReturn(petMapper.toPets(pets)); + this.mockMvc.perform(get("/api/pets/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testUpdatePetSuccess() throws Exception { + given(this.clinicService.findPetById(3)).willReturn(petMapper.toPet(pets.get(0))); + PetDto newPet = pets.get(0); + newPet.setName("Rosy I"); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd")); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + String newPetAsJSON = mapper.writeValueAsString(newPet); + this.mockMvc.perform(put("/api/pets/3") + .content(newPetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().contentType("application/json")) + .andExpect(status().isNoContent()); + + this.mockMvc.perform(get("/api/pets/3") + .accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(3)) + .andExpect(jsonPath("$.name").value("Rosy I")); + + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testUpdatePetError() throws Exception { + PetDto newPet = pets.get(0); + newPet.setName(null); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd")); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + String newPetAsJSON = mapper.writeValueAsString(newPet); + + this.mockMvc.perform(put("/api/pets/3") + .content(newPetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testDeletePetSuccess() throws Exception { + PetDto newPet = pets.get(0); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + String newPetAsJSON = mapper.writeValueAsString(newPet); + given(this.clinicService.findPetById(3)).willReturn(petMapper.toPet(pets.get(0))); + this.mockMvc.perform(delete("/api/pets/3") + .content(newPetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNoContent()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testDeletePetError() throws Exception { + PetDto newPet = pets.get(0); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + String newPetAsJSON = mapper.writeValueAsString(newPet); + given(this.clinicService.findPetById(999)).willReturn(null); + this.mockMvc.perform(delete("/api/pets/999") + .content(newPetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testAddPetSuccess() throws Exception { + PetDto newPet = pets.get(0); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + String newPetAsJSON = mapper.writeValueAsString(newPet); + given(this.clinicService.findPetById(3)).willReturn(petMapper.toPet(pets.get(0))); + this.mockMvc.perform(post("/api/pets") + .content(newPetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()) + .andExpect(header().string(HttpHeaders.LOCATION, "/api/pets/3")); + } + + @Test + @WithMockUser(roles = "OWNER_ADMIN") + void testAddPetError() throws Exception { + PetDto newPet = pets.get(0); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + String newPetAsJSON = mapper.writeValueAsString(newPet); + given(this.clinicService.findPetById(999)).willReturn(null); + this.mockMvc.perform(post("/api/pets") + // set empty JSON to force 400 error + .content("{}").accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/PetTypeRestControllerTests.java b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/PetTypeRestControllerTests.java new file mode 100644 index 0000000..2e21d73 --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/PetTypeRestControllerTests.java @@ -0,0 +1,253 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.samples.petclinic.mapper.PetTypeMapper; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.rest.advice.ExceptionControllerAdvice; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.samples.petclinic.service.clinicService.ApplicationTestConfig; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + + +/** + * Test class for {@link PetTypeRestController} + * + * @author Vitaliy Fedoriv + */ +@SpringBootTest +@ContextConfiguration(classes=ApplicationTestConfig.class) +@WebAppConfiguration +class PetTypeRestControllerTests { + + @Autowired + private PetTypeRestController petTypeRestController; + + @Autowired + private PetTypeMapper petTypeMapper; + + @MockBean + private ClinicService clinicService; + + private MockMvc mockMvc; + + private List petTypes; + + @BeforeEach + void initPetTypes(){ + this.mockMvc = MockMvcBuilders.standaloneSetup(petTypeRestController) + .setControllerAdvice(new ExceptionControllerAdvice()) + .build(); + petTypes = new ArrayList<>(); + + PetType petType = new PetType(); + petType.setId(1); + petType.setName("cat"); + petTypes.add(petType); + + petType = new PetType(); + petType.setId(2); + petType.setName("dog"); + petTypes.add(petType); + + petType = new PetType(); + petType.setId(3); + petType.setName("lizard"); + petTypes.add(petType); + + petType = new PetType(); + petType.setId(4); + petType.setName("snake"); + petTypes.add(petType); + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testGetPetTypeSuccessAsOwnerAdmin() throws Exception { + given(this.clinicService.findPetTypeById(1)).willReturn(petTypes.get(0)); + this.mockMvc.perform(get("/api/pettypes/1") + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("cat")); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testGetPetTypeSuccessAsVetAdmin() throws Exception { + given(this.clinicService.findPetTypeById(1)).willReturn(petTypes.get(0)); + this.mockMvc.perform(get("/api/pettypes/1") + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("cat")); + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testGetPetTypeNotFound() throws Exception { + given(this.clinicService.findPetTypeById(999)).willReturn(null); + this.mockMvc.perform(get("/api/pettypes/999") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testGetAllPetTypesSuccessAsOwnerAdmin() throws Exception { + petTypes.remove(0); + petTypes.remove(1); + given(this.clinicService.findAllPetTypes()).willReturn(petTypes); + this.mockMvc.perform(get("/api/pettypes/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.[0].id").value(2)) + .andExpect(jsonPath("$.[0].name").value("dog")) + .andExpect(jsonPath("$.[1].id").value(4)) + .andExpect(jsonPath("$.[1].name").value("snake")); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testGetAllPetTypesSuccessAsVetAdmin() throws Exception { + petTypes.remove(0); + petTypes.remove(1); + given(this.clinicService.findAllPetTypes()).willReturn(petTypes); + this.mockMvc.perform(get("/api/pettypes/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.[0].id").value(2)) + .andExpect(jsonPath("$.[0].name").value("dog")) + .andExpect(jsonPath("$.[1].id").value(4)) + .andExpect(jsonPath("$.[1].name").value("snake")); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testGetAllPetTypesNotFound() throws Exception { + petTypes.clear(); + given(this.clinicService.findAllPetTypes()).willReturn(petTypes); + this.mockMvc.perform(get("/api/pettypes/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testCreatePetTypeSuccess() throws Exception { + PetType newPetType = petTypes.get(0); + newPetType.setId(null); + ObjectMapper mapper = new ObjectMapper(); + String newPetTypeAsJSON = mapper.writeValueAsString(petTypeMapper.toPetTypeFieldsDto(newPetType)); + this.mockMvc.perform(post("/api/pettypes/") + .content(newPetTypeAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testCreatePetTypeError() throws Exception { + PetType newPetType = petTypes.get(0); + newPetType.setId(null); + newPetType.setName(null); + ObjectMapper mapper = new ObjectMapper(); + String newPetTypeAsJSON = mapper.writeValueAsString(petTypeMapper.toPetTypeDto(newPetType)); + this.mockMvc.perform(post("/api/pettypes/") + .content(newPetTypeAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testUpdatePetTypeSuccess() throws Exception { + given(this.clinicService.findPetTypeById(2)).willReturn(petTypes.get(1)); + PetType newPetType = petTypes.get(1); + newPetType.setName("dog I"); + ObjectMapper mapper = new ObjectMapper(); + String newPetTypeAsJSON = mapper.writeValueAsString(petTypeMapper.toPetTypeDto(newPetType)); + this.mockMvc.perform(put("/api/pettypes/2") + .content(newPetTypeAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().contentType("application/json")) + .andExpect(status().isNoContent()); + + this.mockMvc.perform(get("/api/pettypes/2") + .accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(2)) + .andExpect(jsonPath("$.name").value("dog I")); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testUpdatePetTypeError() throws Exception { + PetType newPetType = petTypes.get(0); + newPetType.setName(""); + ObjectMapper mapper = new ObjectMapper(); + String newPetTypeAsJSON = mapper.writeValueAsString(petTypeMapper.toPetTypeDto(newPetType)); + this.mockMvc.perform(put("/api/pettypes/1") + .content(newPetTypeAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testDeletePetTypeSuccess() throws Exception { + PetType newPetType = petTypes.get(0); + ObjectMapper mapper = new ObjectMapper(); + String newPetTypeAsJSON = mapper.writeValueAsString(newPetType); + given(this.clinicService.findPetTypeById(1)).willReturn(petTypes.get(0)); + this.mockMvc.perform(delete("/api/pettypes/1") + .content(newPetTypeAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNoContent()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testDeletePetTypeError() throws Exception { + PetType newPetType = petTypes.get(0); + ObjectMapper mapper = new ObjectMapper(); + String newPetTypeAsJSON = mapper.writeValueAsString(petTypeMapper.toPetTypeDto(newPetType)); + given(this.clinicService.findPetTypeById(999)).willReturn(null); + this.mockMvc.perform(delete("/api/pettypes/999") + .content(newPetTypeAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNotFound()); + } + +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/SpecialtyRestControllerTests.java b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/SpecialtyRestControllerTests.java new file mode 100644 index 0000000..2eaaa2d --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/SpecialtyRestControllerTests.java @@ -0,0 +1,218 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.samples.petclinic.mapper.SpecialtyMapper; +import org.springframework.samples.petclinic.model.Specialty; +import org.springframework.samples.petclinic.rest.advice.ExceptionControllerAdvice; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.samples.petclinic.service.clinicService.ApplicationTestConfig; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Test class for {@link SpecialtyRestController} + * + * @author Vitaliy Fedoriv + */ +@SpringBootTest +@ContextConfiguration(classes=ApplicationTestConfig.class) +@WebAppConfiguration +class SpecialtyRestControllerTests { + + @Autowired + private SpecialtyRestController specialtyRestController; + + @Autowired + private SpecialtyMapper specialtyMapper; + + @MockBean + private ClinicService clinicService; + + private MockMvc mockMvc; + + private List specialties; + + @BeforeEach + void initSpecialtys(){ + this.mockMvc = MockMvcBuilders.standaloneSetup(specialtyRestController) + .setControllerAdvice(new ExceptionControllerAdvice()) + .build(); + specialties = new ArrayList(); + + Specialty specialty = new Specialty(); + specialty.setId(1); + specialty.setName("radiology"); + specialties.add(specialty); + + specialty = new Specialty(); + specialty.setId(2); + specialty.setName("surgery"); + specialties.add(specialty); + + specialty = new Specialty(); + specialty.setId(3); + specialty.setName("dentistry"); + specialties.add(specialty); + + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testGetSpecialtySuccess() throws Exception { + given(this.clinicService.findSpecialtyById(1)).willReturn(specialties.get(0)); + this.mockMvc.perform(get("/api/specialties/1") + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.name").value("radiology")); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testGetSpecialtyNotFound() throws Exception { + given(this.clinicService.findSpecialtyById(999)).willReturn(null); + this.mockMvc.perform(get("/api/specialties/999") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testGetAllSpecialtysSuccess() throws Exception { + specialties.remove(0); + given(this.clinicService.findAllSpecialties()).willReturn(specialties); + this.mockMvc.perform(get("/api/specialties/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.[0].id").value(2)) + .andExpect(jsonPath("$.[0].name").value("surgery")) + .andExpect(jsonPath("$.[1].id").value(3)) + .andExpect(jsonPath("$.[1].name").value("dentistry")); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testGetAllSpecialtysNotFound() throws Exception { + specialties.clear(); + given(this.clinicService.findAllSpecialties()).willReturn(specialties); + this.mockMvc.perform(get("/api/specialties/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testCreateSpecialtySuccess() throws Exception { + Specialty newSpecialty = specialties.get(0); + newSpecialty.setId(999); + ObjectMapper mapper = new ObjectMapper(); + String newSpecialtyAsJSON = mapper.writeValueAsString(specialtyMapper.toSpecialtyDto(newSpecialty)); + this.mockMvc.perform(post("/api/specialties/") + .content(newSpecialtyAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testCreateSpecialtyError() throws Exception { + Specialty newSpecialty = specialties.get(0); + newSpecialty.setId(null); + newSpecialty.setName(null); + ObjectMapper mapper = new ObjectMapper(); + String newSpecialtyAsJSON = mapper.writeValueAsString(specialtyMapper.toSpecialtyDto(newSpecialty)); + this.mockMvc.perform(post("/api/specialties/") + .content(newSpecialtyAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testUpdateSpecialtySuccess() throws Exception { + given(this.clinicService.findSpecialtyById(2)).willReturn(specialties.get(1)); + Specialty newSpecialty = specialties.get(1); + newSpecialty.setName("surgery I"); + ObjectMapper mapper = new ObjectMapper(); + String newSpecialtyAsJSON = mapper.writeValueAsString(specialtyMapper.toSpecialtyDto(newSpecialty)); + this.mockMvc.perform(put("/api/specialties/2") + .content(newSpecialtyAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().contentType("application/json")) + .andExpect(status().isNoContent()); + + this.mockMvc.perform(get("/api/specialties/2") + .accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(2)) + .andExpect(jsonPath("$.name").value("surgery I")); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testUpdateSpecialtyError() throws Exception { + Specialty newSpecialty = specialties.get(0); + newSpecialty.setName(""); + ObjectMapper mapper = new ObjectMapper(); + String newSpecialtyAsJSON = mapper.writeValueAsString(specialtyMapper.toSpecialtyDto(newSpecialty)); + this.mockMvc.perform(put("/api/specialties/1") + .content(newSpecialtyAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testDeleteSpecialtySuccess() throws Exception { + Specialty newSpecialty = specialties.get(0); + ObjectMapper mapper = new ObjectMapper(); + String newSpecialtyAsJSON = mapper.writeValueAsString(specialtyMapper.toSpecialtyDto(newSpecialty)); + given(this.clinicService.findSpecialtyById(1)).willReturn(specialties.get(0)); + this.mockMvc.perform(delete("/api/specialties/1") + .content(newSpecialtyAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNoContent()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testDeleteSpecialtyError() throws Exception { + Specialty newSpecialty = specialties.get(0); + ObjectMapper mapper = new ObjectMapper(); + String newSpecialtyAsJSON = mapper.writeValueAsString(specialtyMapper.toSpecialtyDto(newSpecialty)); + given(this.clinicService.findSpecialtyById(999)).willReturn(null); + this.mockMvc.perform(delete("/api/specialties/999") + .content(newSpecialtyAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNotFound()); + } +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/UserRestControllerTests.java b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/UserRestControllerTests.java new file mode 100644 index 0000000..ed65ee1 --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/UserRestControllerTests.java @@ -0,0 +1,75 @@ +package org.springframework.samples.petclinic.rest.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.http.MediaType; +import org.springframework.samples.petclinic.mapper.UserMapper; +import org.springframework.samples.petclinic.model.User; +import org.springframework.samples.petclinic.rest.advice.ExceptionControllerAdvice; +import org.springframework.samples.petclinic.rest.controller.UserRestController; +import org.springframework.samples.petclinic.service.UserService; +import org.springframework.samples.petclinic.service.clinicService.ApplicationTestConfig; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@SpringBootTest +@ContextConfiguration(classes = ApplicationTestConfig.class) +@WebAppConfiguration +class UserRestControllerTests { + + @Mock + private UserService userService; + + @Autowired + private UserMapper userMapper; + + @Autowired + private UserRestController userRestController; + + private MockMvc mockMvc; + + @BeforeEach + void initVets() { + this.mockMvc = MockMvcBuilders.standaloneSetup(userRestController) + .setControllerAdvice(new ExceptionControllerAdvice()).build(); + } + + @Test + @WithMockUser(roles = "ADMIN") + void testCreateUserSuccess() throws Exception { + User user = new User(); + user.setUsername("username"); + user.setPassword("password"); + user.setEnabled(true); + user.addRole("OWNER_ADMIN"); + ObjectMapper mapper = new ObjectMapper(); + String newVetAsJSON = mapper.writeValueAsString(userMapper.toUserDto(user)); + this.mockMvc.perform(post("/api/users/") + .content(newVetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + } + + @Test + @WithMockUser(roles = "ADMIN") + void testCreateUserError() throws Exception { + User user = new User(); + user.setUsername(""); // set empty username to force 400 error + user.setPassword("password"); + user.setEnabled(true); + ObjectMapper mapper = new ObjectMapper(); + String newVetAsJSON = mapper.writeValueAsString(userMapper.toUserDto(user)); + this.mockMvc.perform(post("/api/users/") + .content(newVetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/VetRestControllerTests.java b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/VetRestControllerTests.java new file mode 100644 index 0000000..05836e3 --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/VetRestControllerTests.java @@ -0,0 +1,222 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.samples.petclinic.mapper.VetMapper; +import org.springframework.samples.petclinic.model.Vet; +import org.springframework.samples.petclinic.rest.advice.ExceptionControllerAdvice; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.samples.petclinic.service.clinicService.ApplicationTestConfig; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Test class for {@link VetRestController} + * + * @author Vitaliy Fedoriv + */ +@SpringBootTest +@ContextConfiguration(classes=ApplicationTestConfig.class) +@WebAppConfiguration +class VetRestControllerTests { + + @Autowired + private VetRestController vetRestController; + + @Autowired + private VetMapper vetMapper; + + @MockBean + private ClinicService clinicService; + + private MockMvc mockMvc; + + private List vets; + + @BeforeEach + void initVets(){ + this.mockMvc = MockMvcBuilders.standaloneSetup(vetRestController) + .setControllerAdvice(new ExceptionControllerAdvice()) + .build(); + vets = new ArrayList(); + + + Vet vet = new Vet(); + vet.setId(1); + vet.setFirstName("James"); + vet.setLastName("Carter"); + vets.add(vet); + + vet = new Vet(); + vet.setId(2); + vet.setFirstName("Helen"); + vet.setLastName("Leary"); + vets.add(vet); + + vet = new Vet(); + vet.setId(3); + vet.setFirstName("Linda"); + vet.setLastName("Douglas"); + vets.add(vet); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testGetVetSuccess() throws Exception { + given(this.clinicService.findVetById(1)).willReturn(vets.get(0)); + this.mockMvc.perform(get("/api/vets/1") + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.firstName").value("James")); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testGetVetNotFound() throws Exception { + given(this.clinicService.findVetById(-1)).willReturn(null); + this.mockMvc.perform(get("/api/vets/999") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testGetAllVetsSuccess() throws Exception { + given(this.clinicService.findAllVets()).willReturn(vets); + this.mockMvc.perform(get("/api/vets/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.[0].id").value(1)) + .andExpect(jsonPath("$.[0].firstName").value("James")) + .andExpect(jsonPath("$.[1].id").value(2)) + .andExpect(jsonPath("$.[1].firstName").value("Helen")); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testGetAllVetsNotFound() throws Exception { + vets.clear(); + given(this.clinicService.findAllVets()).willReturn(vets); + this.mockMvc.perform(get("/api/vets/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testCreateVetSuccess() throws Exception { + Vet newVet = vets.get(0); + newVet.setId(999); + ObjectMapper mapper = new ObjectMapper(); + String newVetAsJSON = mapper.writeValueAsString(vetMapper.toVetDto(newVet)); + this.mockMvc.perform(post("/api/vets/") + .content(newVetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testCreateVetError() throws Exception { + Vet newVet = vets.get(0); + newVet.setId(null); + newVet.setFirstName(null); + ObjectMapper mapper = new ObjectMapper(); + String newVetAsJSON = mapper.writeValueAsString(vetMapper.toVetDto(newVet)); + this.mockMvc.perform(post("/api/vets/") + .content(newVetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testUpdateVetSuccess() throws Exception { + given(this.clinicService.findVetById(1)).willReturn(vets.get(0)); + Vet newVet = vets.get(0); + newVet.setFirstName("James"); + ObjectMapper mapper = new ObjectMapper(); + String newVetAsJSON = mapper.writeValueAsString(vetMapper.toVetDto(newVet)); + this.mockMvc.perform(put("/api/vets/1") + .content(newVetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().contentType("application/json")) + .andExpect(status().isNoContent()); + + this.mockMvc.perform(get("/api/vets/1") + .accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(1)) + .andExpect(jsonPath("$.firstName").value("James")); + + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testUpdateVetError() throws Exception { + Vet newVet = vets.get(0); + newVet.setFirstName(null); + ObjectMapper mapper = new ObjectMapper(); + String newVetAsJSON = mapper.writeValueAsString(vetMapper.toVetDto(newVet)); + this.mockMvc.perform(put("/api/vets/1") + .content(newVetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testDeleteVetSuccess() throws Exception { + Vet newVet = vets.get(0); + ObjectMapper mapper = new ObjectMapper(); + String newVetAsJSON = mapper.writeValueAsString(vetMapper.toVetDto(newVet)); + given(this.clinicService.findVetById(1)).willReturn(vets.get(0)); + this.mockMvc.perform(delete("/api/vets/1") + .content(newVetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNoContent()); + } + + @Test + @WithMockUser(roles="VET_ADMIN") + void testDeleteVetError() throws Exception { + Vet newVet = vets.get(0); + ObjectMapper mapper = new ObjectMapper(); + String newVetAsJSON = mapper.writeValueAsString(vetMapper.toVetDto(newVet)); + given(this.clinicService.findVetById(-1)).willReturn(null); + this.mockMvc.perform(delete("/api/vets/999") + .content(newVetAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNotFound()); + } + +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/VisitRestControllerTests.java b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/VisitRestControllerTests.java new file mode 100644 index 0000000..74217cc --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/rest/controller/VisitRestControllerTests.java @@ -0,0 +1,254 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ + +package org.springframework.samples.petclinic.rest.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.samples.petclinic.mapper.VisitMapper; +import org.springframework.samples.petclinic.model.Owner; +import org.springframework.samples.petclinic.model.Pet; +import org.springframework.samples.petclinic.model.PetType; +import org.springframework.samples.petclinic.model.Visit; +import org.springframework.samples.petclinic.rest.advice.ExceptionControllerAdvice; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.samples.petclinic.service.clinicService.ApplicationTestConfig; +import org.springframework.security.test.context.support.WithMockUser; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.web.WebAppConfiguration; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import static org.mockito.BDDMockito.given; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +/** + * Test class for {@link VisitRestController} + * + * @author Vitaliy Fedoriv + */ +@SpringBootTest +@ContextConfiguration(classes=ApplicationTestConfig.class) +@WebAppConfiguration +class VisitRestControllerTests { + + @Autowired + private VisitRestController visitRestController; + + @MockBean + private ClinicService clinicService; + + @Autowired + private VisitMapper visitMapper; + + private MockMvc mockMvc; + + private List visits; + + @BeforeEach + void initVisits(){ + this.mockMvc = MockMvcBuilders.standaloneSetup(visitRestController) + .setControllerAdvice(new ExceptionControllerAdvice()) + .build(); + + visits = new ArrayList<>(); + + Owner owner = new Owner(); + owner.setId(1); + owner.setFirstName("Eduardo"); + owner.setLastName("Rodriquez"); + owner.setAddress("2693 Commerce St."); + owner.setCity("McFarland"); + owner.setTelephone("6085558763"); + + PetType petType = new PetType(); + petType.setId(2); + petType.setName("dog"); + + Pet pet = new Pet(); + pet.setId(8); + pet.setName("Rosy"); + pet.setBirthDate(LocalDate.now()); + pet.setOwner(owner); + pet.setType(petType); + + + Visit visit = new Visit(); + visit.setId(2); + visit.setPet(pet); + visit.setDate(LocalDate.now()); + visit.setDescription("rabies shot"); + visits.add(visit); + + visit = new Visit(); + visit.setId(3); + visit.setPet(pet); + visit.setDate(LocalDate.now()); + visit.setDescription("neutered"); + visits.add(visit); + + + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testGetVisitSuccess() throws Exception { + given(this.clinicService.findVisitById(2)).willReturn(visits.get(0)); + this.mockMvc.perform(get("/api/visits/2") + .accept(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(2)) + .andExpect(jsonPath("$.description").value("rabies shot")); + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testGetVisitNotFound() throws Exception { + given(this.clinicService.findVisitById(999)).willReturn(null); + this.mockMvc.perform(get("/api/visits/999") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testGetAllVisitsSuccess() throws Exception { + given(this.clinicService.findAllVisits()).willReturn(visits); + this.mockMvc.perform(get("/api/visits/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.[0].id").value(2)) + .andExpect(jsonPath("$.[0].description").value("rabies shot")) + .andExpect(jsonPath("$.[1].id").value(3)) + .andExpect(jsonPath("$.[1].description").value("neutered")); + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testGetAllVisitsNotFound() throws Exception { + visits.clear(); + given(this.clinicService.findAllVisits()).willReturn(visits); + this.mockMvc.perform(get("/api/visits/") + .accept(MediaType.APPLICATION_JSON)) + .andExpect(status().isNotFound()); + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testCreateVisitSuccess() throws Exception { + Visit newVisit = visits.get(0); + newVisit.setId(999); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + String newVisitAsJSON = mapper.writeValueAsString(visitMapper.toVisitDto(newVisit)); + System.out.println("newVisitAsJSON " + newVisitAsJSON); + this.mockMvc.perform(post("/api/visits/") + .content(newVisitAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isCreated()); + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testCreateVisitError() throws Exception { + Visit newVisit = visits.get(0); + newVisit.setId(null); + newVisit.setDescription(null); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + String newVisitAsJSON = mapper.writeValueAsString(visitMapper.toVisitDto(newVisit)); + this.mockMvc.perform(post("/api/visits/") + .content(newVisitAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testUpdateVisitSuccess() throws Exception { + given(this.clinicService.findVisitById(2)).willReturn(visits.get(0)); + Visit newVisit = visits.get(0); + newVisit.setDescription("rabies shot test"); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + String newVisitAsJSON = mapper.writeValueAsString(visitMapper.toVisitDto(newVisit)); + this.mockMvc.perform(put("/api/visits/2") + .content(newVisitAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(content().contentType("application/json")) + .andExpect(status().isNoContent()); + + this.mockMvc.perform(get("/api/visits/2") + .accept(MediaType.APPLICATION_JSON).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isOk()) + .andExpect(content().contentType("application/json")) + .andExpect(jsonPath("$.id").value(2)) + .andExpect(jsonPath("$.description").value("rabies shot test")); + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testUpdateVisitError() throws Exception { + Visit newVisit = visits.get(0); + newVisit.setDescription(null); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + String newVisitAsJSON = mapper.writeValueAsString(visitMapper.toVisitDto(newVisit)); + this.mockMvc.perform(put("/api/visits/2") + .content(newVisitAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isBadRequest()); + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testDeleteVisitSuccess() throws Exception { + Visit newVisit = visits.get(0); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + String newVisitAsJSON = mapper.writeValueAsString(visitMapper.toVisitDto(newVisit)); + given(this.clinicService.findVisitById(2)).willReturn(visits.get(0)); + this.mockMvc.perform(delete("/api/visits/2") + .content(newVisitAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNoContent()); + } + + @Test + @WithMockUser(roles="OWNER_ADMIN") + void testDeleteVisitError() throws Exception { + Visit newVisit = visits.get(0); + ObjectMapper mapper = new ObjectMapper(); + mapper.registerModule(new JavaTimeModule()); + String newVisitAsJSON = mapper.writeValueAsString(visitMapper.toVisitDto(newVisit)); + given(this.clinicService.findVisitById(999)).willReturn(null); + this.mockMvc.perform(delete("/api/visits/999") + .content(newVisitAsJSON).accept(MediaType.APPLICATION_JSON_VALUE).contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(status().isNotFound()); + } + +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/AbstractClinicServiceTests.java b/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/AbstractClinicServiceTests.java new file mode 100644 index 0000000..aff2ec1 --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/AbstractClinicServiceTests.java @@ -0,0 +1,502 @@ +/* + * Copyright 2002-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.service.clinicService; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.samples.petclinic.model.*; +import org.springframework.samples.petclinic.service.ClinicService; +import org.springframework.samples.petclinic.util.EntityUtils; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + *

Base class for {@link ClinicService} integration tests.

Subclasses should specify Spring context + * configuration using {@link ContextConfiguration @ContextConfiguration} annotation

+ * AbstractclinicServiceTests and its subclasses benefit from the following services provided by the Spring + * TestContext Framework:

  • Spring IoC container caching which spares us unnecessary set up + * time between test execution.
  • Dependency Injection of test fixture instances, meaning that + * we don't need to perform application context lookups. See the use of {@link Autowired @Autowired} on the {@link + * AbstractClinicServiceTests#clinicService clinicService} instance variable, which uses autowiring by + * type.
  • Transaction management, meaning each test method is executed in its own transaction, + * which is automatically rolled back by default. Thus, even if tests insert or otherwise change database state, there + * is no need for a teardown or cleanup script.
  • An {@link org.springframework.context.ApplicationContext + * ApplicationContext} is also inherited and can be used for explicit bean lookup if necessary.
+ * + * @author Ken Krebs + * @author Rod Johnson + * @author Juergen Hoeller + * @author Sam Brannen + * @author Michael Isvy + * @author Vitaliy Fedoriv + */ +abstract class AbstractClinicServiceTests { + + @Autowired + protected ClinicService clinicService; + + @Test + void shouldFindOwnersByLastName() { + Collection owners = this.clinicService.findOwnerByLastName("Davis"); + assertThat(owners.size()).isEqualTo(2); + + owners = this.clinicService.findOwnerByLastName("Daviss"); + assertThat(owners.isEmpty()).isTrue(); + } + + @Test + void shouldFindSingleOwnerWithPet() { + Owner owner = this.clinicService.findOwnerById(1); + assertThat(owner.getLastName()).startsWith("Franklin"); + assertThat(owner.getPets().size()).isEqualTo(1); + assertThat(owner.getPets().get(0).getType()).isNotNull(); + assertThat(owner.getPets().get(0).getType().getName()).isEqualTo("cat"); + } + + @Test + @Transactional + void shouldInsertOwner() { + Collection owners = this.clinicService.findOwnerByLastName("Schultz"); + int found = owners.size(); + + Owner owner = new Owner(); + owner.setFirstName("Sam"); + owner.setLastName("Schultz"); + owner.setAddress("4, Evans Street"); + owner.setCity("Wollongong"); + owner.setTelephone("4444444444"); + this.clinicService.saveOwner(owner); + assertThat(owner.getId().longValue()).isNotEqualTo(0); + assertThat(owner.getPet("null value")).isNull(); + owners = this.clinicService.findOwnerByLastName("Schultz"); + assertThat(owners.size()).isEqualTo(found + 1); + } + + @Test + @Transactional + void shouldUpdateOwner() { + Owner owner = this.clinicService.findOwnerById(1); + String oldLastName = owner.getLastName(); + String newLastName = oldLastName + "X"; + + owner.setLastName(newLastName); + this.clinicService.saveOwner(owner); + + // retrieving new name from database + owner = this.clinicService.findOwnerById(1); + assertThat(owner.getLastName()).isEqualTo(newLastName); + } + + @Test + void shouldFindPetWithCorrectId() { + Pet pet7 = this.clinicService.findPetById(7); + assertThat(pet7.getName()).startsWith("Samantha"); + assertThat(pet7.getOwner().getFirstName()).isEqualTo("Jean"); + + } + +// @Test +// void shouldFindAllPetTypes() { +// Collection petTypes = this.clinicService.findPetTypes(); +// +// PetType petType1 = EntityUtils.getById(petTypes, PetType.class, 1); +// assertThat(petType1.getName()).isEqualTo("cat"); +// PetType petType4 = EntityUtils.getById(petTypes, PetType.class, 4); +// assertThat(petType4.getName()).isEqualTo("snake"); +// } + + @Test + @Transactional + void shouldInsertPetIntoDatabaseAndGenerateId() { + Owner owner6 = this.clinicService.findOwnerById(6); + int found = owner6.getPets().size(); + + Pet pet = new Pet(); + pet.setName("bowser"); + Collection types = this.clinicService.findPetTypes(); + pet.setType(EntityUtils.getById(types, PetType.class, 2)); + pet.setBirthDate(LocalDate.now()); + owner6.addPet(pet); + assertThat(owner6.getPets().size()).isEqualTo(found + 1); + + this.clinicService.savePet(pet); + this.clinicService.saveOwner(owner6); + + owner6 = this.clinicService.findOwnerById(6); + assertThat(owner6.getPets().size()).isEqualTo(found + 1); + // checks that id has been generated + assertThat(pet.getId()).isNotNull(); + } + + @Test + @Transactional + void shouldUpdatePetName() throws Exception { + Pet pet7 = this.clinicService.findPetById(7); + String oldName = pet7.getName(); + + String newName = oldName + "X"; + pet7.setName(newName); + this.clinicService.savePet(pet7); + + pet7 = this.clinicService.findPetById(7); + assertThat(pet7.getName()).isEqualTo(newName); + } + + @Test + void shouldFindVets() { + Collection vets = this.clinicService.findVets(); + + Vet vet = EntityUtils.getById(vets, Vet.class, 3); + assertThat(vet.getLastName()).isEqualTo("Douglas"); + assertThat(vet.getNrOfSpecialties()).isEqualTo(2); + assertThat(vet.getSpecialties().get(0).getName()).isEqualTo("dentistry"); + assertThat(vet.getSpecialties().get(1).getName()).isEqualTo("surgery"); + } + + @Test + @Transactional + void shouldAddNewVisitForPet() { + Pet pet7 = this.clinicService.findPetById(7); + int found = pet7.getVisits().size(); + Visit visit = new Visit(); + pet7.addVisit(visit); + visit.setDescription("test"); + this.clinicService.saveVisit(visit); + this.clinicService.savePet(pet7); + + pet7 = this.clinicService.findPetById(7); + assertThat(pet7.getVisits().size()).isEqualTo(found + 1); + assertThat(visit.getId()).isNotNull(); + } + + @Test + void shouldFindVisitsByPetId() throws Exception { + Collection visits = this.clinicService.findVisitsByPetId(7); + assertThat(visits.size()).isEqualTo(2); + Visit[] visitArr = visits.toArray(new Visit[visits.size()]); + assertThat(visitArr[0].getPet()).isNotNull(); + assertThat(visitArr[0].getDate()).isNotNull(); + assertThat(visitArr[0].getPet().getId()).isEqualTo(7); + } + + @Test + void shouldFindAllPets(){ + Collection pets = this.clinicService.findAllPets(); + Pet pet1 = EntityUtils.getById(pets, Pet.class, 1); + assertThat(pet1.getName()).isEqualTo("Leo"); + Pet pet3 = EntityUtils.getById(pets, Pet.class, 3); + assertThat(pet3.getName()).isEqualTo("Rosy"); + } + + @Test + @Transactional + void shouldDeletePet(){ + Pet pet = this.clinicService.findPetById(1); + this.clinicService.deletePet(pet); + try { + pet = this.clinicService.findPetById(1); + } catch (Exception e) { + pet = null; + } + assertThat(pet).isNull(); + } + + @Test + void shouldFindVisitDyId(){ + Visit visit = this.clinicService.findVisitById(1); + assertThat(visit.getId()).isEqualTo(1); + assertThat(visit.getPet().getName()).isEqualTo("Samantha"); + } + + @Test + void shouldFindAllVisits(){ + Collection visits = this.clinicService.findAllVisits(); + Visit visit1 = EntityUtils.getById(visits, Visit.class, 1); + assertThat(visit1.getPet().getName()).isEqualTo("Samantha"); + Visit visit3 = EntityUtils.getById(visits, Visit.class, 3); + assertThat(visit3.getPet().getName()).isEqualTo("Max"); + } + + @Test + @Transactional + void shouldInsertVisit() { + Collection visits = this.clinicService.findAllVisits(); + int found = visits.size(); + + Pet pet = this.clinicService.findPetById(1); + + Visit visit = new Visit(); + visit.setPet(pet); + visit.setDate(LocalDate.now()); + visit.setDescription("new visit"); + + + this.clinicService.saveVisit(visit); + assertThat(visit.getId().longValue()).isNotEqualTo(0); + + visits = this.clinicService.findAllVisits(); + assertThat(visits.size()).isEqualTo(found + 1); + } + + @Test + @Transactional + void shouldUpdateVisit(){ + Visit visit = this.clinicService.findVisitById(1); + String oldDesc = visit.getDescription(); + String newDesc = oldDesc + "X"; + visit.setDescription(newDesc); + this.clinicService.saveVisit(visit); + visit = this.clinicService.findVisitById(1); + assertThat(visit.getDescription()).isEqualTo(newDesc); + } + + @Test + @Transactional + void shouldDeleteVisit(){ + Visit visit = this.clinicService.findVisitById(1); + this.clinicService.deleteVisit(visit); + try { + visit = this.clinicService.findVisitById(1); + } catch (Exception e) { + visit = null; + } + assertThat(visit).isNull(); + } + + @Test + void shouldFindVetDyId(){ + Vet vet = this.clinicService.findVetById(1); + assertThat(vet.getFirstName()).isEqualTo("James"); + assertThat(vet.getLastName()).isEqualTo("Carter"); + } + + @Test + @Transactional + void shouldInsertVet() { + Collection vets = this.clinicService.findAllVets(); + int found = vets.size(); + + Vet vet = new Vet(); + vet.setFirstName("John"); + vet.setLastName("Dow"); + + this.clinicService.saveVet(vet); + assertThat(vet.getId().longValue()).isNotEqualTo(0); + + vets = this.clinicService.findAllVets(); + assertThat(vets.size()).isEqualTo(found + 1); + } + + @Test + @Transactional + void shouldUpdateVet(){ + Vet vet = this.clinicService.findVetById(1); + String oldLastName = vet.getLastName(); + String newLastName = oldLastName + "X"; + vet.setLastName(newLastName); + this.clinicService.saveVet(vet); + vet = this.clinicService.findVetById(1); + assertThat(vet.getLastName()).isEqualTo(newLastName); + } + + @Test + @Transactional + void shouldDeleteVet(){ + Vet vet = this.clinicService.findVetById(1); + this.clinicService.deleteVet(vet); + try { + vet = this.clinicService.findVetById(1); + } catch (Exception e) { + vet = null; + } + assertThat(vet).isNull(); + } + + @Test + void shouldFindAllOwners(){ + Collection owners = this.clinicService.findAllOwners(); + Owner owner1 = EntityUtils.getById(owners, Owner.class, 1); + assertThat(owner1.getFirstName()).isEqualTo("George"); + Owner owner3 = EntityUtils.getById(owners, Owner.class, 3); + assertThat(owner3.getFirstName()).isEqualTo("Eduardo"); + } + + @Test + @Transactional + void shouldDeleteOwner(){ + Owner owner = this.clinicService.findOwnerById(1); + this.clinicService.deleteOwner(owner); + try { + owner = this.clinicService.findOwnerById(1); + } catch (Exception e) { + owner = null; + } + assertThat(owner).isNull(); + } + + @Test + void shouldFindPetTypeById(){ + PetType petType = this.clinicService.findPetTypeById(1); + assertThat(petType.getName()).isEqualTo("cat"); + } + + @Test + void shouldFindAllPetTypes(){ + Collection petTypes = this.clinicService.findAllPetTypes(); + PetType petType1 = EntityUtils.getById(petTypes, PetType.class, 1); + assertThat(petType1.getName()).isEqualTo("cat"); + PetType petType3 = EntityUtils.getById(petTypes, PetType.class, 3); + assertThat(petType3.getName()).isEqualTo("lizard"); + } + + @Test + @Transactional + void shouldInsertPetType() { + Collection petTypes = this.clinicService.findAllPetTypes(); + int found = petTypes.size(); + + PetType petType = new PetType(); + petType.setName("tiger"); + + this.clinicService.savePetType(petType); + assertThat(petType.getId().longValue()).isNotEqualTo(0); + + petTypes = this.clinicService.findAllPetTypes(); + assertThat(petTypes.size()).isEqualTo(found + 1); + } + + @Test + @Transactional + void shouldUpdatePetType(){ + PetType petType = this.clinicService.findPetTypeById(1); + String oldLastName = petType.getName(); + String newLastName = oldLastName + "X"; + petType.setName(newLastName); + this.clinicService.savePetType(petType); + petType = this.clinicService.findPetTypeById(1); + assertThat(petType.getName()).isEqualTo(newLastName); + } + + @Test + @Transactional + void shouldDeletePetType(){ + PetType petType = this.clinicService.findPetTypeById(1); + this.clinicService.deletePetType(petType); + try { + petType = this.clinicService.findPetTypeById(1); + } catch (Exception e) { + petType = null; + } + assertThat(petType).isNull(); + } + + @Test + void shouldFindSpecialtyById(){ + Specialty specialty = this.clinicService.findSpecialtyById(1); + assertThat(specialty.getName()).isEqualTo("radiology"); + } + + @Test + void shouldFindAllSpecialtys(){ + Collection specialties = this.clinicService.findAllSpecialties(); + Specialty specialty1 = EntityUtils.getById(specialties, Specialty.class, 1); + assertThat(specialty1.getName()).isEqualTo("radiology"); + Specialty specialty3 = EntityUtils.getById(specialties, Specialty.class, 3); + assertThat(specialty3.getName()).isEqualTo("dentistry"); + } + + @Test + @Transactional + void shouldInsertSpecialty() { + Collection specialties = this.clinicService.findAllSpecialties(); + int found = specialties.size(); + + Specialty specialty = new Specialty(); + specialty.setName("dermatologist"); + + this.clinicService.saveSpecialty(specialty); + assertThat(specialty.getId().longValue()).isNotEqualTo(0); + + specialties = this.clinicService.findAllSpecialties(); + assertThat(specialties.size()).isEqualTo(found + 1); + } + + @Test + @Transactional + void shouldUpdateSpecialty(){ + Specialty specialty = this.clinicService.findSpecialtyById(1); + String oldLastName = specialty.getName(); + String newLastName = oldLastName + "X"; + specialty.setName(newLastName); + this.clinicService.saveSpecialty(specialty); + specialty = this.clinicService.findSpecialtyById(1); + assertThat(specialty.getName()).isEqualTo(newLastName); + } + + @Test + @Transactional + void shouldDeleteSpecialty(){ + Specialty specialty = new Specialty(); + specialty.setName("test"); + this.clinicService.saveSpecialty(specialty); + Integer specialtyId = specialty.getId(); + assertThat(specialtyId).isNotNull(); + specialty = this.clinicService.findSpecialtyById(specialtyId); + assertThat(specialty).isNotNull(); + this.clinicService.deleteSpecialty(specialty); + try { + specialty = this.clinicService.findSpecialtyById(specialtyId); + } catch (Exception e) { + specialty = null; + } + assertThat(specialty).isNull(); + } + + @Test + @Transactional + void shouldFindSpecialtiesByNameIn() { + Specialty specialty1 = new Specialty(); + specialty1.setName("radiology"); + specialty1.setId(1); + Specialty specialty2 = new Specialty(); + specialty2.setName("surgery"); + specialty2.setId(2); + Specialty specialty3 = new Specialty(); + specialty3.setName("dentistry"); + specialty3.setId(3); + List expectedSpecialties = List.of(specialty1, specialty2, specialty3); + Set specialtyNames = expectedSpecialties.stream() + .map(Specialty::getName) + .collect(Collectors.toSet()); + Collection actualSpecialties = this.clinicService.findSpecialtiesByNameIn(specialtyNames); + assertThat(actualSpecialties).isNotNull(); + assertThat(actualSpecialties.size()).isEqualTo(expectedSpecialties.size()); + for (Specialty expected : expectedSpecialties) { + assertThat(actualSpecialties.stream() + .anyMatch( + actual -> actual.getName().equals(expected.getName()) + && actual.getId().equals(expected.getId()))).isTrue(); + } + } +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ApplicationTestConfig.java b/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ApplicationTestConfig.java new file mode 100644 index 0000000..79398dc --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ApplicationTestConfig.java @@ -0,0 +1,13 @@ +package org.springframework.samples.petclinic.service.clinicService; + +import org.mockito.MockitoAnnotations; +import org.springframework.boot.test.context.TestConfiguration; + +@TestConfiguration +public class ApplicationTestConfig { + + public ApplicationTestConfig(){ + MockitoAnnotations.openMocks(this); + } + +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ClinicServiceJdbcTests.java b/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ClinicServiceJdbcTests.java new file mode 100644 index 0000000..43724aa --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ClinicServiceJdbcTests.java @@ -0,0 +1,32 @@ +/* + * Copyright 2002-2013 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package org.springframework.samples.petclinic.service.clinicService; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +/** + *

Integration test using the jdbc profile. + * + * @author Thomas Risberg + * @author Michael Isvy + * @see AbstractClinicServiceTests AbstractClinicServiceTests for more details.

+ */ +@SpringBootTest +@ActiveProfiles({"jdbc", "hsqldb"}) +class ClinicServiceJdbcTests extends AbstractClinicServiceTests { + +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ClinicServiceJpaTests.java b/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ClinicServiceJpaTests.java new file mode 100644 index 0000000..1a73050 --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ClinicServiceJpaTests.java @@ -0,0 +1,19 @@ +package org.springframework.samples.petclinic.service.clinicService; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +/** + *

Integration test using the jpa profile. + * + * @author Rod Johnson + * @author Sam Brannen + * @author Michael Isvy + * @see AbstractClinicServiceTests AbstractClinicServiceTests for more details.

+ */ + +@SpringBootTest +@ActiveProfiles({"jpa", "hsqldb"}) +class ClinicServiceJpaTests extends AbstractClinicServiceTests { + +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ClinicServiceSpringDataJpaTests.java b/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ClinicServiceSpringDataJpaTests.java new file mode 100644 index 0000000..b5c57c0 --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/service/clinicService/ClinicServiceSpringDataJpaTests.java @@ -0,0 +1,17 @@ +package org.springframework.samples.petclinic.service.clinicService; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +/** + *

Integration test using the 'Spring Data' profile. + * + * @author Michael Isvy + * @see AbstractClinicServiceTests AbstractClinicServiceTests for more details.

+ */ + +@SpringBootTest +@ActiveProfiles({"spring-data-jpa", "hsqldb"}) +class ClinicServiceSpringDataJpaTests extends AbstractClinicServiceTests { + +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/service/userService/AbstractUserServiceTests.java b/backend/src/test/java/org/springframework/samples/petclinic/service/userService/AbstractUserServiceTests.java new file mode 100644 index 0000000..54ecdde --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/service/userService/AbstractUserServiceTests.java @@ -0,0 +1,35 @@ +package org.springframework.samples.petclinic.service.userService; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.MockitoAnnotations; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.samples.petclinic.model.User; +import org.springframework.samples.petclinic.service.UserService; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +public abstract class AbstractUserServiceTests { + + @Autowired + private UserService userService; + + @BeforeEach + public void init() { + MockitoAnnotations.openMocks(this); + } + + @Test + public void shouldAddUser() throws Exception { + User user = new User(); + user.setUsername("username"); + user.setPassword("password"); + user.setEnabled(true); + user.addRole("OWNER_ADMIN"); + + userService.saveUser(user); + assertThat(user.getRoles().parallelStream().allMatch(role -> role.getName().startsWith("ROLE_")), is(true)); + assertThat(user.getRoles().parallelStream().allMatch(role -> role.getUser() != null), is(true)); + } +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/service/userService/UserServiceJdbcTests.java b/backend/src/test/java/org/springframework/samples/petclinic/service/userService/UserServiceJdbcTests.java new file mode 100644 index 0000000..8cfbdaf --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/service/userService/UserServiceJdbcTests.java @@ -0,0 +1,10 @@ +package org.springframework.samples.petclinic.service.userService; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles({"jdbc", "hsqldb"}) +class UserServiceJdbcTests extends AbstractUserServiceTests { + +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/service/userService/UserServiceJpaTests.java b/backend/src/test/java/org/springframework/samples/petclinic/service/userService/UserServiceJpaTests.java new file mode 100644 index 0000000..0db9a12 --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/service/userService/UserServiceJpaTests.java @@ -0,0 +1,10 @@ +package org.springframework.samples.petclinic.service.userService; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles({"jpa", "hsqldb"}) +class UserServiceJpaTests extends AbstractUserServiceTests { + +} diff --git a/backend/src/test/java/org/springframework/samples/petclinic/service/userService/UserServiceSpringDataJpaTests.java b/backend/src/test/java/org/springframework/samples/petclinic/service/userService/UserServiceSpringDataJpaTests.java new file mode 100644 index 0000000..d7240c8 --- /dev/null +++ b/backend/src/test/java/org/springframework/samples/petclinic/service/userService/UserServiceSpringDataJpaTests.java @@ -0,0 +1,10 @@ +package org.springframework.samples.petclinic.service.userService; + +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles({"spring-data-jpa", "hsqldb"}) +class UserServiceSpringDataJpaTests extends AbstractUserServiceTests { + +} diff --git a/backend/src/test/resources/application.properties b/backend/src/test/resources/application.properties new file mode 100644 index 0000000..f57025f --- /dev/null +++ b/backend/src/test/resources/application.properties @@ -0,0 +1,43 @@ +# active profiles config +# +# application use two active profiles +# +# one for select repository layer +# ------------------------------------------------ +# When using HSQL, use: hsqldb +# When using MySQL, use: mysql +# When using PostgeSQL, use: postgres +# ------------------------------------------------ +# +# one - for select database +# ------------------------------------------------ +# When using Spring jpa, use: jpa +# When using Spring JDBC, use: jdbc +# When using Spring Data JPA, use: spring-data-jpa +# ------------------------------------------------ + +spring.profiles.active=hsqldb,spring-data-jpa + +# ------------------------------------------------ + +server.port=9966 +server.servlet.context-path=/petclinic/ +spring.jpa.open-in-view=false + +# database init +spring.sql.init.schema-locations=classpath*:db/hsqldb/schema.sql +spring.sql.init.data-locations=classpath*:db/hsqldb/data.sql + +spring.messages.basename=messages/messages +logging.level.org.springframework=INFO +#logging.level.org.springframework=DEBUG + +#logging.level.org.hibernate.SQL=DEBUG +#logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE + +# enable the desired authentication type +# by default the authentication is disabled +security.ignored=/** +basic.authentication.enabled=true +petclinic.security.enable=true + diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..a541719 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,9 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * This is a general purpose Gradle build. + * To learn more about Gradle by exploring our Samples at https://docs.gradle.org/8.7/samples + */ +plugins { + idea +} \ No newline at end of file diff --git a/flake/flake.lock b/flake/flake.lock new file mode 100644 index 0000000..ff8b138 --- /dev/null +++ b/flake/flake.lock @@ -0,0 +1,57 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 0, + "narHash": "sha256-q2yjIWFFcTzp5REWQUOU9L6kHdCDmFDpqeix86SOvDc=", + "path": "/nix/store/9g88fck8ggiah5znz5xn2kxzfr6l7cdq-source", + "type": "path" + }, + "original": { + "id": "nixpkgs", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake/flake.nix b/flake/flake.nix new file mode 100644 index 0000000..2744220 --- /dev/null +++ b/flake/flake.nix @@ -0,0 +1,12 @@ +{ + inputs.flake-utils.url = "github:/numtide/flake-utils"; + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem + (system: + let pkgs = nixpkgs.legacyPackages.${system}; in + with pkgs; { + devShells.default = mkShell { + packages = [ jdk21 nodejs_22]; + }; + }); +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..2ea8b6b --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +mapstructVersion=1.6.2 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..4ac3234 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,2 @@ +# This file was generated by the Gradle 'init' task. +# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..a4b76b9530d66f5e68d973ea569d8e19de379189 GIT binary patch literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X literal 0 HcmV?d00001 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..e2847c8 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..f5feea6 --- /dev/null +++ b/gradlew @@ -0,0 +1,252 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s +' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..b672baa --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,8 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.7/userguide/multi_project_builds.html in the Gradle documentation. + */ +include("backend") +rootProject.name = "petclinic"