FIX CONFLICT I18N

This commit is contained in:
AewInformatica 2021-01-24 23:02:11 -03:00
commit f0eb91826f
61 changed files with 1463 additions and 1329 deletions

View file

@ -10,3 +10,12 @@ indent_style = space
[*.{java,xml}] [*.{java,xml}]
indent_size = 4 indent_size = 4
trim_trailing_whitespace = true trim_trailing_whitespace = true
indent_style = tab
tab_width = 4
[{pom,wro}.xml]
indent_size = 2
indent_style = space
[*.{html,sql,less}]
indent_size = 2

6
.gitignore vendored
View file

@ -8,9 +8,5 @@ target/*
*.iml *.iml
/target /target
.sts4-cache/ .sts4-cache/
.vscode/* .vscode
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
_site/ _site/

View file

@ -5,7 +5,7 @@
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0'
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@ -20,7 +20,7 @@ import java.util.Properties;
public class MavenWrapperDownloader { public class MavenWrapperDownloader {
private static final String WRAPPER_VERSION = "0.5.5"; private static final String WRAPPER_VERSION = "0.5.6";
/** /**
* Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
*/ */

Binary file not shown.

View file

@ -1,2 +1,3 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.2/apache-maven-3.6.2-bin.zip distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar

26
.vscode/launch.json vendored
View file

@ -1,26 +0,0 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "java",
"name": "Debug (Launch)-PetClinicApplication<spring-petclinic>",
"request": "launch",
"cwd": "${workspaceFolder}",
"console": "internalConsole",
"stopOnEntry": false,
"mainClass": "org.springframework.samples.petclinic.PetClinicApplication",
"projectName": "spring-petclinic",
"args": ""
},
{
"type": "java",
"name": "Debug (Attach)",
"request": "attach",
"hostName": "localhost",
"port": 0
}
]
}

View file

@ -1,3 +0,0 @@
{
"java.configuration.updateBuildConfiguration": "interactive"
}

19
.vscode/tasks.json vendored
View file

@ -1,19 +0,0 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "verify",
"type": "shell",
"command": "mvn -B verify",
"group": "build"
},
{
"label": "test",
"type": "shell",
"command": "mvn -B test",
"group": "test"
}
]
}

View file

@ -3,7 +3,10 @@ mysql:
ports: ports:
- "3306:3306" - "3306:3306"
environment: environment:
- MYSQL_ROOT_PASSWORD=petclinic - MYSQL_ROOT_PASSWORD=
- MYSQL_ALLOW_EMPTY_PASSWORD=true
- MYSQL_USER=petclinic
- MYSQL_PASSWORD=petclinic
- MYSQL_DATABASE=petclinic - MYSQL_DATABASE=petclinic
volumes: volumes:
- "./conf.d:/etc/mysql/conf.d:ro" - "./conf.d:/etc/mysql/conf.d:ro"

10
mvnw vendored
View file

@ -8,7 +8,7 @@
# "License"); you may not use this file except in compliance # "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at # with the License. You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0 # https://www.apache.org/licenses/LICENSE-2.0'
# #
# Unless required by applicable law or agreed to in writing, # Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an # software distributed under the License is distributed on an
@ -19,7 +19,7 @@
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# ---------------------------------------------------------------------------- # ----------------------------------------------------------------------------
# Maven2 Start Up Batch script # Maven Start Up Batch script
# #
# Required ENV vars: # Required ENV vars:
# ------------------ # ------------------
@ -212,9 +212,9 @@ else
echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
fi fi
if [ -n "$MVNW_REPOURL" ]; then if [ -n "$MVNW_REPOURL" ]; then
jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
else else
jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
fi fi
while IFS="=" read key value; do while IFS="=" read key value; do
case "$key" in (wrapperUrl) jarUrl="$value"; break ;; case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
@ -246,7 +246,7 @@ else
else else
curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
fi fi
else else
if [ "$MVNW_VERBOSE" = true ]; then if [ "$MVNW_VERBOSE" = true ]; then
echo "Falling back to using Java to download" echo "Falling back to using Java to download"

10
mvnw.cmd vendored
View file

@ -7,7 +7,7 @@
@REM "License"); you may not use this file except in compliance @REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at @REM with the License. You may obtain a copy of the License at
@REM @REM
@REM http://www.apache.org/licenses/LICENSE-2.0 @REM https://www.apache.org/licenses/LICENSE-2.0'
@REM @REM
@REM Unless required by applicable law or agreed to in writing, @REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an @REM software distributed under the License is distributed on an
@ -18,7 +18,7 @@
@REM ---------------------------------------------------------------------------- @REM ----------------------------------------------------------------------------
@REM ---------------------------------------------------------------------------- @REM ----------------------------------------------------------------------------
@REM Maven2 Start Up Batch script @REM Maven Start Up Batch script
@REM @REM
@REM Required ENV vars: @REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir @REM JAVA_HOME - location of a JDK home dir
@ -26,7 +26,7 @@
@REM Optional ENV vars @REM Optional ENV vars
@REM M2_HOME - location of maven2's installed home dir @REM M2_HOME - location of maven2's installed home dir
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use @REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@ -120,7 +120,7 @@ SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
@ -134,7 +134,7 @@ if exist %WRAPPER_JAR% (
) )
) else ( ) else (
if not "%MVNW_REPOURL%" == "" ( if not "%MVNW_REPOURL%" == "" (
SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.5/maven-wrapper-0.5.5.jar" SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
) )
if "%MVNW_VERBOSE%" == "true" ( if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ... echo Couldn't find %WRAPPER_JAR%, downloading it ...

135
pom.xml
View file

@ -1,16 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/maven-v4_0_0.xsd"> xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>org.springframework.samples</groupId> <groupId>org.springframework.samples</groupId>
<artifactId>spring-petclinic</artifactId> <artifactId>spring-petclinic</artifactId>
<version>2.2.0.BUILD-SNAPSHOT</version> <version>2.4.0.BUILD-SNAPSHOT</version>
<parent> <parent>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId> <artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.1.RELEASE</version> <version>2.4.1</version>
</parent> </parent>
<name>petclinic</name> <name>petclinic</name>
@ -28,7 +28,8 @@
<wro4j.version>1.8.0</wro4j.version> <wro4j.version>1.8.0</wro4j.version>
<jacoco.version>0.8.5</jacoco.version> <jacoco.version>0.8.5</jacoco.version>
<nohttp-checkstyle.version>0.0.4.RELEASE</nohttp-checkstyle.version>
<spring-format.version>0.0.25</spring-format.version>
</properties> </properties>
<dependencies> <dependencies>
@ -49,6 +50,10 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId> <artifactId>spring-boot-starter-web</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId> <artifactId>spring-boot-starter-thymeleaf</artifactId>
@ -65,10 +70,10 @@
</exclusions> </exclusions>
</dependency> </dependency>
<!-- Databases - Uses HSQL by default --> <!-- Databases - Uses H2 by default -->
<dependency> <dependency>
<groupId>org.hsqldb</groupId> <groupId>com.h2database</groupId>
<artifactId>hsqldb</artifactId> <artifactId>h2</artifactId>
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<dependency> <dependency>
@ -109,18 +114,6 @@
</dependency> </dependency>
<!-- end of webjars --> <!-- end of webjars -->
<!-- Testing -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId> <artifactId>spring-boot-devtools</artifactId>
@ -130,6 +123,53 @@
<build> <build>
<plugins> <plugins>
<plugin>
<groupId>io.spring.javaformat</groupId>
<artifactId>spring-javaformat-maven-plugin</artifactId>
<version>${spring-format.version}</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>validate</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.1.1</version>
<dependencies>
<dependency>
<groupId>com.puppycrawl.tools</groupId>
<artifactId>checkstyle</artifactId>
<version>8.32</version>
</dependency>
<dependency>
<groupId>io.spring.nohttp</groupId>
<artifactId>nohttp-checkstyle</artifactId>
<version>${nohttp-checkstyle.version}</version>
</dependency>
</dependencies>
<executions>
<execution>
<id>nohttp-checkstyle-validation</id>
<phase>validate</phase>
<configuration>
<configLocation>src/checkstyle/nohttp-checkstyle.xml</configLocation>
<suppressionsLocation>src/checkstyle/nohttp-checkstyle-suppressions.xml</suppressionsLocation>
<encoding>UTF-8</encoding>
<sourceDirectories>${basedir}</sourceDirectories>
<includes>**/*</includes>
<excludes>**/.git/**/*,**/.idea/**/*,**/target/**/,**/.flattened-pom.xml,**/*.class</excludes>
</configuration>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin> <plugin>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
@ -228,7 +268,6 @@
</plugins> </plugins>
</build> </build>
<!-- Apache 2 license -->
<licenses> <licenses>
<license> <license>
<name>Apache License, Version 2.0</name> <name>Apache License, Version 2.0</name>
@ -274,4 +313,60 @@
</pluginRepository> </pluginRepository>
</pluginRepositories> </pluginRepositories>
<profiles>
<profile>
<id>m2e</id>
<activation>
<property>
<name>m2e.version</name>
</property>
</activation>
<build>
<pluginManagement>
<plugins>
<!-- This plugin's configuration is used to store Eclipse m2e settings
only. It has no influence on the Maven build itself. -->
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<versionRange>[1,)</versionRange>
<goals>
<goal>check</goal>
</goals>
</pluginExecutionFilter>
<action>
<ignore/>
</action>
</pluginExecution>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<versionRange>[1,)</versionRange>
<goals>
<goal>build-info</goal>
</goals>
</pluginExecutionFilter>
<action>
<ignore/>
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</profile>
</profiles>
</project> </project>

View file

@ -1 +0,0 @@
repo: https://github.com/spring-projects/spring-petclinic.git

View file

@ -1,9 +1,4 @@
# Spring PetClinic Sample Application [![Build Status](https://travis-ci.org/spring-projects/spring-petclinic.png?branch=master)](https://travis-ci.org/spring-projects/spring-petclinic/) # Spring PetClinic Sample Application [![Build Status](https://travis-ci.org/spring-projects/spring-petclinic.png?branch=main)](https://travis-ci.org/spring-projects/spring-petclinic/)
Deploy this sample application to Pivotal Web Services:
<a href="https://push-to.cfapps.io?repo=https%3A%2F%2Fgithub.com%2Fspring-projects%2Fspring-petclinic.git">
<img src="https://push-to.cfapps.io/ui/assets/images/Push-to-Pivotal-Light-with-Shadow.svg" width="180" alt="Push" align="center">
</a>
## Understanding the Spring Petclinic application with a few diagrams ## Understanding the Spring Petclinic application with a few diagrams
<a href="https://speakerdeck.com/michaelisvy/spring-petclinic-sample-application">See the presentation here</a> <a href="https://speakerdeck.com/michaelisvy/spring-petclinic-sample-application">See the presentation here</a>
@ -35,17 +30,19 @@ Our issue tracker is available here: https://github.com/spring-projects/spring-p
## Database configuration ## Database configuration
In its default configuration, Petclinic uses an in-memory database (HSQLDB) which In its default configuration, Petclinic uses an in-memory database (H2) which
gets populated at startup with data. A similar setup is provided for MySql in case a persistent database configuration is needed. gets populated at startup with data. The h2 console is automatically exposed at `http://localhost:8080/h2-console`
Note that whenever the database type is changed, the app needs to be run with a different profile: `spring.profiles.active=mysql` for MySql. and it is possible to inspect the content of the database using the `jdbc:h2:mem:testdb` url.
A similar setup is provided for MySql in case a persistent database configuration is needed. Note that whenever the database type is changed, the app needs to be run with a different profile: `spring.profiles.active=mysql` for MySql.
You could start MySql locally with whatever installer works for your OS, or with docker: You could start MySql locally with whatever installer works for your OS, or with docker:
``` ```
docker run -e MYSQL_ROOT_PASSWORD=petclinic -e MYSQL_DATABASE=petclinic -p 3306:3306 mysql:5.7.8 docker run -e MYSQL_USER=petclinic -e MYSQL_PASSWORD=petclinic -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=petclinic -p 3306:3306 mysql:5.7.8
``` ```
Further documentation is provided [here](https://github.com/spring-projects/spring-petclinic/blob/master/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt). Further documentation is provided [here](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt).
## Working with Petclinic in your IDE ## Working with Petclinic in your IDE
@ -63,43 +60,39 @@ The following items should be installed in your system:
### Steps: ### Steps:
1) On the command line 1) On the command line
``` ```
git clone https://github.com/spring-projects/spring-petclinic.git git clone https://github.com/spring-projects/spring-petclinic.git
``` ```
2) Inside Eclipse or STS 2) Inside Eclipse or STS
``` ```
File -> Import -> Maven -> Existing Maven project File -> Import -> Maven -> Existing Maven project
``` ```
Then either build on the command line `./mvnw generate-resources` or using the Eclipse launcher (right click on project and `Run As -> Maven install`) to generate the css. Run the application main method by right clicking on it and choosing `Run As -> Java Application`. Then either build on the command line `./mvnw generate-resources` or using the Eclipse launcher (right click on project and `Run As -> Maven install`) to generate the css. Run the application main method by right clicking on it and choosing `Run As -> Java Application`.
3) Inside IntelliJ IDEA 3) Inside IntelliJ IDEA
In the main menu, choose `File -> Open` and select the Petclinic [pom.xml](pom.xml). Click on the `Open` button.
In the main menu, choose `File -> Open` and select the Petclinic [pom.xml](pom.xml). Click on the `Open` button. CSS files are generated from the Maven build. You can either build them on the command line `./mvnw generate-resources` or right click on the `spring-petclinic` project then `Maven -> Generates sources and Update Folders`.
CSS files are generated from the Maven build. You can either build them on the command line `./mvnw generate-resources` A run configuration named `PetClinicApplication` should have been created for you if you're using a recent Ultimate version. Otherwise, run the application by right clicking on the `PetClinicApplication` main class and choosing `Run 'PetClinicApplication'`.
or right click on the `spring-petclinic` project then `Maven -> Generates sources and Update Folders`.
A run configuration named `PetClinicApplication` should have been created for you if you're using a recent Ultimate
version. Otherwise, run the application by right clicking on the `PetClinicApplication` main class and choosing
`Run 'PetClinicApplication'`.
4) Navigate to Petclinic 4) Navigate to Petclinic
Visit [http://localhost:8080](http://localhost:8080) in your browser. Visit [http://localhost:8080](http://localhost:8080) in your browser.
## Looking for something in particular? ## Looking for something in particular?
|Spring Boot Configuration | Class or Java property files | |Spring Boot Configuration | Class or Java property files |
|--------------------------|---| |--------------------------|---|
|The Main Class | [PetClinicApplication](https://github.com/spring-projects/spring-petclinic/blob/master/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java) | |The Main Class | [PetClinicApplication](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java) |
|Properties Files | [application.properties](https://github.com/spring-projects/spring-petclinic/blob/master/src/main/resources) | |Properties Files | [application.properties](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources) |
|Caching | [CacheConfiguration](https://github.com/spring-projects/spring-petclinic/blob/master/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java) | |Caching | [CacheConfiguration](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java) |
## Interesting Spring Petclinic branches and forks ## Interesting Spring Petclinic branches and forks
The Spring Petclinic master branch in the main [spring-projects](https://github.com/spring-projects/spring-petclinic) The Spring Petclinic "main" branch in the [spring-projects](https://github.com/spring-projects/spring-petclinic)
GitHub org is the "canonical" implementation, currently based on Spring Boot and Thymeleaf. There are GitHub org is the "canonical" implementation, currently based on Spring Boot and Thymeleaf. There are
[quite a few forks](https://spring-petclinic.github.io/docs/forks.html) in a special GitHub org [quite a few forks](https://spring-petclinic.github.io/docs/forks.html) in a special GitHub org
[spring-petclinic](https://github.com/spring-petclinic). If you have a special interest in a different technology stack [spring-petclinic](https://github.com/spring-petclinic). If you have a special interest in a different technology stack

View file

@ -0,0 +1,8 @@
<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
<suppressions>
<suppress files="[\\/]build.log" checks="NoHttp"/>
<suppress files=".+\.(jar|git|ico|p12|gif|jks|jpg|svg)" checks="NoHttp"/>
</suppressions>

View file

@ -0,0 +1,7 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC
"-//Puppy Crawl//DTD Check Configuration 1.2//EN"
"https://checkstyle.org/dtds/configuration_1_2.dtd">
<module name="com.puppycrawl.tools.checkstyle.Checker">
<module name="io.spring.nohttp.checkstyle.check.NoHttpCheck"/>
</module>

View file

@ -28,8 +28,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(proxyBeanMethods = false) @SpringBootApplication(proxyBeanMethods = false)
public class PetClinicApplication { public class PetClinicApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(PetClinicApplication.class, args); SpringApplication.run(PetClinicApplication.class, args);
} }
} }

View file

@ -31,20 +31,21 @@ import javax.persistence.MappedSuperclass;
*/ */
@MappedSuperclass @MappedSuperclass
public class BaseEntity implements Serializable { public class BaseEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
public Integer getId() { @Id
return id; @GeneratedValue(strategy = GenerationType.IDENTITY)
} private Integer id;
public void setId(Integer id) { public Integer getId() {
this.id = id; return id;
} }
public boolean isNew() { public void setId(Integer id) {
return this.id == null; this.id = id;
} }
public boolean isNew() {
return this.id == null;
}
} }

View file

@ -18,10 +18,9 @@ package org.springframework.samples.petclinic.model;
import javax.persistence.Column; import javax.persistence.Column;
import javax.persistence.MappedSuperclass; import javax.persistence.MappedSuperclass;
/** /**
* Simple JavaBean domain object adds a name property to <code>BaseEntity</code>. Used as a base class for objects * Simple JavaBean domain object adds a name property to <code>BaseEntity</code>. Used as
* needing these properties. * a base class for objects needing these properties.
* *
* @author Ken Krebs * @author Ken Krebs
* @author Juergen Hoeller * @author Juergen Hoeller
@ -29,20 +28,20 @@ import javax.persistence.MappedSuperclass;
@MappedSuperclass @MappedSuperclass
public class NamedEntity extends BaseEntity { public class NamedEntity extends BaseEntity {
@Column(name = "name") @Column(name = "name")
private String name; private String name;
public String getName() { public String getName() {
return this.name; return this.name;
} }
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
} }
@Override @Override
public String toString() { public String toString() {
return this.getName(); return this.getName();
} }
} }

View file

@ -27,28 +27,28 @@ import javax.validation.constraints.NotEmpty;
@MappedSuperclass @MappedSuperclass
public class Person extends BaseEntity { public class Person extends BaseEntity {
@Column(name = "first_name") @Column(name = "first_name")
@NotEmpty @NotEmpty
private String firstName; private String firstName;
@Column(name = "last_name") @Column(name = "last_name")
@NotEmpty @NotEmpty
private String lastName; private String lastName;
public String getFirstName() { public String getFirstName() {
return this.firstName; return this.firstName;
} }
public void setFirstName(String firstName) { public void setFirstName(String firstName) {
this.firstName = firstName; this.firstName = firstName;
} }
public String getLastName() { public String getLastName() {
return this.lastName; return this.lastName;
} }
public void setLastName(String lastName) { public void setLastName(String lastName) {
this.lastName = lastName; this.lastName = lastName;
} }
} }

View file

@ -18,4 +18,3 @@
* The classes in this package represent utilities used by the domain. * The classes in this package represent utilities used by the domain.
*/ */
package org.springframework.samples.petclinic.model; package org.springframework.samples.petclinic.model;

View file

@ -45,108 +45,106 @@ import org.springframework.samples.petclinic.model.Person;
@Entity @Entity
@Table(name = "owners") @Table(name = "owners")
public class Owner extends Person { public class Owner extends Person {
@Column(name = "address")
@NotEmpty
private String address;
@Column(name = "city") @Column(name = "address")
@NotEmpty @NotEmpty
private String city; private String address;
@Column(name = "telephone") @Column(name = "city")
@NotEmpty @NotEmpty
@Digits(fraction = 0, integer = 10) private String city;
private String telephone;
@OneToMany(cascade = CascadeType.ALL, mappedBy = "owner") @Column(name = "telephone")
private Set<Pet> pets; @NotEmpty
@Digits(fraction = 0, integer = 10)
private String telephone;
public String getAddress() { @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner")
return this.address; private Set<Pet> pets;
}
public void setAddress(String address) { public String getAddress() {
this.address = address; return this.address;
} }
public String getCity() { public void setAddress(String address) {
return this.city; this.address = address;
} }
public void setCity(String city) { public String getCity() {
this.city = city; return this.city;
} }
public String getTelephone() { public void setCity(String city) {
return this.telephone; this.city = city;
} }
public void setTelephone(String telephone) { public String getTelephone() {
this.telephone = telephone; return this.telephone;
} }
protected Set<Pet> getPetsInternal() { public void setTelephone(String telephone) {
if (this.pets == null) { this.telephone = telephone;
this.pets = new HashSet<>(); }
}
return this.pets;
}
protected void setPetsInternal(Set<Pet> pets) { protected Set<Pet> getPetsInternal() {
this.pets = pets; if (this.pets == null) {
} this.pets = new HashSet<>();
}
return this.pets;
}
public List<Pet> getPets() { protected void setPetsInternal(Set<Pet> pets) {
List<Pet> sortedPets = new ArrayList<>(getPetsInternal()); this.pets = pets;
PropertyComparator.sort(sortedPets, }
new MutableSortDefinition("name", true, true));
return Collections.unmodifiableList(sortedPets);
}
public void addPet(Pet pet) { public List<Pet> getPets() {
if (pet.isNew()) { List<Pet> sortedPets = new ArrayList<>(getPetsInternal());
getPetsInternal().add(pet); PropertyComparator.sort(sortedPets, new MutableSortDefinition("name", true, true));
} return Collections.unmodifiableList(sortedPets);
pet.setOwner(this); }
}
/** public void addPet(Pet pet) {
* Return the Pet with the given name, or null if none found for this Owner. if (pet.isNew()) {
* getPetsInternal().add(pet);
* @param name to test }
* @return true if pet name is already in use pet.setOwner(this);
*/ }
public Pet getPet(String name) {
return getPet(name, false);
}
/** /**
* Return the Pet with the given name, or null if none found for this Owner. * Return the Pet with the given name, or null if none found for this Owner.
* * @param name to test
* @param name to test * @return true if pet name is already in use
* @return true if pet name is already in use */
*/ public Pet getPet(String name) {
public Pet getPet(String name, boolean ignoreNew) { return getPet(name, false);
name = name.toLowerCase(); }
for (Pet pet : getPetsInternal()) {
if (!ignoreNew || !pet.isNew()) {
String compName = pet.getName();
compName = compName.toLowerCase();
if (compName.equals(name)) {
return pet;
}
}
}
return null;
}
@Override /**
public String toString() { * Return the Pet with the given name, or null if none found for this Owner.
return new ToStringCreator(this) * @param name to test
* @return true if pet name is already in use
*/
public Pet getPet(String name, boolean ignoreNew) {
name = name.toLowerCase();
for (Pet pet : getPetsInternal()) {
if (!ignoreNew || !pet.isNew()) {
String compName = pet.getName();
compName = compName.toLowerCase();
if (compName.equals(name)) {
return pet;
}
}
}
return null;
}
@Override
public String toString() {
return new ToStringCreator(this)
.append("id", this.getId()).append("new", this.isNew()).append("lastName", this.getLastName())
.append("firstName", this.getFirstName()).append("address", this.address).append("city", this.city)
.append("telephone", this.telephone).toString();
}
.append("id", this.getId()).append("new", this.isNew())
.append("lastName", this.getLastName())
.append("firstName", this.getFirstName()).append("address", this.address)
.append("city", this.city).append("telephone", this.telephone).toString();
}
} }

View file

@ -39,102 +39,107 @@ import java.util.Map;
@Controller @Controller
class OwnerController { class OwnerController {
private static final String VIEWS_OWNER_CREATE_OR_UPDATE_FORM = "owners/createOrUpdateOwnerForm"; private static final String VIEWS_OWNER_CREATE_OR_UPDATE_FORM = "owners/createOrUpdateOwnerForm";
private final OwnerRepository owners;
private VisitRepository visits;
private final OwnerRepository owners;
public OwnerController(OwnerRepository clinicService, VisitRepository visits) { private VisitRepository visits;
this.owners = clinicService;
this.visits = visits;
}
@InitBinder public OwnerController(OwnerRepository clinicService, VisitRepository visits) {
public void setAllowedFields(WebDataBinder dataBinder) { this.owners = clinicService;
dataBinder.setDisallowedFields("id"); this.visits = visits;
} }
@GetMapping("/owners/new") @InitBinder
public String initCreationForm(Map<String, Object> model) { public void setAllowedFields(WebDataBinder dataBinder) {
Owner owner = new Owner(); dataBinder.setDisallowedFields("id");
model.put("owner", owner); }
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
@PostMapping("/owners/new") @GetMapping("/owners/new")
public String processCreationForm(@Valid Owner owner, BindingResult result) { public String initCreationForm(Map<String, Object> model) {
if (result.hasErrors()) { Owner owner = new Owner();
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; model.put("owner", owner);
} else { return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
this.owners.save(owner); }
return "redirect:/owners/" + owner.getId();
}
}
@GetMapping("/owners/find") @PostMapping("/owners/new")
public String initFindForm(Map<String, Object> model) { public String processCreationForm(@Valid Owner owner, BindingResult result) {
model.put("owner", new Owner()); if (result.hasErrors()) {
return "owners/findOwners"; return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
} }
else {
this.owners.save(owner);
return "redirect:/owners/" + owner.getId();
}
}
@GetMapping("/owners") @GetMapping("/owners/find")
public String processFindForm(Owner owner, BindingResult result, Map<String, Object> model) { public String initFindForm(Map<String, Object> model) {
model.put("owner", new Owner());
return "owners/findOwners";
}
// allow parameterless GET request for /owners to return all records @GetMapping("/owners")
if (owner.getLastName() == null) { public String processFindForm(Owner owner, BindingResult result, Map<String, Object> model) {
owner.setLastName(""); // empty string signifies broadest possible search
}
// find owners by last name // allow parameterless GET request for /owners to return all records
Collection<Owner> results = this.owners.findByLastName(owner.getLastName()); if (owner.getLastName() == null) {
if (results.isEmpty()) { owner.setLastName(""); // empty string signifies broadest possible search
// no owners found }
result.rejectValue("lastName", "notFound", "not found");
return "owners/findOwners";
} else if (results.size() == 1) {
// 1 owner found
owner = results.iterator().next();
return "redirect:/owners/" + owner.getId();
} else {
// multiple owners found
model.put("selections", results);
return "owners/ownersList";
}
}
@GetMapping("/owners/{ownerId}/edit") // find owners by last name
public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId, Model model) { Collection<Owner> results = this.owners.findByLastName(owner.getLastName());
Owner owner = this.owners.findById(ownerId); if (results.isEmpty()) {
model.addAttribute(owner); // no owners found
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; result.rejectValue("lastName", "notFound", "not found");
} return "owners/findOwners";
}
else if (results.size() == 1) {
// 1 owner found
owner = results.iterator().next();
return "redirect:/owners/" + owner.getId();
}
else {
// multiple owners found
model.put("selections", results);
return "owners/ownersList";
}
}
@PostMapping("/owners/{ownerId}/edit") @GetMapping("/owners/{ownerId}/edit")
public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result, @PathVariable("ownerId") int ownerId) { public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId, Model model) {
if (result.hasErrors()) { Owner owner = this.owners.findById(ownerId);
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; model.addAttribute(owner);
} else { return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
owner.setId(ownerId); }
this.owners.save(owner);
return "redirect:/owners/{ownerId}";
}
}
/** @PostMapping("/owners/{ownerId}/edit")
* Custom handler for displaying an owner. public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result,
* @PathVariable("ownerId") int ownerId) {
* @param ownerId the ID of the owner to display if (result.hasErrors()) {
* @return a ModelMap with the model attributes for the view return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
*/ }
@GetMapping("/owners/{ownerId}") else {
public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) { owner.setId(ownerId);
ModelAndView mav = new ModelAndView("owners/ownerDetails"); this.owners.save(owner);
Owner owner = this.owners.findById(ownerId); return "redirect:/owners/{ownerId}";
for (Pet pet : owner.getPets()) { }
pet.setVisitsInternal(visits.findByPetId(pet.getId())); }
}
mav.addObject(owner); /**
return mav; * Custom handler for displaying an owner.
} * @param ownerId the ID of the owner to display
* @return a ModelMap with the model attributes for the view
*/
@GetMapping("/owners/{ownerId}")
public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) {
ModelAndView mav = new ModelAndView("owners/ownerDetails");
Owner owner = this.owners.findById(ownerId);
for (Pet pet : owner.getPets()) {
pet.setVisitsInternal(visits.findByPetId(pet.getId()));
}
mav.addObject(owner);
return mav;
}
} }

View file

@ -23,9 +23,10 @@ import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
/** /**
* Repository class for <code>Owner</code> domain objects All method names are compliant with Spring Data naming * Repository class for <code>Owner</code> domain objects All method names are compliant
* conventions so this interface can easily be extended for Spring Data. * with Spring Data naming conventions so this interface can easily be extended for Spring
* See: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation * Data. See:
* https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation
* *
* @author Ken Krebs * @author Ken Krebs
* @author Juergen Hoeller * @author Juergen Hoeller
@ -34,31 +35,30 @@ import org.springframework.transaction.annotation.Transactional;
*/ */
public interface OwnerRepository extends Repository<Owner, Integer> { public interface OwnerRepository extends Repository<Owner, Integer> {
/** /**
* Retrieve {@link Owner}s from the data store by last name, returning all owners * Retrieve {@link Owner}s from the data store by last name, returning all owners
* whose last name <i>starts</i> with the given name. * whose last name <i>starts</i> with the given name.
* @param lastName Value to search for * @param lastName Value to search for
* @return a Collection of matching {@link Owner}s (or an empty Collection if none * @return a Collection of matching {@link Owner}s (or an empty Collection if none
* found) * found)
*/ */
@Query("SELECT DISTINCT owner FROM Owner owner left join fetch owner.pets WHERE owner.lastName LIKE :lastName%") @Query("SELECT DISTINCT owner FROM Owner owner left join fetch owner.pets WHERE owner.lastName LIKE :lastName%")
@Transactional(readOnly = true) @Transactional(readOnly = true)
Collection<Owner> findByLastName(@Param("lastName") String lastName); Collection<Owner> findByLastName(@Param("lastName") String lastName);
/** /**
* Retrieve an {@link Owner} from the data store by id. * Retrieve an {@link Owner} from the data store by id.
* @param id the id to search for * @param id the id to search for
* @return the {@link Owner} if found * @return the {@link Owner} if found
*/ */
@Query("SELECT owner FROM Owner owner left join fetch owner.pets WHERE owner.id =:id") @Query("SELECT owner FROM Owner owner left join fetch owner.pets WHERE owner.id =:id")
@Transactional(readOnly = true) @Transactional(readOnly = true)
Owner findById(@Param("id") Integer id); Owner findById(@Param("id") Integer id);
/**
* Save an {@link Owner} to the data store, either inserting or updating it.
* @param owner the {@link Owner} to save
*/
void save(Owner owner);
/**
* Save an {@link Owner} to the data store, either inserting or updating it.
* @param owner the {@link Owner} to save
*/
void save(Owner owner);
} }

View file

@ -48,66 +48,65 @@ import org.springframework.samples.petclinic.visit.Visit;
@Table(name = "pets") @Table(name = "pets")
public class Pet extends NamedEntity { public class Pet extends NamedEntity {
@Column(name = "birth_date") @Column(name = "birth_date")
@DateTimeFormat(pattern = "yyyy-MM-dd") @DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate birthDate; private LocalDate birthDate;
@ManyToOne @ManyToOne
@JoinColumn(name = "type_id") @JoinColumn(name = "type_id")
private PetType type; private PetType type;
@ManyToOne @ManyToOne
@JoinColumn(name = "owner_id") @JoinColumn(name = "owner_id")
private Owner owner; private Owner owner;
@Transient @Transient
private Set<Visit> visits = new LinkedHashSet<>(); private Set<Visit> visits = new LinkedHashSet<>();
public void setBirthDate(LocalDate birthDate) { public void setBirthDate(LocalDate birthDate) {
this.birthDate = birthDate; this.birthDate = birthDate;
} }
public LocalDate getBirthDate() { public LocalDate getBirthDate() {
return this.birthDate; return this.birthDate;
} }
public PetType getType() { public PetType getType() {
return this.type; return this.type;
} }
public void setType(PetType type) { public void setType(PetType type) {
this.type = type; this.type = type;
} }
public Owner getOwner() { public Owner getOwner() {
return this.owner; return this.owner;
} }
protected void setOwner(Owner owner) { protected void setOwner(Owner owner) {
this.owner = owner; this.owner = owner;
} }
protected Set<Visit> getVisitsInternal() { protected Set<Visit> getVisitsInternal() {
if (this.visits == null) { if (this.visits == null) {
this.visits = new HashSet<>(); this.visits = new HashSet<>();
} }
return this.visits; return this.visits;
} }
protected void setVisitsInternal(Collection<Visit> visits) { protected void setVisitsInternal(Collection<Visit> visits) {
this.visits = new LinkedHashSet<>(visits); this.visits = new LinkedHashSet<>(visits);
} }
public List<Visit> getVisits() { public List<Visit> getVisits() {
List<Visit> sortedVisits = new ArrayList<>(getVisitsInternal()); List<Visit> sortedVisits = new ArrayList<>(getVisitsInternal());
PropertyComparator.sort(sortedVisits, PropertyComparator.sort(sortedVisits, new MutableSortDefinition("date", false, false));
new MutableSortDefinition("date", false, false)); return Collections.unmodifiableList(sortedVisits);
return Collections.unmodifiableList(sortedVisits); }
}
public void addVisit(Visit visit) { public void addVisit(Visit visit) {
getVisitsInternal().add(visit); getVisitsInternal().add(visit);
visit.setPetId(this.getId()); visit.setPetId(this.getId());
} }
} }

View file

@ -34,76 +34,80 @@ import java.util.Collection;
@RequestMapping("/owners/{ownerId}") @RequestMapping("/owners/{ownerId}")
class PetController { class PetController {
private static final String VIEWS_PETS_CREATE_OR_UPDATE_FORM = "pets/createOrUpdatePetForm"; private static final String VIEWS_PETS_CREATE_OR_UPDATE_FORM = "pets/createOrUpdatePetForm";
private final PetRepository pets;
private final OwnerRepository owners;
public PetController(PetRepository pets, OwnerRepository owners) { private final PetRepository pets;
this.pets = pets;
this.owners = owners;
}
@ModelAttribute("types") private final OwnerRepository owners;
public Collection<PetType> populatePetTypes() {
return this.pets.findPetTypes();
}
@ModelAttribute("owner") public PetController(PetRepository pets, OwnerRepository owners) {
public Owner findOwner(@PathVariable("ownerId") int ownerId) { this.pets = pets;
return this.owners.findById(ownerId); this.owners = owners;
} }
@InitBinder("owner") @ModelAttribute("types")
public void initOwnerBinder(WebDataBinder dataBinder) { public Collection<PetType> populatePetTypes() {
dataBinder.setDisallowedFields("id"); return this.pets.findPetTypes();
} }
@InitBinder("pet") @ModelAttribute("owner")
public void initPetBinder(WebDataBinder dataBinder) { public Owner findOwner(@PathVariable("ownerId") int ownerId) {
dataBinder.setValidator(new PetValidator()); return this.owners.findById(ownerId);
} }
@GetMapping("/pets/new") @InitBinder("owner")
public String initCreationForm(Owner owner, ModelMap model) { public void initOwnerBinder(WebDataBinder dataBinder) {
Pet pet = new Pet(); dataBinder.setDisallowedFields("id");
owner.addPet(pet); }
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
@PostMapping("/pets/new") @InitBinder("pet")
public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result, ModelMap model) { public void initPetBinder(WebDataBinder dataBinder) {
if (StringUtils.hasLength(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null){ dataBinder.setValidator(new PetValidator());
result.rejectValue("name", "duplicate", "already exists"); }
}
owner.addPet(pet);
if (result.hasErrors()) {
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
} else {
this.pets.save(pet);
return "redirect:/owners/{ownerId}";
}
}
@GetMapping("/pets/{petId}/edit") @GetMapping("/pets/new")
public String initUpdateForm(@PathVariable("petId") int petId, ModelMap model) { public String initCreationForm(Owner owner, ModelMap model) {
Pet pet = this.pets.findById(petId); Pet pet = new Pet();
model.put("pet", pet); owner.addPet(pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM; model.put("pet", pet);
} return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
@PostMapping("/pets/{petId}/edit") @PostMapping("/pets/new")
public String processUpdateForm(@Valid Pet pet, BindingResult result, Owner owner, ModelMap model) { public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result, ModelMap model) {
if (result.hasErrors()) { if (StringUtils.hasLength(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null) {
pet.setOwner(owner); result.rejectValue("name", "duplicate", "already exists");
model.put("pet", pet); }
return VIEWS_PETS_CREATE_OR_UPDATE_FORM; owner.addPet(pet);
} else { if (result.hasErrors()) {
owner.addPet(pet); model.put("pet", pet);
this.pets.save(pet); return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
return "redirect:/owners/{ownerId}"; }
} else {
} this.pets.save(pet);
return "redirect:/owners/{ownerId}";
}
}
@GetMapping("/pets/{petId}/edit")
public String initUpdateForm(@PathVariable("petId") int petId, ModelMap model) {
Pet pet = this.pets.findById(petId);
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
@PostMapping("/pets/{petId}/edit")
public String processUpdateForm(@Valid Pet pet, BindingResult result, Owner owner, ModelMap model) {
if (result.hasErrors()) {
pet.setOwner(owner);
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
else {
owner.addPet(pet);
this.pets.save(pet);
return "redirect:/owners/{ownerId}";
}
}
} }

View file

@ -22,9 +22,10 @@ import org.springframework.data.repository.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
/** /**
* Repository class for <code>Pet</code> domain objects All method names are compliant with Spring Data naming * Repository class for <code>Pet</code> domain objects All method names are compliant
* conventions so this interface can easily be extended for Spring Data. * with Spring Data naming conventions so this interface can easily be extended for Spring
* See: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation * Data. See:
* https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation
* *
* @author Ken Krebs * @author Ken Krebs
* @author Juergen Hoeller * @author Juergen Hoeller
@ -33,27 +34,26 @@ import org.springframework.transaction.annotation.Transactional;
*/ */
public interface PetRepository extends Repository<Pet, Integer> { public interface PetRepository extends Repository<Pet, Integer> {
/** /**
* Retrieve all {@link PetType}s from the data store. * Retrieve all {@link PetType}s from the data store.
* @return a Collection of {@link PetType}s. * @return a Collection of {@link PetType}s.
*/ */
@Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name") @Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name")
@Transactional(readOnly = true) @Transactional(readOnly = true)
List<PetType> findPetTypes(); List<PetType> findPetTypes();
/** /**
* Retrieve a {@link Pet} from the data store by id. * Retrieve a {@link Pet} from the data store by id.
* @param id the id to search for * @param id the id to search for
* @return the {@link Pet} if found * @return the {@link Pet} if found
*/ */
@Transactional(readOnly = true) @Transactional(readOnly = true)
Pet findById(Integer id); Pet findById(Integer id);
/** /**
* Save a {@link Pet} to the data store, either inserting or updating it. * Save a {@link Pet} to the data store, either inserting or updating it.
* @param pet the {@link Pet} to save * @param pet the {@link Pet} to save
*/ */
void save(Pet pet); void save(Pet pet);
} }

View file

@ -21,8 +21,7 @@ import javax.persistence.Table;
import org.springframework.samples.petclinic.model.NamedEntity; import org.springframework.samples.petclinic.model.NamedEntity;
/** /**
* @author Juergen Hoeller * @author Juergen Hoeller Can be Cat, Dog, Hamster...
* Can be Cat, Dog, Hamster...
*/ */
@Entity @Entity
@Table(name = "types") @Table(name = "types")

View file

@ -24,9 +24,10 @@ import org.springframework.format.Formatter;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
/** /**
* Instructs Spring MVC on how to parse and print elements of type 'PetType'. Starting from Spring 3.0, Formatters have * Instructs Spring MVC on how to parse and print elements of type 'PetType'. Starting
* come as an improvement in comparison to legacy PropertyEditors. See the following links for more details: - The * from Spring 3.0, Formatters have come as an improvement in comparison to legacy
* Spring ref doc: https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#format * PropertyEditors. See the following links for more details: - The Spring ref doc:
* https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#format
* *
* @author Mark Fisher * @author Mark Fisher
* @author Juergen Hoeller * @author Juergen Hoeller
@ -35,28 +36,27 @@ import org.springframework.stereotype.Component;
@Component @Component
public class PetTypeFormatter implements Formatter<PetType> { public class PetTypeFormatter implements Formatter<PetType> {
private final PetRepository pets; private final PetRepository pets;
@Autowired
public PetTypeFormatter(PetRepository pets) {
this.pets = pets;
}
@Autowired @Override
public PetTypeFormatter(PetRepository pets) { public String print(PetType petType, Locale locale) {
this.pets = pets; return petType.getName();
} }
@Override @Override
public String print(PetType petType, Locale locale) { public PetType parse(String text, Locale locale) throws ParseException {
return petType.getName(); Collection<PetType> findPetTypes = this.pets.findPetTypes();
} for (PetType type : findPetTypes) {
if (type.getName().equals(text)) {
@Override return type;
public PetType parse(String text, Locale locale) throws ParseException { }
Collection<PetType> findPetTypes = this.pets.findPetTypes(); }
for (PetType type : findPetTypes) { throw new ParseException("type not found: " + text, 0);
if (type.getName().equals(text)) { }
return type;
}
}
throw new ParseException("type not found: " + text, 0);
}
} }

View file

@ -22,7 +22,8 @@ import org.springframework.validation.Validator;
/** /**
* <code>Validator</code> for <code>Pet</code> forms. * <code>Validator</code> for <code>Pet</code> forms.
* <p> * <p>
* We're not using Bean Validation annotations here because it is easier to define such validation rule in Java. * We're not using Bean Validation annotations here because it is easier to define such
* validation rule in Java.
* </p> * </p>
* *
* @author Ken Krebs * @author Ken Krebs
@ -30,35 +31,34 @@ import org.springframework.validation.Validator;
*/ */
public class PetValidator implements Validator { public class PetValidator implements Validator {
private static final String REQUIRED = "required"; private static final String REQUIRED = "required";
@Override @Override
public void validate(Object obj, Errors errors) { public void validate(Object obj, Errors errors) {
Pet pet = (Pet) obj; Pet pet = (Pet) obj;
String name = pet.getName(); String name = pet.getName();
// name validation // name validation
if (!StringUtils.hasLength(name)) { if (!StringUtils.hasLength(name)) {
errors.rejectValue("name", REQUIRED, REQUIRED); errors.rejectValue("name", REQUIRED, REQUIRED);
} }
// type validation // type validation
if (pet.isNew() && pet.getType() == null) { if (pet.isNew() && pet.getType() == null) {
errors.rejectValue("type", REQUIRED, REQUIRED); errors.rejectValue("type", REQUIRED, REQUIRED);
} }
// birth date validation // birth date validation
if (pet.getBirthDate() == null) { if (pet.getBirthDate() == null) {
errors.rejectValue("birthDate", REQUIRED, REQUIRED); errors.rejectValue("birthDate", REQUIRED, REQUIRED);
} }
} }
/**
* This Validator validates *just* Pet instances
*/
@Override
public boolean supports(Class<?> clazz) {
return Pet.class.isAssignableFrom(clazz);
}
/**
* This Validator validates *just* Pet instances
*/
@Override
public boolean supports(Class<?> clazz) {
return Pet.class.isAssignableFrom(clazz);
}
} }

View file

@ -40,55 +40,53 @@ import org.springframework.web.bind.annotation.PostMapping;
@Controller @Controller
class VisitController { class VisitController {
private final VisitRepository visits; private final VisitRepository visits;
private final PetRepository pets;
private final PetRepository pets;
public VisitController(VisitRepository visits, PetRepository pets) { public VisitController(VisitRepository visits, PetRepository pets) {
this.visits = visits; this.visits = visits;
this.pets = pets; this.pets = pets;
} }
@InitBinder @InitBinder
public void setAllowedFields(WebDataBinder dataBinder) { public void setAllowedFields(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields("id"); dataBinder.setDisallowedFields("id");
} }
/** /**
* Called before each and every @RequestMapping annotated method. * Called before each and every @RequestMapping annotated method. 2 goals: - Make sure
* 2 goals: * we always have fresh data - Since we do not use the session scope, make sure that
* - Make sure we always have fresh data * Pet object always has an id (Even though id is not part of the form fields)
* - Since we do not use the session scope, make sure that Pet object always has an id * @param petId
* (Even though id is not part of the form fields) * @return Pet
* */
* @param petId @ModelAttribute("visit")
* @return Pet public Visit loadPetWithVisit(@PathVariable("petId") int petId, Map<String, Object> model) {
*/ Pet pet = this.pets.findById(petId);
@ModelAttribute("visit") pet.setVisitsInternal(this.visits.findByPetId(petId));
public Visit loadPetWithVisit(@PathVariable("petId") int petId, Map<String, Object> model) { model.put("pet", pet);
Pet pet = this.pets.findById(petId); Visit visit = new Visit();
pet.setVisitsInternal(this.visits.findByPetId(petId)); pet.addVisit(visit);
model.put("pet", pet); return visit;
Visit visit = new Visit(); }
pet.addVisit(visit);
return visit;
}
// Spring MVC calls method loadPetWithVisit(...) before initNewVisitForm is called // Spring MVC calls method loadPetWithVisit(...) before initNewVisitForm is called
@GetMapping("/owners/*/pets/{petId}/visits/new") @GetMapping("/owners/*/pets/{petId}/visits/new")
public String initNewVisitForm(@PathVariable("petId") int petId, Map<String, Object> model) { public String initNewVisitForm(@PathVariable("petId") int petId, Map<String, Object> model) {
return "pets/createOrUpdateVisitForm"; return "pets/createOrUpdateVisitForm";
} }
// Spring MVC calls method loadPetWithVisit(...) before processNewVisitForm is called // Spring MVC calls method loadPetWithVisit(...) before processNewVisitForm is called
@PostMapping("/owners/{ownerId}/pets/{petId}/visits/new") @PostMapping("/owners/{ownerId}/pets/{petId}/visits/new")
public String processNewVisitForm(@Valid Visit visit, BindingResult result) { public String processNewVisitForm(@Valid Visit visit, BindingResult result) {
if (result.hasErrors()) { if (result.hasErrors()) {
return "pets/createOrUpdateVisitForm"; return "pets/createOrUpdateVisitForm";
} else { }
this.visits.save(visit); else {
return "redirect:/owners/{ownerId}"; this.visits.save(visit);
} return "redirect:/owners/{ownerId}";
} }
}
} }

View file

@ -32,24 +32,24 @@ import org.springframework.context.annotation.Configuration;
@EnableCaching @EnableCaching
class CacheConfiguration { class CacheConfiguration {
@Bean @Bean
public JCacheManagerCustomizer petclinicCacheConfigurationCustomizer() { public JCacheManagerCustomizer petclinicCacheConfigurationCustomizer() {
return cm -> { return cm -> {
cm.createCache("vets", cacheConfiguration()); cm.createCache("vets", cacheConfiguration());
}; };
} }
/** /**
* Create a simple configuration that enable statistics via the JCache programmatic * Create a simple configuration that enable statistics via the JCache programmatic
* configuration API. * configuration API.
* <p> * <p>
* Within the configuration object that is provided by the JCache API standard, there * Within the configuration object that is provided by the JCache API standard, there
* is only a very limited set of configuration options. The really relevant * is only a very limited set of configuration options. The really relevant
* configuration options (like the size limit) must be set via a configuration * configuration options (like the size limit) must be set via a configuration
* mechanism that is provided by the selected JCache implementation. * mechanism that is provided by the selected JCache implementation.
*/ */
private javax.cache.configuration.Configuration<Object, Object> cacheConfiguration() { private javax.cache.configuration.Configuration<Object, Object> cacheConfiguration() {
return new MutableConfiguration<>().setStatisticsEnabled(true); return new MutableConfiguration<>().setStatisticsEnabled(true);
} }
} }

View file

@ -28,10 +28,10 @@ import org.springframework.web.bind.annotation.GetMapping;
@Controller @Controller
class CrashController { class CrashController {
@GetMapping("/oups") @GetMapping("/oups")
public String triggerException() { public String triggerException() {
throw new RuntimeException("Expected: controller used to showcase what " throw new RuntimeException(
+ "happens when an exception is thrown"); "Expected: controller used to showcase what " + "happens when an exception is thrown");
} }
} }

View file

@ -16,15 +16,15 @@
package org.springframework.samples.petclinic.system; package org.springframework.samples.petclinic.system;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@Controller @Controller
class WelcomeController { class WelcomeController {
@GetMapping("/") @GetMapping("/")
public String welcome() { public String welcome() {
return "welcome"; return "welcome";
} }
} }

View file

@ -45,35 +45,35 @@ import org.springframework.samples.petclinic.model.Person;
@Table(name = "vets") @Table(name = "vets")
public class Vet extends Person { public class Vet extends Person {
@ManyToMany(fetch = FetchType.EAGER) @ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "vet_specialties", joinColumns = @JoinColumn(name = "vet_id"), inverseJoinColumns = @JoinColumn(name = "specialty_id")) @JoinTable(name = "vet_specialties", joinColumns = @JoinColumn(name = "vet_id"),
private Set<Specialty> specialties; inverseJoinColumns = @JoinColumn(name = "specialty_id"))
private Set<Specialty> specialties;
protected Set<Specialty> getSpecialtiesInternal() { protected Set<Specialty> getSpecialtiesInternal() {
if (this.specialties == null) { if (this.specialties == null) {
this.specialties = new HashSet<>(); this.specialties = new HashSet<>();
} }
return this.specialties; return this.specialties;
} }
protected void setSpecialtiesInternal(Set<Specialty> specialties) { protected void setSpecialtiesInternal(Set<Specialty> specialties) {
this.specialties = specialties; this.specialties = specialties;
} }
@XmlElement @XmlElement
public List<Specialty> getSpecialties() { public List<Specialty> getSpecialties() {
List<Specialty> sortedSpecs = new ArrayList<>(getSpecialtiesInternal()); List<Specialty> sortedSpecs = new ArrayList<>(getSpecialtiesInternal());
PropertyComparator.sort(sortedSpecs, PropertyComparator.sort(sortedSpecs, new MutableSortDefinition("name", true, true));
new MutableSortDefinition("name", true, true)); return Collections.unmodifiableList(sortedSpecs);
return Collections.unmodifiableList(sortedSpecs); }
}
public int getNrOfSpecialties() { public int getNrOfSpecialties() {
return getSpecialtiesInternal().size(); return getSpecialtiesInternal().size();
} }
public void addSpecialty(Specialty specialty) { public void addSpecialty(Specialty specialty) {
getSpecialtiesInternal().add(specialty); getSpecialtiesInternal().add(specialty);
} }
} }

View file

@ -30,29 +30,29 @@ import java.util.Map;
@Controller @Controller
class VetController { class VetController {
private final VetRepository vets; private final VetRepository vets;
public VetController(VetRepository clinicService) { public VetController(VetRepository clinicService) {
this.vets = clinicService; this.vets = clinicService;
} }
@GetMapping("/vets.html") @GetMapping("/vets.html")
public String showVetList(Map<String, Object> model) { public String showVetList(Map<String, Object> model) {
// Here we are returning an object of type 'Vets' rather than a collection of Vet // Here we are returning an object of type 'Vets' rather than a collection of Vet
// objects so it is simpler for Object-Xml mapping // objects so it is simpler for Object-Xml mapping
Vets vets = new Vets(); Vets vets = new Vets();
vets.getVetList().addAll(this.vets.findAll()); vets.getVetList().addAll(this.vets.findAll());
model.put("vets", vets); model.put("vets", vets);
return "vets/vetList"; return "vets/vetList";
} }
@GetMapping({ "/vets" }) @GetMapping({ "/vets" })
public @ResponseBody Vets showResourcesVetList() { public @ResponseBody Vets showResourcesVetList() {
// Here we are returning an object of type 'Vets' rather than a collection of Vet // Here we are returning an object of type 'Vets' rather than a collection of Vet
// objects so it is simpler for JSon/Object mapping // objects so it is simpler for JSon/Object mapping
Vets vets = new Vets(); Vets vets = new Vets();
vets.getVetList().addAll(this.vets.findAll()); vets.getVetList().addAll(this.vets.findAll());
return vets; return vets;
} }
} }

View file

@ -23,9 +23,10 @@ import org.springframework.data.repository.Repository;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
/** /**
* Repository class for <code>Vet</code> domain objects All method names are compliant with Spring Data naming * Repository class for <code>Vet</code> domain objects All method names are compliant
* conventions so this interface can easily be extended for Spring Data. * with Spring Data naming conventions so this interface can easily be extended for Spring
* See: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation * Data. See:
* https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation
* *
* @author Ken Krebs * @author Ken Krebs
* @author Juergen Hoeller * @author Juergen Hoeller
@ -34,14 +35,12 @@ import org.springframework.transaction.annotation.Transactional;
*/ */
public interface VetRepository extends Repository<Vet, Integer> { public interface VetRepository extends Repository<Vet, Integer> {
/** /**
* Retrieve all <code>Vet</code>s from the data store. * Retrieve all <code>Vet</code>s from the data store.
* * @return a <code>Collection</code> of <code>Vet</code>s
* @return a <code>Collection</code> of <code>Vet</code>s */
*/ @Transactional(readOnly = true)
@Transactional(readOnly = true) @Cacheable("vets")
@Cacheable("vets") Collection<Vet> findAll() throws DataAccessException;
Collection<Vet> findAll() throws DataAccessException;
} }

View file

@ -22,22 +22,22 @@ import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlRootElement;
/** /**
* Simple domain object representing a list of veterinarians. Mostly here to be used for the 'vets' {@link * Simple domain object representing a list of veterinarians. Mostly here to be used for
* org.springframework.web.servlet.view.xml.MarshallingView}. * the 'vets' {@link org.springframework.web.servlet.view.xml.MarshallingView}.
* *
* @author Arjen Poutsma * @author Arjen Poutsma
*/ */
@XmlRootElement @XmlRootElement
public class Vets { public class Vets {
private List<Vet> vets; private List<Vet> vets;
@XmlElement @XmlElement
public List<Vet> getVetList() { public List<Vet> getVetList() {
if (vets == null) { if (vets == null) {
vets = new ArrayList<>(); vets = new ArrayList<>();
} }
return vets; return vets;
} }
} }

View file

@ -35,46 +35,46 @@ import org.springframework.samples.petclinic.model.BaseEntity;
@Table(name = "visits") @Table(name = "visits")
public class Visit extends BaseEntity { public class Visit extends BaseEntity {
@Column(name = "visit_date") @Column(name = "visit_date")
@DateTimeFormat(pattern = "yyyy-MM-dd") @DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate date; private LocalDate date;
@NotEmpty @NotEmpty
@Column(name = "description") @Column(name = "description")
private String description; private String description;
@Column(name = "pet_id") @Column(name = "pet_id")
private Integer petId; private Integer petId;
/** /**
* Creates a new instance of Visit for the current date * Creates a new instance of Visit for the current date
*/ */
public Visit() { public Visit() {
this.date = LocalDate.now(); this.date = LocalDate.now();
} }
public LocalDate getDate() { public LocalDate getDate() {
return this.date; return this.date;
} }
public void setDate(LocalDate date) { public void setDate(LocalDate date) {
this.date = date; this.date = date;
} }
public String getDescription() { public String getDescription() {
return this.description; return this.description;
} }
public void setDescription(String description) { public void setDescription(String description) {
this.description = description; this.description = description;
} }
public Integer getPetId() { public Integer getPetId() {
return this.petId; return this.petId;
} }
public void setPetId(Integer petId) { public void setPetId(Integer petId) {
this.petId = petId; this.petId = petId;
} }
} }

View file

@ -22,9 +22,10 @@ import org.springframework.data.repository.Repository;
import org.springframework.samples.petclinic.model.BaseEntity; import org.springframework.samples.petclinic.model.BaseEntity;
/** /**
* Repository class for <code>Visit</code> domain objects All method names are compliant with Spring Data naming * Repository class for <code>Visit</code> domain objects All method names are compliant
* conventions so this interface can easily be extended for Spring Data. * with Spring Data naming conventions so this interface can easily be extended for Spring
* See: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation * Data. See:
* https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation
* *
* @author Ken Krebs * @author Ken Krebs
* @author Juergen Hoeller * @author Juergen Hoeller
@ -33,14 +34,13 @@ import org.springframework.samples.petclinic.model.BaseEntity;
*/ */
public interface VisitRepository extends Repository<Visit, Integer> { public interface VisitRepository extends Repository<Visit, Integer> {
/** /**
* Save a <code>Visit</code> to the data store, either inserting or updating it. * Save a <code>Visit</code> to the data store, either inserting or updating it.
* * @param visit the <code>Visit</code> to save
* @param visit the <code>Visit</code> to save * @see BaseEntity#isNew
* @see BaseEntity#isNew */
*/ void save(Visit visit) throws DataAccessException;
void save(Visit visit) throws DataAccessException;
List<Visit> findByPetId(Integer petId); List<Visit> findByPetId(Integer petId);
} }

View file

@ -1,7 +1,7 @@
# database init, supports mysql too # database init, supports mysql too
database=mysql database=mysql
spring.datasource.url=${MYSQL_DB:jdbc:mysql://localhost/petclinic} spring.datasource.url=${MYSQL_URL:jdbc:mysql://localhost/petclinic}
spring.datasource.username=${MYSQL_USER:petclinic@petclinicdb2} spring.datasource.username=${MYSQL_USER:petclinic}
spring.datasource.password=${MYSQL_PASS:petclinic} spring.datasource.password=${MYSQL_PASS:petclinic}
# Uncomment this the first time the app runs # SQL is written to be idempotent so this is safe
spring.datasource.initialization-mode=always spring.datasource.initialization-mode=always

View file

@ -1,5 +1,5 @@
# database init, supports mysql too # database init, supports mysql too
database=hsqldb database=h2
spring.datasource.schema=classpath*:db/${database}/schema.sql spring.datasource.schema=classpath*:db/${database}/schema.sql
spring.datasource.data=classpath*:db/${database}/data.sql spring.datasource.data=classpath*:db/${database}/data.sql
@ -8,12 +8,12 @@ spring.thymeleaf.mode=HTML
# JPA # JPA
spring.jpa.hibernate.ddl-auto=none spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false
# Internationalization # Internationalization
spring.messages.basename=messages/messages spring.messages.basename=messages/messages
# Actuator / Management # Actuator
management.endpoints.web.base-path=/manage
management.endpoints.web.exposure.include=* management.endpoints.web.exposure.include=*
# Logging # Logging

View file

@ -0,0 +1,53 @@
INSERT INTO vets VALUES (1, 'James', 'Carter');
INSERT INTO vets VALUES (2, 'Helen', 'Leary');
INSERT INTO vets VALUES (3, 'Linda', 'Douglas');
INSERT INTO vets VALUES (4, 'Rafael', 'Ortega');
INSERT INTO vets VALUES (5, 'Henry', 'Stevens');
INSERT INTO vets VALUES (6, 'Sharon', 'Jenkins');
INSERT INTO specialties VALUES (1, 'radiology');
INSERT INTO specialties VALUES (2, 'surgery');
INSERT INTO specialties VALUES (3, 'dentistry');
INSERT INTO vet_specialties VALUES (2, 1);
INSERT INTO vet_specialties VALUES (3, 2);
INSERT INTO vet_specialties VALUES (3, 3);
INSERT INTO vet_specialties VALUES (4, 2);
INSERT INTO vet_specialties VALUES (5, 1);
INSERT INTO types VALUES (1, 'cat');
INSERT INTO types VALUES (2, 'dog');
INSERT INTO types VALUES (3, 'lizard');
INSERT INTO types VALUES (4, 'snake');
INSERT INTO types VALUES (5, 'bird');
INSERT INTO types VALUES (6, 'hamster');
INSERT INTO owners VALUES (1, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023');
INSERT INTO owners VALUES (2, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749');
INSERT INTO owners VALUES (3, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763');
INSERT INTO owners VALUES (4, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198');
INSERT INTO owners VALUES (5, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765');
INSERT INTO owners VALUES (6, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654');
INSERT INTO owners VALUES (7, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387');
INSERT INTO owners VALUES (8, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683');
INSERT INTO owners VALUES (9, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435');
INSERT INTO owners VALUES (10, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487');
INSERT INTO pets VALUES (1, 'Leo', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (2, 'Basil', '2012-08-06', 6, 2);
INSERT INTO pets VALUES (3, 'Rosy', '2011-04-17', 2, 3);
INSERT INTO pets VALUES (4, 'Jewel', '2010-03-07', 2, 3);
INSERT INTO pets VALUES (5, 'Iggy', '2010-11-30', 3, 4);
INSERT INTO pets VALUES (6, 'George', '2010-01-20', 4, 5);
INSERT INTO pets VALUES (7, 'Samantha', '2012-09-04', 1, 6);
INSERT INTO pets VALUES (8, 'Max', '2012-09-04', 1, 6);
INSERT INTO pets VALUES (9, 'Lucky', '2011-08-06', 5, 7);
INSERT INTO pets VALUES (10, 'Mulligan', '2007-02-24', 2, 8);
INSERT INTO pets VALUES (11, 'Freddy', '2010-03-09', 5, 9);
INSERT INTO pets VALUES (12, 'Lucky', '2010-06-24', 2, 10);
INSERT INTO pets VALUES (13, 'Sly', '2012-06-08', 1, 10);
INSERT INTO visits VALUES (1, 7, '2013-01-01', 'rabies shot');
INSERT INTO visits VALUES (2, 8, '2013-01-02', 'rabies shot');
INSERT INTO visits VALUES (3, 8, '2013-01-03', 'neutered');
INSERT INTO visits VALUES (4, 7, '2013-01-04', 'spayed');

View file

@ -0,0 +1,64 @@
DROP TABLE vet_specialties IF EXISTS;
DROP TABLE vets IF EXISTS;
DROP TABLE specialties IF EXISTS;
DROP TABLE visits IF EXISTS;
DROP TABLE pets IF EXISTS;
DROP TABLE types IF EXISTS;
DROP TABLE owners IF EXISTS;
CREATE TABLE vets (
id INTEGER IDENTITY PRIMARY KEY,
first_name VARCHAR(30),
last_name VARCHAR(30)
);
CREATE INDEX vets_last_name ON vets (last_name);
CREATE TABLE specialties (
id INTEGER IDENTITY PRIMARY KEY,
name VARCHAR(80)
);
CREATE INDEX specialties_name ON specialties (name);
CREATE TABLE vet_specialties (
vet_id INTEGER NOT NULL,
specialty_id INTEGER NOT NULL
);
ALTER TABLE vet_specialties ADD CONSTRAINT fk_vet_specialties_vets FOREIGN KEY (vet_id) REFERENCES vets (id);
ALTER TABLE vet_specialties ADD CONSTRAINT fk_vet_specialties_specialties FOREIGN KEY (specialty_id) REFERENCES specialties (id);
CREATE TABLE types (
id INTEGER IDENTITY PRIMARY KEY,
name VARCHAR(80)
);
CREATE INDEX types_name ON types (name);
CREATE TABLE owners (
id INTEGER IDENTITY PRIMARY KEY,
first_name VARCHAR(30),
last_name VARCHAR_IGNORECASE(30),
address VARCHAR(255),
city VARCHAR(80),
telephone VARCHAR(20)
);
CREATE INDEX owners_last_name ON owners (last_name);
CREATE TABLE pets (
id INTEGER IDENTITY PRIMARY KEY,
name VARCHAR(30),
birth_date DATE,
type_id INTEGER NOT NULL,
owner_id INTEGER NOT NULL
);
ALTER TABLE pets ADD CONSTRAINT fk_pets_owners FOREIGN KEY (owner_id) REFERENCES owners (id);
ALTER TABLE pets ADD CONSTRAINT fk_pets_types FOREIGN KEY (type_id) REFERENCES types (id);
CREATE INDEX pets_name ON pets (name);
CREATE TABLE visits (
id INTEGER IDENTITY PRIMARY KEY,
pet_id INTEGER NOT NULL,
visit_date DATE,
description VARCHAR(255)
);
ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id);
CREATE INDEX visits_pet_id ON visits (pet_id);

View file

@ -18,8 +18,10 @@
mysql_1_eedb4818d817 | MySQL init process done. Ready for start up. mysql_1_eedb4818d817 | MySQL init process done. Ready for start up.
... ...
2) Create the PetClinic database and user by executing the "db/mysql/{schema,data}.sql" 2) (Once only) create the PetClinic database and user by executing the "db/mysql/user.sql"
scripts (or set "spring.datasource.initialization-mode=always" the first time you run the app). scripts. You can connect to the database running in the docker container using
`mysql -u root -h localhost --protocol tcp`, but you don't need to run the script there
because the petclinic user is already set up if you use the provided `docker-compose.yaml`.
3) Run the app with `spring.profiles.active=mysql` (e.g. as a System property via the command 3) Run the app with `spring.profiles.active=mysql` (e.g. as a System property via the command
line, but any way that sets that property in a Spring Boot app should work). line, but any way that sets that property in a Spring Boot app should work).
@ -33,5 +35,6 @@
To set profiles & properties. To set profiles & properties.
N.B. the "petclinic" database has to exist for the app to work with the JDBC URL value N.B. the "petclinic" database has to exist for the app to work with the JDBC URL value
as it is configured by default. This condition is taken care of by the docker-compose as it is configured by default. This condition is taken care of automatically by the
configuration provided, or by the `schema.sql` if you can run that as root. docker-compose configuration provided, or by the `user.sql` script if you run that as
root.

View file

@ -1,13 +1,3 @@
CREATE DATABASE IF NOT EXISTS petclinic;
ALTER DATABASE petclinic
DEFAULT CHARACTER SET utf8
DEFAULT COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON petclinic.* TO pc@localhost IDENTIFIED BY 'pc';
USE petclinic;
CREATE TABLE IF NOT EXISTS vets ( CREATE TABLE IF NOT EXISTS vets (
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY, id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(30), first_name VARCHAR(30),

View file

@ -0,0 +1,7 @@
CREATE DATABASE IF NOT EXISTS petclinic;
ALTER DATABASE petclinic
DEFAULT CHARACTER SET utf8
DEFAULT COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON petclinic.* TO 'petclinic'@'%' IDENTIFIED BY 'petclinic';

View file

@ -0,0 +1,8 @@
welcome=Bienvenido
required=Es requerido
notFound=No ha sido encontrado
duplicate=Ya se encuentra en uso
nonNumeric=Sólo debe contener numeros
duplicateFormSubmission=No se permite el envío de formularios duplicados
typeMismatch.date=Fecha invalida
typeMismatch.birthDate=Fecha invalida

View file

@ -1,25 +1,25 @@
<!doctype html> <!doctype html>
<html th:fragment="layout (template, menu)"> <html th:fragment="layout (template, menu)">
<head> <head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" type="image/x-icon" th:href="@{/resources/images/favicon.png}"> <link rel="shortcut icon" type="image/x-icon" th:href="@{/resources/images/favicon.png}">
<title th:text="#{title}">PetClinic :: a Spring Framework demonstration</title> <title th:text="#{title}">PetClinic :: a Spring Framework demonstration</title>
<!--[if lt IE 9]> <!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script> <script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script> <script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]--> <![endif]-->
<link rel="stylesheet" th:href="@{/resources/css/petclinic.css}"/> <link rel="stylesheet" th:href="@{/resources/css/petclinic.css}" />
</head> </head>
<body> <body>
@ -67,22 +67,23 @@
</ul> </ul>
</div> </div>
</div> </div>
</div>
</nav> </nav>
<div class="container-fluid"> <div class="container-fluid">
<div class="container xd-container"> <div class="container xd-container">
<th:block th:include="${template}"/>
<br/> <th:block th:include="${template}" />
<br/>
<div class="container"> <br />
<div class="row"> <br />
<div class="col-12 text-center"> <div class="container">
<img src="../static/resources/images/spring-pivotal-logo.png" th:src="@{/resources/images/spring-pivotal-logo.png}" <div class="row">
alt="Sponsored by Pivotal"/></div> <div class="col-12 text-center">
</div> <img src="../static/resources/images/spring-pivotal-logo.png"
th:src="@{/resources/images/spring-pivotal-logo.png}" alt="Sponsored by Pivotal" /></div>
</div> </div>
</div> </div>
</div>
</div> </div>
<script th:src="@{/webjars/jquery/jquery.min.js}"></script> <script th:src="@{/webjars/jquery/jquery.min.js}"></script>

View file

@ -24,7 +24,7 @@
th:text="${#temporals.format(pet.birthDate, dateFormat)}" /></td> th:text="${#temporals.format(pet.birthDate, dateFormat)}" /></td>
<td th:text="${pet.type}" /></td> <td th:text="${pet.type}" /></td>
<td <td
th:text="${pet.owner?.firstName + ' ' + pet.owner?.lastName}" /></td> th:text="${pet.owner?.firstName + ' ' + pet.owner?.lastName}"></td>
</tr> </tr>
</table> </table>

View file

@ -24,12 +24,13 @@ import org.springframework.samples.petclinic.vet.VetRepository;
@SpringBootTest @SpringBootTest
class PetclinicIntegrationTests { class PetclinicIntegrationTests {
@Autowired @Autowired
private VetRepository vets; private VetRepository vets;
@Test
void testFindAll() throws Exception {
vets.findAll();
vets.findAll(); // served from cache
}
@Test
void testFindAll() throws Exception {
vets.findAll();
vets.findAll(); // served from cache
}
} }

View file

@ -34,28 +34,27 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
class ValidatorTests { class ValidatorTests {
private Validator createValidator() { private Validator createValidator() {
LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean(); LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
localValidatorFactoryBean.afterPropertiesSet(); localValidatorFactoryBean.afterPropertiesSet();
return localValidatorFactoryBean; return localValidatorFactoryBean;
} }
@Test @Test
void shouldNotValidateWhenFirstNameEmpty() { void shouldNotValidateWhenFirstNameEmpty() {
LocaleContextHolder.setLocale(Locale.ENGLISH); LocaleContextHolder.setLocale(Locale.ENGLISH);
Person person = new Person(); Person person = new Person();
person.setFirstName(""); person.setFirstName("");
person.setLastName("smith"); person.setLastName("smith");
Validator validator = createValidator(); Validator validator = createValidator();
Set<ConstraintViolation<Person>> constraintViolations = validator Set<ConstraintViolation<Person>> constraintViolations = validator.validate(person);
.validate(person);
assertThat(constraintViolations).hasSize(1); assertThat(constraintViolations).hasSize(1);
ConstraintViolation<Person> violation = constraintViolations.iterator().next(); ConstraintViolation<Person> violation = constraintViolations.iterator().next();
assertThat(violation.getPropertyPath().toString()).isEqualTo("firstName"); assertThat(violation.getPropertyPath().toString()).isEqualTo("firstName");
assertThat(violation.getMessage()).isEqualTo("must not be empty"); assertThat(violation.getMessage()).isEqualTo("must not be empty");
} }
} }

View file

@ -51,181 +51,149 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@WebMvcTest(OwnerController.class) @WebMvcTest(OwnerController.class)
class OwnerControllerTests { class OwnerControllerTests {
private static final int TEST_OWNER_ID = 1; private static final int TEST_OWNER_ID = 1;
@Autowired @Autowired
private MockMvc mockMvc; private MockMvc mockMvc;
@MockBean @MockBean
private OwnerRepository owners; private OwnerRepository owners;
@MockBean @MockBean
private VisitRepository visits; private VisitRepository visits;
private Owner george; private Owner george;
@BeforeEach @BeforeEach
void setup() { void setup() {
george = new Owner(); george = new Owner();
george.setId(TEST_OWNER_ID); george.setId(TEST_OWNER_ID);
george.setFirstName("George"); george.setFirstName("George");
george.setLastName("Franklin"); george.setLastName("Franklin");
george.setAddress("110 W. Liberty St."); george.setAddress("110 W. Liberty St.");
george.setCity("Madison"); george.setCity("Madison");
george.setTelephone("6085551023"); george.setTelephone("6085551023");
Pet max = new Pet(); Pet max = new Pet();
PetType dog = new PetType(); PetType dog = new PetType();
dog.setName("dog"); dog.setName("dog");
max.setId(1); max.setId(1);
max.setType(dog); max.setType(dog);
max.setName("Max"); max.setName("Max");
max.setBirthDate(LocalDate.now()); max.setBirthDate(LocalDate.now());
george.setPetsInternal(Collections.singleton(max)); george.setPetsInternal(Collections.singleton(max));
given(this.owners.findById(TEST_OWNER_ID)).willReturn(george); given(this.owners.findById(TEST_OWNER_ID)).willReturn(george);
Visit visit = new Visit(); Visit visit = new Visit();
visit.setDate(LocalDate.now()); visit.setDate(LocalDate.now());
given(this.visits.findByPetId(max.getId())).willReturn(Collections.singletonList(visit)); given(this.visits.findByPetId(max.getId())).willReturn(Collections.singletonList(visit));
} }
@Test @Test
void testInitCreationForm() throws Exception { void testInitCreationForm() throws Exception {
mockMvc.perform(get("/owners/new")) mockMvc.perform(get("/owners/new")).andExpect(status().isOk()).andExpect(model().attributeExists("owner"))
.andExpect(status().isOk()) .andExpect(view().name("owners/createOrUpdateOwnerForm"));
.andExpect(model().attributeExists("owner")) }
.andExpect(view().name("owners/createOrUpdateOwnerForm"));
}
@Test @Test
void testProcessCreationFormSuccess() throws Exception { void testProcessCreationFormSuccess() throws Exception {
mockMvc.perform(post("/owners/new") mockMvc.perform(post("/owners/new").param("firstName", "Joe").param("lastName", "Bloggs")
.param("firstName", "Joe") .param("address", "123 Caramel Street").param("city", "London").param("telephone", "01316761638"))
.param("lastName", "Bloggs") .andExpect(status().is3xxRedirection());
.param("address", "123 Caramel Street") }
.param("city", "London")
.param("telephone", "01316761638")
)
.andExpect(status().is3xxRedirection());
}
@Test @Test
void testProcessCreationFormHasErrors() throws Exception { void testProcessCreationFormHasErrors() throws Exception {
mockMvc.perform(post("/owners/new") mockMvc.perform(
.param("firstName", "Joe") post("/owners/new").param("firstName", "Joe").param("lastName", "Bloggs").param("city", "London"))
.param("lastName", "Bloggs") .andExpect(status().isOk()).andExpect(model().attributeHasErrors("owner"))
.param("city", "London") .andExpect(model().attributeHasFieldErrors("owner", "address"))
) .andExpect(model().attributeHasFieldErrors("owner", "telephone"))
.andExpect(status().isOk()) .andExpect(view().name("owners/createOrUpdateOwnerForm"));
.andExpect(model().attributeHasErrors("owner")) }
.andExpect(model().attributeHasFieldErrors("owner", "address"))
.andExpect(model().attributeHasFieldErrors("owner", "telephone"))
.andExpect(view().name("owners/createOrUpdateOwnerForm"));
}
@Test @Test
void testInitFindForm() throws Exception { void testInitFindForm() throws Exception {
mockMvc.perform(get("/owners/find")) mockMvc.perform(get("/owners/find")).andExpect(status().isOk()).andExpect(model().attributeExists("owner"))
.andExpect(status().isOk()) .andExpect(view().name("owners/findOwners"));
.andExpect(model().attributeExists("owner")) }
.andExpect(view().name("owners/findOwners"));
}
@Test @Test
void testProcessFindFormSuccess() throws Exception { void testProcessFindFormSuccess() throws Exception {
given(this.owners.findByLastName("")).willReturn(Lists.newArrayList(george, new Owner())); given(this.owners.findByLastName("")).willReturn(Lists.newArrayList(george, new Owner()));
mockMvc.perform(get("/owners")) mockMvc.perform(get("/owners")).andExpect(status().isOk()).andExpect(view().name("owners/ownersList"));
.andExpect(status().isOk()) }
.andExpect(view().name("owners/ownersList"));
}
@Test @Test
void testProcessFindFormByLastName() throws Exception { void testProcessFindFormByLastName() throws Exception {
given(this.owners.findByLastName(george.getLastName())).willReturn(Lists.newArrayList(george)); given(this.owners.findByLastName(george.getLastName())).willReturn(Lists.newArrayList(george));
mockMvc.perform(get("/owners") mockMvc.perform(get("/owners").param("lastName", "Franklin")).andExpect(status().is3xxRedirection())
.param("lastName", "Franklin") .andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID));
) }
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID));
}
@Test @Test
void testProcessFindFormNoOwnersFound() throws Exception { void testProcessFindFormNoOwnersFound() throws Exception {
mockMvc.perform(get("/owners") mockMvc.perform(get("/owners").param("lastName", "Unknown Surname")).andExpect(status().isOk())
.param("lastName", "Unknown Surname") .andExpect(model().attributeHasFieldErrors("owner", "lastName"))
) .andExpect(model().attributeHasFieldErrorCode("owner", "lastName", "notFound"))
.andExpect(status().isOk()) .andExpect(view().name("owners/findOwners"));
.andExpect(model().attributeHasFieldErrors("owner", "lastName")) }
.andExpect(model().attributeHasFieldErrorCode("owner", "lastName", "notFound"))
.andExpect(view().name("owners/findOwners"));
}
@Test @Test
void testInitUpdateOwnerForm() throws Exception { void testInitUpdateOwnerForm() throws Exception {
mockMvc.perform(get("/owners/{ownerId}/edit", TEST_OWNER_ID)) mockMvc.perform(get("/owners/{ownerId}/edit", TEST_OWNER_ID)).andExpect(status().isOk())
.andExpect(status().isOk()) .andExpect(model().attributeExists("owner"))
.andExpect(model().attributeExists("owner")) .andExpect(model().attribute("owner", hasProperty("lastName", is("Franklin"))))
.andExpect(model().attribute("owner", hasProperty("lastName", is("Franklin")))) .andExpect(model().attribute("owner", hasProperty("firstName", is("George"))))
.andExpect(model().attribute("owner", hasProperty("firstName", is("George")))) .andExpect(model().attribute("owner", hasProperty("address", is("110 W. Liberty St."))))
.andExpect(model().attribute("owner", hasProperty("address", is("110 W. Liberty St.")))) .andExpect(model().attribute("owner", hasProperty("city", is("Madison"))))
.andExpect(model().attribute("owner", hasProperty("city", is("Madison")))) .andExpect(model().attribute("owner", hasProperty("telephone", is("6085551023"))))
.andExpect(model().attribute("owner", hasProperty("telephone", is("6085551023")))) .andExpect(view().name("owners/createOrUpdateOwnerForm"));
.andExpect(view().name("owners/createOrUpdateOwnerForm")); }
}
@Test @Test
void testProcessUpdateOwnerFormSuccess() throws Exception { void testProcessUpdateOwnerFormSuccess() throws Exception {
mockMvc.perform(post("/owners/{ownerId}/edit", TEST_OWNER_ID) mockMvc.perform(post("/owners/{ownerId}/edit", TEST_OWNER_ID).param("firstName", "Joe")
.param("firstName", "Joe") .param("lastName", "Bloggs").param("address", "123 Caramel Street").param("city", "London")
.param("lastName", "Bloggs") .param("telephone", "01616291589")).andExpect(status().is3xxRedirection())
.param("address", "123 Caramel Street") .andExpect(view().name("redirect:/owners/{ownerId}"));
.param("city", "London") }
.param("telephone", "01616291589")
)
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Test @Test
void testProcessUpdateOwnerFormHasErrors() throws Exception { void testProcessUpdateOwnerFormHasErrors() throws Exception {
mockMvc.perform(post("/owners/{ownerId}/edit", TEST_OWNER_ID) mockMvc.perform(post("/owners/{ownerId}/edit", TEST_OWNER_ID).param("firstName", "Joe")
.param("firstName", "Joe") .param("lastName", "Bloggs").param("city", "London")).andExpect(status().isOk())
.param("lastName", "Bloggs") .andExpect(model().attributeHasErrors("owner"))
.param("city", "London") .andExpect(model().attributeHasFieldErrors("owner", "address"))
) .andExpect(model().attributeHasFieldErrors("owner", "telephone"))
.andExpect(status().isOk()) .andExpect(view().name("owners/createOrUpdateOwnerForm"));
.andExpect(model().attributeHasErrors("owner")) }
.andExpect(model().attributeHasFieldErrors("owner", "address"))
.andExpect(model().attributeHasFieldErrors("owner", "telephone"))
.andExpect(view().name("owners/createOrUpdateOwnerForm"));
}
@Test @Test
void testShowOwner() throws Exception { void testShowOwner() throws Exception {
mockMvc.perform(get("/owners/{ownerId}", TEST_OWNER_ID)) mockMvc.perform(get("/owners/{ownerId}", TEST_OWNER_ID)).andExpect(status().isOk())
.andExpect(status().isOk()) .andExpect(model().attribute("owner", hasProperty("lastName", is("Franklin"))))
.andExpect(model().attribute("owner", hasProperty("lastName", is("Franklin")))) .andExpect(model().attribute("owner", hasProperty("firstName", is("George"))))
.andExpect(model().attribute("owner", hasProperty("firstName", is("George")))) .andExpect(model().attribute("owner", hasProperty("address", is("110 W. Liberty St."))))
.andExpect(model().attribute("owner", hasProperty("address", is("110 W. Liberty St.")))) .andExpect(model().attribute("owner", hasProperty("city", is("Madison"))))
.andExpect(model().attribute("owner", hasProperty("city", is("Madison")))) .andExpect(model().attribute("owner", hasProperty("telephone", is("6085551023"))))
.andExpect(model().attribute("owner", hasProperty("telephone", is("6085551023")))) .andExpect(model().attribute("owner", hasProperty("pets", not(empty()))))
.andExpect(model().attribute("owner", hasProperty("pets", not(empty())))) .andExpect(model().attribute("owner", hasProperty("pets", new BaseMatcher<List<Pet>>() {
.andExpect(model().attribute("owner", hasProperty("pets", new BaseMatcher<List<Pet>>() {
@Override @Override
public boolean matches(Object item) { public boolean matches(Object item) {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
List<Pet> pets = (List<Pet>) item; List<Pet> pets = (List<Pet>) item;
Pet pet = pets.get(0); Pet pet = pets.get(0);
if (pet.getVisits().isEmpty()) { if (pet.getVisits().isEmpty()) {
return false; return false;
} }
return true; return true;
} }
@Override @Override
public void describeTo(Description description) { public void describeTo(Description description) {
description.appendText("Max did not have any visits"); description.appendText("Max did not have any visits");
}}))) }
.andExpect(view().name("owners/ownerDetails")); }))).andExpect(view().name("owners/ownerDetails"));
} }
} }

View file

@ -39,97 +39,75 @@ import org.springframework.test.web.servlet.MockMvc;
* @author Colin But * @author Colin But
*/ */
@WebMvcTest(value = PetController.class, @WebMvcTest(value = PetController.class,
includeFilters = @ComponentScan.Filter( includeFilters = @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE))
value = PetTypeFormatter.class,
type = FilterType.ASSIGNABLE_TYPE))
class PetControllerTests { class PetControllerTests {
private static final int TEST_OWNER_ID = 1; private static final int TEST_OWNER_ID = 1;
private static final int TEST_PET_ID = 1;
private static final int TEST_PET_ID = 1;
@Autowired @Autowired
private MockMvc mockMvc; private MockMvc mockMvc;
@MockBean @MockBean
private PetRepository pets; private PetRepository pets;
@MockBean @MockBean
private OwnerRepository owners; private OwnerRepository owners;
@BeforeEach @BeforeEach
void setup() { void setup() {
PetType cat = new PetType(); PetType cat = new PetType();
cat.setId(3); cat.setId(3);
cat.setName("hamster"); cat.setName("hamster");
given(this.pets.findPetTypes()).willReturn(Lists.newArrayList(cat)); given(this.pets.findPetTypes()).willReturn(Lists.newArrayList(cat));
given(this.owners.findById(TEST_OWNER_ID)).willReturn(new Owner()); given(this.owners.findById(TEST_OWNER_ID)).willReturn(new Owner());
given(this.pets.findById(TEST_PET_ID)).willReturn(new Pet()); given(this.pets.findById(TEST_PET_ID)).willReturn(new Pet());
} }
@Test @Test
void testInitCreationForm() throws Exception { void testInitCreationForm() throws Exception {
mockMvc.perform(get("/owners/{ownerId}/pets/new", TEST_OWNER_ID)) mockMvc.perform(get("/owners/{ownerId}/pets/new", TEST_OWNER_ID)).andExpect(status().isOk())
.andExpect(status().isOk()) .andExpect(view().name("pets/createOrUpdatePetForm")).andExpect(model().attributeExists("pet"));
.andExpect(view().name("pets/createOrUpdatePetForm")) }
.andExpect(model().attributeExists("pet"));
}
@Test @Test
void testProcessCreationFormSuccess() throws Exception { void testProcessCreationFormSuccess() throws Exception {
mockMvc.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID) mockMvc.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty")
.param("name", "Betty") .param("type", "hamster").param("birthDate", "2015-02-12")).andExpect(status().is3xxRedirection())
.param("type", "hamster") .andExpect(view().name("redirect:/owners/{ownerId}"));
.param("birthDate", "2015-02-12") }
)
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Test @Test
void testProcessCreationFormHasErrors() throws Exception { void testProcessCreationFormHasErrors() throws Exception {
mockMvc.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID) mockMvc.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty").param("birthDate",
.param("name", "Betty") "2015-02-12")).andExpect(model().attributeHasNoErrors("owner"))
.param("birthDate", "2015-02-12") .andExpect(model().attributeHasErrors("pet")).andExpect(model().attributeHasFieldErrors("pet", "type"))
) .andExpect(model().attributeHasFieldErrorCode("pet", "type", "required")).andExpect(status().isOk())
.andExpect(model().attributeHasNoErrors("owner")) .andExpect(view().name("pets/createOrUpdatePetForm"));
.andExpect(model().attributeHasErrors("pet")) }
.andExpect(model().attributeHasFieldErrors("pet", "type"))
.andExpect(model().attributeHasFieldErrorCode("pet", "type", "required"))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
@Test @Test
void testInitUpdateForm() throws Exception { void testInitUpdateForm() throws Exception {
mockMvc.perform(get("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID)) mockMvc.perform(get("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID))
.andExpect(status().isOk()) .andExpect(status().isOk()).andExpect(model().attributeExists("pet"))
.andExpect(model().attributeExists("pet")) .andExpect(view().name("pets/createOrUpdatePetForm"));
.andExpect(view().name("pets/createOrUpdatePetForm")); }
}
@Test @Test
void testProcessUpdateFormSuccess() throws Exception { void testProcessUpdateFormSuccess() throws Exception {
mockMvc.perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID) mockMvc.perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", "Betty")
.param("name", "Betty") .param("type", "hamster").param("birthDate", "2015-02-12")).andExpect(status().is3xxRedirection())
.param("type", "hamster") .andExpect(view().name("redirect:/owners/{ownerId}"));
.param("birthDate", "2015-02-12") }
)
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Test @Test
void testProcessUpdateFormHasErrors() throws Exception { void testProcessUpdateFormHasErrors() throws Exception {
mockMvc.perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID) mockMvc.perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", "Betty")
.param("name", "Betty") .param("birthDate", "2015/02/12")).andExpect(model().attributeHasNoErrors("owner"))
.param("birthDate", "2015/02/12") .andExpect(model().attributeHasErrors("pet")).andExpect(status().isOk())
) .andExpect(view().name("pets/createOrUpdatePetForm"));
.andExpect(model().attributeHasNoErrors("owner")) }
.andExpect(model().attributeHasErrors("pet"))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
} }

View file

@ -40,57 +40,56 @@ import static org.mockito.BDDMockito.given;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class PetTypeFormatterTests { class PetTypeFormatterTests {
@Mock @Mock
private PetRepository pets; private PetRepository pets;
private PetTypeFormatter petTypeFormatter; private PetTypeFormatter petTypeFormatter;
@BeforeEach @BeforeEach
void setup() { void setup() {
this.petTypeFormatter = new PetTypeFormatter(pets); this.petTypeFormatter = new PetTypeFormatter(pets);
} }
@Test @Test
void testPrint() { void testPrint() {
PetType petType = new PetType(); PetType petType = new PetType();
petType.setName("Hamster"); petType.setName("Hamster");
String petTypeName = this.petTypeFormatter.print(petType, Locale.ENGLISH); String petTypeName = this.petTypeFormatter.print(petType, Locale.ENGLISH);
assertThat(petTypeName).isEqualTo("Hamster"); assertThat(petTypeName).isEqualTo("Hamster");
} }
@Test @Test
void shouldParse() throws ParseException { void shouldParse() throws ParseException {
given(this.pets.findPetTypes()).willReturn(makePetTypes()); given(this.pets.findPetTypes()).willReturn(makePetTypes());
PetType petType = petTypeFormatter.parse("Bird", Locale.ENGLISH); PetType petType = petTypeFormatter.parse("Bird", Locale.ENGLISH);
assertThat(petType.getName()).isEqualTo("Bird"); assertThat(petType.getName()).isEqualTo("Bird");
} }
@Test @Test
void shouldThrowParseException() throws ParseException { void shouldThrowParseException() throws ParseException {
given(this.pets.findPetTypes()).willReturn(makePetTypes()); given(this.pets.findPetTypes()).willReturn(makePetTypes());
Assertions.assertThrows(ParseException.class, () -> { Assertions.assertThrows(ParseException.class, () -> {
petTypeFormatter.parse("Fish", Locale.ENGLISH); petTypeFormatter.parse("Fish", Locale.ENGLISH);
}); });
} }
/** /**
* Helper method to produce some sample pet types just for test purpose * Helper method to produce some sample pet types just for test purpose
* * @return {@link Collection} of {@link PetType}
* @return {@link Collection} of {@link PetType} */
*/ private List<PetType> makePetTypes() {
private List<PetType> makePetTypes() { List<PetType> petTypes = new ArrayList<>();
List<PetType> petTypes = new ArrayList<>(); petTypes.add(new PetType() {
petTypes.add(new PetType() { {
{ setName("Dog");
setName("Dog"); }
} });
}); petTypes.add(new PetType() {
petTypes.add(new PetType() { {
{ setName("Bird");
setName("Bird"); }
} });
}); return petTypes;
return petTypes; }
}
} }

View file

@ -39,47 +39,40 @@ import org.springframework.test.web.servlet.MockMvc;
@WebMvcTest(VisitController.class) @WebMvcTest(VisitController.class)
class VisitControllerTests { class VisitControllerTests {
private static final int TEST_PET_ID = 1; private static final int TEST_PET_ID = 1;
@Autowired @Autowired
private MockMvc mockMvc; private MockMvc mockMvc;
@MockBean @MockBean
private VisitRepository visits; private VisitRepository visits;
@MockBean @MockBean
private PetRepository pets; private PetRepository pets;
@BeforeEach @BeforeEach
void init() { void init() {
given(this.pets.findById(TEST_PET_ID)).willReturn(new Pet()); given(this.pets.findById(TEST_PET_ID)).willReturn(new Pet());
} }
@Test @Test
void testInitNewVisitForm() throws Exception { void testInitNewVisitForm() throws Exception {
mockMvc.perform(get("/owners/*/pets/{petId}/visits/new", TEST_PET_ID)) mockMvc.perform(get("/owners/*/pets/{petId}/visits/new", TEST_PET_ID)).andExpect(status().isOk())
.andExpect(status().isOk()) .andExpect(view().name("pets/createOrUpdateVisitForm"));
.andExpect(view().name("pets/createOrUpdateVisitForm")); }
}
@Test @Test
void testProcessNewVisitFormSuccess() throws Exception { void testProcessNewVisitFormSuccess() throws Exception {
mockMvc.perform(post("/owners/*/pets/{petId}/visits/new", TEST_PET_ID) mockMvc.perform(post("/owners/*/pets/{petId}/visits/new", TEST_PET_ID).param("name", "George")
.param("name", "George") .param("description", "Visit Description")).andExpect(status().is3xxRedirection())
.param("description", "Visit Description") .andExpect(view().name("redirect:/owners/{ownerId}"));
) }
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Test @Test
void testProcessNewVisitFormHasErrors() throws Exception { void testProcessNewVisitFormHasErrors() throws Exception {
mockMvc.perform(post("/owners/*/pets/{petId}/visits/new", TEST_PET_ID) mockMvc.perform(post("/owners/*/pets/{petId}/visits/new", TEST_PET_ID).param("name", "George"))
.param("name", "George") .andExpect(model().attributeHasErrors("visit")).andExpect(status().isOk())
) .andExpect(view().name("pets/createOrUpdateVisitForm"));
.andExpect(model().attributeHasErrors("visit")) }
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdateVisitForm"));
}
} }

View file

@ -40,15 +40,24 @@ import org.springframework.transaction.annotation.Transactional;
/** /**
* Integration test of the Service and the Repository layer. * Integration test of the Service and the Repository layer.
* <p> * <p>
* ClinicServiceSpringDataJpaTests subclasses benefit from the following services provided by the Spring * ClinicServiceSpringDataJpaTests subclasses benefit from the following services provided
* TestContext Framework: </p> <ul> <li><strong>Spring IoC container caching</strong> which spares us unnecessary set up * by the Spring TestContext Framework:
* time between test execution.</li> <li><strong>Dependency Injection</strong> of test fixture instances, meaning that * </p>
* we don't need to perform application context lookups. See the use of {@link Autowired @Autowired} on the <code>{@link * <ul>
* ClinicServiceTests#clinicService clinicService}</code> instance variable, which uses autowiring <em>by * <li><strong>Spring IoC container caching</strong> which spares us unnecessary set up
* type</em>. <li><strong>Transaction management</strong>, meaning each test method is executed in its own transaction, * time between test execution.</li>
* which is automatically rolled back by default. Thus, even if tests insert or otherwise change database state, there * <li><strong>Dependency Injection</strong> of test fixture instances, meaning that we
* is no need for a teardown or cleanup script. <li> An {@link org.springframework.context.ApplicationContext * don't need to perform application context lookups. See the use of
* ApplicationContext} is also inherited and can be used for explicit bean lookup if necessary. </li> </ul> * {@link Autowired @Autowired} on the <code>{@link
* ClinicServiceTests#clinicService clinicService}</code> instance variable, which uses
* autowiring <em>by type</em>.
* <li><strong>Transaction management</strong>, meaning each test method is executed in
* its own transaction, which is automatically rolled back by default. Thus, even if tests
* insert or otherwise change database state, there is no need for a teardown or cleanup
* script.
* <li>An {@link org.springframework.context.ApplicationContext ApplicationContext} is
* also inherited and can be used for explicit bean lookup if necessary.</li>
* </ul>
* *
* @author Ken Krebs * @author Ken Krebs
* @author Rod Johnson * @author Rod Johnson
@ -60,159 +69,159 @@ import org.springframework.transaction.annotation.Transactional;
@DataJpaTest(includeFilters = @ComponentScan.Filter(Service.class)) @DataJpaTest(includeFilters = @ComponentScan.Filter(Service.class))
class ClinicServiceTests { class ClinicServiceTests {
@Autowired @Autowired
protected OwnerRepository owners; protected OwnerRepository owners;
@Autowired @Autowired
protected PetRepository pets; protected PetRepository pets;
@Autowired @Autowired
protected VisitRepository visits; protected VisitRepository visits;
@Autowired @Autowired
protected VetRepository vets; protected VetRepository vets;
@Test @Test
void shouldFindOwnersByLastName() { void shouldFindOwnersByLastName() {
Collection<Owner> owners = this.owners.findByLastName("Davis"); Collection<Owner> owners = this.owners.findByLastName("Davis");
assertThat(owners).hasSize(2); assertThat(owners).hasSize(2);
owners = this.owners.findByLastName("Daviss"); owners = this.owners.findByLastName("Daviss");
assertThat(owners).isEmpty(); assertThat(owners).isEmpty();
} }
@Test @Test
void shouldFindSingleOwnerWithPet() { void shouldFindSingleOwnerWithPet() {
Owner owner = this.owners.findById(1); Owner owner = this.owners.findById(1);
assertThat(owner.getLastName()).startsWith("Franklin"); assertThat(owner.getLastName()).startsWith("Franklin");
assertThat(owner.getPets()).hasSize(1); assertThat(owner.getPets()).hasSize(1);
assertThat(owner.getPets().get(0).getType()).isNotNull(); assertThat(owner.getPets().get(0).getType()).isNotNull();
assertThat(owner.getPets().get(0).getType().getName()).isEqualTo("cat"); assertThat(owner.getPets().get(0).getType().getName()).isEqualTo("cat");
} }
@Test @Test
@Transactional @Transactional
void shouldInsertOwner() { void shouldInsertOwner() {
Collection<Owner> owners = this.owners.findByLastName("Schultz"); Collection<Owner> owners = this.owners.findByLastName("Schultz");
int found = owners.size(); int found = owners.size();
Owner owner = new Owner(); Owner owner = new Owner();
owner.setFirstName("Sam"); owner.setFirstName("Sam");
owner.setLastName("Schultz"); owner.setLastName("Schultz");
owner.setAddress("4, Evans Street"); owner.setAddress("4, Evans Street");
owner.setCity("Wollongong"); owner.setCity("Wollongong");
owner.setTelephone("4444444444"); owner.setTelephone("4444444444");
this.owners.save(owner); this.owners.save(owner);
assertThat(owner.getId().longValue()).isNotEqualTo(0); assertThat(owner.getId().longValue()).isNotEqualTo(0);
owners = this.owners.findByLastName("Schultz"); owners = this.owners.findByLastName("Schultz");
assertThat(owners.size()).isEqualTo(found + 1); assertThat(owners.size()).isEqualTo(found + 1);
} }
@Test @Test
@Transactional @Transactional
void shouldUpdateOwner() { void shouldUpdateOwner() {
Owner owner = this.owners.findById(1); Owner owner = this.owners.findById(1);
String oldLastName = owner.getLastName(); String oldLastName = owner.getLastName();
String newLastName = oldLastName + "X"; String newLastName = oldLastName + "X";
owner.setLastName(newLastName); owner.setLastName(newLastName);
this.owners.save(owner); this.owners.save(owner);
// retrieving new name from database // retrieving new name from database
owner = this.owners.findById(1); owner = this.owners.findById(1);
assertThat(owner.getLastName()).isEqualTo(newLastName); assertThat(owner.getLastName()).isEqualTo(newLastName);
} }
@Test @Test
void shouldFindPetWithCorrectId() { void shouldFindPetWithCorrectId() {
Pet pet7 = this.pets.findById(7); Pet pet7 = this.pets.findById(7);
assertThat(pet7.getName()).startsWith("Samantha"); assertThat(pet7.getName()).startsWith("Samantha");
assertThat(pet7.getOwner().getFirstName()).isEqualTo("Jean"); assertThat(pet7.getOwner().getFirstName()).isEqualTo("Jean");
} }
@Test @Test
void shouldFindAllPetTypes() { void shouldFindAllPetTypes() {
Collection<PetType> petTypes = this.pets.findPetTypes(); Collection<PetType> petTypes = this.pets.findPetTypes();
PetType petType1 = EntityUtils.getById(petTypes, PetType.class, 1); PetType petType1 = EntityUtils.getById(petTypes, PetType.class, 1);
assertThat(petType1.getName()).isEqualTo("cat"); assertThat(petType1.getName()).isEqualTo("cat");
PetType petType4 = EntityUtils.getById(petTypes, PetType.class, 4); PetType petType4 = EntityUtils.getById(petTypes, PetType.class, 4);
assertThat(petType4.getName()).isEqualTo("snake"); assertThat(petType4.getName()).isEqualTo("snake");
} }
@Test @Test
@Transactional @Transactional
void shouldInsertPetIntoDatabaseAndGenerateId() { void shouldInsertPetIntoDatabaseAndGenerateId() {
Owner owner6 = this.owners.findById(6); Owner owner6 = this.owners.findById(6);
int found = owner6.getPets().size(); int found = owner6.getPets().size();
Pet pet = new Pet(); Pet pet = new Pet();
pet.setName("bowser"); pet.setName("bowser");
Collection<PetType> types = this.pets.findPetTypes(); Collection<PetType> types = this.pets.findPetTypes();
pet.setType(EntityUtils.getById(types, PetType.class, 2)); pet.setType(EntityUtils.getById(types, PetType.class, 2));
pet.setBirthDate(LocalDate.now()); pet.setBirthDate(LocalDate.now());
owner6.addPet(pet); owner6.addPet(pet);
assertThat(owner6.getPets().size()).isEqualTo(found + 1); assertThat(owner6.getPets().size()).isEqualTo(found + 1);
this.pets.save(pet); this.pets.save(pet);
this.owners.save(owner6); this.owners.save(owner6);
owner6 = this.owners.findById(6); owner6 = this.owners.findById(6);
assertThat(owner6.getPets().size()).isEqualTo(found + 1); assertThat(owner6.getPets().size()).isEqualTo(found + 1);
// checks that id has been generated // checks that id has been generated
assertThat(pet.getId()).isNotNull(); assertThat(pet.getId()).isNotNull();
} }
@Test @Test
@Transactional @Transactional
void shouldUpdatePetName() throws Exception { void shouldUpdatePetName() throws Exception {
Pet pet7 = this.pets.findById(7); Pet pet7 = this.pets.findById(7);
String oldName = pet7.getName(); String oldName = pet7.getName();
String newName = oldName + "X"; String newName = oldName + "X";
pet7.setName(newName); pet7.setName(newName);
this.pets.save(pet7); this.pets.save(pet7);
pet7 = this.pets.findById(7); pet7 = this.pets.findById(7);
assertThat(pet7.getName()).isEqualTo(newName); assertThat(pet7.getName()).isEqualTo(newName);
} }
@Test @Test
void shouldFindVets() { void shouldFindVets() {
Collection<Vet> vets = this.vets.findAll(); Collection<Vet> vets = this.vets.findAll();
Vet vet = EntityUtils.getById(vets, Vet.class, 3); Vet vet = EntityUtils.getById(vets, Vet.class, 3);
assertThat(vet.getLastName()).isEqualTo("Douglas"); assertThat(vet.getLastName()).isEqualTo("Douglas");
assertThat(vet.getNrOfSpecialties()).isEqualTo(2); assertThat(vet.getNrOfSpecialties()).isEqualTo(2);
assertThat(vet.getSpecialties().get(0).getName()).isEqualTo("dentistry"); assertThat(vet.getSpecialties().get(0).getName()).isEqualTo("dentistry");
assertThat(vet.getSpecialties().get(1).getName()).isEqualTo("surgery"); assertThat(vet.getSpecialties().get(1).getName()).isEqualTo("surgery");
} }
@Test @Test
@Transactional @Transactional
void shouldAddNewVisitForPet() { void shouldAddNewVisitForPet() {
Pet pet7 = this.pets.findById(7); Pet pet7 = this.pets.findById(7);
int found = pet7.getVisits().size(); int found = pet7.getVisits().size();
Visit visit = new Visit(); Visit visit = new Visit();
pet7.addVisit(visit); pet7.addVisit(visit);
visit.setDescription("test"); visit.setDescription("test");
this.visits.save(visit); this.visits.save(visit);
this.pets.save(pet7); this.pets.save(pet7);
pet7 = this.pets.findById(7); pet7 = this.pets.findById(7);
assertThat(pet7.getVisits().size()).isEqualTo(found + 1); assertThat(pet7.getVisits().size()).isEqualTo(found + 1);
assertThat(visit.getId()).isNotNull(); assertThat(visit.getId()).isNotNull();
} }
@Test @Test
void shouldFindVisitsByPetId() throws Exception { void shouldFindVisitsByPetId() throws Exception {
Collection<Visit> visits = this.visits.findByPetId(7); Collection<Visit> visits = this.visits.findByPetId(7);
assertThat(visits).hasSize(2); assertThat(visits).hasSize(2);
Visit[] visitArr = visits.toArray(new Visit[visits.size()]); Visit[] visitArr = visits.toArray(new Visit[visits.size()]);
assertThat(visitArr[0].getDate()).isNotNull(); assertThat(visitArr[0].getDate()).isNotNull();
assertThat(visitArr[0].getPetId()).isEqualTo(7); assertThat(visitArr[0].getPetId()).isEqualTo(7);
} }
} }

View file

@ -22,8 +22,8 @@ import org.springframework.orm.ObjectRetrievalFailureException;
import org.springframework.samples.petclinic.model.BaseEntity; import org.springframework.samples.petclinic.model.BaseEntity;
/** /**
* Utility methods for handling entities. Separate from the BaseEntity class mainly because of dependency on the * Utility methods for handling entities. Separate from the BaseEntity class mainly
* ORM-associated ObjectRetrievalFailureException. * because of dependency on the ORM-associated ObjectRetrievalFailureException.
* *
* @author Juergen Hoeller * @author Juergen Hoeller
* @author Sam Brannen * @author Sam Brannen
@ -32,23 +32,22 @@ import org.springframework.samples.petclinic.model.BaseEntity;
*/ */
public abstract class EntityUtils { public abstract class EntityUtils {
/** /**
* Look up the entity of the given class with the given id in the given collection. * Look up the entity of the given class with the given id in the given collection.
* * @param entities the collection to search
* @param entities the collection to search * @param entityClass the entity class to look up
* @param entityClass the entity class to look up * @param entityId the entity id to look up
* @param entityId the entity id to look up * @return the found entity
* @return the found entity * @throws ObjectRetrievalFailureException if the entity was not found
* @throws ObjectRetrievalFailureException if the entity was not found */
*/ public static <T extends BaseEntity> T getById(Collection<T> entities, Class<T> entityClass, int entityId)
public static <T extends BaseEntity> T getById(Collection<T> entities, Class<T> entityClass, int entityId) throws ObjectRetrievalFailureException {
throws ObjectRetrievalFailureException { for (T entity : entities) {
for (T entity : entities) { if (entity.getId() == entityId && entityClass.isInstance(entity)) {
if (entity.getId() == entityId && entityClass.isInstance(entity)) { return entity;
return entity; }
} }
} throw new ObjectRetrievalFailureException(entityClass, entityId);
throw new ObjectRetrievalFailureException(entityClass, entityId); }
}
} }

View file

@ -38,13 +38,14 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@WebMvcTest(controllers = CrashController.class) @WebMvcTest(controllers = CrashController.class)
class CrashControllerTests { class CrashControllerTests {
@Autowired @Autowired
private MockMvc mockMvc; private MockMvc mockMvc;
@Test
void testTriggerException() throws Exception {
mockMvc.perform(get("/oups")).andExpect(view().name("exception"))
.andExpect(model().attributeExists("exception")).andExpect(forwardedUrl("exception"))
.andExpect(status().isOk());
}
@Test
void testTriggerException() throws Exception {
mockMvc.perform(get("/oups")).andExpect(view().name("exception"))
.andExpect(model().attributeExists("exception"))
.andExpect(forwardedUrl("exception")).andExpect(status().isOk());
}
} }

View file

@ -40,43 +40,41 @@ import org.springframework.test.web.servlet.ResultActions;
@WebMvcTest(VetController.class) @WebMvcTest(VetController.class)
class VetControllerTests { class VetControllerTests {
@Autowired @Autowired
private MockMvc mockMvc; private MockMvc mockMvc;
@MockBean @MockBean
private VetRepository vets; private VetRepository vets;
@BeforeEach @BeforeEach
void setup() { void setup() {
Vet james = new Vet(); Vet james = new Vet();
james.setFirstName("James"); james.setFirstName("James");
james.setLastName("Carter"); james.setLastName("Carter");
james.setId(1); james.setId(1);
Vet helen = new Vet(); Vet helen = new Vet();
helen.setFirstName("Helen"); helen.setFirstName("Helen");
helen.setLastName("Leary"); helen.setLastName("Leary");
helen.setId(2); helen.setId(2);
Specialty radiology = new Specialty(); Specialty radiology = new Specialty();
radiology.setId(1); radiology.setId(1);
radiology.setName("radiology"); radiology.setName("radiology");
helen.addSpecialty(radiology); helen.addSpecialty(radiology);
given(this.vets.findAll()).willReturn(Lists.newArrayList(james, helen)); given(this.vets.findAll()).willReturn(Lists.newArrayList(james, helen));
} }
@Test @Test
void testShowVetListHtml() throws Exception { void testShowVetListHtml() throws Exception {
mockMvc.perform(get("/vets.html")) mockMvc.perform(get("/vets.html")).andExpect(status().isOk()).andExpect(model().attributeExists("vets"))
.andExpect(status().isOk()) .andExpect(view().name("vets/vetList"));
.andExpect(model().attributeExists("vets")) }
.andExpect(view().name("vets/vetList"));
}
@Test @Test
void testShowResourcesVetList() throws Exception { void testShowResourcesVetList() throws Exception {
ResultActions actions = mockMvc.perform(get("/vets") ResultActions actions = mockMvc.perform(get("/vets").accept(MediaType.APPLICATION_JSON))
.accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk()); .andExpect(status().isOk());
actions.andExpect(content().contentType(MediaType.APPLICATION_JSON)) actions.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.vetList[0].id").value(1)); .andExpect(jsonPath("$.vetList[0].id").value(1));
} }
} }

View file

@ -25,17 +25,16 @@ import static org.assertj.core.api.Assertions.assertThat;
*/ */
class VetTests { class VetTests {
@Test @Test
void testSerialization() { void testSerialization() {
Vet vet = new Vet(); Vet vet = new Vet();
vet.setFirstName("Zaphod"); vet.setFirstName("Zaphod");
vet.setLastName("Beeblebrox"); vet.setLastName("Beeblebrox");
vet.setId(123); vet.setId(123);
Vet other = (Vet) SerializationUtils Vet other = (Vet) SerializationUtils.deserialize(SerializationUtils.serialize(vet));
.deserialize(SerializationUtils.serialize(vet)); assertThat(other.getFirstName()).isEqualTo(vet.getFirstName());
assertThat(other.getFirstName()).isEqualTo(vet.getFirstName()); assertThat(other.getLastName()).isEqualTo(vet.getLastName());
assertThat(other.getLastName()).isEqualTo(vet.getLastName()); assertThat(other.getId()).isEqualTo(vet.getId());
assertThat(other.getId()).isEqualTo(vet.getId()); }
}
} }