├── .github └── workflows │ └── semgrep.yml ├── .gitignore ├── .gradle └── buildOutputCleanup │ ├── buildOutputCleanup.lock │ ├── cache.properties │ └── outputFiles.bin ├── Dockerfile ├── TODO.md ├── application ├── build.gradle ├── deploy.gradle └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── springframework │ │ │ └── samples │ │ │ └── petclinic │ │ │ ├── PetClinicApplication.java │ │ │ ├── condition │ │ │ └── NonProdCondition.java │ │ │ ├── config │ │ │ ├── AppConfig.java │ │ │ ├── CacheConfig.java │ │ │ └── SecurityConfig.java │ │ │ ├── model │ │ │ ├── BaseEntity.java │ │ │ ├── NamedEntity.java │ │ │ ├── Owner.java │ │ │ ├── Person.java │ │ │ ├── Pet.java │ │ │ ├── PetType.java │ │ │ ├── Specialty.java │ │ │ ├── User.java │ │ │ ├── Vet.java │ │ │ ├── Vets.java │ │ │ ├── Visit.java │ │ │ └── package-info.java │ │ │ ├── repository │ │ │ ├── OwnerRepository.java │ │ │ ├── PetRepository.java │ │ │ ├── UserRepository.java │ │ │ ├── VetRepository.java │ │ │ └── VisitRepository.java │ │ │ ├── security │ │ │ ├── AuthenticationHelper.java │ │ │ ├── JWTAuthenticationFilter.java │ │ │ ├── JWTAuthenticationHelper.java │ │ │ ├── JWTAuthenticationProvider.java │ │ │ ├── JWTLoginFilter.java │ │ │ └── TokenAuthentication.java │ │ │ ├── service │ │ │ ├── ClinicService.java │ │ │ ├── ClinicServiceImpl.java │ │ │ └── DefaultUserDetailsService.java │ │ │ ├── sqlformatter │ │ │ └── HibernateBasicSqlFormatter.java │ │ │ ├── util │ │ │ └── EntityUtils.java │ │ │ └── web │ │ │ ├── CrashController.java │ │ │ ├── IndexController.java │ │ │ ├── OwnerController.java │ │ │ ├── PetController.java │ │ │ ├── PetTypeFormatter.java │ │ │ ├── PetValidator.java │ │ │ ├── TestController.java │ │ │ ├── VetController.java │ │ │ ├── VisitController.java │ │ │ ├── WelcomeController.java │ │ │ ├── api │ │ │ ├── AbstractResourceController.java │ │ │ ├── ApiExceptionHandler.java │ │ │ ├── BadRequestException.java │ │ │ ├── ErrorResource.java │ │ │ ├── FailingApiController.java │ │ │ ├── FieldErrorResource.java │ │ │ ├── InvalidRequestException.java │ │ │ ├── OwnerResource.java │ │ │ ├── PetRequest.java │ │ │ ├── PetResource.java │ │ │ ├── SuperFatalErrorException.java │ │ │ ├── VetResource.java │ │ │ └── VisitResource.java │ │ │ └── package-info.java │ └── resources │ │ ├── application.yml │ │ ├── banner.txt │ │ ├── db │ │ └── migration │ │ │ ├── ddl │ │ │ ├── V2019_01_19_15_12__users.sql │ │ │ ├── V2019_01_19_15_13__specialties.sql │ │ │ ├── V2019_01_19_15_14__owners.sql │ │ │ ├── V2019_01_19_15_15__types.sql │ │ │ ├── V2019_01_19_15_16__vets.sql │ │ │ ├── V2019_01_19_15_17__pets.sql │ │ │ ├── V2019_01_19_15_18__visits.sql │ │ │ └── V2019_01_19_15_19__vet_specialties.sql │ │ │ └── dml │ │ │ ├── V2019_01_19_15_20__vets.sql │ │ │ ├── V2019_01_19_15_21__pet_owners.sql │ │ │ ├── V2019_01_19_15_22__specialties.sql │ │ │ ├── V2019_01_19_15_23__types.sql │ │ │ ├── V2019_01_19_15_24__vet_specialties.sql │ │ │ ├── V2019_01_19_15_25__pets.sql │ │ │ ├── V2019_01_19_15_26__visits.sql │ │ │ └── V2019_01_19_15_27__users.sql │ │ ├── logback-spring.xml │ │ ├── messages │ │ ├── messages.properties │ │ ├── messages_de.properties │ │ └── messages_en.properties │ │ └── spy.properties │ └── test │ ├── java │ └── org │ │ └── springframework │ │ └── samples │ │ └── petclinic │ │ ├── model │ │ └── ValidatorTests.java │ │ ├── service │ │ └── ClinicServiceSpringDataJpaTests.java │ │ └── web │ │ ├── CrashControllerTests.java │ │ ├── OwnerControllerTests.java │ │ ├── PetControllerTests.java │ │ ├── PetTypeFormatterTests.java │ │ ├── VetControllerTests.java │ │ ├── VisitControllerTests.java │ │ └── api │ │ ├── OwnerResourceTests.java │ │ ├── PetResourceTests.java │ │ └── VetResourceTests.java │ └── resources │ ├── allure.properties │ └── logback-test.xml ├── build.gradle ├── docker-compose.yml ├── frontend ├── package.json ├── public │ ├── images │ │ ├── favicon.png │ │ ├── loader.gif │ │ ├── pets.png │ │ ├── platform-bg.png │ │ └── spring-pivotal-logo.png │ ├── index.html │ ├── loading.gif │ └── test.html ├── run.sh ├── server.js ├── src │ ├── Root.tsx │ ├── components │ │ ├── App.tsx │ │ ├── ErrorPage.tsx │ │ ├── Login.tsx │ │ ├── Menu.tsx │ │ ├── NotFoundPage.tsx │ │ ├── WelcomePage.tsx │ │ ├── form │ │ │ ├── Constraints.ts │ │ │ ├── DateInput.tsx │ │ │ ├── FieldFeedbackPanel.tsx │ │ │ ├── Input.tsx │ │ │ └── SelectInput.tsx │ │ ├── owners │ │ │ ├── EditOwnerPage.tsx │ │ │ ├── FindOwnersPage.tsx │ │ │ ├── NewOwnerPage.tsx │ │ │ ├── OwnerEditor.tsx │ │ │ ├── OwnerInformation.tsx │ │ │ ├── OwnersPage.tsx │ │ │ ├── OwnersTable.tsx │ │ │ └── PetsTable.tsx │ │ ├── pets │ │ │ ├── EditPetPage.tsx │ │ │ ├── LoadingPanel.tsx │ │ │ ├── NewPetPage.tsx │ │ │ ├── PetEditor.tsx │ │ │ └── createPetEditorModel.ts │ │ ├── vets │ │ │ └── VetsPage.tsx │ │ └── visits │ │ │ ├── PetDetails.tsx │ │ │ └── VisitsPage.tsx │ ├── configureRoutes.tsx │ ├── main.tsx │ ├── middleware │ │ └── api.ts │ ├── react-datepicker.d.ts │ ├── react-hot-loader.d.ts │ ├── styles │ │ ├── fonts │ │ │ ├── montserrat-webfont.eot │ │ │ ├── montserrat-webfont.svg │ │ │ ├── montserrat-webfont.ttf │ │ │ ├── montserrat-webfont.woff │ │ │ ├── varela_round-webfont.eot │ │ │ ├── varela_round-webfont.svg │ │ │ ├── varela_round-webfont.ttf │ │ │ └── varela_round-webfont.woff │ │ ├── images │ │ │ ├── spring-logo-dataflow-mobile.png │ │ │ └── spring-logo-dataflow.png │ │ └── less │ │ │ ├── header.less │ │ │ ├── petclinic.less │ │ │ ├── responsive.less │ │ │ └── typography.less │ ├── types │ │ └── index.ts │ └── util │ │ └── index.tsx ├── tests │ ├── __tests__ │ │ ├── fetch-mock.js │ │ └── util.test.tsx │ └── components │ │ └── form │ │ └── __tests__ │ │ ├── Constraints.test.tsx │ │ └── Input.test.tsx ├── tsconfig.json ├── tslint.json ├── typings.json ├── webpack.config.js └── webpack.config.prod.js ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── readme.md ├── settings.gradle ├── sonar-project.properties └── ui-tests ├── build.gradle └── src └── test ├── java └── org │ └── springframework │ └── samples │ └── petclinic │ ├── steps │ ├── MainSteps.java │ └── OwnersSteps.java │ ├── tests │ ├── CiUiTest.java │ ├── LocalUiTest.java │ ├── TestDataSource.java │ ├── owners │ │ └── OwnersPageTest.java │ └── pets │ │ └── PetsPageTest.java │ └── util │ ├── JmxUtil.java │ ├── LoginUtil.java │ ├── TestContainerUtil.java │ └── VideoRecordingExtension.java └── resources ├── datasets ├── cleanup.sql ├── owners │ ├── owner-to-edit.xml │ └── owner-to-search.xml ├── pets │ └── owner-to-create-pet.xml └── test_user.xml ├── logback-test.xml ├── spy.properties └── testcontainers.properties /.github/workflows/semgrep.yml: -------------------------------------------------------------------------------- 1 | on: 2 | workflow_dispatch: {} 3 | pull_request: {} 4 | push: 5 | branches: 6 | - main 7 | - master 8 | paths: 9 | - .github/workflows/semgrep.yml 10 | schedule: 11 | # random HH:MM to avoid a load spike on GitHub Actions at 00:00 12 | - cron: 32 19 * * * 13 | name: Semgrep 14 | jobs: 15 | semgrep: 16 | name: semgrep/ci 17 | runs-on: ubuntu-20.04 18 | env: 19 | SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }} 20 | container: 21 | image: returntocorp/semgrep 22 | steps: 23 | - uses: actions/checkout@v3 24 | - run: semgrep ci 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | 5 | ### STS ### 6 | .apt_generated 7 | .classpath 8 | .factorypath 9 | .project 10 | .settings 11 | .springBeans 12 | .sts4-cache 13 | 14 | ### IntelliJ IDEA ### 15 | .idea 16 | *.iws 17 | *.iml 18 | *.ipr 19 | out/ 20 | 21 | ### NetBeans ### 22 | /nbproject/private/ 23 | /nbbuild/ 24 | /dist/ 25 | /nbdist/ 26 | /.nb-gradle/ 27 | 28 | ### Logs ### 29 | *.log 30 | *.log.zip 31 | 32 | ### Frontend ### 33 | frontend/package-lock.json -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/buildOutputCleanup.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/.gradle/buildOutputCleanup/buildOutputCleanup.lock -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/cache.properties: -------------------------------------------------------------------------------- 1 | #Tue Feb 19 07:40:48 EET 2019 2 | gradle.version=5.0 3 | -------------------------------------------------------------------------------- /.gradle/buildOutputCleanup/outputFiles.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/.gradle/buildOutputCleanup/outputFiles.bin -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM java:8 2 | ARG jarFile 3 | ADD ${jarFile} /app.jar 4 | RUN bash -c 'touch /app.jar' 5 | ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"] -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # To be done 2 | - [x] missing pages 3 | - [ ] localization 4 | - [ ] align error messages in validations from server and client 5 | - [ ] Submitting a form should only be possible if no errors in form 6 | - [ ] Replace with 7 | - [ ] Table => Sortable Table (https://github.com/glittershark/reactable/blob/master/README.md) 8 | - [ ] REST calls not resulting in valid JSON (i.e. 500) must be handled correctly (or make sure on server side that we ALWAYS get json). Anyway those errors must be shown to the user not only on the console. 9 | 10 | # Refactoring / Clean up 11 | - [ ] General clean-up as this is an own repository now and not only a branch 12 | - [ ] Centralize fetch calls in own function and add error handling 13 | - [ ] maybe centralize location handling (push / redirect to other url) to remove need of context in components 14 | - [ ] OwnersPage => OwnerPage 15 | - [ ] align singular vs plural in HTTP api endpoints, client folder names and page names 16 | - [ ] centralize loading of an owner (EditOwnerPage, OwnersPage) and handle 404 from API 17 | - [ ] remove isNew from API (use id===null instead) 18 | - [ ] IBaseEntity: id as 'any'? 19 | - [ ] Refactor/Clean-up IPetRequest, IEditablePet (also on Server PetRequest) 20 | - [ ] Base on [spring-petclinic-microservices](https://github.com/spring-petclinic/spring-petclinic-microservices) 21 | - [x] Remove JSP pages and static resources from the Spring Boot backend 22 | 23 | # New Features 24 | - [x] add client-side validation to input fields to show advantage of SPA 25 | - [ ] introduce redux to cache entities on client? (on new branch?) 26 | - [ ] more 'in-place' editing instead of own pages (more SPA-ish feeling) 27 | - [ ] client-side testing 28 | 29 | # Differences from original spring boot example 30 | * Client-side validation 31 | 32 | 33 | -------------------------------------------------------------------------------- /application/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'org.springframework.boot' 2 | apply from: 'deploy.gradle' 3 | 4 | dependencies { 5 | compile 'org.springframework.boot:spring-boot-starter-web' 6 | compile 'org.springframework.boot:spring-boot-starter-actuator' 7 | compile 'org.springframework.boot:spring-boot-starter-data-jpa' 8 | compile 'org.springframework.boot:spring-boot-starter-cache' 9 | compile 'org.springframework.boot:spring-boot-starter-security' 10 | compile 'io.jsonwebtoken:jjwt:0.9.1' 11 | compile 'org.postgresql:postgresql' 12 | compile 'org.flywaydb:flyway-core' 13 | compile('com.fasterxml.jackson.datatype:jackson-datatype-jdk8') 14 | compile('com.fasterxml.jackson.datatype:jackson-datatype-jsr310') 15 | 16 | compile 'javax.cache:cache-api:1.0.0' 17 | compile 'org.ehcache:ehcache:3.1.1' 18 | 19 | testCompile 'org.springframework.boot:spring-boot-starter-test' 20 | testCompile 'org.testcontainers:postgresql:1.10.0' 21 | } 22 | 23 | gradle.taskGraph.whenReady { graph -> 24 | if (graph.hasTask(':application:assembleApp')) { 25 | tasks['bootJar'].with { 26 | from("${rootDir}/frontend/public") { 27 | include "**" 28 | into 'static' 29 | } 30 | } 31 | } 32 | } 33 | 34 | bootJar { 35 | baseName "spring-petclinic" 36 | } -------------------------------------------------------------------------------- /application/deploy.gradle: -------------------------------------------------------------------------------- 1 | task npmInstall(type: Exec) { 2 | inputs.file("${rootDir}/frontend/package.json") 3 | outputs.dir("${rootDir}/frontend/node_modules") 4 | outputs.file("${rootDir}/frontend/package-lock.json") 5 | 6 | workingDir "${rootDir}/frontend" 7 | commandLine 'npm', 'install' 8 | } 9 | 10 | task npmBuild(type: Exec) { 11 | dependsOn(npmInstall) 12 | inputs.dir("${rootDir}/frontend/src") 13 | inputs.dir("${rootDir}/frontend/public") 14 | outputs.dir("${rootDir}/frontend/dist") 15 | 16 | workingDir "${rootDir}/frontend" 17 | commandLine 'npm', 'run', 'build:prod' 18 | } 19 | 20 | task copyJacocoAgentJar(type: Copy) { 21 | def jacocoAgentJarName = "org.jacoco.agent-0.8.2.jar" 22 | def jacocoAgentJar = configurations.getByName('jacocoAgent').filter { 23 | it.name == jacocoAgentJarName 24 | } 25 | if (jacocoAgentJar.empty) { 26 | throw new FileNotFoundException("Can't find the file $jacocoAgentJarName") 27 | } 28 | from(zipTree(jacocoAgentJar.singleFile)) { 29 | include 'jacocoagent.jar' 30 | } 31 | into "${project.buildDir}/jacocoagent" 32 | } 33 | 34 | task assembleApp() { 35 | dependsOn(npmBuild, copyJacocoAgentJar) 36 | finalizedBy(assemble) 37 | } -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.samples.petclinic; 18 | 19 | import org.springframework.boot.SpringApplication; 20 | import org.springframework.boot.autoconfigure.SpringBootApplication; 21 | 22 | /** 23 | * PetClinic Spring Boot Application. 24 | * 25 | */ 26 | @SpringBootApplication 27 | public class PetClinicApplication { 28 | 29 | public static void main(String[] args) { 30 | SpringApplication.run(PetClinicApplication.class, args); 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/condition/NonProdCondition.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.condition; 2 | 3 | import org.springframework.context.annotation.Condition; 4 | import org.springframework.context.annotation.ConditionContext; 5 | import org.springframework.core.type.AnnotatedTypeMetadata; 6 | 7 | import java.util.Arrays; 8 | 9 | public class NonProdCondition implements Condition { 10 | @Override 11 | public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { 12 | return Arrays.stream(context.getEnvironment().getActiveProfiles()) 13 | .noneMatch(profile -> profile.equals("prod")); 14 | } 15 | } -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/config/AppConfig.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.config; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.fasterxml.jackson.databind.SerializationFeature; 5 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 6 | 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | 10 | @Configuration 11 | public class AppConfig { 12 | @Bean 13 | public ObjectMapper objectMapper() { 14 | ObjectMapper objectMapper = new ObjectMapper(); 15 | objectMapper.registerModule(new JavaTimeModule()); 16 | objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); 17 | return objectMapper; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/config/CacheConfig.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.config; 2 | 3 | import org.ehcache.config.CacheConfiguration; 4 | import org.ehcache.config.builders.CacheConfigurationBuilder; 5 | import org.ehcache.config.builders.ResourcePoolsBuilder; 6 | import org.ehcache.config.units.EntryUnit; 7 | import org.ehcache.expiry.Duration; 8 | import org.ehcache.expiry.Expirations; 9 | import org.ehcache.jsr107.Eh107Configuration; 10 | import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer; 11 | import org.springframework.cache.annotation.EnableCaching; 12 | import org.springframework.context.annotation.Bean; 13 | import org.springframework.context.annotation.Configuration; 14 | import org.springframework.context.annotation.Profile; 15 | 16 | import java.util.concurrent.TimeUnit; 17 | 18 | /** 19 | * Cache could be disable in unit test. 20 | */ 21 | @Configuration 22 | @EnableCaching 23 | @Profile("production") 24 | public class CacheConfig { 25 | 26 | @Bean 27 | public JCacheManagerCustomizer cacheManagerCustomizer() { 28 | return cacheManager -> { 29 | CacheConfiguration config = CacheConfigurationBuilder 30 | .newCacheConfigurationBuilder(Object.class, Object.class, 31 | ResourcePoolsBuilder.newResourcePoolsBuilder() 32 | .heap(100, EntryUnit.ENTRIES)) 33 | .withExpiry(Expirations.timeToLiveExpiration(Duration.of(60, TimeUnit.SECONDS))) 34 | .build(); 35 | cacheManager.createCache("vets", Eh107Configuration.fromEhcacheCacheConfiguration(config)); 36 | }; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/model/BaseEntity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.model; 17 | 18 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 19 | import com.fasterxml.jackson.annotation.JsonProperty; 20 | 21 | import javax.persistence.GeneratedValue; 22 | import javax.persistence.GenerationType; 23 | import javax.persistence.Id; 24 | import javax.persistence.MappedSuperclass; 25 | 26 | /** 27 | * Simple JavaBean domain object with an id property. Used as a base class for objects needing this 28 | * property. 29 | * 30 | * @author Ken Krebs 31 | * @author Juergen Hoeller 32 | * @author Igor Dmitriev 33 | */ 34 | @MappedSuperclass 35 | @JsonIgnoreProperties(ignoreUnknown = true) 36 | public class BaseEntity { 37 | @Id 38 | @GeneratedValue(strategy = GenerationType.IDENTITY) 39 | protected Integer id; 40 | 41 | public Integer getId() { 42 | return id; 43 | } 44 | 45 | public void setId(Integer id) { 46 | this.id = id; 47 | } 48 | 49 | @JsonProperty("isNew") 50 | public boolean isNew() { 51 | return this.id == null; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.model; 17 | 18 | import javax.persistence.Column; 19 | import javax.persistence.MappedSuperclass; 20 | 21 | 22 | /** 23 | * Simple JavaBean domain object adds a name property to BaseEntity. Used as a base class for objects 24 | * needing these properties. 25 | * 26 | * @author Ken Krebs 27 | * @author Juergen Hoeller 28 | */ 29 | @MappedSuperclass 30 | public class NamedEntity extends BaseEntity { 31 | 32 | @Column(name = "name") 33 | private String name; 34 | 35 | public String getName() { 36 | return this.name; 37 | } 38 | 39 | public void setName(String name) { 40 | this.name = name; 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | return this.getName(); 46 | } 47 | 48 | } 49 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/model/Person.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.model; 17 | 18 | import javax.persistence.Column; 19 | import javax.persistence.MappedSuperclass; 20 | import javax.validation.constraints.NotEmpty; 21 | import javax.validation.constraints.Pattern; 22 | 23 | /** 24 | * Simple JavaBean domain object representing an person. 25 | * 26 | * @author Ken Krebs 27 | */ 28 | @MappedSuperclass 29 | public class Person extends BaseEntity { 30 | 31 | @Column(name = "first_name") 32 | @NotEmpty 33 | @Pattern(regexp = "[a-z-A-Z]*", message = "First name has invalid characters") 34 | protected String firstName; 35 | 36 | @Column(name = "last_name") 37 | @NotEmpty 38 | @Pattern(regexp = "[a-z-A-Z]*", message = "Last name has invalid characters") 39 | protected String lastName; 40 | 41 | public String getFirstName() { 42 | return this.firstName; 43 | } 44 | 45 | public void setFirstName(String firstName) { 46 | this.firstName = firstName; 47 | } 48 | 49 | public String getLastName() { 50 | return this.lastName; 51 | } 52 | 53 | public void setLastName(String lastName) { 54 | this.lastName = lastName; 55 | } 56 | 57 | 58 | } 59 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/model/Pet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.model; 17 | 18 | import com.fasterxml.jackson.annotation.JsonFormat; 19 | import com.fasterxml.jackson.annotation.JsonIgnore; 20 | 21 | import org.springframework.beans.support.MutableSortDefinition; 22 | import org.springframework.beans.support.PropertyComparator; 23 | 24 | import java.time.LocalDate; 25 | import java.util.ArrayList; 26 | import java.util.Collections; 27 | import java.util.HashSet; 28 | import java.util.List; 29 | import java.util.Set; 30 | 31 | import javax.persistence.CascadeType; 32 | import javax.persistence.Column; 33 | import javax.persistence.Entity; 34 | import javax.persistence.FetchType; 35 | import javax.persistence.JoinColumn; 36 | import javax.persistence.ManyToOne; 37 | import javax.persistence.OneToMany; 38 | import javax.persistence.Table; 39 | 40 | /** 41 | * Simple business object representing a pet. 42 | * 43 | * @author Ken Krebs 44 | * @author Juergen Hoeller 45 | * @author Sam Brannen 46 | */ 47 | @Entity 48 | @Table(name = "pets") 49 | public class Pet extends NamedEntity { 50 | 51 | @Column(name = "birth_date") 52 | @JsonFormat(pattern = "yyyy/MM/dd") 53 | private LocalDate birthDate; 54 | 55 | @ManyToOne 56 | @JoinColumn(name = "type_id") 57 | private PetType type; 58 | 59 | @ManyToOne 60 | @JoinColumn(name = "owner_id") 61 | @JsonIgnore 62 | private Owner owner; 63 | 64 | @OneToMany(cascade = CascadeType.ALL, mappedBy = "pet", fetch = FetchType.EAGER) 65 | private Set visits; 66 | 67 | public LocalDate getBirthDate() { 68 | return this.birthDate; 69 | } 70 | 71 | public void setBirthDate(LocalDate birthDate) { 72 | this.birthDate = birthDate; 73 | } 74 | 75 | public PetType getType() { 76 | return this.type; 77 | } 78 | 79 | public void setType(PetType type) { 80 | this.type = type; 81 | } 82 | 83 | public Owner getOwner() { 84 | return this.owner; 85 | } 86 | 87 | protected void setOwner(Owner owner) { 88 | this.owner = owner; 89 | } 90 | 91 | protected Set getVisitsInternal() { 92 | if (this.visits == null) { 93 | this.visits = new HashSet<>(); 94 | } 95 | return this.visits; 96 | } 97 | 98 | protected void setVisitsInternal(Set visits) { 99 | this.visits = visits; 100 | } 101 | 102 | public List getVisits() { 103 | List sortedVisits = new ArrayList<>(getVisitsInternal()); 104 | PropertyComparator.sort(sortedVisits, new MutableSortDefinition("date", false, false)); 105 | return Collections.unmodifiableList(sortedVisits); 106 | } 107 | 108 | public void addVisit(Visit visit) { 109 | getVisitsInternal().add(visit); 110 | visit.setPet(this); 111 | } 112 | 113 | 114 | } 115 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/model/PetType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.model; 17 | 18 | import javax.persistence.Entity; 19 | import javax.persistence.Table; 20 | 21 | /** 22 | * @author Juergen Hoeller 23 | * Can be Cat, Dog, Hamster... 24 | */ 25 | @Entity 26 | @Table(name = "types") 27 | public class PetType extends NamedEntity { 28 | 29 | } 30 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/model/Specialty.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.model; 17 | 18 | import javax.persistence.Entity; 19 | import javax.persistence.Table; 20 | 21 | /** 22 | * Models a {@link Vet Vet's} specialty (for example, dentistry). 23 | * 24 | * @author Juergen Hoeller 25 | * @author Igor Dmitriev 26 | */ 27 | @Entity 28 | @Table(name = "specialties") 29 | public class Specialty extends NamedEntity { 30 | 31 | } 32 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/model/User.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.model; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.Table; 6 | 7 | @Entity 8 | @Table(name = "users") 9 | public class User extends BaseEntity { 10 | @Column(nullable = false) 11 | private String name; 12 | 13 | @Column(nullable = false) 14 | private String password; 15 | 16 | public String getName() { 17 | return name; 18 | } 19 | 20 | public void setName(String name) { 21 | this.name = name; 22 | } 23 | 24 | public String getPassword() { 25 | return password; 26 | } 27 | 28 | public void setPassword(String password) { 29 | this.password = password; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/model/Vet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.model; 17 | 18 | import java.util.ArrayList; 19 | import java.util.Collections; 20 | import java.util.HashSet; 21 | import java.util.List; 22 | import java.util.Set; 23 | 24 | import javax.persistence.Entity; 25 | import javax.persistence.FetchType; 26 | import javax.persistence.JoinColumn; 27 | import javax.persistence.JoinTable; 28 | import javax.persistence.ManyToMany; 29 | import javax.persistence.Table; 30 | import javax.xml.bind.annotation.XmlElement; 31 | 32 | import org.springframework.beans.support.MutableSortDefinition; 33 | import org.springframework.beans.support.PropertyComparator; 34 | 35 | import com.fasterxml.jackson.annotation.JsonIgnore; 36 | 37 | /** 38 | * Simple JavaBean domain object representing a veterinarian. 39 | * 40 | * @author Ken Krebs 41 | * @author Juergen Hoeller 42 | * @author Sam Brannen 43 | * @author Arjen Poutsma 44 | */ 45 | @Entity 46 | @Table(name = "vets") 47 | public class Vet extends Person { 48 | 49 | @ManyToMany(fetch = FetchType.EAGER) 50 | @JoinTable(name = "vet_specialties", joinColumns = @JoinColumn(name = "vet_id"), 51 | inverseJoinColumns = @JoinColumn(name = "specialty_id")) 52 | private Set specialties; 53 | 54 | protected Set getSpecialtiesInternal() { 55 | if (this.specialties == null) { 56 | this.specialties = new HashSet<>(); 57 | } 58 | return this.specialties; 59 | } 60 | 61 | protected void setSpecialtiesInternal(Set specialties) { 62 | this.specialties = specialties; 63 | } 64 | 65 | @XmlElement 66 | public List getSpecialties() { 67 | List sortedSpecs = new ArrayList<>(getSpecialtiesInternal()); 68 | PropertyComparator.sort(sortedSpecs, new MutableSortDefinition("name", true, true)); 69 | return Collections.unmodifiableList(sortedSpecs); 70 | } 71 | 72 | @JsonIgnore 73 | public int getNrOfSpecialties() { 74 | return getSpecialtiesInternal().size(); 75 | } 76 | 77 | public void addSpecialty(Specialty specialty) { 78 | getSpecialtiesInternal().add(specialty); 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/model/Vets.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.model; 17 | 18 | import java.util.ArrayList; 19 | import java.util.List; 20 | 21 | import javax.xml.bind.annotation.XmlElement; 22 | import javax.xml.bind.annotation.XmlRootElement; 23 | 24 | /** 25 | * Simple domain object representing a list of veterinarians. Mostly here to be used for the 'vets' {@link 26 | * org.springframework.web.servlet.view.xml.MarshallingView}. 27 | * 28 | * @author Arjen Poutsma 29 | */ 30 | @XmlRootElement 31 | public class Vets { 32 | 33 | private List vets; 34 | 35 | @XmlElement 36 | public List getVetList() { 37 | if (vets == null) { 38 | vets = new ArrayList<>(); 39 | } 40 | return vets; 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/model/Visit.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.model; 17 | 18 | import com.fasterxml.jackson.annotation.JsonFormat; 19 | import com.fasterxml.jackson.annotation.JsonIgnore; 20 | 21 | import java.time.LocalDate; 22 | 23 | import javax.persistence.Column; 24 | import javax.persistence.Entity; 25 | import javax.persistence.JoinColumn; 26 | import javax.persistence.ManyToOne; 27 | import javax.persistence.Table; 28 | import javax.validation.constraints.NotEmpty; 29 | 30 | /** 31 | * Simple JavaBean domain object representing a visit. 32 | * 33 | * @author Ken Krebs 34 | * @author Igor Dmitriev 35 | */ 36 | @Entity 37 | @Table(name = "visits") 38 | public class Visit extends BaseEntity { 39 | 40 | /** 41 | * Holds value of property date. 42 | */ 43 | @Column(name = "visit_date") 44 | @JsonFormat(pattern = "yyyy/MM/dd") 45 | private LocalDate date; 46 | 47 | 48 | /** 49 | * Holds value of property description. 50 | */ 51 | @NotEmpty 52 | @Column(name = "description") 53 | private String description; 54 | 55 | /** 56 | * Holds value of property pet. 57 | */ 58 | @ManyToOne 59 | @JoinColumn(name = "pet_id") 60 | @JsonIgnore 61 | private Pet pet; 62 | 63 | 64 | /** 65 | * Creates a new instance of Visit for the current date 66 | */ 67 | public Visit() { 68 | this.date = LocalDate.now(); 69 | } 70 | 71 | 72 | /** 73 | * Getter for property date. 74 | * 75 | * @return Value of property date. 76 | */ 77 | public LocalDate getDate() { 78 | return this.date; 79 | } 80 | 81 | /** 82 | * Setter for property date. 83 | * 84 | * @param date New value of property date. 85 | */ 86 | public void setDate(LocalDate date) { 87 | this.date = date; 88 | } 89 | 90 | /** 91 | * Getter for property description. 92 | * 93 | * @return Value of property description. 94 | */ 95 | public String getDescription() { 96 | return this.description; 97 | } 98 | 99 | /** 100 | * Setter for property description. 101 | * 102 | * @param description New value of property description. 103 | */ 104 | public void setDescription(String description) { 105 | this.description = description; 106 | } 107 | 108 | /** 109 | * Getter for property pet. 110 | * 111 | * @return Value of property pet. 112 | */ 113 | public Pet getPet() { 114 | return this.pet; 115 | } 116 | 117 | /** 118 | * Setter for property pet. 119 | * 120 | * @param pet New value of property pet. 121 | */ 122 | public void setPet(Pet pet) { 123 | this.pet = pet; 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/model/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The classes in this package represent PetClinic's business layer. 3 | */ 4 | package org.springframework.samples.petclinic.model; 5 | 6 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/repository/OwnerRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.repository; 17 | 18 | import java.util.Collection; 19 | 20 | import org.springframework.data.jpa.repository.Query; 21 | import org.springframework.data.repository.Repository; 22 | import org.springframework.data.repository.query.Param; 23 | import org.springframework.samples.petclinic.model.Owner; 24 | 25 | /** 26 | * Repository class for Owner domain objects All method names are compliant with Spring Data naming 27 | * 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 28 | * 29 | * @author Ken Krebs 30 | * @author Juergen Hoeller 31 | * @author Sam Brannen 32 | * @author Michael Isvy 33 | */ 34 | public interface OwnerRepository extends Repository { 35 | 36 | /** 37 | * Retrieve {@link Owner}s from the data store by last name, returning all owners 38 | * whose last name starts with the given name. 39 | * @param lastName Value to search for 40 | * @return a Collection of matching {@link Owner}s (or an empty Collection if none 41 | * found) 42 | */ 43 | @Query("SELECT DISTINCT owner FROM Owner owner left join fetch owner.pets WHERE owner.lastName LIKE :lastName%") 44 | Collection findByLastName(@Param("lastName") String lastName); 45 | 46 | /** 47 | * Retrieve an {@link Owner} from the data store by id. 48 | * @param id the id to search for 49 | * @return the {@link Owner} if found 50 | */ 51 | @Query("SELECT owner FROM Owner owner left join fetch owner.pets WHERE owner.id =:id") 52 | Owner findById(@Param("id") int id); 53 | 54 | /** 55 | * Save an {@link Owner} to the data store, either inserting or updating it. 56 | * @param owner the {@link Owner} to save 57 | */ 58 | void save(Owner owner); 59 | 60 | 61 | } 62 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/repository/PetRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.repository; 17 | 18 | import java.util.List; 19 | 20 | import org.springframework.data.jpa.repository.Query; 21 | import org.springframework.data.repository.Repository; 22 | import org.springframework.samples.petclinic.model.Pet; 23 | import org.springframework.samples.petclinic.model.PetType; 24 | 25 | /** 26 | * Repository class for Pet domain objects All method names are compliant with Spring Data naming 27 | * 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 28 | * 29 | * @author Ken Krebs 30 | * @author Juergen Hoeller 31 | * @author Sam Brannen 32 | * @author Michael Isvy 33 | */ 34 | public interface PetRepository extends Repository { 35 | 36 | /** 37 | * Retrieve all {@link PetType}s from the data store. 38 | * @return a Collection of {@link PetType}s. 39 | */ 40 | @Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name") 41 | List findPetTypes(); 42 | 43 | /** 44 | * Retrieve a {@link Pet} from the data store by id. 45 | * @param id the id to search for 46 | * @return the {@link Pet} if found 47 | */ 48 | Pet findById(int id); 49 | 50 | /** 51 | * Save a {@link Pet} to the data store, either inserting or updating it. 52 | * @param pet the {@link Pet} to save 53 | */ 54 | void save(Pet pet); 55 | 56 | } 57 | 58 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/repository/UserRepository.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.repository; 2 | 3 | 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.samples.petclinic.model.User; 6 | 7 | import java.util.Optional; 8 | 9 | public interface UserRepository extends JpaRepository { 10 | Optional findByName(String name); 11 | } 12 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/repository/VetRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.repository; 17 | 18 | import java.util.Collection; 19 | 20 | import org.springframework.dao.DataAccessException; 21 | import org.springframework.data.repository.Repository; 22 | import org.springframework.samples.petclinic.model.Vet; 23 | 24 | /** 25 | * Repository class for Vet domain objects All method names are compliant with Spring Data naming 26 | * 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 27 | * 28 | * @author Ken Krebs 29 | * @author Juergen Hoeller 30 | * @author Sam Brannen 31 | * @author Michael Isvy 32 | */ 33 | public interface VetRepository extends Repository { 34 | 35 | /** 36 | * Retrieve all Vets from the data store. 37 | * 38 | * @return a Collection of Vets 39 | */ 40 | Collection findAll() throws DataAccessException; 41 | 42 | 43 | } 44 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/repository/VisitRepository.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.repository; 17 | 18 | import java.util.List; 19 | 20 | import org.springframework.data.jpa.repository.JpaRepository; 21 | import org.springframework.samples.petclinic.model.Visit; 22 | 23 | /** 24 | * Repository class for Visit domain objects All method names are compliant with Spring Data naming 25 | * 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 26 | * 27 | * @author Ken Krebs 28 | * @author Juergen Hoeller 29 | * @author Sam Brannen 30 | * @author Michael Isvy 31 | * @author Igor Dmitriev 32 | */ 33 | public interface VisitRepository extends JpaRepository { 34 | 35 | 36 | List findByPetId(Integer petId); 37 | 38 | } 39 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/security/AuthenticationHelper.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.security; 2 | 3 | import org.springframework.security.core.Authentication; 4 | 5 | public interface AuthenticationHelper { 6 | Authentication authenticate(Authentication authentication); 7 | } 8 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/security/JWTAuthenticationFilter.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.security; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | import org.springframework.web.filter.GenericFilterBean; 6 | 7 | import java.io.IOException; 8 | 9 | import javax.servlet.FilterChain; 10 | import javax.servlet.ServletException; 11 | import javax.servlet.ServletRequest; 12 | import javax.servlet.ServletResponse; 13 | import javax.servlet.http.HttpServletRequest; 14 | 15 | public class JWTAuthenticationFilter extends GenericFilterBean { 16 | 17 | private final TokenAuthentication tokenAuthentication; 18 | 19 | public JWTAuthenticationFilter(TokenAuthentication tokenAuthentication) { 20 | this.tokenAuthentication = tokenAuthentication; 21 | } 22 | 23 | @Override 24 | public void doFilter(ServletRequest request, 25 | ServletResponse response, 26 | FilterChain filterChain) 27 | throws IOException, ServletException { 28 | Authentication authentication = tokenAuthentication.getAuthentication((HttpServletRequest) request); 29 | 30 | SecurityContextHolder.getContext().setAuthentication(authentication); 31 | filterChain.doFilter(request, response); 32 | } 33 | } -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/security/JWTAuthenticationHelper.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.security; 2 | 3 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 4 | import org.springframework.security.core.Authentication; 5 | import org.springframework.security.core.userdetails.UserDetails; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | import org.springframework.security.crypto.password.PasswordEncoder; 8 | 9 | import java.util.ArrayList; 10 | 11 | public class JWTAuthenticationHelper implements AuthenticationHelper { 12 | private final UserDetailsService userDetailsService; 13 | private final PasswordEncoder passwordEncoder; 14 | 15 | public JWTAuthenticationHelper(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder) { 16 | this.userDetailsService = userDetailsService; 17 | this.passwordEncoder = passwordEncoder; 18 | } 19 | 20 | @Override 21 | public Authentication authenticate(Authentication authentication) { 22 | UserDetails userDetails = userDetailsService.loadUserByUsername(authentication.getName()); 23 | return authenticateUser(userDetails, authentication); 24 | } 25 | 26 | private Authentication authenticateUser(UserDetails userDetails, Authentication authentication) { 27 | if (userDetails.getUsername().equals(authentication.getName()) && 28 | passwordEncoder.matches(authentication.getCredentials().toString(), userDetails.getPassword())) { 29 | return new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(), new ArrayList<>()); 30 | } 31 | return null; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/security/JWTAuthenticationProvider.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.security; 2 | 3 | import org.springframework.security.authentication.AuthenticationProvider; 4 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 5 | import org.springframework.security.core.Authentication; 6 | 7 | public class JWTAuthenticationProvider implements AuthenticationProvider { 8 | private final AuthenticationHelper authenticationService; 9 | 10 | public JWTAuthenticationProvider(AuthenticationHelper authenticationService) { 11 | this.authenticationService = authenticationService; 12 | } 13 | 14 | @Override 15 | public Authentication authenticate(Authentication authentication) { 16 | return authenticationService.authenticate(authentication); 17 | } 18 | 19 | @Override 20 | public boolean supports(Class authentication) { 21 | return authentication.equals(UsernamePasswordAuthenticationToken.class); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/security/JWTLoginFilter.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.security; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.ObjectMapper; 5 | 6 | import org.springframework.http.HttpStatus; 7 | import org.springframework.http.MediaType; 8 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 9 | import org.springframework.security.core.Authentication; 10 | import org.springframework.security.core.AuthenticationException; 11 | import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; 12 | import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 13 | 14 | import java.io.IOException; 15 | import java.util.Collections; 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | 19 | import javax.servlet.FilterChain; 20 | import javax.servlet.http.HttpServletRequest; 21 | import javax.servlet.http.HttpServletResponse; 22 | 23 | public class JWTLoginFilter extends AbstractAuthenticationProcessingFilter { 24 | 25 | private final TokenAuthentication tokenAuthentication; 26 | private final ObjectMapper objectMapper; 27 | 28 | public JWTLoginFilter(String url, 29 | TokenAuthentication tokenAuthentication, 30 | ObjectMapper objectMapper 31 | ) { 32 | super(new AntPathRequestMatcher(url)); 33 | this.tokenAuthentication = tokenAuthentication; 34 | this.objectMapper = objectMapper; 35 | } 36 | 37 | @Override 38 | public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res) 39 | throws IOException { 40 | 41 | Map map; 42 | try { 43 | TypeReference> typeRef = new TypeReference>() { 44 | }; 45 | map = objectMapper.readValue(req.getInputStream(), typeRef); 46 | } catch (Exception e) { 47 | unsuccessfulAuthenticationResponse(res); 48 | return null; 49 | } 50 | 51 | return getAuthenticationManager().authenticate( 52 | new UsernamePasswordAuthenticationToken( 53 | map.get("username"), 54 | map.get("password"), 55 | Collections.emptyList() 56 | ) 57 | ); 58 | } 59 | 60 | @Override 61 | protected void successfulAuthentication(HttpServletRequest req, 62 | HttpServletResponse res, 63 | FilterChain chain, 64 | Authentication auth) throws IOException { 65 | tokenAuthentication.addAuthentication(res, auth.getName()); 66 | } 67 | 68 | @Override 69 | protected void unsuccessfulAuthentication(HttpServletRequest request, 70 | HttpServletResponse response, 71 | AuthenticationException failed) throws IOException { 72 | unsuccessfulAuthenticationResponse(response); 73 | } 74 | 75 | private void unsuccessfulAuthenticationResponse(HttpServletResponse response) throws IOException { 76 | response.setContentType(MediaType.APPLICATION_JSON_VALUE); 77 | response.setStatus(HttpStatus.UNAUTHORIZED.value()); 78 | response.getWriter().print(HttpStatus.UNAUTHORIZED.getReasonPhrase()); 79 | } 80 | } -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/security/TokenAuthentication.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.security; 2 | 3 | import org.springframework.http.HttpHeaders; 4 | import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; 5 | import org.springframework.security.core.Authentication; 6 | 7 | import java.io.IOException; 8 | import java.time.LocalDateTime; 9 | import java.time.ZoneOffset; 10 | import java.util.Collections; 11 | import java.util.Date; 12 | import java.util.UUID; 13 | 14 | import io.jsonwebtoken.ExpiredJwtException; 15 | import io.jsonwebtoken.Jwts; 16 | import io.jsonwebtoken.SignatureAlgorithm; 17 | import io.jsonwebtoken.SignatureException; 18 | import lombok.extern.slf4j.Slf4j; 19 | 20 | import javax.servlet.http.HttpServletRequest; 21 | import javax.servlet.http.HttpServletResponse; 22 | 23 | @Slf4j 24 | public class TokenAuthentication { 25 | 26 | private static final String SECRET = UUID.randomUUID().toString(); 27 | 28 | private final long expirationTimeSeconds; 29 | private final String tokenPrefix; 30 | 31 | public TokenAuthentication(long expirationTimeSeconds, String tokenPrefix) { 32 | this.expirationTimeSeconds = expirationTimeSeconds; 33 | this.tokenPrefix = tokenPrefix; 34 | } 35 | 36 | void addAuthentication(HttpServletResponse res, String username) throws IOException { 37 | String jwtToken = Jwts.builder() 38 | .setSubject(username) 39 | .setExpiration(Date.from(LocalDateTime.now().plusSeconds(expirationTimeSeconds).atZone(ZoneOffset.UTC).toInstant())) 40 | .signWith(SignatureAlgorithm.HS512, SECRET) 41 | .compact(); 42 | res.addHeader(HttpHeaders.AUTHORIZATION, tokenPrefix + " " + jwtToken); 43 | res.getWriter().write("{\"token\":\"" + jwtToken + "\"}"); 44 | } 45 | 46 | Authentication getAuthentication(HttpServletRequest request) { 47 | String token = request.getHeader(HttpHeaders.AUTHORIZATION); 48 | if (token == null) { 49 | return null; 50 | } 51 | 52 | try { 53 | String user = Jwts.parser() 54 | .setSigningKey(SECRET) 55 | .parseClaimsJws(token.replace(tokenPrefix, "")) 56 | .getBody() 57 | .getSubject(); 58 | 59 | return user != null ? 60 | new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList()) : 61 | null; 62 | } catch (ExpiredJwtException e) { 63 | return null; 64 | } catch (SignatureException e) { 65 | log.info("JWT signature does not match locally computed signature"); 66 | return null; 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/service/ClinicService.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.service; 17 | 18 | import java.util.Collection; 19 | 20 | import org.springframework.dao.DataAccessException; 21 | import org.springframework.samples.petclinic.model.Owner; 22 | import org.springframework.samples.petclinic.model.Pet; 23 | import org.springframework.samples.petclinic.model.PetType; 24 | import org.springframework.samples.petclinic.model.Vet; 25 | import org.springframework.samples.petclinic.model.Visit; 26 | 27 | 28 | /** 29 | * Mostly used as a facade so all controllers have a single point of entry 30 | * 31 | * @author Michael Isvy 32 | */ 33 | public interface ClinicService { 34 | 35 | Collection findPetTypes() throws DataAccessException; 36 | 37 | Owner findOwnerById(int id) throws DataAccessException; 38 | 39 | Pet findPetById(int id) throws DataAccessException; 40 | 41 | void savePet(Pet pet) throws DataAccessException; 42 | 43 | void saveVisit(Visit visit) throws DataAccessException; 44 | 45 | Collection findVets() throws DataAccessException; 46 | 47 | void saveOwner(Owner owner) throws DataAccessException; 48 | 49 | Collection findOwnerByLastName(String lastName) throws DataAccessException; 50 | 51 | Collection findVisitsByPetId(int petId); 52 | 53 | } 54 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/service/DefaultUserDetailsService.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.samples.petclinic.repository.UserRepository; 5 | import org.springframework.security.core.userdetails.User; 6 | import org.springframework.security.core.userdetails.UserDetails; 7 | import org.springframework.security.core.userdetails.UserDetailsService; 8 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 9 | import org.springframework.stereotype.Service; 10 | 11 | import java.util.Collections; 12 | 13 | @Service 14 | public class DefaultUserDetailsService implements UserDetailsService { 15 | private final UserRepository userRepository; 16 | 17 | @Autowired 18 | public DefaultUserDetailsService(UserRepository userRepository) { 19 | this.userRepository = userRepository; 20 | } 21 | 22 | @Override 23 | public UserDetails loadUserByUsername(String username) { 24 | return userRepository.findByName(username) 25 | .map(usr -> new User(usr.getName(), usr.getPassword(), Collections.emptyList())) 26 | .orElseThrow(() -> new UsernameNotFoundException("User with permissions was not found for this username: " + username)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/sqlformatter/HibernateBasicSqlFormatter.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.sqlformatter; 2 | 3 | import com.p6spy.engine.spy.appender.MessageFormattingStrategy; 4 | 5 | import org.hibernate.engine.jdbc.internal.BasicFormatterImpl; 6 | import org.hibernate.engine.jdbc.internal.Formatter; 7 | 8 | public class HibernateBasicSqlFormatter implements MessageFormattingStrategy { 9 | private final Formatter formatter = new BasicFormatterImpl(); 10 | 11 | @Override 12 | public String formatMessage(int connectionId, String now, long elapsed, String category, String prepared, String sql) { 13 | String res = !sql.isEmpty() ? sql : prepared; 14 | if (res.isEmpty()) { 15 | return ""; 16 | } 17 | String template = "Hibernate: %s %s {elapsed: %sms}"; 18 | String batch = "batch".equals(category) ? "batch operation" : ""; 19 | return String.format(template, batch, formatter.format(res), elapsed); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/util/EntityUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.springframework.samples.petclinic.util; 18 | 19 | import java.util.Collection; 20 | 21 | import org.springframework.orm.ObjectRetrievalFailureException; 22 | import org.springframework.samples.petclinic.model.BaseEntity; 23 | 24 | /** 25 | * Utility methods for handling entities. Separate from the BaseEntity class mainly because of dependency on the 26 | * ORM-associated ObjectRetrievalFailureException. 27 | * 28 | * @author Juergen Hoeller 29 | * @author Sam Brannen 30 | * @see org.springframework.samples.petclinic.model.BaseEntity 31 | * @since 29.10.2003 32 | */ 33 | public abstract class EntityUtils { 34 | 35 | private EntityUtils() { 36 | 37 | } 38 | 39 | /** 40 | * Look up the entity of the given class with the given id in the given collection. 41 | * 42 | * @param entities the collection to search 43 | * @param entityClass the entity class to look up 44 | * @param entityId the entity id to look up 45 | * @return the found entity 46 | * @throws ObjectRetrievalFailureException if the entity was not found 47 | */ 48 | public static T getById(Collection entities, Class entityClass, int entityId) 49 | throws ObjectRetrievalFailureException { 50 | for (T entity : entities) { 51 | if (entity.getId() == entityId && entityClass.isInstance(entity)) { 52 | return entity; 53 | } 54 | } 55 | throw new ObjectRetrievalFailureException(entityClass, entityId); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/CrashController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.web; 17 | 18 | import org.springframework.stereotype.Controller; 19 | import org.springframework.web.bind.annotation.GetMapping; 20 | 21 | /** 22 | * Controller used to showcase what happens when an exception is thrown 23 | * 24 | * @author Michael Isvy 25 | *

26 | * Also see how the bean of type 'SimpleMappingExceptionResolver' has been declared inside 27 | * /WEB-INF/mvc-core-config.xml 28 | */ 29 | @Controller 30 | public class CrashController { 31 | 32 | @GetMapping("/oups") 33 | public String triggerException() { 34 | throw new RuntimeException("Expected: controller used to showcase what " + 35 | "happens when an exception is thrown"); 36 | } 37 | 38 | 39 | } 40 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/IndexController.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class IndexController { 8 | @GetMapping("/") 9 | public String index() { 10 | return "index.html"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/PetTypeFormatter.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.web; 17 | 18 | 19 | import java.text.ParseException; 20 | import java.util.Collection; 21 | import java.util.Locale; 22 | 23 | import org.springframework.beans.factory.annotation.Autowired; 24 | import org.springframework.format.Formatter; 25 | import org.springframework.samples.petclinic.model.PetType; 26 | import org.springframework.samples.petclinic.service.ClinicService; 27 | import org.springframework.stereotype.Component; 28 | 29 | /** 30 | * Instructs Spring MVC on how to parse and print elements of type 'PetType'. Starting from Spring 3.0, Formatters have 31 | * come as an improvement in comparison to legacy PropertyEditors. See the following links for more details: - The 32 | * Spring ref doc: http://static.springsource.org/spring/docs/current/spring-framework-reference/html/validation.html#format-Formatter-SPI 33 | * - A nice blog entry from Gordon Dickens: http://gordondickens.com/wordpress/2010/09/30/using-spring-3-0-custom-type-converter/ 34 | *

35 | * Also see how the bean 'conversionService' has been declared inside /WEB-INF/mvc-core-config.xml 36 | * 37 | * @author Mark Fisher 38 | * @author Juergen Hoeller 39 | * @author Michael Isvy 40 | */ 41 | @Component 42 | public class PetTypeFormatter implements Formatter { 43 | 44 | private final ClinicService clinicService; 45 | 46 | 47 | @Autowired 48 | public PetTypeFormatter(ClinicService clinicService) { 49 | this.clinicService = clinicService; 50 | } 51 | 52 | @Override 53 | public String print(PetType petType, Locale locale) { 54 | return petType.getName(); 55 | } 56 | 57 | @Override 58 | public PetType parse(String text, Locale locale) throws ParseException { 59 | Collection findPetTypes = this.clinicService.findPetTypes(); 60 | for (PetType type : findPetTypes) { 61 | if (type.getName().equals(text)) { 62 | return type; 63 | } 64 | } 65 | throw new ParseException("type not found: " + text, 0); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/PetValidator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.web; 17 | 18 | import org.springframework.samples.petclinic.model.Pet; 19 | import org.springframework.util.StringUtils; 20 | import org.springframework.validation.Errors; 21 | import org.springframework.validation.Validator; 22 | 23 | /** 24 | * Validator for Pet forms. 25 | *

26 | * We're not using Bean Validation annotations here because it is easier to define such validation rule in Java. 27 | *

28 | * 29 | * @author Ken Krebs 30 | * @author Juergen Hoeller 31 | */ 32 | public class PetValidator implements Validator { 33 | 34 | private static final String REQUIRED = "required"; 35 | 36 | @Override 37 | public void validate(Object obj, Errors errors) { 38 | Pet pet = (Pet) obj; 39 | String name = pet.getName(); 40 | // name validation 41 | if (!StringUtils.hasLength(name)) { 42 | errors.rejectValue("name", REQUIRED, REQUIRED); 43 | } 44 | 45 | // type validation 46 | if (pet.isNew() && pet.getType() == null) { 47 | errors.rejectValue("type", REQUIRED, REQUIRED); 48 | } 49 | 50 | // birth date validation 51 | if (pet.getBirthDate() == null) { 52 | errors.rejectValue("birthDate", REQUIRED, REQUIRED); 53 | } 54 | } 55 | 56 | /** 57 | * This Validator validates *just* Pet instances 58 | */ 59 | @Override 60 | public boolean supports(Class clazz) { 61 | return Pet.class.isAssignableFrom(clazz); 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/TestController.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web; 2 | 3 | import org.springframework.context.annotation.Conditional; 4 | import org.springframework.samples.petclinic.condition.NonProdCondition; 5 | import org.springframework.stereotype.Controller; 6 | import org.springframework.web.bind.annotation.GetMapping; 7 | 8 | @Controller 9 | //@Profile("!prod") https://stackoverflow.com/questions/25427684/using-profile-in-spring-boot 10 | @Conditional(NonProdCondition.class) 11 | public class TestController { 12 | 13 | @GetMapping("/test") 14 | public String test() { 15 | return "test.html"; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/VetController.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.web; 17 | 18 | import java.util.Map; 19 | 20 | import org.springframework.beans.factory.annotation.Autowired; 21 | import org.springframework.http.MediaType; 22 | import org.springframework.samples.petclinic.model.Vets; 23 | import org.springframework.samples.petclinic.service.ClinicService; 24 | import org.springframework.stereotype.Controller; 25 | import org.springframework.web.bind.annotation.RequestMapping; 26 | import org.springframework.web.bind.annotation.ResponseBody; 27 | 28 | /** 29 | * @author Juergen Hoeller 30 | * @author Mark Fisher 31 | * @author Ken Krebs 32 | * @author Arjen Poutsma 33 | */ 34 | @Controller 35 | public class VetController { 36 | 37 | private final ClinicService clinicService; 38 | 39 | 40 | @Autowired 41 | public VetController(ClinicService clinicService) { 42 | this.clinicService = clinicService; 43 | } 44 | 45 | @RequestMapping(value = {"/vets.html"}) 46 | public String showVetList(Map model) { 47 | // Here we are returning an object of type 'Vets' rather than a collection of Vet objects 48 | // so it is simpler for Object-Xml mapping 49 | Vets vets = new Vets(); 50 | vets.getVetList().addAll(this.clinicService.findVets()); 51 | model.put("vets", vets); 52 | return "vets/vetList"; 53 | } 54 | 55 | @RequestMapping(value = {"/vets.json", "/vets.xml"}) 56 | public 57 | @ResponseBody 58 | Vets showResourcesVetList() { 59 | // Here we are returning an object of type 'Vets' rather than a collection of Vet objects 60 | // so it is simpler for JSon/Object mapping 61 | Vets vets = new Vets(); 62 | vets.getVetList().addAll(this.clinicService.findVets()); 63 | return vets; 64 | } 65 | 66 | 67 | } 68 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/WelcomeController.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web; 2 | 3 | 4 | import org.springframework.stereotype.Controller; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | 7 | @Controller 8 | public class WelcomeController { 9 | 10 | @RequestMapping("/") 11 | public String welcome() { 12 | return "welcome"; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/api/AbstractResourceController.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web.api; 2 | 3 | import org.springframework.web.bind.annotation.CrossOrigin; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | 6 | @RequestMapping("/api") 7 | @CrossOrigin 8 | public class AbstractResourceController { 9 | 10 | } 11 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/api/BadRequestException.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web.api; 2 | 3 | public class BadRequestException extends RuntimeException { 4 | 5 | /** 6 | * 7 | */ 8 | private static final long serialVersionUID = 1L; 9 | 10 | public BadRequestException(String message) { 11 | super(message); 12 | } 13 | 14 | 15 | 16 | } 17 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/api/ErrorResource.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web.api; 2 | import java.util.LinkedHashMap; 3 | import java.util.LinkedList; 4 | import java.util.List; 5 | import java.util.Map; 6 | 7 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 8 | /** 9 | * Credits to: Willie Wheeler (http://springinpractice.com/2013/10/09/generating-json-error-object-responses-with-spring-web-mvc) 10 | * 11 | * @author Willie Wheeler (@williewheeler) 12 | * @author Nils Hartmann 13 | */ 14 | @JsonIgnoreProperties(ignoreUnknown = true) 15 | public class ErrorResource { 16 | private String code; 17 | private String message; 18 | private List globalErrors; 19 | private Map fieldErrors; 20 | 21 | public ErrorResource() { } 22 | 23 | public ErrorResource(String code, String message) { 24 | this.code = code; 25 | this.message = message; 26 | } 27 | 28 | public String getCode() { return code; } 29 | 30 | public void setCode(String code) { this.code = code; } 31 | 32 | public String getMessage() { return message; } 33 | 34 | public void setMessage(String message) { this.message = message; } 35 | 36 | public Map getFieldErrors() { return fieldErrors; } 37 | 38 | public void setFieldErrors(List fieldErrors) { 39 | this.fieldErrors = new LinkedHashMap<>(); 40 | for (FieldErrorResource fieldErrorResource : fieldErrors) { 41 | this.fieldErrors.put(fieldErrorResource.getField(), fieldErrorResource); 42 | } 43 | } 44 | 45 | public void addGlobalError(String message) { 46 | if (globalErrors == null) { 47 | globalErrors = new LinkedList(); 48 | } 49 | 50 | globalErrors.add(message); 51 | } 52 | } -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/api/FailingApiController.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web.api; 2 | 3 | import org.springframework.samples.petclinic.web.CrashController; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.ResponseBody; 6 | import org.springframework.web.bind.annotation.RestController; 7 | 8 | /** 9 | * Controller used to showcase what happens when an exception is thrown 10 | * 11 | * @author NilsHartmann 12 | * @see CrashController 13 | */ 14 | @RestController 15 | public class FailingApiController extends AbstractResourceController { 16 | 17 | @GetMapping("/oups") 18 | @ResponseBody 19 | String failingRequest() { 20 | throw new SuperFatalErrorException("Expected: controller used to showcase what " + 21 | "happens when an exception is thrown"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/api/FieldErrorResource.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | 5 | /** 6 | * Credits to: Willie Wheeler (http://springinpractice.com/2013/10/09/generating-json-error-object-responses-with-spring-web-mvc) 7 | * 8 | * @author Willie Wheeler (@williewheeler) 9 | */ 10 | @JsonIgnoreProperties(ignoreUnknown = true) 11 | public class FieldErrorResource { 12 | private String resource; 13 | private String field; 14 | private String code; 15 | private String message; 16 | 17 | public String getResource() { 18 | return resource; 19 | } 20 | 21 | public void setResource(String resource) { 22 | this.resource = resource; 23 | } 24 | 25 | public String getField() { 26 | return field; 27 | } 28 | 29 | public void setField(String field) { 30 | this.field = field; 31 | } 32 | 33 | public String getCode() { 34 | return code; 35 | } 36 | 37 | public void setCode(String code) { 38 | this.code = code; 39 | } 40 | 41 | public String getMessage() { 42 | return message; 43 | } 44 | 45 | public void setMessage(String message) { 46 | this.message = message; 47 | } 48 | } -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/api/InvalidRequestException.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web.api; 2 | 3 | import org.springframework.validation.Errors; 4 | 5 | /** 6 | * Credits to: Willie Wheeler (http://springinpractice.com/2013/10/09/generating-json-error-object-responses-with-spring-web-mvc) 7 | * 8 | * @author Willie Wheeler (@williewheeler) 9 | */ 10 | public class InvalidRequestException extends RuntimeException { 11 | /** 12 | * 13 | */ 14 | private static final long serialVersionUID = 1L; 15 | private Errors errors; 16 | 17 | public InvalidRequestException(String message, Errors errors) { 18 | super(message); 19 | this.errors = errors; 20 | } 21 | 22 | public Errors getErrors() { 23 | return errors; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/api/PetRequest.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web.api; 2 | 3 | import com.fasterxml.jackson.annotation.JsonFormat; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | import java.time.LocalDate; 7 | 8 | import javax.validation.constraints.NotNull; 9 | import javax.validation.constraints.Size; 10 | 11 | public class PetRequest { 12 | private Integer id; 13 | @JsonFormat(pattern = "yyyy/MM/dd") 14 | @NotNull 15 | private LocalDate birthDate; 16 | @Size(min = 2, max = 14 ) 17 | private String name; 18 | 19 | Integer typeId; 20 | 21 | 22 | public LocalDate getBirthDate() { 23 | return birthDate; 24 | } 25 | 26 | public void setBirthDate(LocalDate birthDate) { 27 | this.birthDate = birthDate; 28 | } 29 | 30 | public String getName() { 31 | return name; 32 | } 33 | 34 | public void setName(String name) { 35 | this.name = name; 36 | } 37 | 38 | public int getTypeId() { 39 | return typeId; 40 | } 41 | 42 | public void setTypeId(int typeId) { 43 | this.typeId = typeId; 44 | } 45 | 46 | public Integer getId() { 47 | return id; 48 | } 49 | 50 | public void setId(Integer id) { 51 | this.id = id; 52 | } 53 | 54 | @JsonProperty("isNew") 55 | public boolean isNew() { 56 | return this.id == null; 57 | } 58 | 59 | @Override 60 | public String toString() { 61 | return "PetRequest [id=" + id + ", birthDate=" + birthDate + ", name=" + name + ", typeId=" + typeId + "]"; 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/api/SuperFatalErrorException.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web.api; 2 | 3 | public class SuperFatalErrorException extends RuntimeException { 4 | 5 | /** 6 | * 7 | */ 8 | private static final long serialVersionUID = 1L; 9 | 10 | public SuperFatalErrorException(String message) { 11 | super(message); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/api/VetResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.web.api; 17 | 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.samples.petclinic.model.Vet; 20 | import org.springframework.samples.petclinic.service.ClinicService; 21 | import org.springframework.web.bind.annotation.GetMapping; 22 | import org.springframework.web.bind.annotation.RestController; 23 | 24 | import java.util.Collection; 25 | 26 | /** 27 | */ 28 | @RestController 29 | public class VetResource extends AbstractResourceController { 30 | 31 | private final ClinicService clinicService; 32 | 33 | @Autowired 34 | public VetResource(ClinicService clinicService) { 35 | this.clinicService = clinicService; 36 | } 37 | 38 | @GetMapping(value = "/vets") 39 | public Collection showResourcesVetList() { 40 | return this.clinicService.findVets(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/api/VisitResource.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2002-2013 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.springframework.samples.petclinic.web.api; 17 | 18 | import org.springframework.beans.factory.annotation.Autowired; 19 | import org.springframework.http.HttpStatus; 20 | import org.springframework.samples.petclinic.model.Pet; 21 | import org.springframework.samples.petclinic.model.Visit; 22 | import org.springframework.samples.petclinic.service.ClinicService; 23 | import org.springframework.validation.BindingResult; 24 | import org.springframework.web.bind.annotation.PathVariable; 25 | import org.springframework.web.bind.annotation.PostMapping; 26 | import org.springframework.web.bind.annotation.RequestBody; 27 | import org.springframework.web.bind.annotation.ResponseStatus; 28 | import org.springframework.web.bind.annotation.RestController; 29 | 30 | import javax.validation.Valid; 31 | 32 | /** 33 | * @author Juergen Hoeller 34 | * @author Ken Krebs 35 | * @author Arjen Poutsma 36 | * @author Michael Isvy 37 | */ 38 | @RestController 39 | public class VisitResource extends AbstractResourceController { 40 | 41 | private final ClinicService clinicService; 42 | 43 | @Autowired 44 | public VisitResource(ClinicService clinicService) { 45 | this.clinicService = clinicService; 46 | } 47 | 48 | @PostMapping("/owners/{ownerId}/pets/{petId}/visits") 49 | @ResponseStatus(HttpStatus.NO_CONTENT) 50 | public void create(@PathVariable("petId") int petId, @Valid @RequestBody Visit visit, BindingResult bindingResult) { 51 | if (bindingResult.hasErrors()) { 52 | throw new InvalidRequestException("Visit is invalid", bindingResult); 53 | } 54 | 55 | final Pet pet = clinicService.findPetById(petId); 56 | if (pet == null) { 57 | throw new BadRequestException("Pet with Id '" + petId + "' is unknown."); 58 | } 59 | 60 | pet.addVisit(visit); 61 | 62 | clinicService.saveVisit(visit); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /application/src/main/java/org/springframework/samples/petclinic/web/package-info.java: -------------------------------------------------------------------------------- 1 | /** 2 | * The classes in this package represent PetClinic's web presentation layer. 3 | */ 4 | package org.springframework.samples.petclinic.web; 5 | 6 | -------------------------------------------------------------------------------- /application/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | url: jdbc:p6spy:postgresql://localhost:5432/petclinic 4 | username: petclinic 5 | password: q9KqUiu2vqnAuf 6 | driverClassName: com.p6spy.engine.spy.P6SpyDriver 7 | 8 | data: 9 | jpa: 10 | repositories: 11 | bootstrap-mode: deferred 12 | 13 | jpa: 14 | database-platform: org.hibernate.dialect.PostgreSQL9Dialect 15 | hibernate: 16 | ddl-auto: none 17 | properties: 18 | hibernate: 19 | default_schema: public 20 | show_sql: false 21 | use_sql_comments: true 22 | format_sql: true 23 | jdbc: 24 | time_zone: "UTC" 25 | lob: 26 | non_contextual_creation: true 27 | open-in-view: false 28 | flyway: 29 | locations: ["classpath:db/migration/ddl", "classpath:db/migration/dml"] 30 | 31 | security: 32 | custom: 33 | login-url: "/login" 34 | excluded-api: "/actuator/**, /, /login, /test, /css/**, /fonts/**, /img/**, /js/**, /favicon.ico" 35 | token: 36 | expiration-time-seconds: 600 37 | token-prefix: "Bearer" -------------------------------------------------------------------------------- /application/src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | |\ _,,,--,,_ 4 | /,`.-'`' ._ \-;;,_ 5 | _______ __|,4- ) )_ .;.(__`'-'__ ___ __ _ ___ _______ 6 | | | '---''(_/._)-'(_\_) | | | | | | | | | 7 | | _ | ___|_ _| | | | | |_| | | | __ _ _ 8 | | |_| | |___ | | | | | | | | | | \ \ \ \ 9 | | ___| ___| | | | _| |___| | _ | | _| \ \ \ \ 10 | | | | |___ | | | |_| | | | | | | |_ ) ) ) ) 11 | |___| |_______| |___| |_______|_______|___|_| |__|___|_______| / / / / 12 | ==================================================================/_/_/_/ 13 | 14 | :: Built with Spring Boot :: ${spring-boot.version} 15 | 16 | -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/ddl/V2019_01_19_15_12__users.sql: -------------------------------------------------------------------------------- 1 | CREATE SEQUENCE IF NOT EXISTS users_seq 2 | START WITH 1 3 | INCREMENT BY 1; 4 | 5 | CREATE TABLE IF NOT EXISTS users ( 6 | id INT DEFAULT nextval('users_seq') PRIMARY KEY, 7 | name VARCHAR(100) NOT NULL, 8 | password VARCHAR(100) NOT NULL 9 | ); -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/ddl/V2019_01_19_15_13__specialties.sql: -------------------------------------------------------------------------------- 1 | CREATE SEQUENCE IF NOT EXISTS specialties_seq 2 | START WITH 1 3 | INCREMENT BY 1; 4 | 5 | CREATE TABLE IF NOT EXISTS specialties ( 6 | id INT DEFAULT nextval('specialties_seq') PRIMARY KEY, 7 | name VARCHAR(80) 8 | ); -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/ddl/V2019_01_19_15_14__owners.sql: -------------------------------------------------------------------------------- 1 | CREATE SEQUENCE IF NOT EXISTS pet_owners_seq 2 | START WITH 1 3 | INCREMENT BY 1; 4 | 5 | CREATE TABLE IF NOT EXISTS pet_owners ( 6 | id INT DEFAULT nextval('pet_owners_seq') PRIMARY KEY, 7 | first_name VARCHAR(32), 8 | last_name VARCHAR(32), 9 | address VARCHAR(255), 10 | city VARCHAR(80), 11 | telephone VARCHAR(20) 12 | ); -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/ddl/V2019_01_19_15_15__types.sql: -------------------------------------------------------------------------------- 1 | CREATE SEQUENCE IF NOT EXISTS types_seq 2 | START WITH 1 3 | INCREMENT BY 1; 4 | 5 | CREATE TABLE IF NOT EXISTS types ( 6 | id INT DEFAULT nextval('types_seq') PRIMARY KEY, 7 | name VARCHAR(80) 8 | ); -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/ddl/V2019_01_19_15_16__vets.sql: -------------------------------------------------------------------------------- 1 | CREATE SEQUENCE IF NOT EXISTS vets_seq 2 | START WITH 1 3 | INCREMENT BY 1; 4 | 5 | CREATE TABLE IF NOT EXISTS vets ( 6 | id INT DEFAULT nextval('vets_seq') PRIMARY KEY, 7 | first_name VARCHAR(32), 8 | last_name VARCHAR(32) 9 | ); -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/ddl/V2019_01_19_15_17__pets.sql: -------------------------------------------------------------------------------- 1 | CREATE SEQUENCE IF NOT EXISTS pets_seq 2 | START WITH 1 3 | INCREMENT BY 1; 4 | 5 | CREATE TABLE IF NOT EXISTS pets ( 6 | id INT DEFAULT nextval('pets_seq') PRIMARY KEY, 7 | name VARCHAR(32), 8 | birth_date DATE, 9 | type_id INT NOT NULL, 10 | owner_id INT NOT NULL , 11 | CONSTRAINT pet_owner_fk 12 | FOREIGN KEY (owner_id) REFERENCES pet_owners, 13 | CONSTRAINT pet_type_fk 14 | FOREIGN KEY (type_id) REFERENCES types 15 | ); -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/ddl/V2019_01_19_15_18__visits.sql: -------------------------------------------------------------------------------- 1 | CREATE SEQUENCE IF NOT EXISTS visits_seq 2 | START WITH 1 3 | INCREMENT BY 1; 4 | 5 | CREATE TABLE IF NOT EXISTS visits ( 6 | id INT DEFAULT nextval('visits_seq') PRIMARY KEY, 7 | pet_id INT, 8 | visit_date DATE, 9 | description VARCHAR(255), 10 | CONSTRAINT visit_pet_fk 11 | FOREIGN KEY (pet_id) REFERENCES pets 12 | ); -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/ddl/V2019_01_19_15_19__vet_specialties.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE vet_specialties ( 2 | vet_id INT NOT NULL, 3 | specialty_id INT NOT NULL, 4 | FOREIGN KEY (vet_id) REFERENCES vets (id), 5 | FOREIGN KEY (specialty_id) REFERENCES specialties (id), 6 | UNIQUE (vet_id,specialty_id) 7 | ); -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/dml/V2019_01_19_15_20__vets.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO vets(first_name, last_name) VALUES ('James', 'Carter'); 2 | INSERT INTO vets(first_name, last_name) VALUES ('Helen', 'Leary'); 3 | INSERT INTO vets(first_name, last_name) VALUES ('Linda', 'Douglas'); 4 | INSERT INTO vets(first_name, last_name) VALUES ('Rafael', 'Ortega'); 5 | INSERT INTO vets(first_name, last_name) VALUES ('Henry', 'Stevens'); 6 | INSERT INTO vets(first_name, last_name) VALUES ('Sharon', 'Jenkins'); 7 | -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/dml/V2019_01_19_15_21__pet_owners.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO pet_owners(first_name, last_name, address, city, telephone) VALUES ('George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023'); 2 | INSERT INTO pet_owners(first_name, last_name, address, city, telephone) VALUES ('Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749'); 3 | INSERT INTO pet_owners(first_name, last_name, address, city, telephone) VALUES ('Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763'); 4 | INSERT INTO pet_owners(first_name, last_name, address, city, telephone) VALUES ('Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198'); 5 | INSERT INTO pet_owners(first_name, last_name, address, city, telephone) VALUES ('Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765'); 6 | INSERT INTO pet_owners(first_name, last_name, address, city, telephone) VALUES ('Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654'); 7 | INSERT INTO pet_owners(first_name, last_name, address, city, telephone) VALUES ('Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387'); 8 | INSERT INTO pet_owners(first_name, last_name, address, city, telephone) VALUES ('Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683'); 9 | INSERT INTO pet_owners(first_name, last_name, address, city, telephone) VALUES ('David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435'); 10 | INSERT INTO pet_owners(first_name, last_name, address, city, telephone) VALUES ('Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487'); -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/dml/V2019_01_19_15_22__specialties.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO specialties(name) VALUES ('radiology'); 2 | INSERT INTO specialties(name) VALUES ('surgery'); 3 | INSERT INTO specialties(name) VALUES ('dentistry'); 4 | -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/dml/V2019_01_19_15_23__types.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO types(name) VALUES ('cat'); 2 | INSERT INTO types(name) VALUES ('dog'); 3 | INSERT INTO types(name) VALUES ('lizard'); 4 | INSERT INTO types(name) VALUES ('snake'); 5 | INSERT INTO types(name) VALUES ('bird'); 6 | INSERT INTO types(name) VALUES ('hamster'); 7 | -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/dml/V2019_01_19_15_24__vet_specialties.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO vet_specialties(vet_id, specialty_id) VALUES (2, 1); 2 | INSERT INTO vet_specialties(vet_id, specialty_id) VALUES (3, 2); 3 | INSERT INTO vet_specialties(vet_id, specialty_id) VALUES (3, 3); 4 | INSERT INTO vet_specialties(vet_id, specialty_id) VALUES (4, 2); 5 | INSERT INTO vet_specialties(vet_id, specialty_id) VALUES (5, 1); -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/dml/V2019_01_19_15_25__pets.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('Leo', '2010-09-07', 1, 1); 2 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('Basil', '2012-08-06', 6, 2); 3 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('Rosy', '2011-04-17', 2, 3); 4 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('Jewel', '2010-03-07', 2, 3); 5 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('Iggy', '2010-11-30', 3, 4); 6 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('George', '2010-01-20', 4, 5); 7 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('Samantha', '2012-09-04', 1, 6); 8 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('Max', '2012-09-04', 1, 6); 9 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('Lucky', '2011-08-06', 5, 7); 10 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('Mulligan', '2007-02-24', 2, 8); 11 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('Freddy', '2010-03-09', 5, 9); 12 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('Lucky', '2010-06-24', 2, 10); 13 | INSERT INTO pets(name, birth_date, type_id, owner_id) VALUES ('Sly', '2012-06-08', 1, 10); -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/dml/V2019_01_19_15_26__visits.sql: -------------------------------------------------------------------------------- 1 | INSERT INTO visits(pet_id, visit_date, description) VALUES (7, '2013-01-01', 'rabies shot'); 2 | INSERT INTO visits(pet_id, visit_date, description) VALUES (8, '2013-01-02', 'rabies shot'); 3 | INSERT INTO visits(pet_id, visit_date, description) VALUES (8, '2013-01-03', 'neutered'); 4 | INSERT INTO visits(pet_id, visit_date, description) VALUES (7, '2013-01-04', 'spayed'); -------------------------------------------------------------------------------- /application/src/main/resources/db/migration/dml/V2019_01_19_15_27__users.sql: -------------------------------------------------------------------------------- 1 | /* username = test, password = testovich */ 2 | INSERT INTO users (name, password) VALUES ('test', '$2a$10$1DpgNZQZksMz1X/N9nr3N.b9VPLvJGfA98znA0H.B0P0OkQubAMn2'); 3 | -------------------------------------------------------------------------------- /application/src/main/resources/logback-spring.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | %date{HH:mm:ss} %-5level [%thread] - [%logger{0}]- %msg%n 9 | 10 | 11 | 12 | 13 | ${LOG_HOME}/${LOG_FILE_NAME} 14 | 15 | 16 | %date [%-5level] [%thread] - [%logger] - %msg%n 17 | 18 | 19 | 21 | ${LOG_HOME}/${LOG_FILE_NAME}.%i.log.zip 22 | 1 23 | 100 24 | 25 | 26 | 28 | 1000MB 29 | 30 | 31 | 32 | 33 | 512 34 | 0 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /application/src/main/resources/messages/messages.properties: -------------------------------------------------------------------------------- 1 | welcome=Welcome 2 | required=is required 3 | notFound=has not been found 4 | duplicate=is already in use 5 | nonNumeric=must be all numeric 6 | duplicateFormSubmission=Duplicate form submission is not allowed 7 | typeMismatch.date=invalid date 8 | typeMismatch.birthDate=invalid date 9 | -------------------------------------------------------------------------------- /application/src/main/resources/messages/messages_de.properties: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/application/src/main/resources/messages/messages_de.properties -------------------------------------------------------------------------------- /application/src/main/resources/messages/messages_en.properties: -------------------------------------------------------------------------------- 1 | # This file is intentionally empty. Message look-ups will fall back to the default "messages.properties" file. -------------------------------------------------------------------------------- /application/src/main/resources/spy.properties: -------------------------------------------------------------------------------- 1 | driverlist=org.postgresql.Driver 2 | appender=com.p6spy.engine.spy.appender.StdoutLogger 3 | logMessageFormat=org.springframework.samples.petclinic.sqlformatter.HibernateBasicSqlFormatter 4 | excludecategories=info,debug,result,resultset 5 | databaseDialectDateFormat=YYYY-MM-dd HH:mm:ss 6 | -------------------------------------------------------------------------------- /application/src/test/java/org/springframework/samples/petclinic/model/ValidatorTests.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.model; 2 | 3 | import static org.assertj.core.api.Assertions.assertThat; 4 | 5 | import java.util.Locale; 6 | import java.util.Set; 7 | 8 | import javax.validation.ConstraintViolation; 9 | import javax.validation.Validator; 10 | 11 | import org.junit.jupiter.api.Test; 12 | import org.springframework.context.i18n.LocaleContextHolder; 13 | import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean; 14 | 15 | /** 16 | * @author Michael Isvy 17 | * Simple test to make sure that Bean Validation is working 18 | * (useful when upgrading to a new version of Hibernate Validator/ Bean Validation) 19 | */ 20 | public class ValidatorTests { 21 | 22 | private Validator createValidator() { 23 | LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean(); 24 | localValidatorFactoryBean.afterPropertiesSet(); 25 | return localValidatorFactoryBean; 26 | } 27 | 28 | @Test 29 | public void shouldNotValidateWhenFirstNameEmpty() { 30 | 31 | LocaleContextHolder.setLocale(Locale.ENGLISH); 32 | Person person = new Person(); 33 | person.setFirstName(""); 34 | person.setLastName("smith"); 35 | 36 | Validator validator = createValidator(); 37 | Set> constraintViolations = validator.validate(person); 38 | 39 | assertThat(constraintViolations.size()).isEqualTo(1); 40 | ConstraintViolation violation = constraintViolations.iterator().next(); 41 | assertThat(violation.getPropertyPath().toString()).isEqualTo("firstName"); 42 | assertThat(violation.getMessage()).isEqualTo("must not be empty"); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /application/src/test/java/org/springframework/samples/petclinic/web/CrashControllerTests.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web; 2 | 3 | import org.junit.Before; 4 | import org.junit.jupiter.api.Disabled; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.context.SpringBootTest; 10 | import org.springframework.samples.petclinic.PetClinicApplication; 11 | import org.springframework.test.context.junit.jupiter.SpringExtension; 12 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 13 | import org.springframework.test.context.web.WebAppConfiguration; 14 | import org.springframework.test.web.servlet.MockMvc; 15 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 16 | 17 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 18 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 19 | 20 | /** 21 | * Test class for {@link CrashController} 22 | * 23 | * @author Colin But 24 | */ 25 | @ExtendWith(SpringExtension.class) 26 | @SpringBootTest(classes = PetClinicApplication.class) 27 | @WebAppConfiguration 28 | // Waiting https://github.com/spring-projects/spring-boot/issues/5574 29 | @Disabled 30 | public class CrashControllerTests { 31 | 32 | @Autowired 33 | private CrashController crashController; 34 | 35 | private MockMvc mockMvc; 36 | 37 | @Before 38 | public void setup() { 39 | this.mockMvc = MockMvcBuilders 40 | .standaloneSetup(crashController) 41 | //.setHandlerExceptionResolvers(new SimpleMappingExceptionResolver()) 42 | .build(); 43 | } 44 | 45 | @Test 46 | public void testTriggerException() throws Exception { 47 | mockMvc.perform(get("/oups")) 48 | .andExpect(view().name("exception")) 49 | .andExpect(model().attributeExists("exception")) 50 | .andExpect(forwardedUrl("exception")) 51 | .andExpect(status().isOk()); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /application/src/test/java/org/springframework/samples/petclinic/web/PetTypeFormatterTests.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web; 2 | 3 | import org.junit.Before; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.junit.runner.RunWith; 8 | import org.mockito.Mock; 9 | import org.mockito.Mockito; 10 | import org.mockito.junit.jupiter.MockitoExtension; 11 | import org.mockito.runners.MockitoJUnitRunner; 12 | import org.springframework.samples.petclinic.model.PetType; 13 | import org.springframework.samples.petclinic.service.ClinicService; 14 | 15 | import java.text.ParseException; 16 | import java.util.ArrayList; 17 | import java.util.Collection; 18 | import java.util.Locale; 19 | 20 | import static org.junit.Assert.assertEquals; 21 | 22 | /** 23 | * Test class for {@link PetTypeFormatter} 24 | * 25 | * @author Colin But 26 | */ 27 | @ExtendWith(MockitoExtension.class) 28 | public class PetTypeFormatterTests { 29 | 30 | @Mock 31 | private ClinicService clinicService; 32 | 33 | private PetTypeFormatter petTypeFormatter; 34 | 35 | @BeforeEach 36 | public void setup() { 37 | petTypeFormatter = new PetTypeFormatter(clinicService); 38 | } 39 | 40 | @Test 41 | public void testPrint() { 42 | PetType petType = new PetType(); 43 | petType.setName("Hamster"); 44 | String petTypeName = petTypeFormatter.print(petType, Locale.ENGLISH); 45 | assertEquals("Hamster", petTypeName); 46 | } 47 | 48 | @Test 49 | public void shouldParse() throws ParseException { 50 | Mockito.when(clinicService.findPetTypes()).thenReturn(makePetTypes()); 51 | PetType petType = petTypeFormatter.parse("Bird", Locale.ENGLISH); 52 | assertEquals("Bird", petType.getName()); 53 | } 54 | 55 | /*@Test(expected = ParseException.class) 56 | public void shouldThrowParseException() throws ParseException { 57 | Mockito.when(clinicService.findPetTypes()).thenReturn(makePetTypes()); 58 | petTypeFormatter.parse("Fish", Locale.ENGLISH); 59 | }*/ 60 | 61 | /** 62 | * Helper method to produce some sample pet types just for test purpose 63 | * 64 | * @return {@link Collection} of {@link PetType} 65 | */ 66 | private Collection makePetTypes() { 67 | Collection petTypes = new ArrayList<>(); 68 | petTypes.add(new PetType() { 69 | { 70 | setName("Dog"); 71 | } 72 | }); 73 | petTypes.add(new PetType() { 74 | { 75 | setName("Bird"); 76 | } 77 | }); 78 | return petTypes; 79 | } 80 | 81 | } 82 | -------------------------------------------------------------------------------- /application/src/test/java/org/springframework/samples/petclinic/web/VetControllerTests.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web; 2 | 3 | import org.assertj.core.util.Lists; 4 | import org.junit.Before; 5 | import org.junit.jupiter.api.BeforeEach; 6 | import org.junit.jupiter.api.Test; 7 | import org.junit.jupiter.api.extension.ExtendWith; 8 | import org.junit.runner.RunWith; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 11 | import org.springframework.boot.test.mock.mockito.MockBean; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.samples.petclinic.model.Specialty; 14 | import org.springframework.samples.petclinic.model.Vet; 15 | import org.springframework.samples.petclinic.service.ClinicService; 16 | import org.springframework.security.core.userdetails.UserDetailsService; 17 | import org.springframework.test.context.junit.jupiter.SpringExtension; 18 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 19 | import org.springframework.test.context.junit4.SpringRunner; 20 | import org.springframework.test.web.servlet.MockMvc; 21 | import org.springframework.test.web.servlet.ResultActions; 22 | 23 | import static org.hamcrest.xml.HasXPath.hasXPath; 24 | import static org.mockito.BDDMockito.given; 25 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 26 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 27 | 28 | /** 29 | * Test class for the {@link VetController} 30 | */ 31 | @WebMvcTest(VetController.class) 32 | @MockBean(UserDetailsService.class) 33 | @ExtendWith(SpringExtension.class) 34 | public class VetControllerTests { 35 | 36 | @Autowired 37 | private MockMvc mockMvc; 38 | 39 | @MockBean 40 | private ClinicService clinicService; 41 | 42 | @BeforeEach 43 | public void setup() { 44 | Vet james = new Vet(); 45 | james.setFirstName("James"); 46 | james.setLastName("Carter"); 47 | james.setId(1); 48 | Vet helen = new Vet(); 49 | helen.setFirstName("Helen"); 50 | helen.setLastName("Leary"); 51 | helen.setId(2); 52 | Specialty radiology = new Specialty(); 53 | radiology.setId(1); 54 | radiology.setName("radiology"); 55 | helen.addSpecialty(radiology); 56 | given(this.clinicService.findVets()).willReturn(Lists.newArrayList(james, helen)); 57 | } 58 | 59 | @Test 60 | public void testShowVetListHtml() throws Exception { 61 | mockMvc.perform(get("/vets.html")) 62 | .andExpect(status().isOk()) 63 | .andExpect(model().attributeExists("vets")) 64 | .andExpect(view().name("vets/vetList")); 65 | } 66 | 67 | @Test 68 | public void testShowResourcesVetList() throws Exception { 69 | ResultActions actions = mockMvc.perform(get("/vets.json").accept(MediaType.APPLICATION_JSON)) 70 | .andExpect(status().isOk()); 71 | actions.andExpect(content().contentType("application/json;charset=UTF-8")) 72 | .andExpect(jsonPath("$.vetList[0].id").value(1)); 73 | } 74 | 75 | @Test 76 | public void testShowVetListXml() throws Exception { 77 | mockMvc.perform(get("/vets.xml").accept(MediaType.APPLICATION_XML)) 78 | .andExpect(status().isOk()) 79 | .andExpect(content().contentType(MediaType.APPLICATION_XML_VALUE)) 80 | .andExpect(content().node(hasXPath("/vets/vetList[id=1]/id"))); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /application/src/test/java/org/springframework/samples/petclinic/web/VisitControllerTests.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web; 2 | 3 | import org.junit.Before; 4 | import org.junit.jupiter.api.BeforeEach; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.samples.petclinic.model.Pet; 12 | import org.springframework.samples.petclinic.service.ClinicService; 13 | import org.springframework.security.core.userdetails.UserDetailsService; 14 | import org.springframework.test.context.junit.jupiter.SpringExtension; 15 | import org.springframework.test.context.junit4.SpringRunner; 16 | import org.springframework.test.web.servlet.MockMvc; 17 | 18 | import static org.mockito.BDDMockito.given; 19 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 20 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view; 24 | 25 | /** 26 | * Test class for {@link VisitController} 27 | * 28 | * @author Colin But 29 | */ 30 | 31 | @ExtendWith(SpringExtension.class) 32 | @WebMvcTest(VisitController.class) 33 | @MockBean(UserDetailsService.class) 34 | public class VisitControllerTests { 35 | 36 | private static final int TEST_PET_ID = 1; 37 | 38 | @Autowired 39 | private MockMvc mockMvc; 40 | 41 | @MockBean 42 | private ClinicService clinicService; 43 | 44 | @BeforeEach 45 | public void init() { 46 | given(this.clinicService.findPetById(TEST_PET_ID)).willReturn(new Pet()); 47 | } 48 | 49 | @Test 50 | public void testInitNewVisitForm() throws Exception { 51 | mockMvc.perform(get("/owners/*/pets/{petId}/visits/new", TEST_PET_ID)) 52 | .andExpect(status().isOk()) 53 | .andExpect(view().name("pets/createOrUpdateVisitForm")); 54 | } 55 | 56 | @Test 57 | public void testProcessNewVisitFormSuccess() throws Exception { 58 | mockMvc.perform(post("/owners/*/pets/{petId}/visits/new", TEST_PET_ID) 59 | .param("name", "George") 60 | .param("description", "Visit Description") 61 | ) 62 | .andExpect(status().is3xxRedirection()) 63 | .andExpect(view().name("redirect:/owners/{ownerId}")); 64 | } 65 | 66 | @Test 67 | public void testProcessNewVisitFormHasErrors() throws Exception { 68 | mockMvc.perform(post("/owners/*/pets/{petId}/visits/new", TEST_PET_ID) 69 | .param("name", "George") 70 | ) 71 | .andExpect(model().attributeHasErrors("visit")) 72 | .andExpect(status().isOk()) 73 | .andExpect(view().name("pets/createOrUpdateVisitForm")); 74 | } 75 | 76 | @Test 77 | public void testShowVisits() throws Exception { 78 | mockMvc.perform(get("/owners/*/pets/{petId}/visits", TEST_PET_ID)) 79 | .andExpect(status().isOk()) 80 | .andExpect(model().attributeExists("visits")) 81 | .andExpect(view().name("visitList")); 82 | } 83 | 84 | 85 | } 86 | -------------------------------------------------------------------------------- /application/src/test/java/org/springframework/samples/petclinic/web/api/PetResourceTests.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web.api; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.junit.jupiter.api.extension.ExtendWith; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 8 | import org.springframework.boot.test.mock.mockito.MockBean; 9 | import org.springframework.http.MediaType; 10 | import org.springframework.samples.petclinic.model.Owner; 11 | import org.springframework.samples.petclinic.model.Pet; 12 | import org.springframework.samples.petclinic.model.PetType; 13 | import org.springframework.samples.petclinic.service.ClinicService; 14 | import org.springframework.security.core.userdetails.UserDetailsService; 15 | import org.springframework.test.context.junit.jupiter.SpringExtension; 16 | import org.springframework.test.context.junit4.SpringRunner; 17 | import org.springframework.test.web.servlet.MockMvc; 18 | 19 | import static org.mockito.BDDMockito.given; 20 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 21 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; 22 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 24 | 25 | @ExtendWith(SpringExtension.class) 26 | @WebMvcTest(PetResource.class) 27 | @MockBean(UserDetailsService.class) 28 | public class PetResourceTests { 29 | 30 | @Autowired 31 | private MockMvc mvc; 32 | 33 | @MockBean 34 | ClinicService clinicService; 35 | 36 | @Test 37 | public void shouldGetAPetInJSonFormat() throws Exception { 38 | 39 | Pet pet = setupPet(); 40 | 41 | given(clinicService.findPetById(2)).willReturn(pet); 42 | 43 | mvc.perform(get("/api/owners/2/pets/2") // 44 | .accept(MediaType.APPLICATION_JSON)) // 45 | .andExpect(status().isOk()) // 46 | .andExpect(content().contentType("application/json;charset=UTF-8")) // 47 | .andExpect(jsonPath("$.id").value(2)) // 48 | .andExpect(jsonPath("$.name").value("Basil")) // 49 | .andExpect(jsonPath("$.typeId").value(6)); // 50 | } 51 | 52 | private Pet setupPet() { 53 | Owner owner = new Owner(); 54 | owner.setFirstName("George"); 55 | owner.setLastName("Bush"); 56 | 57 | Pet pet = new Pet(); 58 | 59 | pet.setName("Basil"); 60 | pet.setId(2); 61 | 62 | PetType petType = new PetType(); 63 | petType.setId(6); 64 | pet.setType(petType); 65 | 66 | owner.addPet(pet); 67 | return pet; 68 | } 69 | } -------------------------------------------------------------------------------- /application/src/test/java/org/springframework/samples/petclinic/web/api/VetResourceTests.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.web.api; 2 | 3 | import org.junit.Ignore; 4 | import org.junit.jupiter.api.Disabled; 5 | import org.junit.jupiter.api.Test; 6 | import org.junit.jupiter.api.extension.ExtendWith; 7 | import org.junit.runner.RunWith; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 10 | import org.springframework.boot.test.mock.mockito.MockBean; 11 | import org.springframework.http.MediaType; 12 | import org.springframework.samples.petclinic.model.Vet; 13 | import org.springframework.samples.petclinic.service.ClinicService; 14 | import org.springframework.security.core.userdetails.UserDetailsService; 15 | import org.springframework.test.context.junit.jupiter.SpringExtension; 16 | import org.springframework.test.context.junit4.SpringRunner; 17 | import org.springframework.test.web.servlet.MockMvc; 18 | 19 | import java.util.Arrays; 20 | 21 | import static org.mockito.BDDMockito.given; 22 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 23 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 24 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 25 | 26 | @ExtendWith(SpringExtension.class) 27 | @WebMvcTest(VetResource.class) 28 | @MockBean(UserDetailsService.class) 29 | public class VetResourceTests { 30 | 31 | @Autowired 32 | private MockMvc mvc; 33 | 34 | @MockBean 35 | ClinicService clinicService; 36 | 37 | @Test 38 | @Disabled 39 | public void shouldGetAListOfVetsInJSonFormat() throws Exception { 40 | 41 | Vet vet = new Vet(); 42 | vet.setId(1); 43 | 44 | given(clinicService.findVets()).willReturn(Arrays.asList(vet)); 45 | 46 | mvc.perform(get("/api/vets.json") // 47 | .accept(MediaType.APPLICATION_JSON)) // 48 | .andExpect(status().isOk()) // 49 | .andExpect(jsonPath("$[0].id").value(1)); 50 | } 51 | 52 | } -------------------------------------------------------------------------------- /application/src/test/resources/allure.properties: -------------------------------------------------------------------------------- 1 | allure.results.directory=build/allure-results -------------------------------------------------------------------------------- /application/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | 4 | application: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | args: 9 | jarFile: application/build/libs/spring-petclinic-1.7.0-SNAPSHOT.jar 10 | command: -m 2056m 11 | volumes: 12 | - ./application/build/jacocoagent:/jacocoagent 13 | - ./ui-tests/build/jacoco:/jacocoreport 14 | ports: 15 | - 8070:8080 16 | - 9999:9999 17 | entrypoint: [ 18 | "java", 19 | "-Dcom.sun.management.jmxremote", 20 | "-Dcom.sun.management.jmxremote.local.only=false", 21 | "-Dcom.sun.management.jmxremote.ssl=false", 22 | "-Dcom.sun.management.jmxremote.authenticate=false", 23 | "-Dcom.sun.management.jmxremote.port=9999", 24 | "-Dcom.sun.management.jmxremote.rmi.port=9999", 25 | "-Djava.rmi.server.hostname=localhost", 26 | "-Djava.security.egd=file:/dev/./urandom", 27 | "-javaagent:/jacocoagent/jacocoagent.jar=output=file,destfile=/jacocoreport/test.exec,append=false,dumponexit=false,jmx=true", 28 | "-jar", 29 | "/app.jar" 30 | ] 31 | environment: 32 | - SPRING_DATASOURCE_URL=jdbc:p6spy:postgresql://postgres:5432/petclinic 33 | depends_on: 34 | - postgres 35 | 36 | postgres: 37 | image: postgres:9.6.1 38 | ports: 39 | - 5422:5432 40 | environment: 41 | - POSTGRES_DB=petclinic 42 | - POSTGRES_USER=petclinic 43 | - POSTGRES_PASSWORD=q9KqUiu2vqnAuf -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spring-petclinic-reactjs", 3 | "version": "0.0.1", 4 | "description": "Spring Boot Pet Clinic Sample Application with React Frontend", 5 | "main": "index.js", 6 | "repository": "https://github.com/spring-petclinic/spring-petclinic-reactjs", 7 | "scripts": { 8 | "postinstall": "typings install", 9 | "install-types": "typings install", 10 | "test": "jest", 11 | "test:watch": "jest --watchAll --no-cache", 12 | "start": "node server.js", 13 | "build:clean": "rimraf ./public/dist && webpack --config webpack.config.js", 14 | "build:prod": "rimraf ./public/dist && NODE_ENV=production webpack --config webpack.config.prod.js" 15 | }, 16 | "author": "Nils Hartmann", 17 | "license": "ISC", 18 | "dependencies": { 19 | "@types/moment": "^2.13.0", 20 | "bootstrap": "^3.3.7", 21 | "classnames": "^2.2.5", 22 | "moment": "^2.15.1", 23 | "react": "^15.0.0", 24 | "react-datepicker": "^0.29.0", 25 | "react-dom": "^15.0.0", 26 | "react-router": "^2.7.0", 27 | "redbox-react": "^1.2.3", 28 | "whatwg-fetch": "^1.0.0", 29 | "react-loader-advanced": "^1.7.1" 30 | }, 31 | "devDependencies": { 32 | "es-cookie": "^1.2.0", 33 | "babel-core": "^6.4.0", 34 | "babel-loader": "^6.2.1", 35 | "babel-preset-es2015": "^6.3.13", 36 | "babel-preset-react": "^6.3.13", 37 | "babel-preset-stage-0": "^6.3.13", 38 | "chalk": "^1.1.3", 39 | "cross-env": "^1.0.7", 40 | "css-loader": "^0.23.1", 41 | "enzyme": "^2.5.1", 42 | "extract-text-webpack-plugin": "^1.0.1", 43 | "file-loader": "^0.9.0", 44 | "jest": "^16.0.1", 45 | "jest-fetch-mock": "^1.0.6", 46 | "less": "^2.7.1", 47 | "less-loader": "^2.2.3", 48 | "react-addons-test-utils": "^15.3.2", 49 | "react-hot-loader": "^3.0.0-beta.0", 50 | "rimraf": "^2.5.0", 51 | "style-loader": "^0.13.0", 52 | "ts-jest": "^0.1.8", 53 | "ts-loader": "^0.8.2", 54 | "tslint": "^3.15.1", 55 | "tslint-loader": "^2.1.5", 56 | "typescript": "~2.0.2", 57 | "typings": "^1.3.2", 58 | "url-loader": "^0.5.7", 59 | "webpack": "^1.12.10", 60 | "webpack-dev-server": "^1.14.1" 61 | }, 62 | "jest": { 63 | "scriptPreprocessor": "./node_modules/ts-jest/preprocessor.js", 64 | "testRegex": "(/__tests__/.*\\.(test|spec))\\.(ts|tsx|js)$", 65 | "moduleFileExtensions": [ 66 | "ts", 67 | "tsx", 68 | "js" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /frontend/public/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/public/images/favicon.png -------------------------------------------------------------------------------- /frontend/public/images/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/public/images/loader.gif -------------------------------------------------------------------------------- /frontend/public/images/pets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/public/images/pets.png -------------------------------------------------------------------------------- /frontend/public/images/platform-bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/public/images/platform-bg.png -------------------------------------------------------------------------------- /frontend/public/images/spring-pivotal-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/public/images/spring-pivotal-logo.png -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Spring Boot Petclinic React Example 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 21 | 22 | -------------------------------------------------------------------------------- /frontend/public/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/public/loading.gif -------------------------------------------------------------------------------- /frontend/public/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 42 | 43 | 50 | 51 |
52 |

UI testing

53 | 54 |
55 | 56 | -------------------------------------------------------------------------------- /frontend/run.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | PORT=4444 npm start 4 | -------------------------------------------------------------------------------- /frontend/src/Root.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | const { Router } = require('react-router'); 3 | 4 | // Routes 5 | import configureRoutes from './configureRoutes'; 6 | 7 | // https://github.com/gaearon/react-hot-boilerplate/pull/61#issuecomment-218333616 8 | // https://github.com/rybon/counter-hmr/blob/e651ce25b3a307f13ca53c977f9e8709ba873407/src/components/Root.jsx 9 | const Root = () => ( 10 | 11 | {configureRoutes()} 12 | 13 | ); 14 | 15 | export default Root; 16 | -------------------------------------------------------------------------------- /frontend/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import Menu from './Menu'; 3 | 4 | const NavBar = ({location, children}: {location?: any, children?: any}) => ( 5 |
6 | 7 |
8 |
9 | ); 10 | 11 | const Main = ({location, children}: {location?: any, children?: any}) => ( 12 |
13 |
14 |
15 | {children} 16 |
17 |
18 |
19 | Sponsored by Pivotal
20 |
21 |
22 |
23 |
24 |
25 | ); 26 | export default ({location, children}) => ( 27 |
28 | {location.pathname === '/login' ?
: } 29 |
30 | ); 31 | -------------------------------------------------------------------------------- /frontend/src/components/ErrorPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | interface IErrorPageState { 4 | error?: { 5 | status: string; 6 | message: string; 7 | }; 8 | } 9 | 10 | export default class ErrorPage extends React.Component { 11 | constructor() { 12 | super(); 13 | this.state = {}; 14 | } 15 | 16 | componentDidMount() { 17 | fetch('http://localhost:8080/api/oups') 18 | .then(response => response.json()) 19 | .then(error => this.setState({error})); 20 | } 21 | 22 | render() { 23 | const { error } = this.state; 24 | 25 | return 26 | 27 | 28 |

Something happened...

29 | { error ? 30 | 31 |

Status: {error.status}

32 |

Message: {error.message}

33 |
34 | : 35 |

Unkown error

36 | } 37 |
; 38 | } 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/src/components/Menu.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | const MenuItem = ({active, url, title, children}: { active: boolean, url: string, title: string, children?: any }) => ( 4 |
  • 5 | {children} 6 |
  • 7 | ); 8 | 9 | export default ({name}: { name: string }) => ( 10 | 40 | ); 41 | -------------------------------------------------------------------------------- /frontend/src/components/NotFoundPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default () =>

    The requested page has not been found.

    ; 4 | -------------------------------------------------------------------------------- /frontend/src/components/WelcomePage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default () => ( 4 | 5 |

    Welcome

    6 |
    7 |
    8 | 9 |
    10 |
    11 |
    12 | ); 13 | -------------------------------------------------------------------------------- /frontend/src/components/form/Constraints.ts: -------------------------------------------------------------------------------- 1 | import { IConstraint } from '../../types'; 2 | 3 | export const NotEmpty: IConstraint = { 4 | message: 'Enter at least one character', 5 | validate: (value) => { 6 | return !!value && value.length > 0; 7 | } 8 | }; 9 | 10 | export const Digits = (digits: number): IConstraint => { 11 | const reg = new RegExp('^\\d{1,' + digits + '}$'); 12 | return { 13 | message: 'Must be a number with at most ' + digits + ' digits', 14 | validate: (value) => { 15 | return !!value && value.match(reg) !== null; 16 | } 17 | }; 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/src/components/form/DateInput.tsx: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import * as React from 'react'; 4 | 5 | const ReactDatePicker = require('react-datepicker'); 6 | import * as moment from 'moment'; 7 | 8 | import { IError, IInputChangeHandler } from '../../types'; 9 | 10 | import FieldFeedbackPanel from './FieldFeedbackPanel'; 11 | 12 | export default ({object, error, id, name, label, onChange}: { object: any, error: IError, id: string, name: string, label: string, onChange: IInputChangeHandler }) => { 13 | 14 | const handleOnChange = value => { 15 | const dateString = value ? value.format('YYYY/MM/DD') : null; 16 | onChange(name, dateString, null); 17 | }; 18 | 19 | const selectedValue = object[name] ? moment(object[name], 'YYYY/MM/DD') : null; 20 | const fieldError = error && error.fieldErrors[name]; 21 | const valid = !fieldError && selectedValue != null; 22 | 23 | const cssGroup = `form-group ${fieldError ? 'has-error' : ''}`; 24 | 25 | return ( 26 |
    27 | 28 | 29 |
    30 | 31 | 32 | 33 |
    34 |
    35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /frontend/src/components/form/FieldFeedbackPanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { IFieldError } from '../../types'; 4 | 5 | export default ({valid, fieldError}: {valid: boolean, fieldError: IFieldError}) => { 6 | if (valid) { 7 | return ; 8 | } 9 | 10 | if (fieldError) { 11 | return ( 12 | 13 | {fieldError.message} 14 | ); 15 | } 16 | 17 | return null; 18 | }; 19 | -------------------------------------------------------------------------------- /frontend/src/components/form/Input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { IConstraint, IError, IInputChangeHandler } from '../../types'; 4 | 5 | import FieldFeedbackPanel from './FieldFeedbackPanel'; 6 | 7 | const NoConstraint: IConstraint = { 8 | message: '', 9 | validate: v => true 10 | }; 11 | 12 | 13 | export default ({type, object, error, name, id, constraint = NoConstraint, label, onChange}: {type: string, object: any, error: IError, name?: string, id?: string, constraint?: IConstraint, label: string, onChange: IInputChangeHandler }) => { 14 | 15 | const handleOnChange = event => { 16 | const { value } = event.target; 17 | 18 | // run validation (if any) 19 | let error = null; 20 | const fieldError = constraint.validate(value) === false ? { field: name, message: constraint.message } : null; 21 | 22 | // invoke callback 23 | onChange(name, value, fieldError); 24 | }; 25 | 26 | const value = object[name]; 27 | const fieldError = error && error.fieldErrors[name]; 28 | const valid = !fieldError && value !== null && value !== undefined; 29 | 30 | const cssGroup = `form-group ${fieldError ? 'has-error' : ''}`; 31 | 32 | return ( 33 |
    34 | 35 | 36 |
    37 | 38 | 39 | 40 |
    41 |
    42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /frontend/src/components/form/SelectInput.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { IError, IInputChangeHandler, ISelectOption } from '../../types'; 4 | 5 | import FieldFeedbackPanel from './FieldFeedbackPanel'; 6 | 7 | export default ({object, error, id, name, label, options, onChange}: { object: any, error: IError, id: string, name: string, label: string, options: ISelectOption[], onChange: IInputChangeHandler }) => { 8 | 9 | const handleOnChange = event => { 10 | console.log('select on change', event.target.value); 11 | onChange(name, event.target.value, null); 12 | }; 13 | 14 | const selectedValue = object[name] || ''; 15 | const fieldError = error && error.fieldErrors[name]; 16 | const valid = !fieldError && selectedValue !== ''; 17 | 18 | const cssGroup = `form-group ${fieldError ? 'has-error' : ''}`; 19 | 20 | return ( 21 |
    22 | 23 | 24 |
    25 | 28 | 29 |
    30 |
    31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /frontend/src/components/owners/EditOwnerPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import OwnerEditor from './OwnerEditor'; 3 | 4 | import {IOwner} from '../../types'; 5 | import {url} from '../../util'; 6 | import * as Cookies from 'es-cookie'; 7 | 8 | interface IEditOwnerPageProps { 9 | params?: { ownerId?: string }; 10 | } 11 | 12 | interface IEditOwnerPageState { 13 | owner: IOwner; 14 | } 15 | 16 | export default class EditOwnerPage extends React.Component { 17 | componentDidMount() { 18 | const {params} = this.props; 19 | 20 | if (params && params.ownerId) { 21 | const fetchUrl = url(`api/owners/${params.ownerId}`); 22 | const fetchParams = { 23 | method: 'GET', 24 | headers: { 25 | 'Authorization': 'Bearer ' + Cookies.get('user') 26 | } 27 | }; 28 | fetch(fetchUrl, fetchParams) 29 | .then(response => response.json()) 30 | .then(owner => this.setState({owner})); 31 | } 32 | } 33 | 34 | render() { 35 | const owner = this.state && this.state.owner; 36 | if (owner) { 37 | return ; 38 | } 39 | return null; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/components/owners/NewOwnerPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import OwnerEditor from './OwnerEditor'; 3 | 4 | import { IOwner } from '../../types'; 5 | 6 | const newOwner = (): IOwner => ({ 7 | id: null, 8 | isNew: true, 9 | firstName: '', 10 | lastName: '', 11 | address: '', 12 | city: '', 13 | telephone: '', 14 | pets: [] 15 | }); 16 | 17 | export default () => ; 18 | 19 | // export default class NewOwnerPage extends React.Component { 20 | 21 | // context: IRouterContext; 22 | 23 | // static contextTypes = { 24 | // router: React.PropTypes.object.isRequired 25 | // }; 26 | 27 | // constructor() { 28 | // super(); 29 | // this.onInputChange = this.onInputChange.bind(this); 30 | // this.onSubmit = this.onSubmit.bind(this); 31 | 32 | // this.state = { owner: newOwner() }; 33 | // } 34 | 35 | // onSubmit(event) { 36 | // event.preventDefault(); 37 | 38 | // const { owner } = this.state; 39 | 40 | // submitForm('/api/owner', owner, (status, response) => { 41 | // if (status === 201) { 42 | // const newOwner = response as IOwner; 43 | // this.context.router.push({ 44 | // pathname: '/owners/' + newOwner.id 45 | // }); 46 | // } else { 47 | // console.log('ERROR?!...', response); 48 | // this.setState({ error: response }); 49 | // } 50 | // }); 51 | // } 52 | 53 | // onInputChange(name: string, value: string) { 54 | // const { owner } = this.state; 55 | // const modifiedOwner = Object.assign({}, owner, { [name]: value }); 56 | // this.setState({ owner: modifiedOwner }); 57 | // } 58 | 59 | // render() { 60 | // const { owner, error } = this.state; 61 | // return ( 62 | // 63 | //

    New Owner

    64 | //
    65 | //
    66 | // 67 | // 68 | // 69 | // 70 | // 71 | //
    72 | //
    73 | //
    74 | // 75 | //
    76 | //
    77 | //
    78 | //
    79 | // ); 80 | // } 81 | // } -------------------------------------------------------------------------------- /frontend/src/components/owners/OwnerEditor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { IRouter, Link } from 'react-router'; 4 | import { url, submitForm } from '../../util'; 5 | 6 | import Input from '../form/Input'; 7 | 8 | import { Digits, NotEmpty } from '../form/Constraints'; 9 | 10 | import { IInputChangeHandler, IFieldError, IError, IOwner, IRouterContext } from '../../types'; 11 | 12 | interface IOwnerEditorProps { 13 | initialOwner?: IOwner; 14 | } 15 | 16 | interface IOwnerEditorState { 17 | owner?: IOwner; 18 | error?: IError; 19 | }; 20 | 21 | export default class OwnerEditor extends React.Component { 22 | 23 | context: IRouterContext; 24 | 25 | static contextTypes = { 26 | router: React.PropTypes.object.isRequired 27 | }; 28 | 29 | constructor(props) { 30 | super(props); 31 | this.onInputChange = this.onInputChange.bind(this); 32 | this.onSubmit = this.onSubmit.bind(this); 33 | 34 | this.state = { 35 | owner: Object.assign({}, props.initialOwner) 36 | }; 37 | } 38 | 39 | onSubmit(event) { 40 | event.preventDefault(); 41 | 42 | const { owner } = this.state; 43 | 44 | const url = owner.isNew ? 'api/owners' : 'api/owners/' + owner.id; 45 | submitForm(owner.isNew ? 'POST' : 'PUT', url, owner, (status, response) => { 46 | if (status === 200 || status === 201) { 47 | const newOwner = response as IOwner; 48 | this.context.router.push({ 49 | pathname: '/owners/' + newOwner.id 50 | }); 51 | } else { 52 | console.log('ERROR?!...', response); 53 | this.setState({ error: response }); 54 | } 55 | }); 56 | } 57 | 58 | onInputChange(name: string, value: string, fieldError: IFieldError) { 59 | const { owner, error } = this.state; 60 | const modifiedOwner = Object.assign({}, owner, { [name]: value }); 61 | const newFieldErrors = error ? Object.assign({}, error.fieldErrors, {[name]: fieldError }) : {[name]: fieldError }; 62 | this.setState({ 63 | owner: modifiedOwner, 64 | error: { fieldErrors: newFieldErrors } 65 | }); 66 | } 67 | 68 | render() { 69 | const { owner, error } = this.state; 70 | return ( 71 | 72 |

    New Owner

    73 |
    74 |
    75 | 76 | 77 | 78 | 79 | 80 |
    81 |
    82 |
    83 | 84 |
    85 |
    86 |
    87 |
    88 | ); 89 | } 90 | } -------------------------------------------------------------------------------- /frontend/src/components/owners/OwnerInformation.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Link } from 'react-router'; 4 | import { IOwner } from '../../types'; 5 | 6 | export default ({owner}: { owner: IOwner }) => ( 7 |
    8 |

    Owner Information

    9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 |
    Name{owner.firstName} {owner.lastName}
    Address{owner.address}
    City{owner.city}
    Telephone{owner.telephone}
    30 | 31 | Edit Owner 32 |   33 | Add New Pet 34 |
    35 | ); 36 | -------------------------------------------------------------------------------- /frontend/src/components/owners/OwnersPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {Link} from 'react-router'; 4 | import {IOwner} from '../../types'; 5 | import {url} from '../../util'; 6 | 7 | import OwnerInformation from './OwnerInformation'; 8 | import PetsTable from './PetsTable'; 9 | import * as Cookies from 'es-cookie'; 10 | 11 | interface IOwnersPageProps { 12 | params?: { ownerId?: string }; 13 | } 14 | 15 | interface IOwnerPageState { 16 | owner?: IOwner; 17 | } 18 | 19 | export default class OwnersPage extends React.Component { 20 | 21 | constructor() { 22 | super(); 23 | 24 | this.state = {}; 25 | } 26 | 27 | componentDidMount() { 28 | const {params} = this.props; 29 | if (params && params.ownerId) { 30 | const fetchUrl = url(`api/owners/${params.ownerId}`); 31 | const fetchParams = { 32 | method: 'GET', 33 | headers: { 34 | 'Authorization': 'Bearer ' + Cookies.get('user') 35 | } 36 | }; 37 | fetch(fetchUrl, fetchParams) 38 | .then(response => response.json()) 39 | .then(owner => this.setState({owner})); 40 | } 41 | } 42 | 43 | render() { 44 | const {owner} = this.state; 45 | 46 | if (!owner) { 47 | return

    No Owner loaded

    ; 48 | } 49 | 50 | return ( 51 | 52 | 53 | 54 | 55 | ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /frontend/src/components/owners/OwnersTable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { IOwner } from '../../types'; 4 | import {Link} from 'react-router'; 5 | 6 | const renderRow = (owner: IOwner) => ( 7 | 8 | 9 | {owner.firstName} {owner.lastName} 10 | 11 | {owner.address} 12 | {owner.city} 13 | {owner.telephone} 14 | {owner.pets.map(pet => pet.name).join(', ')} 15 | 16 | ); 17 | 18 | const renderOwners = (owners: IOwner[]) => ( 19 |
    20 |

    {owners.length} Owners found

    21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {owners.map(renderRow)} 33 | 34 |
    NameAddressCityTelephonePets
    35 |
    36 | ); 37 | 38 | const emptyOwners = () => ( 39 |
    40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 |
    NameAddressCityTelephonePets
    53 |
    54 | ); 55 | 56 | export default ({owners}: { owners: IOwner[] }) => owners ? renderOwners(owners) : emptyOwners(); 57 | -------------------------------------------------------------------------------- /frontend/src/components/owners/PetsTable.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { Link } from 'react-router'; 4 | import { IOwner, IPet } from '../../types'; 5 | 6 | const VisitsTable = ({ownerId, pet}: { ownerId: number, pet: IPet }) => ( 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | {pet.visits.map(visit => ( 16 | 17 | 18 | 19 | 20 | ))} 21 | 22 | 25 | 28 | 29 | 30 |
    Visit DateDescription
    {visit.date}{visit.description}
    23 | Edit Pet 24 | 26 | Add Visit 27 |
    31 | ); 32 | 33 | export default ({owner}: { owner: IOwner }) => ( 34 |
    35 |

    Pets and Visits

    36 | 37 | 38 | {owner.pets.map(pet => ( 39 | 40 | 50 | 53 | 54 | ))} 55 | 56 |
    41 |
    42 |
    Name
    43 |
    {pet.name}
    44 |
    Birth Date
    45 |
    {pet.birthDate}
    46 |
    Type
    47 |
    {pet.type.name}
    48 |
    49 |
    51 | 52 |
    57 |
    58 | ); 59 | -------------------------------------------------------------------------------- /frontend/src/components/pets/EditPetPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {IOwner, IEditablePet, ISelectOption} from '../../types'; 4 | import * as Cookies from 'es-cookie'; 5 | import {url} from '../../util'; 6 | 7 | import LoadingPanel from './LoadingPanel'; 8 | import PetEditor from './PetEditor'; 9 | 10 | import createPetEditorModel from './createPetEditorModel'; 11 | 12 | interface IEditPetPageProps { 13 | params: { 14 | ownerId: string, 15 | petId: string 16 | }; 17 | } 18 | 19 | interface IEditPetPageState { 20 | pet?: IEditablePet; 21 | owner?: IOwner; 22 | pettypes?: ISelectOption[]; 23 | }; 24 | 25 | export default class EditPetPage extends React.Component { 26 | 27 | componentDidMount() { 28 | const {params} = this.props; 29 | 30 | const fetchUrl = url(`api/owners/${params.ownerId}/pets/${params.petId}`); 31 | const fetchParams = { 32 | method: 'GET', 33 | headers: { 34 | 'Authorization': 'Bearer ' + Cookies.get('user') 35 | } 36 | }; 37 | const loadPetPromise = fetch(fetchUrl).then(response => response.json()); 38 | 39 | createPetEditorModel(this.props.params.ownerId, loadPetPromise) 40 | .then(model => this.setState(model)); 41 | } 42 | 43 | render() { 44 | if (!this.state) { 45 | return ; 46 | } 47 | 48 | return ; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/components/pets/LoadingPanel.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | export default () => ( 4 | 5 |

    Pet

    6 |
    7 |
    8 | Loading... 9 |
    10 |
    11 |
    12 | ); 13 | -------------------------------------------------------------------------------- /frontend/src/components/pets/NewPetPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { IOwner, IEditablePet, ISelectOption } from '../../types'; 4 | 5 | import { url } from '../../util'; 6 | 7 | import LoadingPanel from './LoadingPanel'; 8 | import PetEditor from './PetEditor'; 9 | 10 | import createPetEditorModel from './createPetEditorModel'; 11 | 12 | interface INewPetPageProps { 13 | params: { ownerId: string }; 14 | } 15 | 16 | interface INewPetPageState { 17 | pet?: IEditablePet; 18 | owner?: IOwner; 19 | pettypes?: ISelectOption[]; 20 | }; 21 | 22 | const NEW_PET: IEditablePet = { 23 | id: null, 24 | isNew: true, 25 | name: '', 26 | birthDate: null, 27 | typeId: null 28 | }; 29 | 30 | export default class NewPetPage extends React.Component { 31 | 32 | componentDidMount() { 33 | createPetEditorModel(this.props.params.ownerId, Promise.resolve(NEW_PET)) 34 | .then(model => this.setState(model)); 35 | } 36 | 37 | render() { 38 | if (!this.state) { 39 | return ; 40 | } 41 | 42 | return ; 43 | } 44 | } -------------------------------------------------------------------------------- /frontend/src/components/pets/PetEditor.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { IRouter, Link } from 'react-router'; 4 | import { url, submitForm } from '../../util'; 5 | 6 | import Input from '../form/Input'; 7 | import DateInput from '../form/DateInput'; 8 | import SelectInput from '../form/SelectInput'; 9 | 10 | import { IError, IOwner, IPetRequest, IEditablePet, IPet, IPetType, IRouterContext, ISelectOption } from '../../types'; 11 | 12 | interface IPetEditorProps { 13 | pet: IEditablePet; 14 | owner: IOwner; 15 | pettypes: ISelectOption[]; 16 | } 17 | 18 | interface IPetEditorState { 19 | editablePet?: IEditablePet; 20 | error?: IError; 21 | }; 22 | 23 | export default class PetEditor extends React.Component { 24 | 25 | context: IRouterContext; 26 | 27 | static contextTypes = { 28 | router: React.PropTypes.object.isRequired 29 | }; 30 | 31 | constructor(props) { 32 | super(props); 33 | this.onInputChange = this.onInputChange.bind(this); 34 | this.onSubmit = this.onSubmit.bind(this); 35 | 36 | this.state = { editablePet: Object.assign({}, props.pet ) }; 37 | } 38 | 39 | onSubmit(event) { 40 | event.preventDefault(); 41 | 42 | const { owner } = this.props; 43 | const { editablePet } = this.state; 44 | 45 | const request: IPetRequest = { 46 | birthDate: editablePet.birthDate, 47 | name: editablePet.name, 48 | typeId: editablePet.typeId 49 | }; 50 | 51 | const url = editablePet.isNew ? 'api/owners/' + owner.id + '/pets' : 'api/owners/' + owner.id + '/pets/' + editablePet.id; 52 | submitForm(editablePet.isNew ? 'POST' : 'PUT', url, request, (status, response) => { 53 | if (status === 204) { 54 | this.context.router.push({ 55 | pathname: '/owners/' + owner.id 56 | }); 57 | } else { 58 | console.log('ERROR?!...', response); 59 | this.setState({ error: response }); 60 | } 61 | }); 62 | } 63 | 64 | onInputChange(name: string, value: string) { 65 | const { editablePet } = this.state; 66 | const modifiedPet = Object.assign({}, editablePet, { [name]: value }); 67 | 68 | this.setState({ editablePet: modifiedPet }); 69 | } 70 | 71 | render() { 72 | const { owner, pettypes } = this.props; 73 | const { editablePet, error } = this.state; 74 | 75 | const formLabel = editablePet.isNew ? 'Add Pet' : 'Update Pet'; 76 | 77 | return ( 78 | 79 |

    {formLabel}

    80 |
    81 |
    82 |
    83 | 84 |
    {owner.firstName} {owner.lastName}
    85 |
    86 | 87 | 88 | 89 | 90 |
    91 |
    92 |
    93 | 94 |
    95 |
    96 |
    97 |
    98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /frontend/src/components/pets/createPetEditorModel.ts: -------------------------------------------------------------------------------- 1 | import { IPetType, ISelectOption } from '../../types'; 2 | import { url, submitForm } from '../../util'; 3 | import * as Cookies from 'es-cookie'; 4 | 5 | const toSelectOptions = (pettypes: IPetType[]): ISelectOption[] => pettypes.map(pettype => ({ value: pettype.id, name: pettype.name })); 6 | export default (ownerId: string, petLoaderPromise: Promise): Promise => { 7 | console.log(Cookies.getAll()); 8 | const fetchParams = { 9 | method: 'GET', 10 | headers: { 11 | 'Authorization': 'Bearer ' + Cookies.get('user') 12 | } 13 | }; 14 | 15 | return Promise.all( 16 | [fetch(url('api/pettypes'), fetchParams) 17 | .then(response => response.json()) 18 | .then(toSelectOptions), 19 | fetch(url('api/owners/' + ownerId), fetchParams) 20 | .then(response => response.json()), 21 | petLoaderPromise, 22 | ] 23 | ).then(results => ({ 24 | pettypes: results[0], 25 | owner: results[1], 26 | pet: results[2] 27 | })); 28 | }; 29 | -------------------------------------------------------------------------------- /frontend/src/components/vets/VetsPage.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import {IRouter, Link} from 'react-router'; 4 | import {url} from '../../util'; 5 | 6 | import {IVet} from '../../types'; 7 | import * as Cookies from 'es-cookie'; 8 | import Loader from 'react-loader-advanced'; 9 | 10 | interface IVetsPageState { 11 | vets: IVet[]; 12 | isDataLoading: boolean; 13 | } 14 | 15 | export default class VetsPage extends React.Component { 16 | constructor() { 17 | super(); 18 | 19 | this.state = {vets: [], isDataLoading: true}; 20 | } 21 | 22 | componentDidMount() { 23 | const requestUrl = url('api/vets'); 24 | 25 | const fetchParams = { 26 | method: 'GET', 27 | headers: { 28 | 'Authorization': 'Bearer ' + Cookies.get('user') 29 | } 30 | }; 31 | 32 | fetch(requestUrl, fetchParams) 33 | .then(response => response.json()) 34 | .then(vets => { 35 | this.setState({vets, isDataLoading: false}); 36 | }); 37 | } 38 | 39 | render() { 40 | const {vets, isDataLoading} = this.state; 41 | this.state = {vets: this.state.vets, isDataLoading: true}; 42 | 43 | if (!vets) { 44 | return

    Veterinarians

    ; 45 | } 46 | 47 | return ( 48 | 49 |

    Veterinarians

    50 | }> 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | {vets.map(vet => ( 60 | 61 | 62 | 63 | 64 | ))} 65 | 66 |
    NameSpecialties
    {vet.firstName} {vet.lastName}{vet.specialties.length > 0 ? vet.specialties.map(specialty => specialty.name).join(', ') : 'none'}
    67 |
    68 |
    69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /frontend/src/components/visits/PetDetails.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | 3 | import { IOwner, IPet } from '../../types'; 4 | 5 | export default ({owner, pet}: { owner: IOwner, pet: IPet }) => ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
    NameBirth DateTypeOwner
    {pet.name}{pet.birthDate}{pet.type.name}{owner.firstName} {owner.lastName}
    24 | ); -------------------------------------------------------------------------------- /frontend/src/configureRoutes.tsx: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | import {Route} from 'react-router'; 3 | 4 | import App from './components/App'; 5 | 6 | import WelcomePage from './components/WelcomePage'; 7 | import FindOwnersPage from './components/owners/FindOwnersPage'; 8 | import OwnersPage from './components/owners/OwnersPage'; 9 | import NewOwnerPage from './components/owners/NewOwnerPage'; 10 | import EditOwnerPage from './components/owners/EditOwnerPage'; 11 | import NewPetPage from './components/pets/NewPetPage'; 12 | import EditPetPage from './components/pets/EditPetPage'; 13 | import VisitsPage from './components/visits/VisitsPage'; 14 | import VetsPage from './components/vets/VetsPage'; 15 | import Login from './components/Login'; 16 | import NotFoundPage from './components/NotFoundPage'; 17 | import * as Cookies from 'es-cookie'; 18 | 19 | function isLoggedIn() { 20 | return Cookies.get('user'); 21 | } 22 | 23 | function checkAuth(nextState, replace) { 24 | if (!isLoggedIn()) { 25 | replace({ 26 | pathname: '/login' 27 | }); 28 | } 29 | } 30 | 31 | export default () => ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | 47 | -------------------------------------------------------------------------------- /frontend/src/main.tsx: -------------------------------------------------------------------------------- 1 | // React and Hot Loader 2 | import * as React from 'react'; 3 | import * as ReactDOM from 'react-dom'; 4 | import { AppContainer } from 'react-hot-loader'; 5 | 6 | require('./styles/less/petclinic.less'); 7 | 8 | // The Application 9 | import Root from './Root'; 10 | 11 | // Render Application 12 | const mountPoint = document.getElementById('mount'); 13 | ReactDOM.render( 14 | , 15 | mountPoint 16 | ); 17 | 18 | declare var module: any; 19 | if (module.hot) { 20 | module.hot.accept('./Root', () => { 21 | const NextApp = require('./Root').default; 22 | ReactDOM.render( 23 | 24 | 25 | , 26 | mountPoint 27 | ); 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /frontend/src/middleware/api.ts: -------------------------------------------------------------------------------- 1 | // // taken from by https://github.com/reactjs/redux/blob/master/examples/real-world/middleware/api.js 2 | 3 | // type IApiCallMethod = 'GET' | 'POST' | 'PATCH'; 4 | 5 | // export interface IApiCallData { 6 | // endpoint: string; 7 | // method?: IApiCallMethod; 8 | // // fetching, onSuccess, onFailure 9 | // types: string[]; 10 | // body?: any; 11 | // successMessage?: string; 12 | // } 13 | 14 | // const CALL_API = 'CALL_API'; 15 | 16 | // export interface IApiAction { 17 | // CALL_API: IApiCallData; 18 | // } 19 | 20 | // function doApiCall(store, apiCallData: IApiCallData) { 21 | 22 | // const params: any = { 23 | // headers: {} 24 | // }; 25 | // params.method = apiCallData.method || 'GET'; 26 | // params.body = JSON.stringify(apiCallData.body); 27 | // if (params.body) { 28 | // params.headers['Accept'] = 'application/json'; 29 | // params.headers['Content-Type'] = 'application/json'; 30 | // } 31 | 32 | 33 | // return fetch(`http://localhost:8080${apiCallData.endpoint}`, params) 34 | // .then(response => 35 | // response.json().then(json => ({ json, response })) 36 | // ).then(({ json, response }) => { 37 | // if (!response.ok) { 38 | // return Promise.reject(json); 39 | // } 40 | 41 | // return json; 42 | // }); 43 | // } 44 | 45 | // export default store => next => action => { 46 | // function actionWith(data) { 47 | // const finalAction = Object.assign({}, action, data); 48 | // delete finalAction[CALL_API]; 49 | // return finalAction; 50 | // } 51 | 52 | // const callAPI: IApiCallData = action[CALL_API]; 53 | // if (typeof callAPI === 'undefined') { 54 | // // not an API call 55 | // return next(action); 56 | // } 57 | 58 | 59 | // const { endpoint, types } = callAPI; 60 | 61 | // const [requestType, successType, failureType] = types; 62 | 63 | // // before sending the request: send action that request begins 64 | // next(actionWith({ type: requestType })); 65 | 66 | // return doApiCall(store, callAPI).then( 67 | // response => next(actionWith({payload: response, type: successType, success: callAPI.successMessage})), 68 | // error => next(actionWith({ 69 | // type: failureType, 70 | // error: error.message || 'Something bad happened' 71 | // })) 72 | // ); 73 | // }; 74 | -------------------------------------------------------------------------------- /frontend/src/react-datepicker.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from 'react'; 2 | declare module 'react-datepicker' { 3 | export class ReactDatePicker extends React.Component { } 4 | } -------------------------------------------------------------------------------- /frontend/src/react-hot-loader.d.ts: -------------------------------------------------------------------------------- 1 | declare module "react-hot-loader" { 2 | export var AppContainer: any; 3 | } -------------------------------------------------------------------------------- /frontend/src/styles/fonts/montserrat-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/src/styles/fonts/montserrat-webfont.eot -------------------------------------------------------------------------------- /frontend/src/styles/fonts/montserrat-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/src/styles/fonts/montserrat-webfont.ttf -------------------------------------------------------------------------------- /frontend/src/styles/fonts/montserrat-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/src/styles/fonts/montserrat-webfont.woff -------------------------------------------------------------------------------- /frontend/src/styles/fonts/varela_round-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/src/styles/fonts/varela_round-webfont.eot -------------------------------------------------------------------------------- /frontend/src/styles/fonts/varela_round-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/src/styles/fonts/varela_round-webfont.ttf -------------------------------------------------------------------------------- /frontend/src/styles/fonts/varela_round-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/src/styles/fonts/varela_round-webfont.woff -------------------------------------------------------------------------------- /frontend/src/styles/images/spring-logo-dataflow-mobile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/src/styles/images/spring-logo-dataflow-mobile.png -------------------------------------------------------------------------------- /frontend/src/styles/images/spring-logo-dataflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/frontend/src/styles/images/spring-logo-dataflow.png -------------------------------------------------------------------------------- /frontend/src/styles/less/header.less: -------------------------------------------------------------------------------- 1 | .navbar { 2 | border-top: 4px solid #6db33f; 3 | background-color: #34302d; 4 | margin-bottom: 0px; 5 | border-bottom: 0; 6 | border-left: 0; 7 | border-right: 0; 8 | } 9 | 10 | .navbar a.navbar-brand { 11 | background: url("../images/spring-logo-dataflow.png") -1px -1px no-repeat; 12 | margin: 12px 0 6px; 13 | width: 229px; 14 | height: 46px; 15 | display: inline-block; 16 | text-decoration: none; 17 | padding: 0; 18 | } 19 | 20 | .navbar a.navbar-brand span { 21 | display: block; 22 | width: 229px; 23 | height: 46px; 24 | background: url("../images/spring-logo-dataflow.png") -1px -48px no-repeat; 25 | opacity: 0; 26 | -moz-transition: opacity 0.12s ease-in-out; 27 | -webkit-transition: opacity 0.12s ease-in-out; 28 | -o-transition: opacity 0.12s ease-in-out; 29 | } 30 | 31 | .navbar a:hover.navbar-brand span { 32 | opacity: 1; 33 | } 34 | 35 | .navbar li > a, .navbar-text { 36 | font-family: "montserratregular", sans-serif; 37 | text-shadow: none; 38 | font-size: 14px; 39 | 40 | /* line-height: 14px; */ 41 | padding: 28px 20px; 42 | transition: all 0.15s; 43 | -webkit-transition: all 0.15s; 44 | -moz-transition: all 0.15s; 45 | -o-transition: all 0.15s; 46 | -ms-transition: all 0.15s; 47 | } 48 | 49 | .navbar li > a { 50 | text-transform: uppercase; 51 | } 52 | 53 | .navbar .navbar-text { 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | .navbar li:hover > a { 58 | color: #eeeeee; 59 | background-color: #6db33f; 60 | } 61 | 62 | .navbar-toggle { 63 | border-width: 0; 64 | 65 | .icon-bar + .icon-bar { 66 | margin-top: 3px; 67 | } 68 | .icon-bar { 69 | width: 19px; 70 | height: 3px; 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /frontend/src/styles/less/responsive.less: -------------------------------------------------------------------------------- 1 | @media (max-width: 768px) { 2 | .navbar-toggle { 3 | position:absolute; 4 | z-index: 9999; 5 | left:0px; 6 | top:0px; 7 | } 8 | 9 | .navbar a.navbar-brand { 10 | display: block; 11 | margin: 0 auto 0 auto; 12 | width: 148px; 13 | height: 50px; 14 | float: none; 15 | background: url("../images/spring-logo-dataflow-mobile.png") 0 center no-repeat; 16 | } 17 | 18 | .homepage-billboard .homepage-subtitle { 19 | font-size: 21px; 20 | line-height: 21px; 21 | } 22 | 23 | .navbar a.navbar-brand span { 24 | display: none; 25 | } 26 | 27 | .navbar { 28 | border-top-width: 0; 29 | } 30 | 31 | .xd-container { 32 | margin-top: 20px; 33 | margin-bottom: 30px; 34 | } 35 | 36 | .index-page--subtitle { 37 | margin-top: 10px; 38 | margin-bottom: 30px; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /frontend/src/styles/less/typography.less: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'varela_roundregular'; 3 | 4 | src: url('../fonts/varela_round-webfont.eot'); 5 | src: url('../fonts/varela_round-webfont.eot?#iefix') format('embedded-opentype'), 6 | url('../fonts/varela_round-webfont.woff') format('woff'), 7 | url('../fonts/varela_round-webfont.ttf') format('truetype'), 8 | url('../fonts/varela_round-webfont.svg#varela_roundregular') format('svg'); 9 | font-weight: normal; 10 | font-style: normal; 11 | } 12 | 13 | @font-face { 14 | font-family: 'montserratregular'; 15 | src: url('../fonts/montserrat-webfont.eot'); 16 | src: url('../fonts/montserrat-webfont.eot?#iefix') format('embedded-opentype'), 17 | url('../fonts/montserrat-webfont.woff') format('woff'), 18 | url('../fonts/montserrat-webfont.ttf') format('truetype'), 19 | url('../fonts/montserrat-webfont.svg#montserratregular') format('svg'); 20 | font-weight: normal; 21 | font-style: normal; 22 | } 23 | 24 | body, h1, h2, h3, p, input { 25 | margin: 0; 26 | font-weight: 400; 27 | font-family: "varela_roundregular", sans-serif; 28 | color: #34302d; 29 | } 30 | 31 | h1 { 32 | font-size: 24px; 33 | line-height: 30px; 34 | font-family: "montserratregular", sans-serif; 35 | } 36 | 37 | h2 { 38 | font-size: 18px; 39 | font-weight: 700; 40 | line-height: 24px; 41 | margin-bottom: 10px; 42 | font-family: "montserratregular", sans-serif; 43 | } 44 | 45 | h3 { 46 | font-size: 16px; 47 | line-height: 24px; 48 | margin-bottom: 10px; 49 | font-weight: 700; 50 | } 51 | 52 | p { 53 | //font-size: 15px; 54 | //line-height: 24px; 55 | } 56 | 57 | strong { 58 | font-weight: 700; 59 | font-family: "montserratregular", sans-serif; 60 | } 61 | -------------------------------------------------------------------------------- /frontend/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import { IRouter } from 'react-router'; 2 | 3 | // ------------------------------------ ROUTER ------------------------------------ 4 | export interface IRouterContext { 5 | router: IRouter; 6 | }; 7 | 8 | // ------------------------------------ UTIL -------------------------------------- 9 | export type IHttpMethod = 'POST' | 'PUT' | 'GET'; 10 | 11 | 12 | // ------------------------------------ ERROR ------------------------------------ 13 | export interface IFieldError { 14 | field: string; 15 | message: string; 16 | } 17 | 18 | interface IFieldErrors { 19 | [index: string]: IFieldError; 20 | }; 21 | 22 | export interface IError { 23 | fieldErrors: IFieldErrors; 24 | } 25 | 26 | 27 | // ------------------------------------ FORM -------------------------------------- 28 | export interface IConstraint { 29 | message: string; 30 | validate: (value: any) => boolean; 31 | } 32 | 33 | export type IInputChangeHandler = (name: string, value: string, error: IFieldError) => void; 34 | 35 | export interface ISelectOption { 36 | value: string|number; 37 | name: string; 38 | }; 39 | 40 | // ------------------------------------ MODEL .------------------------------------ 41 | 42 | interface IBaseEntity { 43 | id: number; 44 | isNew: boolean; 45 | }; 46 | 47 | interface INamedEntity extends IBaseEntity { 48 | name: string; 49 | } 50 | 51 | interface IPerson extends IBaseEntity { 52 | firstName: string; 53 | lastName: string; 54 | } 55 | 56 | export interface IVisit extends IBaseEntity { 57 | date: Date; 58 | description: string; 59 | }; 60 | 61 | export interface IPetType extends INamedEntity { 62 | }; 63 | 64 | export type IPetTypeId = number; 65 | 66 | export interface IPet extends INamedEntity { 67 | birthDate: Date; 68 | type: IPetType; 69 | visits: IVisit[]; 70 | }; 71 | 72 | // TODO 73 | export interface IEditablePet extends INamedEntity { 74 | birthDate?: string; 75 | typeId?: IPetTypeId; 76 | } 77 | 78 | export interface IPetRequest { 79 | name: string; 80 | birthDate?: string; 81 | typeId: IPetTypeId; 82 | } 83 | 84 | export interface IOwner extends IPerson { 85 | 86 | address: string; 87 | city: string; 88 | telephone: string; 89 | pets: IPet[]; 90 | }; 91 | 92 | export interface ISpecialty extends INamedEntity { 93 | }; 94 | 95 | export interface IVet extends IPerson { 96 | specialties: ISpecialty[]; 97 | }; 98 | -------------------------------------------------------------------------------- /frontend/src/util/index.tsx: -------------------------------------------------------------------------------- 1 | import {IHttpMethod} from '../types'; 2 | import * as Cookies from 'es-cookie'; 3 | 4 | declare var __API_SERVER_URL__; 5 | const BACKEND_URL = (typeof __API_SERVER_URL__ === 'undefined' ? 'http://localhost:8080' : __API_SERVER_URL__); 6 | 7 | export const url = (path: string): string => `${BACKEND_URL}/${path}`; 8 | 9 | /** 10 | * path: relative PATH without host and port (i.e. '/api/123') 11 | * data: object that will be passed as request body 12 | * onSuccess: callback handler if request succeeded. Succeeded means it could technically be handled (i.e. valid json is returned) 13 | * regardless of the HTTP status code. 14 | */ 15 | export const submitForm = (method: IHttpMethod, path: string, data: any, onSuccess: (status: number, response: any) => void) => { 16 | const requestUrl = url(path); 17 | const token = Cookies.get('user'); 18 | 19 | const fetchParams = { 20 | method: method, 21 | headers: { 22 | 'Accept': 'application/json', 23 | 'Content-Type': 'application/json', 24 | 'Authorization': 'Bearer ' + token 25 | }, 26 | body: JSON.stringify(data) 27 | }; 28 | 29 | console.log('Submitting to ' + method + ' ' + requestUrl); 30 | return fetch(requestUrl, fetchParams) 31 | .then(response => response.status === 204 ? onSuccess(response.status, {}) : response.json().then(result => onSuccess(response.status, result))) 32 | .catch(error => console.log(error)); 33 | }; 34 | -------------------------------------------------------------------------------- /frontend/tests/__tests__/fetch-mock.js: -------------------------------------------------------------------------------- 1 | // Taken from: https://github.com/jefflau/jest-fetch-mock/blob/828b822c20aaa36d702458c971bde83544347c5b/src/index.js 2 | // Removed syntax not supported in Node 4.5 3 | 4 | /** 5 | * Copyright 2004-present Facebook. All Rights Reserved. 6 | * 7 | * Implements basic mock for the fetch interface use `whatwg-fetch` polyfill. 8 | * 9 | * See https://fetch.spec.whatwg.org/ 10 | */ 11 | 12 | require('whatwg-fetch'); 13 | 14 | const ActualResponse = Response; 15 | 16 | function ResponseWrapper(body, init) { 17 | if ( 18 | typeof body.constructor === 'function' && 19 | body.constructor.__isFallback 20 | ) { 21 | const response = new ActualResponse(null, init); 22 | response.body = body; 23 | 24 | const actualClone = response.clone; 25 | response.clone = () => { 26 | const clone = actualClone.call(response); 27 | const __body = body.tee(); 28 | const body1 = __body[0]; 29 | const body2 = __body[1]; 30 | response.body = body1; 31 | clone.body = body2; 32 | return clone; 33 | }; 34 | 35 | return response; 36 | } 37 | 38 | return new ActualResponse(body, init); 39 | } 40 | 41 | const fetch = jest.fn(); 42 | fetch.Headers = Headers; 43 | fetch.Response = ResponseWrapper; 44 | fetch.Request = Request; 45 | fetch.mockResponse = (body, init) => { 46 | fetch.mockImplementation( 47 | () => Promise.resolve(new ResponseWrapper(body, init)) 48 | ); 49 | }; 50 | 51 | fetch.mockResponseOnce = (body, init) => { 52 | fetch.mockImplementationOnce( 53 | () => Promise.resolve(new ResponseWrapper(body, init)) 54 | ); 55 | }; 56 | 57 | // fetch.mockResponses = (...responses) => { 58 | // responses.forEach(([body, init]) => { 59 | // fetch.mockImplementationOnce( 60 | // () => Promise.resolve(new ResponseWrapper(body, init)) 61 | // ); 62 | // }) 63 | // }; 64 | 65 | // Default mock is just a empty string. 66 | fetch.mockResponse(''); 67 | 68 | module.exports = fetch; 69 | -------------------------------------------------------------------------------- /frontend/tests/__tests__/util.test.tsx: -------------------------------------------------------------------------------- 1 | require('jest'); 2 | 3 | import { url, submitForm } from '../../src/util'; 4 | 5 | import * as React from 'react'; 6 | 7 | fetch = require('./fetch-mock'); 8 | const fetchMock: any = fetch; 9 | 10 | describe('util', () => { 11 | describe('url', () => { 12 | it('returns url with full path', () => { 13 | expect(url('xxx')).toBe('http://localhost:8080/xxx'); 14 | }); 15 | }); 16 | 17 | describe('submitForm', () => { 18 | beforeEach(() => fetchMock.mockClear()); 19 | 20 | it('submits all data', () => { 21 | fetchMock.mockResponse(JSON.stringify({ 'x': 'y' }), { status: 200 }); 22 | return submitForm('POST', '/some-enzyme', { name: 'Test' }, (status, response) => { 23 | // make sure request data is passed to fetch as expected 24 | expect(fetchMock.mock.calls.length).toBe(1); 25 | expect(fetchMock.mock.calls[0][0]).toBe('http://localhost:8080//some-enzyme'); 26 | expect(fetchMock.mock.calls[0][1].method).toBe('POST'); 27 | expect(fetchMock.mock.calls[0][1].body).toEqual(JSON.stringify({ name: 'Test' })); 28 | 29 | // make sure response from fetch ist corrently passed to the onSuccess callback 30 | expect(status).toBe(200); 31 | expect(response).toEqual({ 'x': 'y' }); 32 | }); 33 | }); 34 | 35 | it('works with No Content (204) responses', () => { 36 | fetchMock.mockResponse('', { status: 204 }); 37 | return submitForm('PUT', '/somewhere', { name: 'Test' }, (status, response) => { 38 | expect(fetchMock.mock.calls.length).toBe(1); 39 | expect(status).toBe(204); 40 | expect(response).toEqual({}); 41 | }); 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /frontend/tests/components/form/__tests__/Constraints.test.tsx: -------------------------------------------------------------------------------- 1 | require('jest'); 2 | 3 | import * as Constraints from '../../../../src/components/form/Constraints'; 4 | 5 | const afunc = ({a}) => {}; 6 | 7 | describe('Constraints', () => { 8 | describe('NotEmpty', () => { 9 | it('should return false for an empty string', () => { 10 | expect(Constraints.NotEmpty.validate('')).toBe(false); 11 | }); 12 | it('should return false for null', () => { 13 | expect(Constraints.NotEmpty.validate(null)).toBe(false); 14 | }); 15 | it('should return false for undefined', () => { 16 | expect(Constraints.NotEmpty.validate(undefined)).toBe(false); 17 | }); 18 | it('should return true for a string', () => { 19 | expect(Constraints.NotEmpty.validate('Hello World')).toBe(true); 20 | }); 21 | }); 22 | 23 | describe('Digits', () => { 24 | it('should return true for a valid length', () => { 25 | expect(Constraints.Digits(3).validate('123')).toBe(true); 26 | expect(Constraints.Digits(3).validate('1')).toBe(true); 27 | }); 28 | it('should return false for a string containing non-digits', () => { 29 | expect(Constraints.Digits(3).validate('1x3')).toBe(false); 30 | }); 31 | it('should return false for a number that is too long', () => { 32 | expect(Constraints.Digits(3).validate('1234')).toBe(false); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /frontend/tests/components/form/__tests__/Input.test.tsx: -------------------------------------------------------------------------------- 1 | require('jest'); 2 | import * as React from 'react'; 3 | import { shallow } from 'enzyme'; 4 | 5 | import { NotEmpty } from '../../../../src/components/form/Constraints'; 6 | import FieldFeedbackPanel from '../../../../src/components/form/FieldFeedbackPanel'; 7 | import { IInputChangeHandler, IError, IConstraint } from '../../../../src/types'; 8 | 9 | import Input from '../../../../src/components/form/Input'; 10 | 11 | describe('Input', () => { 12 | 13 | const onChange = (name, value, error) => { 14 | onChangeResult = { name, value, error }; 15 | }; 16 | 17 | let object = null; 18 | let onChangeResult = null; 19 | 20 | beforeEach(() => { 21 | object = { 22 | myField: 'blabla' 23 | }; 24 | 25 | onChangeResult = null; 26 | }); 27 | 28 | it('should render correctly without field error', () => { 29 | const error = { 30 | fieldErrors: {} 31 | }; 32 | 33 | const input = shallow(); 39 | 40 | // Make sure label is rendered correctly 41 | expect(input.find('.control-label').text()).toBe('My Field'); 42 | 43 | // Make sure input field's value is correct 44 | expect(input.find('input').props().value).toBe('blabla'); 45 | 46 | // we don't have any errors 47 | expect(input.find('.has-error').length).toBe(0); 48 | expect(input.find(FieldFeedbackPanel).props().valid).toBe(true); 49 | 50 | // change to new value 51 | input.find('input').simulate('change', { target: { value: 'My new value' } }); 52 | 53 | // make sure callback is called 54 | expect(onChangeResult).toBeTruthy(); 55 | expect(onChangeResult.name).toBe('myField'); 56 | expect(onChangeResult.value).toBe('My new value'); 57 | expect(onChangeResult.error).toBeFalsy(); 58 | }); 59 | 60 | it('should render correctly with field error', () => { 61 | 62 | const error = { 63 | fieldErrors: { 64 | myField: { 65 | field: 'myField', 66 | message: 'There was an error' 67 | } 68 | } 69 | }; 70 | 71 | const input = shallow(); 77 | 78 | // Make sure label is rendered correctly 79 | expect(input.find('.control-label').text()).toBe('My Field'); 80 | 81 | // Make sure input field's value is correct 82 | expect(input.find('input').props().value).toBe('blabla'); 83 | 84 | // we don't have any errors 85 | expect(input.find('.has-error').length).toBe(1); 86 | expect(input.find(FieldFeedbackPanel).props().valid).toBe(false); 87 | expect(input.find(FieldFeedbackPanel).props().fieldError).toBe(error.fieldErrors.myField); 88 | }); 89 | 90 | it('should checked constrains on input change', () => { 91 | 92 | const error = { 93 | fieldErrors: {} 94 | }; 95 | 96 | const constraint: IConstraint = { 97 | message: 'Invalid', 98 | validate: jest.fn() 99 | }; 100 | 101 | const input = shallow(); 108 | 109 | input.find('input').simulate('change', { target: { value: 'My new value' } }); 110 | expect(constraint.validate).toHaveBeenCalledWith('My new value'); 111 | }); 112 | }); 113 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "module": "commonjs", 5 | "noImplicitAny": false, 6 | "preserveConstEnums": true, 7 | "removeComments": true, 8 | "target": "ES6", 9 | "allowJs": true, 10 | "outDir": "public/dist", 11 | "sourceMap": true 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | "typings/browser.d.ts", 16 | "typings/browser", 17 | "tests" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "class-name": true, 4 | "comment-format": [ 5 | true, 6 | "check-space" 7 | ], 8 | "indent": [ 9 | true, 10 | "spaces" 11 | ], 12 | "no-duplicate-variable": true, 13 | "no-eval": true, 14 | "no-internal-module": true, 15 | "no-trailing-whitespace": true, 16 | "no-unsafe-finally": true, 17 | "no-var-keyword": true, 18 | "one-line": [ 19 | true, 20 | "check-open-brace", 21 | "check-whitespace" 22 | ], 23 | "quotemark": [ 24 | true, 25 | "single" 26 | ], 27 | "semicolon": [ 28 | true, 29 | "always" 30 | ], 31 | "triple-equals": [ 32 | true, 33 | "allow-null-check" 34 | ], 35 | "typedef-whitespace": [ 36 | true, 37 | { 38 | "call-signature": "nospace", 39 | "index-signature": "nospace", 40 | "parameter": "nospace", 41 | "property-declaration": "nospace", 42 | "variable-declaration": "nospace" 43 | } 44 | ], 45 | "variable-name": [ 46 | true, 47 | "ban-keywords" 48 | ], 49 | "whitespace": [ 50 | true, 51 | "check-branch", 52 | "check-decl", 53 | "check-operator", 54 | "check-separator", 55 | "check-type" 56 | ] 57 | } 58 | } -------------------------------------------------------------------------------- /frontend/typings.json: -------------------------------------------------------------------------------- 1 | { 2 | "globalDependencies": { 3 | "classnames": "registry:dt/classnames#0.0.0+20160316155526", 4 | "enzyme": "registry:dt/enzyme#2.5.1+20161019142319", 5 | "jest": "registry:dt/jest#15.1.1+20160919141445", 6 | "node": "registry:dt/node#6.0.0+20160921192128", 7 | "react": "registry:dt/react#0.14.0+20161008064207", 8 | "react-dom": "registry:dt/react-dom#0.14.0+20160412154040", 9 | "react-router/history": "registry:dt/react-router/history#2.0.0+20160830150755", 10 | "whatwg-fetch": "registry:dt/whatwg-fetch#0.0.0+20160829180742", 11 | "whatwg-streams": "registry:dt/whatwg-streams#0.0.0+20160829180742" 12 | }, 13 | "dependencies": { 14 | "react-router": "registry:npm/react-router#2.4.0+20160915183637" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | const port = process.env.PORT || 3000; 5 | 6 | const entries = [ 7 | 'webpack-dev-server/client?http://localhost:' + port, 8 | 'webpack/hot/only-dev-server', 9 | 'react-hot-loader/patch', 10 | './src/main.tsx' 11 | ]; 12 | 13 | 14 | module.exports = { 15 | devtool: 'source-map', 16 | entry: entries, 17 | output: { 18 | path: path.join(__dirname, 'public/dist/'), 19 | filename: 'bundle.js', 20 | publicPath: '/dist/' 21 | /* redbox-react/README.md */ 22 | // ,devtoolModuleFilenameTemplate: '/[absolute-resource-path]' 23 | }, 24 | plugins: [ 25 | new webpack.HotModuleReplacementPlugin(), 26 | new webpack.DefinePlugin({ 27 | __API_SERVER_URL__: JSON.stringify('http://localhost:8080') 28 | }) 29 | ], 30 | resolve: { 31 | extensions: ['', '.ts', '.tsx', '.js'] 32 | }, 33 | resolveLoader: { 34 | 'fallback': path.join(__dirname, 'node_modules') 35 | }, 36 | module: { 37 | preLoaders: [ 38 | { 39 | test: /\.tsx?$/, 40 | loader: 'tslint', 41 | include: path.join(__dirname, 'src') 42 | } 43 | ], 44 | loaders: [ 45 | { 46 | test: /\.css$/, 47 | loader: 'style!css' 48 | }, 49 | { 50 | test: /\.less$/, 51 | loader: 'style!css!less', 52 | include: path.join(__dirname, 'src/styles') 53 | }, 54 | { 55 | test: /\.(png|jpg|gif)$/, 56 | loader: 'url?limit=25000' 57 | }, 58 | { 59 | test: /\.(eot|svg|ttf|woff|woff2)$/, 60 | loader: 'file?name=public/fonts/[name].[ext]' 61 | }, 62 | 63 | { 64 | test: /\.tsx?$/, 65 | loader: 'babel!ts', 66 | include: path.join(__dirname, 'src') 67 | } 68 | ] 69 | }, 70 | tslint: { 71 | emitErrors: true, 72 | failOnHint: true 73 | } 74 | }; 75 | -------------------------------------------------------------------------------- /frontend/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | const port = process.env.PORT || 3000; 5 | 6 | const entries = [ 7 | './src/main.tsx' 8 | ]; 9 | 10 | 11 | module.exports = { 12 | devtool: 'source-map', 13 | entry: entries, 14 | output: { 15 | path: path.join(__dirname, 'public/dist/'), 16 | filename: 'bundle.js', 17 | publicPath: '/dist/' 18 | /* redbox-react/README.md */ 19 | // ,devtoolModuleFilenameTemplate: '/[absolute-resource-path]' 20 | }, 21 | plugins: [ 22 | new webpack.DefinePlugin({ 23 | 'process.env': { 24 | 'NODE_ENV': JSON.stringify('production'), 25 | }, 26 | __API_SERVER_URL__: JSON.stringify('http://application:8080') 27 | }) 28 | ], 29 | resolve: { 30 | extensions: ['', '.ts', '.tsx', '.js'] 31 | }, 32 | resolveLoader: { 33 | 'fallback': path.join(__dirname, 'node_modules') 34 | }, 35 | module: { 36 | preLoaders: [ 37 | { 38 | test: /\.tsx?$/, 39 | loader: 'tslint', 40 | include: path.join(__dirname, 'src') 41 | } 42 | ], 43 | loaders: [ 44 | { 45 | test: /\.css$/, 46 | loader: 'style!css' 47 | }, 48 | { 49 | test: /\.less$/, 50 | loader: 'style!css!less', 51 | include: path.join(__dirname, 'src/styles') 52 | }, 53 | { 54 | test: /\.(png|jpg)$/, 55 | loader: 'url?limit=25000' 56 | }, 57 | { 58 | test: /\.(eot|svg|ttf|woff|woff2)$/, 59 | loader: 'file?name=public/fonts/[name].[ext]' 60 | }, 61 | 62 | { 63 | test: /\.tsx?$/, 64 | loader: 'babel!ts', 65 | include: path.join(__dirname, 'src') 66 | } 67 | ] 68 | }, 69 | tslint: { 70 | emitErrors: true, 71 | failOnHint: true 72 | } 73 | }; 74 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.daemon=true 2 | org.gradle.caching=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/igor-dmitriev/spring-petclinic-reactjs-ui-tests/503ee48c5c0ef4881452eac34129eefcebe21a34/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.0-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS="-Xmx64m" 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'spring-petclinic-reactjs-ui-tests' 2 | include 'application' 3 | include 'ui-tests' 4 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | # Required metadata 2 | sonar.projectKey=java-sonar-runner-simple 3 | sonar.projectName=Simple Java project analyzed with the SonarQube Runner 4 | sonar.projectVersion=1.0 5 | 6 | # Comma-separated paths to directories with sources (required) 7 | sonar.sources=src 8 | 9 | # Language 10 | sonar.language=java 11 | 12 | # Encoding of the source files 13 | sonar.sourceEncoding=UTF-8 -------------------------------------------------------------------------------- /ui-tests/build.gradle: -------------------------------------------------------------------------------- 1 | dependencies { 2 | testCompile 'com.codeborne:selenide:5.1.0' 3 | testCompile 'org.testcontainers:selenium:1.10.6' 4 | testCompile 'com.jayway.restassured:rest-assured:2.9.0' 5 | testCompile 'com.github.database-rider:rider-core:1.4.0' 6 | testCompile 'com.github.database-rider:rider-junit5:1.5.2' 7 | 8 | compile 'org.postgresql:postgresql' 9 | compile 'com.zaxxer:HikariCP' 10 | compile 'org.springframework.boot:spring-boot-starter-logging' 11 | } 12 | 13 | task testUi() { 14 | doFirst { 15 | project.ext."uiTests" = true 16 | } 17 | dependsOn('createJacocoCoverageFile', ':application:assembleApp') 18 | finalizedBy(':ui-tests:test') 19 | } 20 | 21 | task createJacocoCoverageFile() { 22 | def f = new File("${project.buildDir}/jacoco") 23 | f.mkdirs() 24 | file("${f.getAbsolutePath()}/test.exec").createNewFile() 25 | } 26 | 27 | test { 28 | onlyIf { 29 | project.hasProperty("uiTests") 30 | } 31 | } 32 | 33 | sonarqube { 34 | skipProject = true 35 | } -------------------------------------------------------------------------------- /ui-tests/src/test/java/org/springframework/samples/petclinic/steps/MainSteps.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.steps; 2 | 3 | import com.codeborne.selenide.Selenide; 4 | import com.codeborne.selenide.WebDriverRunner; 5 | import com.jayway.restassured.RestAssured; 6 | 7 | import org.apache.http.HttpStatus; 8 | import org.openqa.selenium.Cookie; 9 | 10 | import java.net.URLEncoder; 11 | import java.nio.charset.StandardCharsets; 12 | 13 | import lombok.RequiredArgsConstructor; 14 | import lombok.SneakyThrows; 15 | 16 | @RequiredArgsConstructor 17 | public class MainSteps { 18 | private static final String TEST_USERNAME = "test"; 19 | private static final String TEST_PASSWORD = "testovich"; 20 | 21 | private final String homePath; 22 | private final String apiLoginPath; 23 | 24 | @SneakyThrows 25 | public void loginUsingApi(String username, String password) { 26 | String token = RestAssured.given() 27 | .body("{\n" + 28 | "\"username\":\"" + username + "\",\n" + 29 | "\"password\":\"" + password + "\"\n" + 30 | "}\n") 31 | .post(apiLoginPath) 32 | .then() 33 | .assertThat() 34 | .statusCode(HttpStatus.SC_OK) 35 | .and() 36 | .extract() 37 | .response() 38 | .jsonPath() 39 | .get("token"); 40 | 41 | String cookieValue = URLEncoder.encode(token, StandardCharsets.UTF_8.toString()); 42 | 43 | Cookie cookie = new Cookie("user", cookieValue, "/"); 44 | WebDriverRunner.getWebDriver().manage().addCookie(cookie); 45 | } 46 | 47 | public void fastLogin() { 48 | Selenide.open(homePath + "/test.html"); 49 | loginUsingApi(TEST_USERNAME, TEST_PASSWORD); 50 | } 51 | 52 | public void openFindOwnersTab() { 53 | Selenide.open("/#/owners/list"); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /ui-tests/src/test/java/org/springframework/samples/petclinic/steps/OwnersSteps.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.steps; 2 | 3 | import com.codeborne.selenide.ElementsCollection; 4 | 5 | import static com.codeborne.selenide.Condition.exactText; 6 | import static com.codeborne.selenide.Selectors.byText; 7 | import static com.codeborne.selenide.Selenide.$; 8 | import static com.codeborne.selenide.Selenide.$$; 9 | 10 | public class OwnersSteps { 11 | private static final int NAME_COLUMN_INDEX = 0; 12 | private static final int ADDRESS_COLUMN_INDEX = 1; 13 | private static final int CITY_COLUMN_INDEX = 2; 14 | private static final int TELEPHONE_COLUMN_INDEX = 3; 15 | private static final int PETS_COLUMN_INDEX = 4; 16 | 17 | public void searchOwnersByLastName(String lastName) { 18 | $("#owner-last-name-input").val(lastName); 19 | $(byText("Find Owner")).click(); 20 | } 21 | 22 | public void assertOwnersTableHasSize(int expectedSize) { 23 | $$("#owners-table tbody tr").shouldHaveSize(expectedSize); 24 | } 25 | 26 | public void assertOwnersTableHasData(int row, String expectedFullName, String expectedAddress, String expectedCity, String expectedTelephone, String expectedPets) { 27 | ElementsCollection owners = $$("#owners-table tbody tr:nth-child(" + row + ") td"); 28 | owners.get(NAME_COLUMN_INDEX).shouldHave(exactText(expectedFullName)); 29 | owners.get(ADDRESS_COLUMN_INDEX).shouldHave(exactText(expectedAddress)); 30 | owners.get(CITY_COLUMN_INDEX).shouldHave(exactText(expectedCity)); 31 | owners.get(TELEPHONE_COLUMN_INDEX).shouldHave(exactText(expectedTelephone)); 32 | owners.get(PETS_COLUMN_INDEX).shouldHave(exactText(expectedPets)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ui-tests/src/test/java/org/springframework/samples/petclinic/tests/CiUiTest.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.tests; 2 | 3 | import com.codeborne.selenide.Configuration; 4 | import com.codeborne.selenide.WebDriverRunner; 5 | import com.codeborne.selenide.logevents.SelenideLogger; 6 | 7 | import org.junit.jupiter.api.AfterEach; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.extension.RegisterExtension; 10 | import org.openqa.selenium.remote.DesiredCapabilities; 11 | import org.springframework.samples.petclinic.util.JmxUtil; 12 | import org.springframework.samples.petclinic.util.TestContainerUtil; 13 | import org.springframework.samples.petclinic.util.VideoRecordingExtension; 14 | import org.testcontainers.containers.BrowserWebDriverContainer; 15 | import org.testcontainers.containers.DockerComposeContainer; 16 | import org.testcontainers.containers.wait.strategy.Wait; 17 | 18 | import java.io.File; 19 | import java.time.Duration; 20 | 21 | import io.qameta.allure.selenide.AllureSelenide; 22 | 23 | public abstract class CiUiTest extends TestDataSource { 24 | private static DockerComposeContainer dockerComposeContainer = new DockerComposeContainer(new File("../docker-compose.yml")) 25 | .withLocalCompose(true) 26 | .withExposedService("application_1", 8080, Wait.forListeningPort().withStartupTimeout(Duration.ofMinutes(3))) 27 | .withExposedService("postgres_1", 5432); 28 | 29 | private static BrowserWebDriverContainer chrome; 30 | 31 | static { 32 | dockerComposeContainer.start(); 33 | chrome = new BrowserWebDriverContainer() 34 | .withCapabilities(DesiredCapabilities.chrome()); 35 | TestContainerUtil.linkContainersNetworks(dockerComposeContainer, chrome, "application_1"); 36 | chrome.start(); 37 | Configuration.baseUrl = "http://application:8080"; 38 | WebDriverRunner.setWebDriver(chrome.getWebDriver()); 39 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 40 | JmxUtil.generateJacocoDump(); 41 | dockerComposeContainer.stop(); 42 | chrome.stop(); 43 | })); 44 | } 45 | 46 | @BeforeEach 47 | void setUp() { 48 | SelenideLogger.addListener("allure", new AllureSelenide().savePageSource(false)); // tracing 49 | } 50 | 51 | @AfterEach 52 | void tearDown() { 53 | SelenideLogger.removeListener("allure"); 54 | } 55 | 56 | @RegisterExtension 57 | public static VideoRecordingExtension videoRecordingExtension = new VideoRecordingExtension(chrome); 58 | 59 | @Override 60 | protected String jdbcHost() { 61 | return dockerComposeContainer.getServiceHost("postgres_1", 5432); 62 | } 63 | 64 | @Override 65 | protected int jdbcPort() { 66 | return dockerComposeContainer.getServicePort("postgres_1", 5432); 67 | } 68 | 69 | protected String homePath() { 70 | return "http://application:8080"; 71 | } 72 | 73 | protected String apiLoginPath() { 74 | return "http://localhost:8070/login"; 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /ui-tests/src/test/java/org/springframework/samples/petclinic/tests/LocalUiTest.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.tests; 2 | 3 | import com.codeborne.selenide.Configuration; 4 | import com.codeborne.selenide.logevents.SelenideLogger; 5 | 6 | import org.junit.jupiter.api.AfterEach; 7 | import org.junit.jupiter.api.BeforeAll; 8 | import org.junit.jupiter.api.BeforeEach; 9 | 10 | import io.qameta.allure.selenide.AllureSelenide; 11 | 12 | public abstract class LocalUiTest extends TestDataSource { 13 | @BeforeAll 14 | public static void setUpClass() { 15 | Configuration.baseUrl = "http://localhost:3000"; 16 | } 17 | 18 | @BeforeEach 19 | void setUp() { 20 | SelenideLogger.addListener("allure", new AllureSelenide().savePageSource(false)); // tracing 21 | } 22 | 23 | @AfterEach 24 | void tearDown() { 25 | SelenideLogger.removeListener("allure"); 26 | } 27 | 28 | @Override 29 | protected String jdbcHost() { 30 | return "127.0.0.1"; 31 | } 32 | 33 | @Override 34 | protected int jdbcPort() { 35 | return 5432; 36 | } 37 | 38 | protected String homePath() { 39 | return "http://localhost:3000"; 40 | } 41 | 42 | protected String apiLoginPath() { 43 | return "http://localhost:8080/login"; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ui-tests/src/test/java/org/springframework/samples/petclinic/tests/TestDataSource.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.tests; 2 | 3 | import com.p6spy.engine.spy.P6DataSource; 4 | import com.zaxxer.hikari.HikariConfig; 5 | import com.zaxxer.hikari.HikariDataSource; 6 | 7 | import javax.sql.DataSource; 8 | 9 | import static java.lang.String.format; 10 | 11 | public abstract class TestDataSource { 12 | protected DataSource dataSource() { 13 | HikariConfig hikariConfig = new HikariConfig(); 14 | hikariConfig.setJdbcUrl(format("jdbc:postgresql://%s:%d/petclinic", jdbcHost(), jdbcPort())); 15 | hikariConfig.setUsername("petclinic"); 16 | hikariConfig.setPassword("q9KqUiu2vqnAuf"); 17 | hikariConfig.setConnectionTestQuery("SELECT 1"); 18 | hikariConfig.setMinimumIdle(3); 19 | hikariConfig.setMaximumPoolSize(10); 20 | return new P6DataSource(new HikariDataSource(hikariConfig)); 21 | } 22 | 23 | protected abstract String jdbcHost(); 24 | 25 | protected abstract int jdbcPort(); 26 | } 27 | -------------------------------------------------------------------------------- /ui-tests/src/test/java/org/springframework/samples/petclinic/tests/pets/PetsPageTest.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.tests.pets; 2 | 3 | import com.codeborne.selenide.ElementsCollection; 4 | import com.codeborne.selenide.Selenide; 5 | import com.github.database.rider.core.api.connection.ConnectionHolder; 6 | import com.github.database.rider.core.api.dataset.DataSet; 7 | import com.github.database.rider.core.api.dataset.SeedStrategy; 8 | import com.github.database.rider.junit5.DBUnitExtension; 9 | 10 | import org.junit.jupiter.api.Test; 11 | import org.junit.jupiter.api.extension.ExtendWith; 12 | import org.springframework.samples.petclinic.steps.MainSteps; 13 | import org.springframework.samples.petclinic.tests.CiUiTest; 14 | 15 | import static com.codeborne.selenide.Condition.text; 16 | import static com.codeborne.selenide.Selectors.byText; 17 | import static com.codeborne.selenide.Selenide.$; 18 | import static com.codeborne.selenide.Selenide.$$; 19 | 20 | @ExtendWith(DBUnitExtension.class) 21 | public class PetsPageTest extends CiUiTest { 22 | private MainSteps mainSteps = new MainSteps(homePath(), apiLoginPath()); 23 | public ConnectionHolder connectionHolder = () -> dataSource().getConnection(); 24 | 25 | @Test 26 | @DataSet( 27 | value = { 28 | "datasets/test_user.xml", 29 | "datasets/pets/owner-to-create-pet.xml" 30 | }, 31 | executeScriptsBefore = "datasets/cleanup.sql", 32 | strategy = SeedStrategy.INSERT 33 | ) 34 | void shouldCreatePet() { 35 | /*open("/"); 36 | $("#username").val("test"); 37 | $("#password").val("testovich"); 38 | $("#login-button").click(); 39 | $(linkText("FIND OWNERS")).click(); 40 | 41 | $(linkText("Jean Coleman")).click(); 42 | $(byText("Add New Pet")).click(); 43 | */ 44 | mainSteps.fastLogin(); 45 | Selenide.open("/#/owners/1000/pets/new"); // fast open 46 | 47 | $("#name").val("Dan"); 48 | $("#birth-date").val("2019-02-02"); 49 | $(byText("Jean")).click(); 50 | $("#type").selectOption("dog"); 51 | $("#add-pet-button").click(); 52 | 53 | ElementsCollection pets = $$("#pets-and-visits-table tbody tr:nth-child(1) td dl dd"); 54 | pets.get(0).shouldHave(text("Dan")); 55 | pets.get(1).shouldHave(text("2019/02/02")); 56 | pets.get(2).shouldHave(text("dog")); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /ui-tests/src/test/java/org/springframework/samples/petclinic/util/JmxUtil.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.util; 2 | 3 | import javax.management.MBeanServerConnection; 4 | import javax.management.ObjectName; 5 | import javax.management.remote.JMXConnector; 6 | import javax.management.remote.JMXConnectorFactory; 7 | import javax.management.remote.JMXServiceURL; 8 | 9 | import lombok.SneakyThrows; 10 | 11 | public class JmxUtil { 12 | @SneakyThrows 13 | public static void generateJacocoDump() { 14 | JMXServiceURL jmxURL = new JMXServiceURL("service:jmx:rmi:///jndi/rmi://localhost:9999/jmxrmi"); 15 | try (JMXConnector jmxc = JMXConnectorFactory.connect(jmxURL)) { 16 | MBeanServerConnection connection = jmxc.getMBeanServerConnection(); 17 | ObjectName objName = new ObjectName("org.jacoco:type=Runtime"); 18 | connection.invoke(objName, "dump", 19 | new Object[]{true}, 20 | new String[]{Boolean.TYPE.getName()} 21 | ); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ui-tests/src/test/java/org/springframework/samples/petclinic/util/LoginUtil.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.util; 2 | 3 | import com.codeborne.selenide.Selenide; 4 | import com.codeborne.selenide.WebDriverRunner; 5 | import com.jayway.restassured.RestAssured; 6 | 7 | import org.apache.http.HttpStatus; 8 | import org.openqa.selenium.Cookie; 9 | 10 | import java.net.URLEncoder; 11 | import java.nio.charset.StandardCharsets; 12 | 13 | import lombok.RequiredArgsConstructor; 14 | import lombok.SneakyThrows; 15 | 16 | @RequiredArgsConstructor 17 | public class LoginUtil { 18 | 19 | private static final String TEST_USERNAME = "test"; 20 | private static final String TEST_PASSWORD = "testovich"; 21 | 22 | private final String homePath; 23 | private final String apiLoginPath; 24 | 25 | @SneakyThrows 26 | public void loginUsingApi(String username, String password) { 27 | String token = RestAssured.given() 28 | .body("{\n" + 29 | "\"username\":\"" + username + "\",\n" + 30 | "\"password\":\"" + password + "\"\n" + 31 | "}\n") 32 | .post(apiLoginPath) 33 | .then() 34 | .assertThat() 35 | .statusCode(HttpStatus.SC_OK) 36 | .and() 37 | .extract() 38 | .response() 39 | .jsonPath() 40 | .get("token"); 41 | 42 | String cookieValue = URLEncoder.encode(token, StandardCharsets.UTF_8.toString()); 43 | 44 | Cookie cookie = new Cookie("user", cookieValue, "/"); 45 | WebDriverRunner.getWebDriver().manage().addCookie(cookie); 46 | } 47 | 48 | public void fastLogin() { 49 | Selenide.open(homePath + "/test.html"); 50 | loginUsingApi(TEST_USERNAME, TEST_PASSWORD); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ui-tests/src/test/java/org/springframework/samples/petclinic/util/TestContainerUtil.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.util; 2 | 3 | import com.github.dockerjava.api.DockerClient; 4 | import com.github.dockerjava.api.command.InspectContainerResponse; 5 | 6 | import org.junit.runner.Description; 7 | import org.junit.runners.model.Statement; 8 | import org.testcontainers.DockerClientFactory; 9 | import org.testcontainers.containers.DockerComposeContainer; 10 | import org.testcontainers.containers.GenericContainer; 11 | import org.testcontainers.containers.Network; 12 | import org.testcontainers.containers.SocatContainer; 13 | import org.testcontainers.containers.traits.LinkableContainer; 14 | 15 | import java.lang.reflect.Field; 16 | 17 | import lombok.SneakyThrows; 18 | 19 | public class TestContainerUtil { 20 | public static void linkContainersNetworks(DockerComposeContainer composeContainer, GenericContainer genericContainer, String applicationName) { 21 | String network = findApplicationServiceNetwork(applicationName, composeContainer); 22 | Network tcNet = createNetwork(network); 23 | genericContainer.withNetwork(tcNet); 24 | } 25 | 26 | private static Network createNetwork(String network) { 27 | return new Network() { 28 | @Override 29 | public String getId() { 30 | return network; 31 | } 32 | 33 | @Override 34 | public void close() { 35 | 36 | } 37 | 38 | @Override 39 | public Statement apply(Statement base, Description description) { 40 | return null; 41 | } 42 | }; 43 | } 44 | 45 | @SneakyThrows 46 | private static String findApplicationServiceNetwork(String applicationName, DockerComposeContainer compose) { 47 | Field ambassadorContainerField = compose.getClass().getDeclaredField("ambassadorContainer"); 48 | ambassadorContainerField.setAccessible(true); 49 | SocatContainer ambassadorContainer = (SocatContainer) ambassadorContainerField.get(compose); 50 | LinkableContainer linkedContainer = ambassadorContainer.getLinkedContainers().get(applicationName); 51 | 52 | DockerClient client = DockerClientFactory.instance().client(); 53 | InspectContainerResponse containerInfo = client.inspectContainerCmd(linkedContainer.getContainerName()).exec(); 54 | String networkName = containerInfo.getNetworkSettings().getNetworks().keySet().stream().findFirst().get(); 55 | return client.inspectNetworkCmd().withNetworkId(networkName).exec().getId(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ui-tests/src/test/java/org/springframework/samples/petclinic/util/VideoRecordingExtension.java: -------------------------------------------------------------------------------- 1 | package org.springframework.samples.petclinic.util; 2 | 3 | import com.codeborne.selenide.Configuration; 4 | 5 | import org.junit.jupiter.api.extension.AfterTestExecutionCallback; 6 | import org.junit.jupiter.api.extension.BeforeTestExecutionCallback; 7 | import org.junit.jupiter.api.extension.ExtensionContext; 8 | import org.testcontainers.containers.GenericContainer; 9 | import org.testcontainers.containers.VncRecordingContainer; 10 | 11 | import java.io.File; 12 | import java.time.LocalDateTime; 13 | import java.time.format.DateTimeFormatter; 14 | 15 | import static java.time.format.DateTimeFormatter.ofPattern; 16 | 17 | public class VideoRecordingExtension implements BeforeTestExecutionCallback, AfterTestExecutionCallback { 18 | 19 | private final GenericContainer targetContainer; 20 | 21 | private VncRecordingContainer vncRecordingContainer; 22 | private static final String FILE_NAME_PATTERN = "%s-%s-%s.flv"; 23 | private static final DateTimeFormatter DATE_TIME_FORMATTER = ofPattern("dd-MM-YYYY__HH-mm-ss"); 24 | 25 | public VideoRecordingExtension(GenericContainer targetContainer) { 26 | this.targetContainer = targetContainer; 27 | } 28 | 29 | @Override 30 | public void beforeTestExecution(ExtensionContext context) { 31 | vncRecordingContainer = new VncRecordingContainer(targetContainer) 32 | .withVncPassword("secret") 33 | .withVncPort(5900); 34 | vncRecordingContainer.start(); 35 | } 36 | 37 | @Override 38 | public void afterTestExecution(ExtensionContext context) { 39 | try { 40 | context.getExecutionException().ifPresent(throwable -> handleFailedTest(context)); 41 | } finally { 42 | vncRecordingContainer.stop(); 43 | } 44 | } 45 | 46 | private void handleFailedTest(ExtensionContext context) { 47 | String fileName = String.format( 48 | FILE_NAME_PATTERN, 49 | context.getRequiredTestClass().getSimpleName(), 50 | context.getRequiredTestMethod().getName(), 51 | DATE_TIME_FORMATTER.format(LocalDateTime.now()) 52 | ); 53 | File path = new File(Configuration.reportsFolder + "/" + fileName); 54 | vncRecordingContainer.saveRecordingToFile(path); 55 | } 56 | } -------------------------------------------------------------------------------- /ui-tests/src/test/resources/datasets/cleanup.sql: -------------------------------------------------------------------------------- 1 | truncate table users cascade; 2 | truncate table specialties cascade; 3 | truncate table pet_owners cascade; 4 | truncate table types cascade; 5 | truncate table vets cascade; 6 | truncate table pets cascade; 7 | truncate table visits cascade; 8 | truncate table vet_specialties cascade; -------------------------------------------------------------------------------- /ui-tests/src/test/resources/datasets/owners/owner-to-edit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /ui-tests/src/test/resources/datasets/owners/owner-to-search.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ui-tests/src/test/resources/datasets/pets/owner-to-create-pet.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /ui-tests/src/test/resources/datasets/test_user.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /ui-tests/src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | %d{HH:mm:ss.SSS} [%thread] %-5level %logger - %msg%n 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /ui-tests/src/test/resources/spy.properties: -------------------------------------------------------------------------------- 1 | driverlist=org.postgresql.Driver 2 | appender=com.p6spy.engine.spy.appender.StdoutLogger 3 | excludecategories=info,debug,result,resultset 4 | databaseDialectDateFormat=YYYY-MM-dd HH:mm:ss -------------------------------------------------------------------------------- /ui-tests/src/test/resources/testcontainers.properties: -------------------------------------------------------------------------------- 1 | checks.disable=true --------------------------------------------------------------------------------