Compare commits

..

2 commits

Author SHA1 Message Date
Oded Shopen
809e6f1c1c Example of using RAG in Vets retreival, as well as refactoring, bug fixes and enhancements 2024-09-23 16:39:16 +01:00
Oded Shopen
290bb439d1 Chatbot for Spring Petclinic.
Supports quering the owners, also guides the user through adding a pet to an owner
2024-09-11 13:09:56 +01:00
68 changed files with 3926 additions and 757 deletions

View file

@ -1,11 +0,0 @@
# Not actually used by the devcontainer, but it is used by gitpod
ARG VARIANT=17-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/java:0-${VARIANT}
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
ARG USER=vscode
VOLUME /home/$USER/.m2
VOLUME /home/$USER/.gradle
ARG JAVA_VERSION=17.0.7-ms
RUN sudo mkdir /home/$USER/.m2 /home/$USER/.gradle && sudo chown $USER:$USER /home/$USER/.m2 /home/$USER/.gradle
RUN bash -lc '. /usr/local/sdkman/bin/sdkman-init.sh && sdk install java $JAVA_VERSION && sdk use java $JAVA_VERSION'

View file

@ -1,5 +1,5 @@
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-java-with-maven
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
name: Java CI with Maven
@ -26,4 +26,4 @@ jobs:
distribution: 'adopt'
cache: maven
- name: Build with Maven Wrapper
run: ./mvnw -B verify
run: ./mvnw -B package

58
.gitignore vendored
View file

@ -1,51 +1,21 @@
HELP.md
pom.xml.bak
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
.gradle
build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/test/**/build/
### STS ###
.attach_pid*
.apt_generated
target/*
bin/*
build/*
.gradle/*
.settings/*
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
bin/
!**/src/main/**/bin/
!**/src/test/**/bin/
### IntelliJ IDEA ###
.factorypath
.attach_pid*
.idea
*.iws
*.iml
*.ipr
out/
!**/src/main/**/out/
!**/src/test/**/out/
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
### VS Code ###
.vscode/
### SDK Man ###
.sdkmanrc
### CSS ###
/target
.sts4-cache/
.vscode
_site/
*.css
!petclinic.css
**/creds.yaml
.tanzu/
tanzu.yml
**/.DS_Store

View file

@ -1,19 +1,3 @@
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
wrapperVersion=3.3.2
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.7/apache-maven-3.9.7-bin.zip

View file

@ -0,0 +1,25 @@
apiVersion: gateway.networking.k8s.io/v1beta1
kind: HTTPRoute
metadata:
name: petclinic-route
annotations:
healthcheck.gslb.tanzu.vmware.com/service: spring-petclinic
healthcheck.gslb.tanzu.vmware.com/path: /
healthcheck.gslb.tanzu.vmware.com/port: "80"
spec:
parentRefs:
- group: gateway.networking.k8s.io
kind: Gateway
name: default-gateway
sectionName: http-petclinic
rules:
- backendRefs:
- group: ""
kind: Service
name: spring-petclinic
port: 8080
weight: 1
matches:
- path:
type: PathPrefix
value: /

View file

@ -0,0 +1,18 @@
apiVersion: apps.tanzu.vmware.com/v1
kind: ContainerApp
metadata:
creationTimestamp: null
name: spring-petclinic
spec:
nonSecretEnv:
- name: SPRING_PROFILES_ACTIVE
value: openai
build:
buildpacks: {}
nonSecretEnv:
- name: BP_JVM_VERSION
value: "17"
path: ../..
ports:
- name: main
port: 8080

View file

@ -1,10 +1,10 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.0'
id 'io.spring.dependency-management' version '1.1.6'
id 'org.graalvm.buildtools.native' version '0.10.3'
id 'org.cyclonedx.bom' version '1.10.0'
id 'io.spring.javaformat' version '0.0.43'
id 'org.springframework.boot' version '3.3.2'
id 'io.spring.dependency-management' version '1.1.5'
id 'org.graalvm.buildtools.native' version '0.10.2'
id 'org.cyclonedx.bom' version '1.8.2'
id 'io.spring.javaformat' version '0.0.41'
id "io.spring.nohttp" version "0.0.11"
}
@ -15,7 +15,7 @@ apply plugin: 'io.spring.javaformat'
gradle.startParameter.excludedTaskNames += [ "checkFormatAot", "checkFormatAotTest" ]
group = 'org.springframework.samples'
version = '3.4.0'
version = '3.3.0'
java {
sourceCompatibility = JavaVersion.VERSION_17
@ -23,28 +23,23 @@ java {
repositories {
mavenCentral()
maven { url 'https://repo.spring.io/milestone' }
}
ext.checkstyleVersion = "10.20.1"
ext.springJavaformatCheckstyleVersion = "0.0.43"
ext.webjarsLocatorLiteVersion = "1.0.1"
ext.webjarsFontawesomeVersion = "4.7.0"
ext.webjarsBootstrapVersion = "5.3.3"
ext.springAiVersion = "1.0.0-M2"
dependencies {
// Workaround for AOT issue (https://github.com/spring-projects/spring-framework/pull/33949) -->
implementation 'io.projectreactor:reactor-core'
implementation 'org.springframework.ai:spring-ai-azure-openai-spring-boot-starter'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'io.micrometer:micrometer-registry-prometheus'
implementation 'javax.cache:cache-api'
implementation 'jakarta.xml.bind:jakarta.xml.bind-api'
runtimeOnly 'org.springframework.boot:spring-boot-starter-actuator'
runtimeOnly "org.webjars:webjars-locator-lite:${webjarsLocatorLiteVersion}"
runtimeOnly "org.webjars.npm:bootstrap:${webjarsBootstrapVersion}"
runtimeOnly "org.webjars.npm:font-awesome:${webjarsFontawesomeVersion}"
runtimeOnly 'com.github.ben-manes.caffeine:caffeine'
@ -57,8 +52,14 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-docker-compose'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.testcontainers:mysql'
checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${springJavaformatCheckstyleVersion}"
checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}"
checkstyle 'io.spring.javaformat:spring-javaformat-checkstyle:0.0.41'
checkstyle 'com.puppycrawl.tools:checkstyle:10.16.0'
}
dependencyManagement {
imports {
mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
}
}
tasks.named('test') {

View file

@ -1,6 +1,6 @@
services:
mysql:
image: mysql:9.1
image: mysql:8.4
ports:
- "3306:3306"
environment:
@ -11,11 +11,15 @@ services:
- MYSQL_DATABASE=petclinic
volumes:
- "./conf.d:/etc/mysql/conf.d:ro"
profiles:
- mysql
postgres:
image: postgres:17.0
image: postgres:16.3
ports:
- "5432:5432"
environment:
- POSTGRES_PASSWORD=petclinic
- POSTGRES_USER=petclinic
- POSTGRES_DB=petclinic
profiles:
- postgres

Binary file not shown.

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

7
gradlew vendored
View file

@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -86,8 +84,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum

2
gradlew.bat vendored
View file

@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################

View file

@ -1,73 +0,0 @@
---
apiVersion: v1
kind: Secret
metadata:
name: demo-db
type: servicebinding.io/postgresql
stringData:
type: "postgresql"
provider: "postgresql"
host: "demo-db"
port: "5432"
database: "petclinic"
username: "user"
password: "pass"
---
apiVersion: v1
kind: Service
metadata:
name: demo-db
spec:
ports:
- port: 5432
selector:
app: demo-db
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: demo-db
labels:
app: demo-db
spec:
selector:
matchLabels:
app: demo-db
template:
metadata:
labels:
app: demo-db
spec:
containers:
- image: postgres:17
name: postgresql
env:
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: demo-db
key: username
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: demo-db
key: password
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: demo-db
key: database
ports:
- containerPort: 5432
name: postgresql
livenessProbe:
tcpSocket:
port: postgresql
readinessProbe:
tcpSocket:
port: postgresql
startupProbe:
tcpSocket:
port: postgresql

View file

@ -1,64 +0,0 @@
---
apiVersion: v1
kind: Service
metadata:
name: petclinic
spec:
type: NodePort
ports:
- port: 80
targetPort: 8080
selector:
app: petclinic
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: petclinic
labels:
app: petclinic
spec:
replicas: 1
selector:
matchLabels:
app: petclinic
template:
metadata:
labels:
app: petclinic
spec:
containers:
- name: workload
image: dsyer/petclinic
env:
- name: SPRING_PROFILES_ACTIVE
value: postgres
- name: SERVICE_BINDING_ROOT
value: /bindings
- name: SPRING_APPLICATION_JSON
value: |
{
"management.endpoint.health.probes.add-additional-paths": true
}
ports:
- name: http
containerPort: 8080
livenessProbe:
httpGet:
path: /livez
port: http
readinessProbe:
httpGet:
path: /readyz
port: http
volumeMounts:
- mountPath: /bindings/secret
name: binding
readOnly: true
volumes:
- name: binding
projected:
sources:
- secret:
name: demo-db

128
pom.xml
View file

@ -1,18 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
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>
<groupId>org.springframework.samples</groupId>
<artifactId>spring-petclinic</artifactId>
<version>3.3.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<relativePath></relativePath>
<version>3.3.3</version>
</parent>
<groupId>org.springframework.samples</groupId>
<artifactId>spring-petclinic</artifactId>
<version>3.4.0-SNAPSHOT</version>
<name>petclinic</name>
<properties>
@ -21,27 +20,29 @@
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<!-- Important for reproducible builds. Update using e.g. ./mvnw versions:set
-DnewVersion=... -->
<project.build.outputTimestamp>2024-11-28T14:37:52Z</project.build.outputTimestamp>
<!-- Important for reproducible builds. Update using e.g. ./mvnw versions:set -DnewVersion=... -->
<project.build.outputTimestamp>2023-05-10T07:42:50Z</project.build.outputTimestamp>
<!-- Web dependencies -->
<webjars-locator.version>1.0.1</webjars-locator.version>
<webjars-bootstrap.version>5.3.3</webjars-bootstrap.version>
<webjars-font-awesome.version>4.7.0</webjars-font-awesome.version>
<checkstyle.version>10.20.1</checkstyle.version>
<checkstyle.version>10.16.0</checkstyle.version>
<jacoco.version>0.8.12</jacoco.version>
<libsass.version>0.2.29</libsass.version>
<lifecycle-mapping>1.0.0</lifecycle-mapping>
<maven-checkstyle.version>3.6.0</maven-checkstyle.version>
<maven-checkstyle.version>3.3.1</maven-checkstyle.version>
<nohttp-checkstyle.version>0.0.11</nohttp-checkstyle.version>
<spring-format.version>0.0.43</spring-format.version>
<spring-format.version>0.0.41</spring-format.version>
<spring-ai.version>1.0.0-M2</spring-ai.version>
</properties>
<dependencies>
<!-- Spring and Spring Boot dependencies -->
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-azure-openai-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
@ -71,11 +72,6 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<!-- Workaround for AOT issue (https://github.com/spring-projects/spring-framework/pull/33949) -->
<groupId>io.projectreactor</groupId>
<artifactId>reactor-core</artifactId>
</dependency>
<!-- Databases - Uses H2 by default -->
<dependency>
@ -105,11 +101,6 @@
</dependency>
<!-- Webjars -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>webjars-locator-lite</artifactId>
<version>${webjars-locator.version}</version>
</dependency>
<dependency>
<groupId>org.webjars.npm</groupId>
<artifactId>bootstrap</artifactId>
@ -152,13 +143,20 @@
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>io.micrometer</groupId>
<artifactId>micrometer-registry-prometheus</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.ai</groupId>
<artifactId>spring-ai-bom</artifactId>
<version>${spring-ai.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
@ -173,8 +171,7 @@
<configuration>
<rules>
<requireJavaVersion>
<message>This build requires at least Java ${java.version},
update your JVM, and
<message>This build requires at least Java ${java.version}, update your JVM, and
run the build again</message>
<version>${java.version}</version>
</requireJavaVersion>
@ -189,10 +186,10 @@
<version>${spring-format.version}</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>validate</goal>
</goals>
<phase>validate</phase>
</execution>
</executions>
</plugin>
@ -215,17 +212,19 @@
<executions>
<execution>
<id>nohttp-checkstyle-validation</id>
<goals>
<goal>check</goal>
</goals>
<phase>validate</phase>
<configuration>
<configLocation>src/checkstyle/nohttp-checkstyle.xml</configLocation>
<sourceDirectories>${basedir}</sourceDirectories>
<includes>**/*</includes>
<excludes>**/.git/**/*,**/.idea/**/*,**/target/**/,**/.flattened-pom.xml,**/*.class</excludes>
<propertyExpansion>config_loc=${basedir}/src/checkstyle/</propertyExpansion>
<propertyExpansion>
config_loc=${basedir}/src/checkstyle/
</propertyExpansion>
</configuration>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
@ -266,10 +265,10 @@
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
<phase>prepare-package</phase>
</execution>
</executions>
</plugin>
@ -287,13 +286,13 @@
<!-- Spring Boot Actuator displays sbom-related information if a CycloneDX SBOM file is
present at the classpath -->
<plugin>
<?m2e ignore?>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
@ -303,38 +302,39 @@
<repositories>
<repository>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<snapshots>
<enabled>true</enabled>
</snapshots>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<snapshots>
<enabled>false</enabled>
</snapshots>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
@ -349,11 +349,11 @@
<executions>
<execution>
<id>unpack</id>
<?m2e execute onConfiguration,onIncremental?>
<phase>generate-resources</phase>
<goals>
<goal>unpack</goal>
</goals>
<?m2e execute onConfiguration,onIncremental?>
<phase>generate-resources</phase>
<configuration>
<artifactItems>
<artifactItem>
@ -372,20 +372,21 @@
<groupId>com.gitlab.haynes</groupId>
<artifactId>libsass-maven-plugin</artifactId>
<version>${libsass.version}</version>
<configuration>
<inputPath>${basedir}/src/main/scss/</inputPath>
<outputPath>${basedir}/src/main/resources/static/resources/css/</outputPath>
<includePath>${project.build.directory}/webjars/META-INF/resources/webjars/bootstrap/${webjars-bootstrap.version}/scss/</includePath>
</configuration>
<executions>
<execution>
<phase>generate-resources</phase>
<?m2e execute onConfiguration,onIncremental?>
<goals>
<goal>compile</goal>
</goals>
<phase>generate-resources</phase>
</execution>
</executions>
<configuration>
<inputPath>${basedir}/src/main/scss/</inputPath>
<outputPath>${basedir}/src/main/resources/static/resources/css/</outputPath>
<includePath>
${project.build.directory}/webjars/META-INF/resources/webjars/bootstrap/${webjars-bootstrap.version}/scss/</includePath>
</configuration>
</plugin>
</plugins>
</build>
@ -419,7 +420,7 @@
</goals>
</pluginExecutionFilter>
<action>
<ignore></ignore>
<ignore />
</action>
</pluginExecution>
<pluginExecution>
@ -432,7 +433,7 @@
</goals>
</pluginExecutionFilter>
<action>
<ignore></ignore>
<ignore />
</action>
</pluginExecution>
<pluginExecution>
@ -445,7 +446,7 @@
</goals>
</pluginExecutionFilter>
<action>
<ignore></ignore>
<ignore />
</action>
</pluginExecution>
</pluginExecutions>
@ -457,4 +458,5 @@
</build>
</profile>
</profiles>
</project>

View file

@ -1,4 +1,4 @@
# Spring PetClinic Sample Application [![Build Status](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml/badge.svg)](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml)[![Build Status](https://github.com/spring-projects/spring-petclinic/actions/workflows/gradle-build.yml/badge.svg)](https://github.com/spring-projects/spring-petclinic/actions/workflows/gradle-build.yml)
# Spring PetClinic Sample Application [![Build Status](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml/badge.svg)](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml)
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/spring-projects/spring-petclinic) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=7517918)
@ -52,29 +52,52 @@ A similar setup is provided for MySQL and PostgreSQL if a persistent database co
You can start MySQL or PostgreSQL locally with whatever installer works for your OS or use docker:
```bash
docker run -e MYSQL_USER=petclinic -e MYSQL_PASSWORD=petclinic -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=petclinic -p 3306:3306 mysql:9.1
docker run -e MYSQL_USER=petclinic -e MYSQL_PASSWORD=petclinic -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=petclinic -p 3306:3306 mysql:8.4
```
or
```bash
docker run -e POSTGRES_USER=petclinic -e POSTGRES_PASSWORD=petclinic -e POSTGRES_DB=petclinic -p 5432:5432 postgres:17.0
docker run -e POSTGRES_USER=petclinic -e POSTGRES_PASSWORD=petclinic -e POSTGRES_DB=petclinic -p 5432:5432 postgres:16.3
```
Further documentation is provided for [MySQL](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/mysql/petclinic_db_setup_mysql.txt)
and [PostgreSQL](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources/db/postgres/petclinic_db_setup_postgres.txt).
Instead of vanilla `docker` you can also use the provided `docker-compose.yml` file to start the database containers. Each one has a service named after the Spring profile:
Instead of vanilla `docker` you can also use the provided `docker-compose.yml` file to start the database containers. Each one has a profile just like the Spring profile:
```bash
docker compose up mysql
docker-compose --profile mysql up
```
or
```bash
docker compose up postgres
docker-compose --profile postgres up
```
## Integrating the Spring AI Chatbot
Spring Petclinic integrates a Chatbot that allows you to interact with the application in a natural language. Here are some examples of what you could ask:
1. Please list the owners that come to the clinic.
2. How many vets are there?
3. Is there an owner named Betty?
4. Which owners have dogs?
5. Add a dog for Betty. Its name is Moopsie.
![alt text](spring-ai.png)
By default, The Spring AI Chatbot is disabled and will return the message `Chat is currently unavailable. Please try again later.`.
Spring Petclinic currently supports OpenAI or Azure's OpenAI as the LLM provider.
In order to enable Spring AI, perform the following steps:
1. Decide which provider you want to use. By default, the `spring-ai-azure-openai-spring-boot-starter` dependency is enabled. You can change it to `spring-ai-openai-spring-boot-starter`in either`pom.xml` or in `build.gradle`, depending on your build tool of choice.
2. Copy `src/main/resources/creds-template.yaml` into `src/main/resources/creds.yaml`, and edit its contents with your API key and API endpoint. Refer to OpenAI's or Azure's documentation for further information on how to obtain these. You only need to populate the provider you're using - either openai, or azure-openai.
3. Boot your application with the `openai` profile. This profile will work for both LLM providers. You can boot the application with that profile using any of the following:
- For maven: `mvn -Dspring-boot.run.profiles=openai spring-boot:run`
- For Gradle: `./gradlew bootRun --args='--spring.profiles.active=openai'`
- For a standard jar file: `SPRING_PROFILES_ACTIVE=openai java -jar build/libs/spring-petclinic-3.3.0.jar` or `SPRING_PROFILES_ACTIVE=openai java -jar target/spring-petclinic-3.3.0-SNAPSHOT.jar`.
## Test Applications

BIN
spring-ai.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

View file

@ -28,6 +28,7 @@ public class PetClinicRuntimeHints implements RuntimeHintsRegistrar {
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
hints.resources().registerPattern("db/*"); // https://github.com/spring-projects/spring-boot/issues/32654
hints.resources().registerPattern("messages/*");
hints.resources().registerPattern("META-INF/resources/webjars/*");
hints.resources().registerPattern("mysql-default-conf");
hints.serialization().registerType(BaseEntity.class);
hints.serialization().registerType(Person.class);

View file

@ -0,0 +1,31 @@
package org.springframework.samples.petclinic.genai;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.ai.chat.memory.InMemoryChatMemory;
import org.springframework.ai.embedding.EmbeddingModel;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
/**
* A Configuration class for beans used by the Chat Client.
*
* @author Oded Shopen
*/
@Configuration
@Profile("openai")
public class AIBeanConfiguration {
@Bean
public ChatMemory chatMemory() {
return new InMemoryChatMemory();
}
@Bean
VectorStore vectorStore(EmbeddingModel embeddingModel) {
return new SimpleVectorStore(embeddingModel);
}
}

View file

@ -0,0 +1,72 @@
package org.springframework.samples.petclinic.genai;
import java.util.List;
import org.springframework.ai.document.Document;
import org.springframework.ai.vectorstore.SearchRequest;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.context.annotation.Profile;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.samples.petclinic.owner.Owner;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.samples.petclinic.vet.VetRepository;
import org.springframework.stereotype.Service;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Functions that are invoked by the LLM will use this bean to query the system of record
* for information such as listing owners and vers, or adding pets to an owner.
*
* @author Oded Shopen
*/
@Service
@Profile("openai")
public class AIDataProvider {
private final OwnerRepository ownerRepository;
private final VectorStore vectorStore;
public AIDataProvider(OwnerRepository ownerRepository, VetRepository vetRepository, VectorStore vectorStore) {
this.ownerRepository = ownerRepository;
this.vectorStore = vectorStore;
}
public OwnersResponse getAllOwners() {
Pageable pageable = PageRequest.of(0, 100);
Page<Owner> ownerPage = ownerRepository.findAll(pageable);
return new OwnersResponse(ownerPage.getContent());
}
public VetResponse getVets(VetRequest request) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper();
String vetAsJson = objectMapper.writeValueAsString(request.vet());
SearchRequest sr = SearchRequest.from(SearchRequest.defaults()).withQuery(vetAsJson).withTopK(20);
if (request.vet() == null) {
// Provide a limit of 50 results when zero parameters are sent
sr = sr.withTopK(50);
}
List<Document> topMatches = this.vectorStore.similaritySearch(sr);
List<String> results = topMatches.stream().map(document -> document.getContent()).toList();
return new VetResponse(results);
}
public AddedPetResponse addPetToOwner(AddPetRequest request) {
Owner owner = ownerRepository.findById(request.ownerId());
owner.addPet(request.pet());
this.ownerRepository.save(owner);
return new AddedPetResponse(owner);
}
public OwnerResponse addOwnerToPetclinic(OwnerRequest ownerRequest) {
ownerRepository.save(ownerRequest.owner());
return new OwnerResponse(ownerRequest.owner());
}
}

View file

@ -0,0 +1,93 @@
package org.springframework.samples.petclinic.genai;
import java.util.List;
import java.util.function.Function;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Description;
import org.springframework.context.annotation.Profile;
import org.springframework.samples.petclinic.owner.Owner;
import org.springframework.samples.petclinic.owner.Pet;
import org.springframework.samples.petclinic.vet.Vet;
import com.fasterxml.jackson.core.JsonProcessingException;
/**
* This class defines the @Bean functions that the LLM provider will invoke when it
* requires more Information on a given topic. The currently available functions enable
* the LLM to get the list of owners and their pets, get information about the
* veterinarians, and add a pet to an owner.
*
* @author Oded Shopen
*/
@Configuration
@Profile("openai")
class AIFunctionConfiguration {
// The @Description annotation helps the model understand when to call the function
@Bean
@Description("List the owners that the pet clinic has")
public Function<OwnerRequest, OwnersResponse> listOwners(AIDataProvider petclinicAiProvider) {
return request -> {
return petclinicAiProvider.getAllOwners();
};
}
@Bean
@Description("List the veterinarians that the pet clinic has")
public Function<VetRequest, VetResponse> listVets(AIDataProvider petclinicAiProvider) {
return request -> {
try {
return petclinicAiProvider.getVets(request);
}
catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
};
}
@Bean
@Description("Add a pet with the specified petTypeId, " + "to an owner identified by the ownerId. "
+ "The allowed Pet types IDs are only: " + "1 - cat" + "2 - dog" + "3 - lizard" + "4 - snake" + "5 - bird"
+ "6 - hamster")
public Function<AddPetRequest, AddedPetResponse> addPetToOwner(AIDataProvider petclinicAiProvider) {
return request -> {
return petclinicAiProvider.addPetToOwner(request);
};
}
@Bean
@Description("Add a new pet owner to the pet clinic. "
+ "The Owner must include a first name and a last name as two separate words, "
+ "plus an address and a 10-digit phone number")
public Function<OwnerRequest, OwnerResponse> addOwnerToPetclinic(AIDataProvider petclinicAiDataProvider) {
return request -> {
return petclinicAiDataProvider.addOwnerToPetclinic(request);
};
}
}
record AddPetRequest(Pet pet, Integer ownerId) {
};
record OwnerRequest(Owner owner) {
};
record OwnersResponse(List<Owner> owners) {
};
record OwnerResponse(Owner owner) {
};
record AddedPetResponse(Owner owner) {
};
record VetResponse(List<String> vet) {
};
record VetRequest(Vet vet) {
}

View file

@ -0,0 +1,66 @@
package org.springframework.samples.petclinic.genai;
import static org.springframework.ai.chat.client.advisor.AbstractChatMemoryAdvisor.DEFAULT_CHAT_MEMORY_CONVERSATION_ID;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.ai.chat.memory.ChatMemory;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* This REST controller is being invoked by the in order to interact with the LLM
*
* @author Oded Shopen
*/
@RestController
@RequestMapping("/")
@Profile("openai")
public class PetclinicChatClient {
// ChatModel is the primary interfaces for interacting with an LLM
// it is a request/response interface that implements the ModelModel
// interface. Make suer to visit the source code of the ChatModel and
// checkout the interfaces in the core spring ai package.
private final ChatClient chatClient;
public PetclinicChatClient(ChatClient.Builder builder, ChatMemory chatMemory) {
// @formatter:off
this.chatClient = builder
.defaultSystem("""
You are a friendly AI assistant designed to help with the management of a veterinarian pet clinic called Spring Petclinic.
Your job is to answer questions about and to perform actions on the user's behalf, mainly around
veterinarians, owners, owners' pets and owners' visits.
You are required to answer an a professional manner. If you don't know the answer, politely tell the user
you don't know the answer, then ask the user a followup question to try and clarify the question they are asking.
If you do know the answer, provide the answer but do not provide any additional followup questions.
When dealing with vets, if the user is unsure about the returned results, explain that there may be additional data that was not returned.
Only if the user is asking about the total number of all vets, answer that there are a lot and ask for some additional criteria.
For owners, pets or visits - provide the correct data.
""")
.defaultAdvisors(
// Chat memory helps us keep context when using the chatbot for up to 10 previous messages.
new MessageChatMemoryAdvisor(chatMemory, DEFAULT_CHAT_MEMORY_CONVERSATION_ID, 10), // CHAT MEMORY
new SimpleLoggerAdvisor()
)
.build();
}
@PostMapping("/chatclient")
public String exchange(@RequestBody String query) {
//All chatbot messages go through this endpoint and are passed to the LLM
return
this.chatClient
.prompt()
.user(
u ->
u.text(query)
)
.call()
.content();
}
}

View file

@ -0,0 +1,25 @@
package org.springframework.samples.petclinic.genai;
import org.springframework.context.annotation.Profile;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* This REST controller implements a default behavior for the chat client when AI profile
* is not in use. It will return a default message that chat is not available.
*
* @author Oded Shopen
*/
@RestController
@RequestMapping("/")
@Profile("!openai")
public class PetclinicDisabledChatClient {
@PostMapping("/chatclient")
public String exchange(@RequestBody String query) {
return "Chat is currently unavailable. Please try again later.";
}
}

View file

@ -0,0 +1,105 @@
package org.springframework.samples.petclinic.genai;
import java.io.File;
import java.io.IOException;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.document.Document;
import org.springframework.ai.document.DocumentReader;
import org.springframework.ai.reader.JsonReader;
import org.springframework.ai.vectorstore.SimpleVectorStore;
import org.springframework.ai.vectorstore.VectorStore;
import org.springframework.boot.context.event.ApplicationStartedEvent;
import org.springframework.context.annotation.Profile;
import org.springframework.context.event.EventListener;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.samples.petclinic.vet.Vet;
import org.springframework.samples.petclinic.vet.VetRepository;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Loads the veterinarians data into a vector store for the purpose of RAG functionality.
*
* @author Oded Shopen
*/
@Component
@Profile("openai")
public class VectorStoreController {
private final Logger logger = LoggerFactory.getLogger(VectorStoreController.class);
private final VectorStore vectorStore;
private final VetRepository vetRepository;
public VectorStoreController(VectorStore vectorStore, VetRepository vetRepository) throws IOException {
this.vectorStore = vectorStore;
this.vetRepository = vetRepository;
}
@EventListener
public void loadVetDataToVectorStoreOnStartup(ApplicationStartedEvent event) throws IOException {
Resource resource = new ClassPathResource("vectorstore.json");
// Check if file exists
if (resource.exists()) {
// In order to save on AI credits, use a pre-embedded database that was saved
// to
// disk based on the current data in the h2 data.sql file
File file = resource.getFile();
((SimpleVectorStore) this.vectorStore).load(file);
logger.info("vector store loaded from existing vectorstore.json file in the classpath");
return;
}
// If vectorstore.json is deleted, the data will be loaded on startup every time.
// Warning - this can be costly in terms of credits used with the AI provider.
// Fetches all Vet entites and creates a document per vet
Pageable pageable = PageRequest.of(0, Integer.MAX_VALUE);
Page<Vet> vetsPage = vetRepository.findAll(pageable);
Resource vetsAsJson = convertListToJsonResource(vetsPage.getContent());
DocumentReader reader = new JsonReader(vetsAsJson);
List<Document> documents = reader.get();
// add the documents to the vector store
this.vectorStore.add(documents);
if (vectorStore instanceof SimpleVectorStore) {
var file = File.createTempFile("vectorstore", ".json");
((SimpleVectorStore) this.vectorStore).save(file);
logger.info("vector store contents written to {}", file.getAbsolutePath());
}
logger.info("vector store loaded with {} documents", documents.size());
}
public Resource convertListToJsonResource(List<Vet> vets) {
ObjectMapper objectMapper = new ObjectMapper();
try {
// Convert List<Vet> to JSON string
String json = objectMapper.writeValueAsString(vets);
// Convert JSON string to byte array
byte[] jsonBytes = json.getBytes();
// Create a ByteArrayResource from the byte array
return new ByteArrayResource(jsonBytes);
}
catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
}

View file

@ -32,6 +32,8 @@ import jakarta.persistence.MappedSuperclass;
@MappedSuperclass
public class BaseEntity implements Serializable {
private static final long serialVersionUID = -3856744164839586177L;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

View file

@ -17,7 +17,6 @@ package org.springframework.samples.petclinic.model;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.validation.constraints.NotBlank;
/**
* Simple JavaBean domain object adds a name property to <code>BaseEntity</code>. Used as
@ -25,13 +24,13 @@ import jakarta.validation.constraints.NotBlank;
*
* @author Ken Krebs
* @author Juergen Hoeller
* @author Wick Dynex
*/
@MappedSuperclass
public class NamedEntity extends BaseEntity {
private static final long serialVersionUID = -1827620691768236760L;
@Column(name = "name")
@NotBlank
private String name;
public String getName() {

View file

@ -27,6 +27,8 @@ import jakarta.validation.constraints.NotBlank;
@MappedSuperclass
public class Person extends BaseEntity {
private static final long serialVersionUID = -5934070342233945557L;
@Column(name = "first_name")
@NotBlank
private String firstName;

View file

@ -41,12 +41,13 @@ import jakarta.validation.constraints.NotBlank;
* @author Sam Brannen
* @author Michael Isvy
* @author Oliver Drotbohm
* @author Wick Dynex
*/
@Entity
@Table(name = "owners")
public class Owner extends Person {
private static final long serialVersionUID = 7676019169107660494L;
@Column(name = "address")
@NotBlank
private String address;
@ -63,7 +64,7 @@ public class Owner extends Person {
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "owner_id")
@OrderBy("name")
private final List<Pet> pets = new ArrayList<>();
private List<Pet> pets = new ArrayList<>();
public String getAddress() {
return this.address;
@ -102,7 +103,7 @@ public class Owner extends Person {
/**
* Return the Pet with the given name, or null if none found for this Owner.
* @param name to test
* @return the Pet with the given name, or null if no such Pet exists for this Owner
* @return a pet if pet name is already in use
*/
public Pet getPet(String name) {
return getPet(name, false);
@ -111,7 +112,7 @@ public class Owner extends Person {
/**
* Return the Pet with the given id, or null if none found for this Owner.
* @param id to test
* @return the Pet with the given id, or null if no such Pet exists for this Owner
* @return a pet if pet id is already in use
*/
public Pet getPet(Integer id) {
for (Pet pet : getPets()) {
@ -128,10 +129,10 @@ public class Owner extends Person {
/**
* Return the Pet with the given name, or null if none found for this Owner.
* @param name to test
* @param ignoreNew whether to ignore new pets (pets that are not saved yet)
* @return the Pet with the given name, or null if no such Pet exists for this Owner
* @return a pet if pet name is already in use
*/
public Pet getPet(String name, boolean ignoreNew) {
name = name.toLowerCase();
for (Pet pet : getPets()) {
String compName = pet.getName();
if (compName != null && compName.equalsIgnoreCase(name)) {

View file

@ -16,7 +16,7 @@
package org.springframework.samples.petclinic.owner;
import java.util.List;
import java.util.Optional;
import java.util.Map;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
@ -32,16 +32,15 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import jakarta.validation.Valid;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* @author Juergen Hoeller
* @author Ken Krebs
* @author Arjen Poutsma
* @author Michael Isvy
* @author Wick Dynex
*/
@Controller
class OwnerController {
@ -50,8 +49,8 @@ class OwnerController {
private final OwnerRepository owners;
public OwnerController(OwnerRepository owners) {
this.owners = owners;
public OwnerController(OwnerRepository clinicService) {
this.owners = clinicService;
}
@InitBinder
@ -61,14 +60,13 @@ class OwnerController {
@ModelAttribute("owner")
public Owner findOwner(@PathVariable(name = "ownerId", required = false) Integer ownerId) {
return ownerId == null ? new Owner()
: this.owners.findById(ownerId)
.orElseThrow(() -> new IllegalArgumentException("Owner not found with id: " + ownerId
+ ". Please ensure the ID is correct " + "and the owner exists in the database."));
return ownerId == null ? new Owner() : this.owners.findById(ownerId);
}
@GetMapping("/owners/new")
public String initCreationForm() {
public String initCreationForm(Map<String, Object> model) {
Owner owner = new Owner();
model.put("owner", owner);
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
@ -127,11 +125,13 @@ class OwnerController {
private Page<Owner> findPaginatedForOwnersLastName(int page, String lastname) {
int pageSize = 5;
Pageable pageable = PageRequest.of(page - 1, pageSize);
return owners.findByLastNameStartingWith(lastname, pageable);
return owners.findByLastName(lastname, pageable);
}
@GetMapping("/owners/{ownerId}/edit")
public String initUpdateOwnerForm() {
public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId, Model model) {
Owner owner = this.owners.findById(ownerId);
model.addAttribute(owner);
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
@ -143,12 +143,6 @@ class OwnerController {
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
if (owner.getId() != ownerId) {
result.rejectValue("id", "mismatch", "The owner ID in the form does not match the URL.");
redirectAttributes.addFlashAttribute("error", "Owner ID mismatch. Please try again.");
return "redirect:/owners/{ownerId}/edit";
}
owner.setId(ownerId);
this.owners.save(owner);
redirectAttributes.addFlashAttribute("message", "Owner Values Updated");
@ -163,9 +157,7 @@ class OwnerController {
@GetMapping("/owners/{ownerId}")
public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) {
ModelAndView mav = new ModelAndView("owners/ownerDetails");
Optional<Owner> optionalOwner = this.owners.findById(ownerId);
Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException(
"Owner not found with id: " + ownerId + ". Please ensure the ID is correct "));
Owner owner = this.owners.findById(ownerId);
mav.addObject(owner);
return mav;
}

View file

@ -16,13 +16,12 @@
package org.springframework.samples.petclinic.owner;
import java.util.List;
import java.util.Optional;
import jakarta.annotation.Nonnull;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.data.repository.query.Param;
import org.springframework.transaction.annotation.Transactional;
/**
@ -35,15 +34,15 @@ import org.springframework.transaction.annotation.Transactional;
* @author Juergen Hoeller
* @author Sam Brannen
* @author Michael Isvy
* @author Wick Dynex
*/
public interface OwnerRepository extends JpaRepository<Owner, Integer> {
public interface OwnerRepository extends Repository<Owner, Integer> {
/**
* Retrieve all {@link PetType}s from the data store.
* @return a Collection of {@link PetType}s.
*/
@Query("SELECT ptype FROM PetType ptype ORDER BY ptype.name")
@Transactional(readOnly = true)
List<PetType> findPetTypes();
/**
@ -53,26 +52,31 @@ public interface OwnerRepository extends JpaRepository<Owner, Integer> {
* @return a Collection of matching {@link Owner}s (or an empty Collection if none
* found)
*/
Page<Owner> findByLastNameStartingWith(String lastName, Pageable pageable);
@Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName% ")
@Transactional(readOnly = true)
Page<Owner> findByLastName(@Param("lastName") String lastName, Pageable pageable);
/**
* Retrieve an {@link Owner} from the data store by id.
* <p>
* This method returns an {@link Optional} containing the {@link Owner} if found. If
* no {@link Owner} is found with the provided id, it will return an empty
* {@link Optional}.
* </p>
* @param id the id to search for
* @return an {@link Optional} containing the {@link Owner} if found, or an empty
* {@link Optional} if not found.
* @throws IllegalArgumentException if the id is null (assuming null is not a valid
* input for id)
* @return the {@link Owner} if found
*/
Optional<Owner> findById(@Nonnull Integer id);
@Query("SELECT owner FROM Owner owner left join fetch owner.pets WHERE owner.id =:id")
@Transactional(readOnly = true)
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);
/**
* Returns all the owners from data store
**/
@Query("SELECT owner FROM Owner owner")
@Transactional(readOnly = true)
Page<Owner> findAll(Pageable pageable);
}

View file

@ -39,12 +39,13 @@ import jakarta.persistence.Table;
* @author Ken Krebs
* @author Juergen Hoeller
* @author Sam Brannen
* @author Wick Dynex
*/
@Entity
@Table(name = "pets")
public class Pet extends NamedEntity {
private static final long serialVersionUID = 622048308893169889L;
@Column(name = "birth_date")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate birthDate;
@ -56,7 +57,7 @@ public class Pet extends NamedEntity {
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "pet_id")
@OrderBy("visit_date ASC")
private final Set<Visit> visits = new LinkedHashSet<>();
private Set<Visit> visits = new LinkedHashSet<>();
public void setBirthDate(LocalDate birthDate) {
this.birthDate = birthDate;

View file

@ -17,7 +17,6 @@ package org.springframework.samples.petclinic.owner;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Optional;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
@ -38,7 +37,6 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
* @author Juergen Hoeller
* @author Ken Krebs
* @author Arjen Poutsma
* @author Wick Dynex
*/
@Controller
@RequestMapping("/owners/{ownerId}")
@ -59,9 +57,11 @@ class PetController {
@ModelAttribute("owner")
public Owner findOwner(@PathVariable("ownerId") int ownerId) {
Optional<Owner> optionalOwner = this.owners.findById(ownerId);
Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException(
"Owner not found with id: " + ownerId + ". Please ensure the ID is correct "));
Owner owner = this.owners.findById(ownerId);
if (owner == null) {
throw new IllegalArgumentException("Owner ID not found: " + ownerId);
}
return owner;
}
@ -73,9 +73,10 @@ class PetController {
return new Pet();
}
Optional<Owner> optionalOwner = this.owners.findById(ownerId);
Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException(
"Owner not found with id: " + ownerId + ". Please ensure the ID is correct "));
Owner owner = this.owners.findById(ownerId);
if (owner == null) {
throw new IllegalArgumentException("Owner ID not found: " + ownerId);
}
return owner.getPet(petId);
}
@ -93,46 +94,51 @@ class PetController {
public String initCreationForm(Owner owner, ModelMap model) {
Pet pet = new Pet();
owner.addPet(pet);
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
@PostMapping("/pets/new")
public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result,
public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result, ModelMap model,
RedirectAttributes redirectAttributes) {
if (StringUtils.hasText(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null)
if (StringUtils.hasText(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null) {
result.rejectValue("name", "duplicate", "already exists");
}
LocalDate currentDate = LocalDate.now();
if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(currentDate)) {
result.rejectValue("birthDate", "typeMismatch.birthDate");
}
owner.addPet(pet);
if (result.hasErrors()) {
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
owner.addPet(pet);
this.owners.save(owner);
redirectAttributes.addFlashAttribute("message", "New Pet has been Added");
return "redirect:/owners/{ownerId}";
}
@GetMapping("/pets/{petId}/edit")
public String initUpdateForm() {
public String initUpdateForm(Owner owner, @PathVariable("petId") int petId, ModelMap model,
RedirectAttributes redirectAttributes) {
Pet pet = owner.getPet(petId);
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
@PostMapping("/pets/{petId}/edit")
public String processUpdateForm(Owner owner, @Valid Pet pet, BindingResult result,
public String processUpdateForm(@Valid Pet pet, BindingResult result, Owner owner, ModelMap model,
RedirectAttributes redirectAttributes) {
String petName = pet.getName();
// checking if the pet name already exist for the owner
if (StringUtils.hasText(petName)) {
Pet existingPet = owner.getPet(petName, false);
if (existingPet != null && !existingPet.getId().equals(pet.getId())) {
Pet existingPet = owner.getPet(petName.toLowerCase(), false);
if (existingPet != null && existingPet.getId() != pet.getId()) {
result.rejectValue("name", "duplicate", "already exists");
}
}
@ -143,6 +149,7 @@ class PetController {
}
if (result.hasErrors()) {
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}

View file

@ -27,4 +27,6 @@ import jakarta.persistence.Table;
@Table(name = "types")
public class PetType extends NamedEntity {
private static final long serialVersionUID = -7611995145056548231L;
}

View file

@ -35,6 +35,8 @@ import jakarta.validation.constraints.NotBlank;
@Table(name = "visits")
public class Visit extends BaseEntity {
private static final long serialVersionUID = -8061148591973721283L;
@Column(name = "visit_date")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate date;

View file

@ -16,7 +16,6 @@
package org.springframework.samples.petclinic.owner;
import java.util.Map;
import java.util.Optional;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
@ -36,7 +35,6 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
* @author Arjen Poutsma
* @author Michael Isvy
* @author Dave Syer
* @author Wick Dynex
*/
@Controller
class VisitController {
@ -62,9 +60,7 @@ class VisitController {
@ModelAttribute("visit")
public Visit loadPetWithVisit(@PathVariable("ownerId") int ownerId, @PathVariable("petId") int petId,
Map<String, Object> model) {
Optional<Owner> optionalOwner = owners.findById(ownerId);
Owner owner = optionalOwner.orElseThrow(() -> new IllegalArgumentException(
"Owner not found with id: " + ownerId + ". Please ensure the ID is correct "));
Owner owner = this.owners.findById(ownerId);
Pet pet = owner.getPet(petId);
model.put("pet", pet);

View file

@ -29,4 +29,6 @@ import jakarta.persistence.Table;
@Table(name = "specialties")
public class Specialty extends NamedEntity {
private static final long serialVersionUID = 5551869401872945493L;
}

View file

@ -25,13 +25,16 @@ import org.springframework.beans.support.MutableSortDefinition;
import org.springframework.beans.support.PropertyComparator;
import org.springframework.samples.petclinic.model.Person;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;
import jakarta.xml.bind.annotation.XmlElement;
/**
* Simple JavaBean domain object representing a veterinarian.
@ -45,6 +48,8 @@ import jakarta.xml.bind.annotation.XmlElement;
@Table(name = "vets")
public class Vet extends Person {
private static final long serialVersionUID = 2216866745632621103L;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "vet_specialties", joinColumns = @JoinColumn(name = "vet_id"),
inverseJoinColumns = @JoinColumn(name = "specialty_id"))
@ -57,17 +62,24 @@ public class Vet extends Person {
return this.specialties;
}
@XmlElement
protected void setSpecialtiesInternal(Set<Specialty> specialties) {
this.specialties = specialties;
}
@JsonProperty("specialties")
@JsonSerialize(as = ArrayList.class)
public List<Specialty> getSpecialties() {
List<Specialty> sortedSpecs = new ArrayList<>(getSpecialtiesInternal());
PropertyComparator.sort(sortedSpecs, new MutableSortDefinition("name", true, true));
return Collections.unmodifiableList(sortedSpecs);
}
@JsonIgnore
public int getNrOfSpecialties() {
return getSpecialtiesInternal().size();
}
@JsonIgnore
public void addSpecialty(Specialty specialty) {
getSpecialtiesInternal().add(specialty);
}

View file

@ -37,8 +37,8 @@ class VetController {
private final VetRepository vetRepository;
public VetController(VetRepository vetRepository) {
this.vetRepository = vetRepository;
public VetController(VetRepository clinicService) {
this.vetRepository = clinicService;
}
@GetMapping("/vets.html")
@ -61,7 +61,7 @@ class VetController {
}
private Page<Vet> findPaginated(int page) {
int pageSize = 5;
int pageSize = 20;
Pageable pageable = PageRequest.of(page - 1, pageSize);
return vetRepository.findAll(pageable);
}

View file

@ -19,6 +19,7 @@ import org.springframework.cache.annotation.Cacheable;
import org.springframework.dao.DataAccessException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.Repository;
import org.springframework.transaction.annotation.Transactional;
@ -45,6 +46,13 @@ public interface VetRepository extends Repository<Vet, Integer> {
@Cacheable("vets")
Collection<Vet> findAll() throws DataAccessException;
/**
* Count the number of <code>Vet</code>s in the data store.
*/
@Transactional(readOnly = true)
@Query("SELECT COUNT(v) FROM Vet v")
Integer countVets() throws DataAccessException;
/**
* Retrieve all <code>Vet</code>s from data store in Pages
* @param pageable

View file

@ -0,0 +1,18 @@
spring.config.import=optional:classpath:/creds.yaml
#These apply when using spring-ai-azure-openai-spring-boot-starter
spring.ai.azure.openai.chat.options.functions=listOwners,listVets,addPetToOwner,addOwnerToPetclinic
spring.ai.azure.openai.chat.options.temperature: 0.7
#These apply when using spring-ai-openai-spring-boot-starter
#spring.ai.openai.chat.options.functions=listOwners,listVets,addPetToOwner
#spring.ai.openai.chat.options.functions=addOwnerToPetclinic
spring.ai.openai.chat.options.temperature: 0.7
#Enable Spring AI by default
spring.ai.chat.client.enabled=true
logging.level.org.springframework.ai.chat.client.advisor=DEBUG

View file

@ -1,5 +1,6 @@
# database init, supports mysql too
database=h2
spring.sql.init.schema-locations=classpath*:db/${database}/schema.sql
spring.sql.init.data-locations=classpath*:db/${database}/data.sql
@ -8,7 +9,7 @@ spring.thymeleaf.mode=HTML
# JPA
spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false
spring.jpa.open-in-view=true
# Internationalization
spring.messages.basename=messages/messages
@ -23,3 +24,9 @@ logging.level.org.springframework=INFO
# Maximum time static resources should be cached
spring.web.resources.cache.cachecontrol.max-age=12h
#Disable Spring AI by default
spring.ai.chat.client.enabled=false
#Currently these properties require dummy values when a spring AI is in the classpath, even when chat is disabled.
spring.ai.azure.openai.api-key=dummy
spring.ai.azure.openai.endpoint=dummy

View file

@ -0,0 +1,17 @@
spring:
ai:
#These parameters only apply when using the spring-ai-azure-openai-spring-boot-starter dependency:
azure:
openai:
api-key: ""
endpoint: ""
chat:
options:
deployment-name: "gpt-4o"
#These parameters only apply when using the spring-ai-openai-spring-boot-starter dependency:
openai:
api-key: ""
endpoint: ""
chat:
options:
deployment-name: "gpt-4o"

View file

@ -4,16 +4,523 @@ INSERT INTO vets VALUES (default, 'Linda', 'Douglas');
INSERT INTO vets VALUES (default, 'Rafael', 'Ortega');
INSERT INTO vets VALUES (default, 'Henry', 'Stevens');
INSERT INTO vets VALUES (default, 'Sharon', 'Jenkins');
INSERT INTO vets VALUES (default, 'Matthew', 'Alexander');
INSERT INTO vets VALUES (default, 'Alice', 'Anderson');
INSERT INTO vets VALUES (default, 'James', 'Rogers');
INSERT INTO vets VALUES (default, 'Lauren', 'Butler');
INSERT INTO vets VALUES (default, 'Cheryl', 'Rodriguez');
INSERT INTO vets VALUES (default, 'Laura', 'Martin');
INSERT INTO vets VALUES (default, 'Ashley', 'Henderson');
INSERT INTO vets VALUES (default, 'Walter', 'Moore');
INSERT INTO vets VALUES (default, 'Benjamin', 'Hill');
INSERT INTO vets VALUES (default, 'Matthew', 'Myers');
INSERT INTO vets VALUES (default, 'Jean', 'Henderson');
INSERT INTO vets VALUES (default, 'David', 'Phillips');
INSERT INTO vets VALUES (default, 'Jacqueline', 'Ross');
INSERT INTO vets VALUES (default, 'Jacqueline', 'Perry');
INSERT INTO vets VALUES (default, 'William', 'Walker');
INSERT INTO vets VALUES (default, 'Christine', 'Garcia');
INSERT INTO vets VALUES (default, 'Patricia', 'Myers');
INSERT INTO vets VALUES (default, 'Michael', 'Gonzalez');
INSERT INTO vets VALUES (default, 'Joseph', 'Ross');
INSERT INTO vets VALUES (default, 'Paul', 'Walker');
INSERT INTO vets VALUES (default, 'Tyler', 'Reed');
INSERT INTO vets VALUES (default, 'Doris', 'Allen');
INSERT INTO vets VALUES (default, 'Julia', 'Allen');
INSERT INTO vets VALUES (default, 'Walter', 'Cox');
INSERT INTO vets VALUES (default, 'Samantha', 'Walker');
INSERT INTO vets VALUES (default, 'Marie', 'Rodriguez');
INSERT INTO vets VALUES (default, 'Andrew', 'Taylor');
INSERT INTO vets VALUES (default, 'Dorothy', 'Turner');
INSERT INTO vets VALUES (default, 'Aaron', 'Bryant');
INSERT INTO vets VALUES (default, 'Rose', 'Sanders');
INSERT INTO vets VALUES (default, 'Patrick', 'Bell');
INSERT INTO vets VALUES (default, 'Melissa', 'Hall');
INSERT INTO vets VALUES (default, 'Joshua', 'Stewart');
INSERT INTO vets VALUES (default, 'Teresa', 'Walker');
INSERT INTO vets VALUES (default, 'Doris', 'Taylor');
INSERT INTO vets VALUES (default, 'Thomas', 'Perez');
INSERT INTO vets VALUES (default, 'Evelyn', 'Ward');
INSERT INTO vets VALUES (default, 'Lauren', 'Smith');
INSERT INTO vets VALUES (default, 'Ashley', 'Morgan');
INSERT INTO vets VALUES (default, 'Helen', 'Wood');
INSERT INTO vets VALUES (default, 'Deborah', 'Russell');
INSERT INTO vets VALUES (default, 'Alice', 'Bennett');
INSERT INTO vets VALUES (default, 'Timothy', 'Jackson');
INSERT INTO vets VALUES (default, 'Marie', 'Williams');
INSERT INTO vets VALUES (default, 'Aaron', 'Richardson');
INSERT INTO vets VALUES (default, 'Donna', 'Davis');
INSERT INTO vets VALUES (default, 'Daniel', 'Richardson');
INSERT INTO vets VALUES (default, 'Julia', 'Butler');
INSERT INTO vets VALUES (default, 'Doris', 'Russell');
INSERT INTO vets VALUES (default, 'Paul', 'Baker');
INSERT INTO vets VALUES (default, 'Mary', 'Jones');
INSERT INTO vets VALUES (default, 'Alice', 'Peterson');
INSERT INTO vets VALUES (default, 'Eric', 'Robinson');
INSERT INTO vets VALUES (default, 'Henry', 'Smith');
INSERT INTO vets VALUES (default, 'Scott', 'Ford');
INSERT INTO vets VALUES (default, 'Jeffrey', 'King');
INSERT INTO vets VALUES (default, 'Laura', 'Baker');
INSERT INTO vets VALUES (default, 'Alice', 'Ross');
INSERT INTO vets VALUES (default, 'Patrick', 'Howard');
INSERT INTO vets VALUES (default, 'Hannah', 'Price');
INSERT INTO vets VALUES (default, 'Dennis', 'Price');
INSERT INTO vets VALUES (default, 'Joshua', 'Jackson');
INSERT INTO vets VALUES (default, 'Kenneth', 'White');
INSERT INTO vets VALUES (default, 'Mildred', 'Jones');
INSERT INTO vets VALUES (default, 'Evelyn', 'Stewart');
INSERT INTO vets VALUES (default, 'Jacob', 'Stewart');
INSERT INTO vets VALUES (default, 'Douglas', 'Bryant');
INSERT INTO vets VALUES (default, 'Walter', 'Bailey');
INSERT INTO vets VALUES (default, 'Linda', 'Bennett');
INSERT INTO vets VALUES (default, 'Alice', 'Thomas');
INSERT INTO vets VALUES (default, 'Rose', 'Allen');
INSERT INTO vets VALUES (default, 'Daniel', 'White');
INSERT INTO vets VALUES (default, 'Rebecca', 'Rogers');
INSERT INTO vets VALUES (default, 'Melissa', 'Garcia');
INSERT INTO vets VALUES (default, 'Jennifer', 'Phillips');
INSERT INTO vets VALUES (default, 'Tyler', 'Bailey');
INSERT INTO vets VALUES (default, 'Doris', 'Thomas');
INSERT INTO vets VALUES (default, 'Alice', 'Gonzalez');
INSERT INTO vets VALUES (default, 'Sandra', 'Scott');
INSERT INTO vets VALUES (default, 'Helen', 'Ford');
INSERT INTO vets VALUES (default, 'Helen', 'Jackson');
INSERT INTO vets VALUES (default, 'Richard', 'Miller');
INSERT INTO vets VALUES (default, 'Jacob', 'Brooks');
INSERT INTO vets VALUES (default, 'Richard', 'Hill');
INSERT INTO vets VALUES (default, 'Eric', 'Bailey');
INSERT INTO vets VALUES (default, 'Jason', 'Morgan');
INSERT INTO vets VALUES (default, 'Laura', 'Young');
INSERT INTO vets VALUES (default, 'Susan', 'Cooper');
INSERT INTO vets VALUES (default, 'Gary', 'Torres');
INSERT INTO vets VALUES (default, 'Julia', 'James');
INSERT INTO vets VALUES (default, 'Stephanie', 'Young');
INSERT INTO vets VALUES (default, 'Patrick', 'Hayes');
INSERT INTO vets VALUES (default, 'Megan', 'Roberts');
INSERT INTO vets VALUES (default, 'Stephen', 'Rivera');
INSERT INTO vets VALUES (default, 'David', 'Thompson');
INSERT INTO vets VALUES (default, 'Lauren', 'Adams');
INSERT INTO vets VALUES (default, 'Samuel', 'Wilson');
INSERT INTO vets VALUES (default, 'Rose', 'Edwards');
INSERT INTO vets VALUES (default, 'Janet', 'Jones');
INSERT INTO vets VALUES (default, 'Cheryl', 'Smith');
INSERT INTO vets VALUES (default, 'Alice', 'Roberts');
INSERT INTO vets VALUES (default, 'Nicholas', 'Walker');
INSERT INTO vets VALUES (default, 'Nicholas', 'Rodriguez');
INSERT INTO vets VALUES (default, 'Carol', 'Ford');
INSERT INTO vets VALUES (default, 'Thomas', 'Hughes');
INSERT INTO vets VALUES (default, 'Dennis', 'Brooks');
INSERT INTO vets VALUES (default, 'Doris', 'Phillips');
INSERT INTO vets VALUES (default, 'Timothy', 'Ford');
INSERT INTO vets VALUES (default, 'Susan', 'Howard');
INSERT INTO vets VALUES (default, 'Janet', 'Stewart');
INSERT INTO vets VALUES (default, 'Helen', 'Martin');
INSERT INTO vets VALUES (default, 'Rose', 'Jenkins');
INSERT INTO vets VALUES (default, 'Rebecca', 'Parker');
INSERT INTO vets VALUES (default, 'Ryan', 'Barnes');
INSERT INTO vets VALUES (default, 'Ruth', 'Nguyen');
INSERT INTO vets VALUES (default, 'Samantha', 'Rivera');
INSERT INTO vets VALUES (default, 'Alice', 'Roberts');
INSERT INTO vets VALUES (default, 'Keith', 'Howard');
INSERT INTO vets VALUES (default, 'Charles', 'Simmons');
INSERT INTO vets VALUES (default, 'Ryan', 'Kelly');
INSERT INTO vets VALUES (default, 'Martha', 'Campbell');
INSERT INTO vets VALUES (default, 'Mary', 'Thompson');
INSERT INTO vets VALUES (default, 'Eric', 'Wilson');
INSERT INTO vets VALUES (default, 'Charles', 'Russell');
INSERT INTO vets VALUES (default, 'David', 'Rodriguez');
INSERT INTO vets VALUES (default, 'Alice', 'Watson');
INSERT INTO vets VALUES (default, 'Margaret', 'Wright');
INSERT INTO vets VALUES (default, 'Dennis', 'Robinson');
INSERT INTO vets VALUES (default, 'Margaret', 'Turner');
INSERT INTO vets VALUES (default, 'Cheryl', 'Garcia');
INSERT INTO vets VALUES (default, 'Scott', 'Alexander');
INSERT INTO vets VALUES (default, 'Aaron', 'Price');
INSERT INTO vets VALUES (default, 'Patrick', 'Anderson');
INSERT INTO vets VALUES (default, 'Justin', 'Bell');
INSERT INTO vets VALUES (default, 'Melissa', 'Ward');
INSERT INTO vets VALUES (default, 'Paul', 'Perry');
INSERT INTO vets VALUES (default, 'David', 'Clark');
INSERT INTO vets VALUES (default, 'Marie', 'Robinson');
INSERT INTO vets VALUES (default, 'Sandra', 'Cooper');
INSERT INTO vets VALUES (default, 'Lauren', 'Price');
INSERT INTO vets VALUES (default, 'Ashley', 'Martin');
INSERT INTO vets VALUES (default, 'Ruth', 'Jenkins');
INSERT INTO vets VALUES (default, 'Daniel', 'Morris');
INSERT INTO vets VALUES (default, 'Lauren', 'Nguyen');
INSERT INTO vets VALUES (default, 'Charles', 'Torres');
INSERT INTO vets VALUES (default, 'Justin', 'Griffin');
INSERT INTO vets VALUES (default, 'Douglas', 'Jackson');
INSERT INTO vets VALUES (default, 'Gloria', 'Henderson');
INSERT INTO vets VALUES (default, 'Martha', 'Parker');
INSERT INTO vets VALUES (default, 'Jean', 'Martin');
INSERT INTO vets VALUES (default, 'Eric', 'Griffin');
INSERT INTO vets VALUES (default, 'Lauren', 'Wood');
INSERT INTO vets VALUES (default, 'Gary', 'Coleman');
INSERT INTO vets VALUES (default, 'Larry', 'Robinson');
INSERT INTO vets VALUES (default, 'William', 'Cook');
INSERT INTO vets VALUES (default, 'Jacob', 'Jackson');
INSERT INTO vets VALUES (default, 'George', 'Kelly');
INSERT INTO vets VALUES (default, 'Lauren', 'Perez');
INSERT INTO vets VALUES (default, 'Margaret', 'Hall');
INSERT INTO vets VALUES (default, 'Doris', 'Ross');
INSERT INTO vets VALUES (default, 'Adam', 'Miller');
INSERT INTO vets VALUES (default, 'Lauren', 'Richardson');
INSERT INTO vets VALUES (default, 'John', 'Thomas');
INSERT INTO vets VALUES (default, 'Robert', 'Ross');
INSERT INTO vets VALUES (default, 'William', 'Martin');
INSERT INTO vets VALUES (default, 'Maria', 'Sanchez');
INSERT INTO vets VALUES (default, 'Teresa', 'Morgan');
INSERT INTO vets VALUES (default, 'Janet', 'Perry');
INSERT INTO vets VALUES (default, 'Ruby', 'Rogers');
INSERT INTO vets VALUES (default, 'Rose', 'Patterson');
INSERT INTO vets VALUES (default, 'Alexander', 'Ramirez');
INSERT INTO vets VALUES (default, 'Ruth', 'Ross');
INSERT INTO vets VALUES (default, 'Doris', 'Campbell');
INSERT INTO vets VALUES (default, 'Patrick', 'Alexander');
INSERT INTO vets VALUES (default, 'Eric', 'Cox');
INSERT INTO vets VALUES (default, 'Rebecca', 'Myers');
INSERT INTO vets VALUES (default, 'Mildred', 'Long');
INSERT INTO vets VALUES (default, 'Rebecca', 'Ramirez');
INSERT INTO vets VALUES (default, 'Jeffrey', 'Butler');
INSERT INTO vets VALUES (default, 'James', 'Walker');
INSERT INTO vets VALUES (default, 'Melissa', 'Rodriguez');
INSERT INTO vets VALUES (default, 'David', 'Williams');
INSERT INTO vets VALUES (default, 'Megan', 'Henderson');
INSERT INTO vets VALUES (default, 'Patricia', 'Phillips');
INSERT INTO vets VALUES (default, 'Eric', 'Jackson');
INSERT INTO vets VALUES (default, 'Carol', 'Wood');
INSERT INTO vets VALUES (default, 'Andrew', 'Wright');
INSERT INTO vets VALUES (default, 'Anthony', 'Gonzalez');
INSERT INTO vets VALUES (default, 'Shirley', 'Martinez');
INSERT INTO vets VALUES (default, 'Janet', 'Foster');
INSERT INTO vets VALUES (default, 'Justin', 'Watson');
INSERT INTO vets VALUES (default, 'Janet', 'Thomas');
INSERT INTO vets VALUES (default, 'Melissa', 'Taylor');
INSERT INTO vets VALUES (default, 'Angela', 'Cook');
INSERT INTO vets VALUES (default, 'Jeffrey', 'Perez');
INSERT INTO vets VALUES (default, 'Matthew', 'Thomas');
INSERT INTO vets VALUES (default, 'Joshua', 'Adams');
INSERT INTO vets VALUES (default, 'Walter', 'Wright');
INSERT INTO vets VALUES (default, 'Henry', 'Diaz');
INSERT INTO vets VALUES (default, 'Melissa', 'Simmons');
INSERT INTO vets VALUES (default, 'Mary', 'Perez');
INSERT INTO vets VALUES (default, 'Carol', 'Collins');
INSERT INTO vets VALUES (default, 'Alice', 'Henderson');
INSERT INTO vets VALUES (default, 'Marie', 'Allen');
INSERT INTO vets VALUES (default, 'Linda', 'Robinson');
INSERT INTO vets VALUES (default, 'Samantha', 'Simmons');
INSERT INTO vets VALUES (default, 'Jennifer', 'Peterson');
INSERT INTO vets VALUES (default, 'Henry', 'Wright');
INSERT INTO vets VALUES (default, 'Angela', 'Russell');
INSERT INTO vets VALUES (default, 'Jean', 'Wilson');
INSERT INTO vets VALUES (default, 'George', 'Clark');
INSERT INTO vets VALUES (default, 'Lauren', 'Johnson');
INSERT INTO vets VALUES (default, 'Alice', 'James');
INSERT INTO vets VALUES (default, 'Dorothy', 'Lewis');
INSERT INTO vets VALUES (default, 'Carolyn', 'Hayes');
INSERT INTO vets VALUES (default, 'Lauren', 'Coleman');
INSERT INTO vets VALUES (default, 'Evelyn', 'Ramirez');
INSERT INTO vets VALUES (default, 'Alexander', 'Stewart');
INSERT INTO vets VALUES (default, 'John', 'Peterson');
INSERT INTO vets VALUES (default, 'Patrick', 'Edwards');
INSERT INTO vets VALUES (default, 'Evelyn', 'Collins');
INSERT INTO vets VALUES (default, 'Anthony', 'Perez');
INSERT INTO vets VALUES (default, 'Andrew', 'Lewis');
INSERT INTO vets VALUES (default, 'Richard', 'James');
INSERT INTO vets VALUES (default, 'William', 'Garcia');
INSERT INTO vets VALUES (default, 'Helen', 'Patterson');
INSERT INTO vets VALUES (default, 'Walter', 'Lewis');
INSERT INTO vets VALUES (default, 'Doris', 'Hill');
INSERT INTO vets VALUES (default, 'Scott', 'Phillips');
INSERT INTO vets VALUES (default, 'Elizabeth', 'Wood');
INSERT INTO vets VALUES (default, 'Paul', 'Hayes');
INSERT INTO vets VALUES (default, 'Mark', 'Howard');
INSERT INTO vets VALUES (default, 'Barbara', 'Coleman');
INSERT INTO vets VALUES (default, 'Andrew', 'Wood');
INSERT INTO vets VALUES (default, 'Ruth', 'Moore');
INSERT INTO vets VALUES (default, 'Sandra', 'Brooks');
INSERT INTO vets VALUES (default, 'Eric', 'Garcia');
INSERT INTO vets VALUES (default, 'Deborah', 'Ward');
INSERT INTO vets VALUES (default, 'James', 'Davis');
INSERT INTO vets VALUES (default, 'Samantha', 'Adams');
INSERT INTO vets VALUES (default, 'Nicholas', 'Johnson');
INSERT INTO vets VALUES (default, 'Joshua', 'Murphy');
INSERT INTO vets VALUES (default, 'Rebecca', 'Flores');
INSERT INTO vets VALUES (default, 'Emma', 'Bell');
INSERT INTO vets VALUES (default, 'Jerry', 'Nelson');
INSERT INTO vets VALUES (default, 'Ruth', 'Cox');
INSERT INTO vets VALUES (default, 'Gloria', 'Powell');
INSERT INTO vets VALUES (default, 'Shirley', 'Clark');
INSERT INTO vets VALUES (default, 'Larry', 'Bryant');
INSERT INTO vets VALUES (default, 'George', 'Brown');
INSERT INTO specialties VALUES (default, 'radiology');
INSERT INTO specialties VALUES (default, 'surgery');
INSERT INTO specialties VALUES (default, '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);
-- First, let's make sure we have 5 specialties
INSERT INTO specialties (name) VALUES ('radiology');
INSERT INTO specialties (name) VALUES ('surgery');
INSERT INTO specialties (name) VALUES ('dentistry');
INSERT INTO specialties (name) VALUES ('cardiology');
INSERT INTO specialties (name) VALUES ('anesthesia');
INSERT INTO vet_specialties VALUES ('220', '2');
INSERT INTO vet_specialties VALUES ('131', '1');
INSERT INTO vet_specialties VALUES ('58', '3');
INSERT INTO vet_specialties VALUES ('43', '4');
INSERT INTO vet_specialties VALUES ('110', '3');
INSERT INTO vet_specialties VALUES ('63', '5');
INSERT INTO vet_specialties VALUES ('206', '4');
INSERT INTO vet_specialties VALUES ('29', '3');
INSERT INTO vet_specialties VALUES ('189', '3');
INSERT INTO vet_specialties VALUES ('202', '4');
INSERT INTO vet_specialties VALUES ('75', '4');
INSERT INTO vet_specialties VALUES ('156', '3');
INSERT INTO vet_specialties VALUES ('218', '5');
INSERT INTO vet_specialties VALUES ('152', '4');
INSERT INTO vet_specialties VALUES ('173', '2');
INSERT INTO vet_specialties VALUES ('251', '2');
INSERT INTO vet_specialties VALUES ('99', '3');
INSERT INTO vet_specialties VALUES ('157', '2');
INSERT INTO vet_specialties VALUES ('250', '3');
INSERT INTO vet_specialties VALUES ('37', '1');
INSERT INTO vet_specialties VALUES ('197', '3');
INSERT INTO vet_specialties VALUES ('121', '3');
INSERT INTO vet_specialties VALUES ('176', '1');
INSERT INTO vet_specialties VALUES ('72', '3');
INSERT INTO vet_specialties VALUES ('160', '1');
INSERT INTO vet_specialties VALUES ('56', '5');
INSERT INTO vet_specialties VALUES ('248', '4');
INSERT INTO vet_specialties VALUES ('49', '5');
INSERT INTO vet_specialties VALUES ('61', '5');
INSERT INTO vet_specialties VALUES ('29', '4');
INSERT INTO vet_specialties VALUES ('238', '2');
INSERT INTO vet_specialties VALUES ('176', '4');
INSERT INTO vet_specialties VALUES ('46', '4');
INSERT INTO vet_specialties VALUES ('189', '5');
INSERT INTO vet_specialties VALUES ('56', '2');
INSERT INTO vet_specialties VALUES ('200', '5');
INSERT INTO vet_specialties VALUES ('70', '3');
INSERT INTO vet_specialties VALUES ('213', '5');
INSERT INTO vet_specialties VALUES ('106', '2');
INSERT INTO vet_specialties VALUES ('219', '2');
INSERT INTO vet_specialties VALUES ('81', '3');
INSERT INTO vet_specialties VALUES ('126', '5');
INSERT INTO vet_specialties VALUES ('255', '2');
INSERT INTO vet_specialties VALUES ('175', '4');
INSERT INTO vet_specialties VALUES ('71', '2');
INSERT INTO vet_specialties VALUES ('206', '1');
INSERT INTO vet_specialties VALUES ('85', '2');
INSERT INTO vet_specialties VALUES ('177', '1');
INSERT INTO vet_specialties VALUES ('118', '2');
INSERT INTO vet_specialties VALUES ('256', '4');
INSERT INTO vet_specialties VALUES ('136', '3');
INSERT INTO vet_specialties VALUES ('44', '5');
INSERT INTO vet_specialties VALUES ('236', '4');
INSERT INTO vet_specialties VALUES ('2', '5');
INSERT INTO vet_specialties VALUES ('88', '1');
INSERT INTO vet_specialties VALUES ('86', '1');
INSERT INTO vet_specialties VALUES ('155', '2');
INSERT INTO vet_specialties VALUES ('122', '5');
INSERT INTO vet_specialties VALUES ('61', '3');
INSERT INTO vet_specialties VALUES ('24', '5');
INSERT INTO vet_specialties VALUES ('14', '4');
INSERT INTO vet_specialties VALUES ('159', '2');
INSERT INTO vet_specialties VALUES ('67', '4');
INSERT INTO vet_specialties VALUES ('182', '2');
INSERT INTO vet_specialties VALUES ('10', '1');
INSERT INTO vet_specialties VALUES ('174', '4');
INSERT INTO vet_specialties VALUES ('166', '5');
INSERT INTO vet_specialties VALUES ('91', '3');
INSERT INTO vet_specialties VALUES ('244', '5');
INSERT INTO vet_specialties VALUES ('135', '2');
INSERT INTO vet_specialties VALUES ('222', '1');
INSERT INTO vet_specialties VALUES ('151', '3');
INSERT INTO vet_specialties VALUES ('79', '4');
INSERT INTO vet_specialties VALUES ('182', '3');
INSERT INTO vet_specialties VALUES ('249', '1');
INSERT INTO vet_specialties VALUES ('133', '1');
INSERT INTO vet_specialties VALUES ('127', '4');
INSERT INTO vet_specialties VALUES ('123', '5');
INSERT INTO vet_specialties VALUES ('248', '5');
INSERT INTO vet_specialties VALUES ('99', '1');
INSERT INTO vet_specialties VALUES ('252', '5');
INSERT INTO vet_specialties VALUES ('221', '1');
INSERT INTO vet_specialties VALUES ('123', '2');
INSERT INTO vet_specialties VALUES ('16', '3');
INSERT INTO vet_specialties VALUES ('180', '2');
INSERT INTO vet_specialties VALUES ('198', '3');
INSERT INTO vet_specialties VALUES ('214', '3');
INSERT INTO vet_specialties VALUES ('144', '2');
INSERT INTO vet_specialties VALUES ('140', '4');
INSERT INTO vet_specialties VALUES ('233', '2');
INSERT INTO vet_specialties VALUES ('87', '5');
INSERT INTO vet_specialties VALUES ('12', '4');
INSERT INTO vet_specialties VALUES ('2', '3');
INSERT INTO vet_specialties VALUES ('156', '4');
INSERT INTO vet_specialties VALUES ('105', '4');
INSERT INTO vet_specialties VALUES ('144', '3');
INSERT INTO vet_specialties VALUES ('31', '2');
INSERT INTO vet_specialties VALUES ('228', '5');
INSERT INTO vet_specialties VALUES ('232', '3');
INSERT INTO vet_specialties VALUES ('33', '1');
INSERT INTO vet_specialties VALUES ('195', '2');
INSERT INTO vet_specialties VALUES ('12', '1');
INSERT INTO vet_specialties VALUES ('180', '1');
INSERT INTO vet_specialties VALUES ('122', '2');
INSERT INTO vet_specialties VALUES ('101', '1');
INSERT INTO vet_specialties VALUES ('211', '2');
INSERT INTO vet_specialties VALUES ('175', '2');
INSERT INTO vet_specialties VALUES ('246', '1');
INSERT INTO vet_specialties VALUES ('251', '4');
INSERT INTO vet_specialties VALUES ('13', '4');
INSERT INTO vet_specialties VALUES ('217', '2');
INSERT INTO vet_specialties VALUES ('171', '3');
INSERT INTO vet_specialties VALUES ('125', '5');
INSERT INTO vet_specialties VALUES ('129', '3');
INSERT INTO vet_specialties VALUES ('217', '3');
INSERT INTO vet_specialties VALUES ('68', '4');
INSERT INTO vet_specialties VALUES ('13', '2');
INSERT INTO vet_specialties VALUES ('214', '2');
INSERT INTO vet_specialties VALUES ('128', '1');
INSERT INTO vet_specialties VALUES ('253', '2');
INSERT INTO vet_specialties VALUES ('36', '1');
INSERT INTO vet_specialties VALUES ('210', '2');
INSERT INTO vet_specialties VALUES ('81', '5');
INSERT INTO vet_specialties VALUES ('237', '5');
INSERT INTO vet_specialties VALUES ('78', '3');
INSERT INTO vet_specialties VALUES ('244', '1');
INSERT INTO vet_specialties VALUES ('37', '4');
INSERT INTO vet_specialties VALUES ('230', '2');
INSERT INTO vet_specialties VALUES ('9', '3');
INSERT INTO vet_specialties VALUES ('249', '5');
INSERT INTO vet_specialties VALUES ('210', '5');
INSERT INTO vet_specialties VALUES ('33', '5');
INSERT INTO vet_specialties VALUES ('177', '2');
INSERT INTO vet_specialties VALUES ('92', '3');
INSERT INTO vet_specialties VALUES ('18', '5');
INSERT INTO vet_specialties VALUES ('82', '4');
INSERT INTO vet_specialties VALUES ('185', '1');
INSERT INTO vet_specialties VALUES ('70', '1');
INSERT INTO vet_specialties VALUES ('146', '2');
INSERT INTO vet_specialties VALUES ('60', '2');
INSERT INTO vet_specialties VALUES ('157', '4');
INSERT INTO vet_specialties VALUES ('43', '1');
INSERT INTO vet_specialties VALUES ('124', '4');
INSERT INTO vet_specialties VALUES ('185', '2');
INSERT INTO vet_specialties VALUES ('92', '2');
INSERT INTO vet_specialties VALUES ('152', '5');
INSERT INTO vet_specialties VALUES ('161', '3');
INSERT INTO vet_specialties VALUES ('178', '3');
INSERT INTO vet_specialties VALUES ('53', '2');
INSERT INTO vet_specialties VALUES ('93', '5');
INSERT INTO vet_specialties VALUES ('168', '2');
INSERT INTO vet_specialties VALUES ('19', '4');
INSERT INTO vet_specialties VALUES ('158', '2');
INSERT INTO vet_specialties VALUES ('240', '4');
INSERT INTO vet_specialties VALUES ('246', '5');
INSERT INTO vet_specialties VALUES ('166', '4');
INSERT INTO vet_specialties VALUES ('134', '5');
INSERT INTO vet_specialties VALUES ('151', '4');
INSERT INTO vet_specialties VALUES ('113', '1');
INSERT INTO vet_specialties VALUES ('114', '4');
INSERT INTO vet_specialties VALUES ('107', '3');
INSERT INTO vet_specialties VALUES ('200', '2');
INSERT INTO vet_specialties VALUES ('17', '4');
INSERT INTO vet_specialties VALUES ('140', '5');
INSERT INTO vet_specialties VALUES ('196', '2');
INSERT INTO vet_specialties VALUES ('108', '1');
INSERT INTO vet_specialties VALUES ('102', '3');
INSERT INTO vet_specialties VALUES ('83', '4');
INSERT INTO vet_specialties VALUES ('79', '1');
INSERT INTO vet_specialties VALUES ('91', '4');
INSERT INTO vet_specialties VALUES ('30', '4');
INSERT INTO vet_specialties VALUES ('165', '3');
INSERT INTO vet_specialties VALUES ('34', '1');
INSERT INTO vet_specialties VALUES ('204', '4');
INSERT INTO vet_specialties VALUES ('30', '5');
INSERT INTO vet_specialties VALUES ('84', '5');
INSERT INTO vet_specialties VALUES ('187', '5');
INSERT INTO vet_specialties VALUES ('127', '1');
INSERT INTO vet_specialties VALUES ('229', '5');
INSERT INTO vet_specialties VALUES ('71', '1');
INSERT INTO vet_specialties VALUES ('253', '3');
INSERT INTO vet_specialties VALUES ('102', '1');
INSERT INTO vet_specialties VALUES ('195', '1');
INSERT INTO vet_specialties VALUES ('149', '3');
INSERT INTO vet_specialties VALUES ('238', '5');
INSERT INTO vet_specialties VALUES ('113', '3');
INSERT INTO vet_specialties VALUES ('105', '1');
INSERT INTO vet_specialties VALUES ('111', '2');
INSERT INTO vet_specialties VALUES ('20', '1');
INSERT INTO vet_specialties VALUES ('52', '4');
INSERT INTO vet_specialties VALUES ('226', '5');
INSERT INTO vet_specialties VALUES ('216', '1');
INSERT INTO vet_specialties VALUES ('136', '5');
INSERT INTO vet_specialties VALUES ('250', '2');
INSERT INTO vet_specialties VALUES ('229', '2');
INSERT INTO vet_specialties VALUES ('198', '4');
INSERT INTO vet_specialties VALUES ('73', '1');
INSERT INTO vet_specialties VALUES ('128', '4');
INSERT INTO vet_specialties VALUES ('161', '4');
INSERT INTO vet_specialties VALUES ('111', '1');
INSERT INTO vet_specialties VALUES ('146', '1');
INSERT INTO vet_specialties VALUES ('60', '5');
INSERT INTO vet_specialties VALUES ('235', '5');
INSERT INTO vet_specialties VALUES ('174', '2');
INSERT INTO vet_specialties VALUES ('225', '5');
INSERT INTO vet_specialties VALUES ('224', '1');
INSERT INTO vet_specialties VALUES ('256', '1');
INSERT INTO vet_specialties VALUES ('132', '3');
INSERT INTO vet_specialties VALUES ('211', '4');
INSERT INTO vet_specialties VALUES ('104', '4');
INSERT INTO vet_specialties VALUES ('112', '3');
INSERT INTO vet_specialties VALUES ('213', '1');
INSERT INTO vet_specialties VALUES ('108', '5');
INSERT INTO vet_specialties VALUES ('143', '2');
INSERT INTO vet_specialties VALUES ('141', '5');
INSERT INTO vet_specialties VALUES ('159', '1');
INSERT INTO vet_specialties VALUES ('228', '3');
INSERT INTO vet_specialties VALUES ('75', '5');
INSERT INTO vet_specialties VALUES ('36', '5');
INSERT INTO vet_specialties VALUES ('237', '1');
INSERT INTO vet_specialties VALUES ('83', '5');
INSERT INTO vet_specialties VALUES ('124', '1');
INSERT INTO vet_specialties VALUES ('133', '2');
INSERT INTO vet_specialties VALUES ('121', '5');
INSERT INTO vet_specialties VALUES ('240', '5');
INSERT INTO vet_specialties VALUES ('142', '3');
INSERT INTO vet_specialties VALUES ('193', '3');
INSERT INTO vet_specialties VALUES ('252', '3');
INSERT INTO vet_specialties VALUES ('118', '4');
INSERT INTO vet_specialties VALUES ('26', '3');
INSERT INTO vet_specialties VALUES ('197', '2');
INSERT INTO vet_specialties VALUES ('17', '2');
INSERT INTO vet_specialties VALUES ('78', '2');
INSERT INTO vet_specialties VALUES ('26', '5');
INSERT INTO vet_specialties VALUES ('196', '1');
INSERT INTO vet_specialties VALUES ('141', '2');
INSERT INTO vet_specialties VALUES ('224', '2');
INSERT INTO vet_specialties VALUES ('223', '5');
INSERT INTO vet_specialties VALUES ('170', '3');
INSERT INTO vet_specialties VALUES ('9', '2');
INSERT INTO vet_specialties VALUES ('94', '1');
INSERT INTO vet_specialties VALUES ('230', '3');
INSERT INTO vet_specialties VALUES ('131', '5');
INSERT INTO vet_specialties VALUES ('117', '3');
INSERT INTO vet_specialties VALUES ('223', '4');
INSERT INTO vet_specialties VALUES ('236', '2');
INSERT INTO vet_specialties VALUES ('97', '2');
INSERT INTO vet_specialties VALUES ('143', '1');
INSERT INTO vet_specialties VALUES ('173', '1');
INSERT INTO vet_specialties VALUES ('132', '5');
INSERT INTO vet_specialties VALUES ('138', '1');
INSERT INTO vet_specialties VALUES ('80', '5');
INSERT INTO vet_specialties VALUES ('6', '5');
INSERT INTO vet_specialties VALUES ('216', '3');
INSERT INTO vet_specialties VALUES ('135', '5');
INSERT INTO vet_specialties VALUES ('160', '3');
INSERT INTO vet_specialties VALUES ('97', '4');
INSERT INTO types VALUES (default, 'cat');
INSERT INTO types VALUES (default, 'dog');
@ -51,3 +558,5 @@ INSERT INTO visits VALUES (default, 7, '2013-01-01', 'rabies shot');
INSERT INTO visits VALUES (default, 8, '2013-01-02', 'rabies shot');
INSERT INTO visits VALUES (default, 8, '2013-01-03', 'neutered');
INSERT INTO visits VALUES (default, 7, '2013-01-04', 'spayed');

View file

@ -0,0 +1,262 @@
-- Create a list of first names and last names
WITH first_names AS (
SELECT 'James' AS name UNION ALL
SELECT 'Mary' UNION ALL
SELECT 'John' UNION ALL
SELECT 'Patricia' UNION ALL
SELECT 'Robert' UNION ALL
SELECT 'Linda' UNION ALL
SELECT 'Michael' UNION ALL
SELECT 'Barbara' UNION ALL
SELECT 'William' UNION ALL
SELECT 'Elizabeth' UNION ALL
SELECT 'David' UNION ALL
SELECT 'Jennifer' UNION ALL
SELECT 'Richard' UNION ALL
SELECT 'Maria' UNION ALL
SELECT 'Charles' UNION ALL
SELECT 'Susan' UNION ALL
SELECT 'Joseph' UNION ALL
SELECT 'Margaret' UNION ALL
SELECT 'Thomas' UNION ALL
SELECT 'Dorothy' UNION ALL
SELECT 'Daniel' UNION ALL
SELECT 'Helen' UNION ALL
SELECT 'Matthew' UNION ALL
SELECT 'Sandra' UNION ALL
SELECT 'Anthony' UNION ALL
SELECT 'Ashley' UNION ALL
SELECT 'Mark' UNION ALL
SELECT 'Donna' UNION ALL
SELECT 'Paul' UNION ALL
SELECT 'Carol' UNION ALL
SELECT 'Andrew' UNION ALL
SELECT 'Ruth' UNION ALL
SELECT 'Joshua' UNION ALL
SELECT 'Shirley' UNION ALL
SELECT 'Kenneth' UNION ALL
SELECT 'Angela' UNION ALL
SELECT 'Kevin' UNION ALL
SELECT 'Melissa' UNION ALL
SELECT 'Brian' UNION ALL
SELECT 'Deborah' UNION ALL
SELECT 'George' UNION ALL
SELECT 'Stephanie' UNION ALL
SELECT 'Edward' UNION ALL
SELECT 'Rebecca' UNION ALL
SELECT 'Ronald' UNION ALL
SELECT 'Laura' UNION ALL
SELECT 'Timothy' UNION ALL
SELECT 'Helen' UNION ALL
SELECT 'Jason' UNION ALL
SELECT 'Alice' UNION ALL
SELECT 'Jeffrey' UNION ALL
SELECT 'Judith' UNION ALL
SELECT 'Ryan' UNION ALL
SELECT 'Jacqueline' UNION ALL
SELECT 'Jacob' UNION ALL
SELECT 'Frances' UNION ALL
SELECT 'Gary' UNION ALL
SELECT 'Martha' UNION ALL
SELECT 'Nicholas' UNION ALL
SELECT 'Teresa' UNION ALL
SELECT 'Eric' UNION ALL
SELECT 'Doris' UNION ALL
SELECT 'Stephen' UNION ALL
SELECT 'Gloria' UNION ALL
SELECT 'Larry' UNION ALL
SELECT 'Evelyn' UNION ALL
SELECT 'Justin' UNION ALL
SELECT 'Jean' UNION ALL
SELECT 'Scott' UNION ALL
SELECT 'Cheryl' UNION ALL
SELECT 'Brandon' UNION ALL
SELECT 'Mildred' UNION ALL
SELECT 'Benjamin' UNION ALL
SELECT 'Katherine' UNION ALL
SELECT 'Adam' UNION ALL
SELECT 'Samantha' UNION ALL
SELECT 'Samuel' UNION ALL
SELECT 'Janet' UNION ALL
SELECT 'Alexander' UNION ALL
SELECT 'Megan' UNION ALL
SELECT 'Patrick' UNION ALL
SELECT 'Carolyn' UNION ALL
SELECT 'Jack' UNION ALL
SELECT 'Hannah' UNION ALL
SELECT 'Dennis' UNION ALL
SELECT 'Christine' UNION ALL
SELECT 'Jerry' UNION ALL
SELECT 'Emma' UNION ALL
SELECT 'Tyler' UNION ALL
SELECT 'Lauren' UNION ALL
SELECT 'Aaron' UNION ALL
SELECT 'Alice' UNION ALL
SELECT 'Henry' UNION ALL
SELECT 'Julia' UNION ALL
SELECT 'Douglas' UNION ALL
SELECT 'Marie' UNION ALL
SELECT 'Keith' UNION ALL
SELECT 'Ruby' UNION ALL
SELECT 'Walter' UNION ALL
SELECT 'Rose'
),
last_names AS (
SELECT 'Smith' AS name UNION ALL
SELECT 'Johnson' UNION ALL
SELECT 'Williams' UNION ALL
SELECT 'Jones' UNION ALL
SELECT 'Brown' UNION ALL
SELECT 'Davis' UNION ALL
SELECT 'Miller' UNION ALL
SELECT 'Wilson' UNION ALL
SELECT 'Moore' UNION ALL
SELECT 'Taylor' UNION ALL
SELECT 'Anderson' UNION ALL
SELECT 'Thomas' UNION ALL
SELECT 'Jackson' UNION ALL
SELECT 'White' UNION ALL
SELECT 'Harris' UNION ALL
SELECT 'Martin' UNION ALL
SELECT 'Thompson' UNION ALL
SELECT 'Garcia' UNION ALL
SELECT 'Martinez' UNION ALL
SELECT 'Robinson' UNION ALL
SELECT 'Clark' UNION ALL
SELECT 'Rodriguez' UNION ALL
SELECT 'Lewis' UNION ALL
SELECT 'Lee' UNION ALL
SELECT 'Walker' UNION ALL
SELECT 'Hall' UNION ALL
SELECT 'Allen' UNION ALL
SELECT 'Young' UNION ALL
SELECT 'King' UNION ALL
SELECT 'Wright' UNION ALL
SELECT 'Scott' UNION ALL
SELECT 'Torres' UNION ALL
SELECT 'Nguyen' UNION ALL
SELECT 'Hill' UNION ALL
SELECT 'Adams' UNION ALL
SELECT 'Baker' UNION ALL
SELECT 'Nelson' UNION ALL
SELECT 'Carter' UNION ALL
SELECT 'Mitchell' UNION ALL
SELECT 'Perez' UNION ALL
SELECT 'Roberts' UNION ALL
SELECT 'Turner' UNION ALL
SELECT 'Phillips' UNION ALL
SELECT 'Campbell' UNION ALL
SELECT 'Parker' UNION ALL
SELECT 'Evans' UNION ALL
SELECT 'Edwards' UNION ALL
SELECT 'Collins' UNION ALL
SELECT 'Stewart' UNION ALL
SELECT 'Sanchez' UNION ALL
SELECT 'Morris' UNION ALL
SELECT 'Rogers' UNION ALL
SELECT 'Reed' UNION ALL
SELECT 'Cook' UNION ALL
SELECT 'Morgan' UNION ALL
SELECT 'Bell' UNION ALL
SELECT 'Murphy' UNION ALL
SELECT 'Bailey' UNION ALL
SELECT 'Rivera' UNION ALL
SELECT 'Cooper' UNION ALL
SELECT 'Richardson' UNION ALL
SELECT 'Cox' UNION ALL
SELECT 'Howard' UNION ALL
SELECT 'Ward' UNION ALL
SELECT 'Torres' UNION ALL
SELECT 'Peterson' UNION ALL
SELECT 'Gray' UNION ALL
SELECT 'Ramirez' UNION ALL
SELECT 'James' UNION ALL
SELECT 'Watson' UNION ALL
SELECT 'Brooks' UNION ALL
SELECT 'Kelly' UNION ALL
SELECT 'Sanders' UNION ALL
SELECT 'Price' UNION ALL
SELECT 'Bennett' UNION ALL
SELECT 'Wood' UNION ALL
SELECT 'Barnes' UNION ALL
SELECT 'Ross' UNION ALL
SELECT 'Henderson' UNION ALL
SELECT 'Coleman' UNION ALL
SELECT 'Jenkins' UNION ALL
SELECT 'Perry' UNION ALL
SELECT 'Powell' UNION ALL
SELECT 'Long' UNION ALL
SELECT 'Patterson' UNION ALL
SELECT 'Hughes' UNION ALL
SELECT 'Flores' UNION ALL
SELECT 'Washington' UNION ALL
SELECT 'Butler' UNION ALL
SELECT 'Simmons' UNION ALL
SELECT 'Foster' UNION ALL
SELECT 'Gonzalez' UNION ALL
SELECT 'Bryant' UNION ALL
SELECT 'Alexander' UNION ALL
SELECT 'Russell' UNION ALL
SELECT 'Griffin' UNION ALL
SELECT 'Diaz' UNION ALL
SELECT 'Hayes' UNION ALL
SELECT 'Myers' UNION ALL
SELECT 'Ford'
),
random_names AS (
SELECT
first_names.name AS first_name,
last_names.name AS last_name
FROM
first_names
CROSS JOIN
last_names
ORDER BY
RAND()
LIMIT 250
)
INSERT INTO vets (first_name, last_name)
SELECT first_name, last_name FROM random_names;
-- Add specialties for 80% of the vets
WITH vet_ids AS (
SELECT id
FROM vets
ORDER BY RAND()
LIMIT 200 -- 80% of 1000
),
specialties AS (
SELECT id
FROM specialties
),
random_specialties AS (
SELECT
vet_ids.id AS vet_id,
specialties.id AS specialty_id
FROM
vet_ids
CROSS JOIN
specialties
ORDER BY
RAND()
LIMIT 300 -- 2 specialties per vet on average
)
INSERT INTO vet_specialties (vet_id, specialty_id)
SELECT
vet_id,
specialty_id
FROM (
SELECT
vet_id,
specialty_id,
ROW_NUMBER() OVER (PARTITION BY vet_id ORDER BY RAND()) AS rn
FROM
random_specialties
) tmp
WHERE
rn <= 2; -- Assign at most 2 specialties per vet
-- The remaining 20% of vets will have no specialties, so no need for additional insertion commands

View file

@ -1,9 +0,0 @@
welcome=خوش آمدید
required=الزامی
notFound=یافت نشد
duplicate=قبلا استفاده شده
nonNumeric=باید عددی باشد
duplicateFormSubmission=ارسال تکراری فرم مجاز نیست
typeMismatch.date=تاریخ نامعتبر
typeMismatch.birthDate=تاریخ تولد نامعتبر

View file

@ -1,8 +0,0 @@
welcome=Bem-vindo
required=E necessario
notFound=Nao foi encontrado
duplicate=Ja esta em uso
nonNumeric=Deve ser tudo numerico
duplicateFormSubmission=O envio duplicado de formulario nao e permitido
typeMismatch.date=Data invalida
typeMismatch.birthDate=Data de nascimento invalida

View file

@ -1,9 +0,0 @@
welcome=Добро пожаловать
required=необходимо
notFound=не найдено
duplicate=уже используется
nonNumeric=должно быть все числовое значение
duplicateFormSubmission=Дублирование формы не допускается
typeMismatch.date=неправильная даные
typeMismatch.birthDate=неправильная дата

View file

@ -1,9 +0,0 @@
welcome=hoş geldiniz
required=gerekli
notFound=bulunamadı
duplicate=zaten kullanılıyor
nonNumeric=sadece sayısal olmalıdır
duplicateFormSubmission=Formun tekrar gönderilmesine izin verilmez
typeMismatch.date=geçersiz tarih
typeMismatch.birthDate=geçersiz tarih

View file

@ -9387,6 +9387,99 @@ table td.action-column {
hr {
border-top: 1px dotted #34302D; }
/* Chatbox container */
.chatbox {
position: fixed;
bottom: 10px;
right: 10px;
width: 300px;
background-color: #f1f1f1;
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column; }
.chatbox.minimized .chatbox-content {
height: 40px;
/* Height when minimized (header only) */ }
.chatbox.minimized .chatbox-messages,
.chatbox.minimized .chatbox-footer {
display: none; }
/* Header styling */
.chatbox-header {
background-color: #075E54;
color: white;
padding: 10px;
text-align: center;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
cursor: pointer; }
/* Chatbox content styling */
.chatbox-content {
display: flex;
flex-direction: column;
height: 400px;
/* Adjust to desired height */
overflow: hidden;
/* Hide overflow to make it scrollable */ }
.chatbox-messages {
flex-grow: 1;
overflow-y: auto;
/* Allows vertical scrolling */
padding: 10px; }
/* Chat bubbles styling */
.chat-bubble {
max-width: 80%;
padding: 10px;
border-radius: 20px;
margin-bottom: 10px;
position: relative;
word-wrap: break-word;
font-size: 14px; }
.chat-bubble strong {
font-weight: bold; }
.chat-bubble em {
font-style: italic; }
.chat-bubble.user {
background-color: #dcf8c6;
/* WhatsApp-style light green */
margin-left: auto;
text-align: right;
border-bottom-right-radius: 0; }
.chat-bubble.bot {
background-color: #ffffff;
margin-right: auto;
text-align: left;
border-bottom-left-radius: 0;
border: 1px solid #e1e1e1; }
/* Input field and button */
.chatbox-footer {
padding: 10px;
background-color: #f9f9f9;
display: flex; }
.chatbox-footer input {
flex-grow: 1;
padding: 10px;
border-radius: 20px;
border: 1px solid #ccc;
margin-right: 10px;
outline: none; }
.chatbox-footer button {
background-color: #075E54;
color: white;
border: none;
padding: 10px;
border-radius: 50%;
cursor: pointer; }
.chatbox-footer button:hover {
background-color: #128C7E; }
@font-face {
font-family: 'varela_roundregular';
src: url("../fonts/varela_round-webfont.eot");
@ -9518,3 +9611,117 @@ strong {
margin-bottom: 30px; } }
/*# sourceMappingURL=../../../../../../target/petclinic.css.map */
/* Chatbox container */
.chatbox {
position: fixed;
bottom: 10px;
right: 10px;
width: 300px;
background-color: #f1f1f1;
border-radius: 10px;
box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
}
/* Header styling */
.chatbox-header {
background-color: #075E54;
color: white;
padding: 10px;
text-align: center;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
cursor: pointer;
}
/* Chatbox content styling */
.chatbox-content {
display: flex;
flex-direction: column;
height: 400px; /* Adjust to desired height */
overflow: hidden; /* Hide overflow to make it scrollable */
}
/* Minimize style */
.chatbox.minimized .chatbox-content {
height: 40px; /* Height when minimized (header only) */
}
.chatbox.minimized .chatbox-messages,
.chatbox.minimized .chatbox-footer {
display: none;
}
.chatbox-messages {
flex-grow: 1;
overflow-y: auto; /* Allows vertical scrolling */
padding: 10px;
}
/* Chat bubbles styling */
.chat-bubble {
max-width: 80%;
padding: 10px;
border-radius: 20px;
margin-bottom: 10px;
position: relative;
word-wrap: break-word;
font-size: 14px;
}
/* Ensure bold and italic styles are handled */
.chat-bubble strong {
font-weight: bold;
}
.chat-bubble em {
font-style: italic;
}
.chat-bubble.user {
background-color: #dcf8c6; /* WhatsApp-style light green */
margin-left: auto;
text-align: right;
border-bottom-right-radius: 0;
}
.chat-bubble.bot {
background-color: #ffffff;
margin-right: auto;
text-align: left;
border-bottom-left-radius: 0;
border: 1px solid #e1e1e1;
}
/* Input field and button */
.chatbox-footer {
padding: 10px;
background-color: #f9f9f9;
display: flex;
}
.chatbox-footer input {
flex-grow: 1;
padding: 10px;
border-radius: 20px;
border: 1px solid #ccc;
margin-right: 10px;
outline: none;
}
.chatbox-footer button {
background-color: #075E54;
color: white;
border: none;
padding: 10px;
border-radius: 50%;
cursor: pointer;
}
.chatbox-footer button:hover {
background-color: #128C7E;
}

116
src/main/resources/templates/fragments/layout.html Normal file → Executable file
View file

@ -17,7 +17,7 @@
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
<link th:href="@{/webjars/font-awesome/css/font-awesome.min.css}" rel="stylesheet">
<link th:href="@{/webjars/font-awesome/4.7.0/css/font-awesome.min.css}" rel="stylesheet">
<link rel="stylesheet" th:href="@{/resources/css/petclinic.css}" />
</head>
@ -70,6 +70,7 @@
</div>
</div>
</nav>
<div class="container-fluid">
<div class="container xd-container">
@ -87,7 +88,118 @@
</div>
</div>
<script th:src="@{/webjars/bootstrap/dist/js/bootstrap.bundle.min.js}"></script>
<div class="chatbox" id="chatbox">
<div class="chatbox-header" onclick="toggleChatbox()">
Chat with Us!
</div>
<div class="chatbox-content" id="chatbox-content">
<div class="chatbox-messages" id="chatbox-messages">
<!-- Chat messages will be dynamically inserted here -->
</div>
<div class="chatbox-footer">
<input type="text" id="chatbox-input" placeholder="Type a message..." onkeydown="handleKeyPress(event)" />
<button onclick="sendMessage()">Send</button>
</div>
</div>
</div>
<!-- JavaScript for handling chatbox interaction -->
<script>
function appendMessage(message, type) {
const chatMessages = document.getElementById('chatbox-messages');
const messageElement = document.createElement('div');
messageElement.classList.add('chat-bubble', type);
// Convert Markdown to HTML
const htmlContent = marked.parse(message); // Use marked.parse() for newer versions
messageElement.innerHTML = htmlContent;
chatMessages.appendChild(messageElement);
// Scroll to the bottom of the chatbox to show the latest message
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function toggleChatbox() {
const chatbox = document.getElementById('chatbox');
const chatboxContent = document.getElementById('chatbox-content');
if (chatbox.classList.contains('minimized')) {
chatbox.classList.remove('minimized');
chatboxContent.style.height = '400px'; // Set to initial height when expanded
} else {
chatbox.classList.add('minimized');
chatboxContent.style.height = '40px'; // Set to minimized height
}
}
function sendMessage() {
const query = document.getElementById('chatbox-input').value;
// Only send if there's a message
if (!query.trim()) return;
// Clear the input field after sending the message
document.getElementById('chatbox-input').value = '';
// Display user message in the chatbox
appendMessage(query, 'user');
// Send the message to the backend
fetch('/chatclient', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(query),
})
.then(response => response.text())
.then(responseText => {
// Display the response from the server in the chatbox
appendMessage(responseText, 'bot');
})
.catch(error => console.error('Error:', error));
}
function handleKeyPress(event) {
if (event.key === "Enter") {
event.preventDefault(); // Prevents adding a newline
sendMessage(); // Send the message when Enter is pressed
}
}
// Save chat messages to localStorage
function saveChatMessages() {
const messages = document.getElementById('chatbox-messages').innerHTML;
localStorage.setItem('chatMessages', messages);
}
// Load chat messages from localStorage
function loadChatMessages() {
const messages = localStorage.getItem('chatMessages');
if (messages) {
document.getElementById('chatbox-messages').innerHTML = messages;
document.getElementById('chatbox-messages').scrollTop = document.getElementById('chatbox-messages').scrollHeight;
}
}
// Call loadChatMessages when the page loads
window.onload = loadChatMessages;
// Ensure messages are saved when navigating away
window.onbeforeunload = saveChatMessages;
</script>
<script th:src="@{/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js}"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
</body>

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,88 @@
.chatbox {
position: fixed;
bottom: 0;
right: 0;
width: 300px;
border-radius: 10px 10px 0 0;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
font-family: Arial, sans-serif;
background-color: #f0f0f0;
overflow: hidden;
&.minimized {
height: 40px;
}
&-header {
background-color: $spring-green;
color: white;
padding: 10px;
cursor: pointer;
font-weight: bold;
text-align: center;
}
&-content {
height: 400px;
display: flex;
flex-direction: column;
justify-content: space-between;
background-color: white;
transition: height 0.3s ease;
}
&-messages {
padding: 10px;
overflow-y: auto;
height: 90%;
display: flex;
flex-direction: column;
gap: 10px;
.chat-bubble {
padding: 10px;
border-radius: 15px;
max-width: 70%;
word-wrap: break-word;
&.user {
background-color: $spring-brown;
color: white;
align-self: flex-end;
}
&.bot {
background-color: $spring-grey;
color: black;
align-self: flex-start;
}
}
}
&-footer {
display: flex;
padding: 10px;
background-color: $spring-light-grey;
input {
flex: 1;
padding: 10px;
border-radius: 5px;
border: 1px solid $spring-brown;
}
button {
margin-left: 10px;
padding: 10px 20px;
background-color: $spring-green;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
&:hover {
background-color: darken($spring-green, 10%);
}
}
}
}

View file

@ -21,6 +21,20 @@ $spring-brown: #34302D;
$spring-grey: #838789;
$spring-light-grey: #f1f1f1;
$chatbox-bg-color: #f1f1f1;
$chatbox-header-bg-color: #075E54;
$chatbox-header-text-color: white;
$chatbox-height: 400px;
$chatbox-border-radius: 10px;
$chatbox-box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
$chatbox-bubble-user-bg-color: #dcf8c6;
$chatbox-bubble-bot-bg-color: #ffffff;
$chatbox-bubble-border-color: #e1e1e1;
$chatbox-footer-bg-color: #f9f9f9;
$chatbox-input-border-color: #ccc;
$chatbox-button-bg-color: #075E54;
$chatbox-button-hover-bg-color: #128C7E;
$body-bg: $spring-light-grey;
$text-color: $spring-brown;
$link-color: $spring-dark-green;
@ -209,6 +223,118 @@ hr {
border-top: 1px dotted $spring-brown;
}
/* Chatbox container */
.chatbox {
position: fixed;
bottom: 10px;
right: 10px;
width: 300px;
background-color: $chatbox-bg-color;
border-radius: $chatbox-border-radius;
box-shadow: $chatbox-box-shadow;
display: flex;
flex-direction: column;
&.minimized {
.chatbox-content {
height: 40px; /* Height when minimized (header only) */
}
.chatbox-messages,
.chatbox-footer {
display: none;
}
}
}
/* Header styling */
.chatbox-header {
background-color: $chatbox-header-bg-color;
color: $chatbox-header-text-color;
padding: 10px;
text-align: center;
border-top-left-radius: $chatbox-border-radius;
border-top-right-radius: $chatbox-border-radius;
cursor: pointer;
}
/* Chatbox content styling */
.chatbox-content {
display: flex;
flex-direction: column;
height: $chatbox-height; /* Adjust to desired height */
overflow: hidden; /* Hide overflow to make it scrollable */
}
.chatbox-messages {
flex-grow: 1;
overflow-y: auto; /* Allows vertical scrolling */
padding: 10px;
}
/* Chat bubbles styling */
.chat-bubble {
max-width: 80%;
padding: 10px;
border-radius: 20px;
margin-bottom: 10px;
position: relative;
word-wrap: break-word;
font-size: 14px;
strong {
font-weight: bold;
}
em {
font-style: italic;
}
&.user {
background-color: $chatbox-bubble-user-bg-color; /* WhatsApp-style light green */
margin-left: auto;
text-align: right;
border-bottom-right-radius: 0;
}
&.bot {
background-color: $chatbox-bubble-bot-bg-color;
margin-right: auto;
text-align: left;
border-bottom-left-radius: 0;
border: 1px solid $chatbox-bubble-border-color;
}
}
/* Input field and button */
.chatbox-footer {
padding: 10px;
background-color: $chatbox-footer-bg-color;
display: flex;
}
.chatbox-footer input {
flex-grow: 1;
padding: 10px;
border-radius: 20px;
border: 1px solid $chatbox-input-border-color;
margin-right: 10px;
outline: none;
}
.chatbox-footer button {
background-color: $chatbox-button-bg-color;
color: white;
border: none;
padding: 10px;
border-radius: 50%;
cursor: pointer;
&:hover {
background-color: $chatbox-button-hover-bg-color;
}
}
@import "typography.scss";
@import "header.scss";
@import "responsive.scss";
@import "chatbot.scss";

View file

@ -46,7 +46,7 @@ class MySqlIntegrationTests {
@ServiceConnection
@Container
static MySQLContainer<?> container = new MySQLContainer<>("mysql:9.1");
static MySQLContainer<?> container = new MySQLContainer<>("mysql:8.4");
@LocalServerPort
int port;
@ -58,7 +58,7 @@ class MySqlIntegrationTests {
private RestTemplateBuilder builder;
@Test
void testFindAll() {
void testFindAll() throws Exception {
vets.findAll();
vets.findAll(); // served from cache
}

View file

@ -36,7 +36,7 @@ public class MysqlTestApplication {
@Profile("mysql")
@Bean
static MySQLContainer<?> container() {
return new MySQLContainer<>("mysql:9.1");
return new MySQLContainer<>("mysql:8.4");
}
public static void main(String[] args) {

View file

@ -44,7 +44,7 @@ public class PetClinicIntegrationTests {
private RestTemplateBuilder builder;
@Test
void testFindAll() {
void testFindAll() throws Exception {
vets.findAll();
vets.findAll(); // served from cache
}

View file

@ -17,7 +17,6 @@
package org.springframework.samples.petclinic;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assumptions.assumeTrue;
import java.util.Arrays;
@ -49,7 +48,7 @@ import org.springframework.web.client.RestTemplate;
import org.testcontainers.DockerClientFactory;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = { "spring.docker.compose.skip.in-tests=false", //
"spring.docker.compose.start.arguments=--force-recreate,--renew-anon-volumes,postgres" })
"spring.docker.compose.profiles.active=postgres" })
@ActiveProfiles("postgres")
@DisabledInNativeImage
public class PostgresIntegrationTests {
@ -72,7 +71,7 @@ public class PostgresIntegrationTests {
new SpringApplicationBuilder(PetClinicApplication.class) //
.profiles("postgres") //
.properties( //
"spring.docker.compose.start.arguments=postgres" //
"spring.docker.compose.profiles.active=postgres" //
) //
.listeners(new PropertiesLogger()) //
.run(args);
@ -115,16 +114,7 @@ public class PostgresIntegrationTests {
Arrays.sort(names);
for (String name : names) {
String resolved = environment.getProperty(name);
assertNotNull(resolved, "resolved environment property: " + name + " is null.");
Object sourceProperty = source.getProperty(name);
assertNotNull(sourceProperty, "source property was expecting an object but is null.");
assertNotNull(sourceProperty.toString(), "source property toString() returned null.");
String value = sourceProperty.toString();
String value = source.getProperty(name).toString();
if (resolved.equals(value)) {
log.info(name + "=" + resolved);
}

View file

@ -20,18 +20,17 @@ import org.assertj.core.util.Lists;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.test.context.aot.DisabledInAotMode;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.time.LocalDate;
import java.util.Optional;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan;
@ -44,16 +43,16 @@ import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
/**
* Test class for {@link OwnerController}
*
* @author Colin But
* @author Wick Dynex
*/
@WebMvcTest(OwnerController.class)
@DisabledInNativeImage
@ -65,7 +64,7 @@ class OwnerControllerTests {
@Autowired
private MockMvc mockMvc;
@MockitoBean
@MockBean
private OwnerRepository owners;
private Owner george() {
@ -91,12 +90,12 @@ class OwnerControllerTests {
void setup() {
Owner george = george();
given(this.owners.findByLastNameStartingWith(eq("Franklin"), any(Pageable.class)))
given(this.owners.findByLastName(eq("Franklin"), any(Pageable.class)))
.willReturn(new PageImpl<>(Lists.newArrayList(george)));
given(this.owners.findAll(any(Pageable.class))).willReturn(new PageImpl<>(Lists.newArrayList(george)));
given(this.owners.findById(TEST_OWNER_ID)).willReturn(Optional.of(george));
given(this.owners.findById(TEST_OWNER_ID)).willReturn(george);
Visit visit = new Visit();
visit.setDate(LocalDate.now());
george.getPet("Max").getVisits().add(visit);
@ -144,14 +143,14 @@ class OwnerControllerTests {
@Test
void testProcessFindFormSuccess() throws Exception {
Page<Owner> tasks = new PageImpl<>(Lists.newArrayList(george(), new Owner()));
when(this.owners.findByLastNameStartingWith(anyString(), any(Pageable.class))).thenReturn(tasks);
Mockito.when(this.owners.findByLastName(anyString(), any(Pageable.class))).thenReturn(tasks);
mockMvc.perform(get("/owners?page=1")).andExpect(status().isOk()).andExpect(view().name("owners/ownersList"));
}
@Test
void testProcessFindFormByLastName() throws Exception {
Page<Owner> tasks = new PageImpl<>(Lists.newArrayList(george()));
when(this.owners.findByLastNameStartingWith(eq("Franklin"), any(Pageable.class))).thenReturn(tasks);
Mockito.when(this.owners.findByLastName(eq("Franklin"), any(Pageable.class))).thenReturn(tasks);
mockMvc.perform(get("/owners?page=1").param("lastName", "Franklin"))
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID));
@ -160,7 +159,7 @@ class OwnerControllerTests {
@Test
void testProcessFindFormNoOwnersFound() throws Exception {
Page<Owner> tasks = new PageImpl<>(Lists.newArrayList());
when(this.owners.findByLastNameStartingWith(eq("Unknown Surname"), any(Pageable.class))).thenReturn(tasks);
Mockito.when(this.owners.findByLastName(eq("Unknown Surname"), any(Pageable.class))).thenReturn(tasks);
mockMvc.perform(get("/owners?page=1").param("lastName", "Unknown Surname"))
.andExpect(status().isOk())
.andExpect(model().attributeHasFieldErrors("owner", "lastName"))
@ -230,24 +229,4 @@ class OwnerControllerTests {
.andExpect(view().name("owners/ownerDetails"));
}
@Test
public void testProcessUpdateOwnerFormWithIdMismatch() throws Exception {
int pathOwnerId = 1;
Owner owner = new Owner();
owner.setId(2);
owner.setFirstName("John");
owner.setLastName("Doe");
owner.setAddress("Center Street");
owner.setCity("New York");
owner.setTelephone("0123456789");
when(owners.findById(pathOwnerId)).thenReturn(Optional.of(owner));
mockMvc.perform(MockMvcRequestBuilders.post("/owners/{ownerId}/edit", pathOwnerId).flashAttr("owner", owner))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/owners/" + pathOwnerId + "/edit"))
.andExpect(flash().attributeExists("error"));
}
}

View file

@ -18,20 +18,16 @@ package org.springframework.samples.petclinic.owner;
import org.assertj.core.util.Lists;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.test.context.aot.DisabledInAotMode;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDate;
import java.util.Optional;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
@ -43,7 +39,6 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
* Test class for the {@link PetController}
*
* @author Colin But
* @author Wick Dynex
*/
@WebMvcTest(value = PetController.class,
includeFilters = @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE))
@ -58,7 +53,7 @@ class PetControllerTests {
@Autowired
private MockMvc mockMvc;
@MockitoBean
@MockBean
private OwnerRepository owners;
@BeforeEach
@ -67,17 +62,11 @@ class PetControllerTests {
cat.setId(3);
cat.setName("hamster");
given(this.owners.findPetTypes()).willReturn(Lists.newArrayList(cat));
Owner owner = new Owner();
Pet pet = new Pet();
Pet dog = new Pet();
owner.addPet(pet);
owner.addPet(dog);
pet.setId(TEST_PET_ID);
dog.setId(TEST_PET_ID + 1);
pet.setName("petty");
dog.setName("doggy");
given(this.owners.findById(TEST_OWNER_ID)).willReturn(Optional.of(owner));
given(this.owners.findById(TEST_OWNER_ID)).willReturn(owner);
}
@Test
@ -98,37 +87,8 @@ class PetControllerTests {
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Nested
class ProcessCreationFormHasErrors {
@Test
void testProcessCreationFormWithBlankName() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "\t \n")
.param("birthDate", "2015-02-12"))
.andExpect(model().attributeHasNoErrors("owner"))
.andExpect(model().attributeHasErrors("pet"))
.andExpect(model().attributeHasFieldErrors("pet", "name"))
.andExpect(model().attributeHasFieldErrorCode("pet", "name", "required"))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
@Test
void testProcessCreationFormWithDuplicateName() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "petty")
.param("birthDate", "2015-02-12"))
.andExpect(model().attributeHasNoErrors("owner"))
.andExpect(model().attributeHasErrors("pet"))
.andExpect(model().attributeHasFieldErrors("pet", "name"))
.andExpect(model().attributeHasFieldErrorCode("pet", "name", "duplicate"))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
@Test
void testProcessCreationFormWithMissingPetType() throws Exception {
void testProcessCreationFormHasErrors() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty")
.param("birthDate", "2015-02-12"))
@ -140,22 +100,6 @@ class PetControllerTests {
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
@Test
void testProcessCreationFormWithInvalidBirthDate() throws Exception {
LocalDate currentDate = LocalDate.now();
String futureBirthDate = currentDate.plusMonths(1).toString();
mockMvc
.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty")
.param("birthDate", futureBirthDate))
.andExpect(model().attributeHasNoErrors("owner"))
.andExpect(model().attributeHasErrors("pet"))
.andExpect(model().attributeHasFieldErrors("pet", "birthDate"))
.andExpect(model().attributeHasFieldErrorCode("pet", "birthDate", "typeMismatch.birthDate"))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
@Test
void testInitUpdateForm() throws Exception {
mockMvc.perform(get("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID))
@ -164,8 +108,6 @@ class PetControllerTests {
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
}
@Test
void testProcessUpdateFormSuccess() throws Exception {
mockMvc
@ -176,33 +118,15 @@ class PetControllerTests {
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Nested
class ProcessUpdateFormHasErrors {
@Test
void testProcessUpdateFormWithInvalidBirthDate() throws Exception {
void testProcessUpdateFormHasErrors() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", " ")
.perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", "Betty")
.param("birthDate", "2015/02/12"))
.andExpect(model().attributeHasNoErrors("owner"))
.andExpect(model().attributeHasErrors("pet"))
.andExpect(model().attributeHasFieldErrors("pet", "birthDate"))
.andExpect(model().attributeHasFieldErrorCode("pet", "birthDate", "typeMismatch"))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
@Test
void testProcessUpdateFormWithBlankName() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", " ")
.param("birthDate", "2015-02-12"))
.andExpect(model().attributeHasNoErrors("owner"))
.andExpect(model().attributeHasErrors("pet"))
.andExpect(model().attributeHasFieldErrors("pet", "name"))
.andExpect(model().attributeHasFieldErrorCode("pet", "name", "required"))
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
}
}

View file

@ -68,7 +68,7 @@ class PetTypeFormatterTests {
}
@Test
void shouldThrowParseException() {
void shouldThrowParseException() throws ParseException {
given(this.pets.findPetTypes()).willReturn(makePetTypes());
Assertions.assertThrows(ParseException.class, () -> {
petTypeFormatter.parse("Fish", Locale.ENGLISH);
@ -82,11 +82,15 @@ class PetTypeFormatterTests {
private List<PetType> makePetTypes() {
List<PetType> petTypes = new ArrayList<>();
petTypes.add(new PetType() {
private static final long serialVersionUID = 4182992965923515553L;
{
setName("Dog");
}
});
petTypes.add(new PetType() {
private static final long serialVersionUID = 1823182409934678856L;
{
setName("Bird");
}

View file

@ -1,117 +0,0 @@
/*
* Copyright 2012-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.owner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.validation.Errors;
import org.springframework.validation.MapBindingResult;
import java.time.LocalDate;
import java.util.HashMap;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Test class for {@link PetValidator}
*
* @author Wick Dynex
*/
@ExtendWith(MockitoExtension.class)
@DisabledInNativeImage
public class PetValidatorTests {
private PetValidator petValidator;
private Pet pet;
private PetType petType;
private Errors errors;
private static final String petName = "Buddy";
private static final String petTypeName = "Dog";
private static final LocalDate petBirthDate = LocalDate.of(1990, 1, 1);
@BeforeEach
void setUp() {
petValidator = new PetValidator();
pet = new Pet();
petType = new PetType();
errors = new MapBindingResult(new HashMap<>(), "pet");
}
@Test
void testValidate() {
petType.setName(petTypeName);
pet.setName(petName);
pet.setType(petType);
pet.setBirthDate(petBirthDate);
petValidator.validate(pet, errors);
assertFalse(errors.hasErrors());
}
@Nested
class ValidateHasErrors {
@Test
void testValidateWithInvalidPetName() {
petType.setName(petTypeName);
pet.setName("");
pet.setType(petType);
pet.setBirthDate(petBirthDate);
petValidator.validate(pet, errors);
assertTrue(errors.hasFieldErrors("name"));
}
@Test
void testValidateWithInvalidPetType() {
pet.setName(petName);
pet.setType(null);
pet.setBirthDate(petBirthDate);
petValidator.validate(pet, errors);
assertTrue(errors.hasFieldErrors("type"));
}
@Test
void testValidateWithInvalidBirthDate() {
petType.setName(petTypeName);
pet.setName(petName);
pet.setType(petType);
pet.setBirthDate(null);
petValidator.validate(pet, errors);
assertTrue(errors.hasFieldErrors("birthDate"));
}
}
}

View file

@ -28,17 +28,14 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.test.context.aot.DisabledInAotMode;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import java.util.Optional;
/**
* Test class for {@link VisitController}
*
* @author Colin But
* @author Wick Dynex
*/
@WebMvcTest(VisitController.class)
@DisabledInNativeImage
@ -52,7 +49,7 @@ class VisitControllerTests {
@Autowired
private MockMvc mockMvc;
@MockitoBean
@MockBean
private OwnerRepository owners;
@BeforeEach
@ -61,7 +58,7 @@ class VisitControllerTests {
Pet pet = new Pet();
owner.addPet(pet);
pet.setId(TEST_PET_ID);
given(this.owners.findById(TEST_OWNER_ID)).willReturn(Optional.of(owner));
given(this.owners.findById(TEST_OWNER_ID)).willReturn(owner);
}
@Test

View file

@ -20,13 +20,13 @@ import static org.assertj.core.api.Assertions.assertThat;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Optional;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase.Replace;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.samples.petclinic.owner.Owner;
@ -36,6 +36,7 @@ import org.springframework.samples.petclinic.owner.PetType;
import org.springframework.samples.petclinic.owner.Visit;
import org.springframework.samples.petclinic.vet.Vet;
import org.springframework.samples.petclinic.vet.VetRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
@ -66,7 +67,7 @@ import org.springframework.transaction.annotation.Transactional;
* @author Michael Isvy
* @author Dave Syer
*/
@DataJpaTest
@DataJpaTest(includeFilters = @ComponentScan.Filter(Service.class))
// Ensure that if the mysql profile is active we connect to the real database:
@AutoConfigureTestDatabase(replace = Replace.NONE)
// @TestPropertySource("/application-postgres.properties")
@ -82,18 +83,16 @@ class ClinicServiceTests {
@Test
void shouldFindOwnersByLastName() {
Page<Owner> owners = this.owners.findByLastNameStartingWith("Davis", pageable);
Page<Owner> owners = this.owners.findByLastName("Davis", pageable);
assertThat(owners).hasSize(2);
owners = this.owners.findByLastNameStartingWith("Daviss", pageable);
owners = this.owners.findByLastName("Daviss", pageable);
assertThat(owners).isEmpty();
}
@Test
void shouldFindSingleOwnerWithPet() {
Optional<Owner> optionalOwner = this.owners.findById(1);
assertThat(optionalOwner).isPresent();
Owner owner = optionalOwner.get();
Owner owner = this.owners.findById(1);
assertThat(owner.getLastName()).startsWith("Franklin");
assertThat(owner.getPets()).hasSize(1);
assertThat(owner.getPets().get(0).getType()).isNotNull();
@ -103,7 +102,7 @@ class ClinicServiceTests {
@Test
@Transactional
void shouldInsertOwner() {
Page<Owner> owners = this.owners.findByLastNameStartingWith("Schultz", pageable);
Page<Owner> owners = this.owners.findByLastName("Schultz", pageable);
int found = (int) owners.getTotalElements();
Owner owner = new Owner();
@ -115,16 +114,14 @@ class ClinicServiceTests {
this.owners.save(owner);
assertThat(owner.getId()).isNotZero();
owners = this.owners.findByLastNameStartingWith("Schultz", pageable);
owners = this.owners.findByLastName("Schultz", pageable);
assertThat(owners.getTotalElements()).isEqualTo(found + 1);
}
@Test
@Transactional
void shouldUpdateOwner() {
Optional<Owner> optionalOwner = this.owners.findById(1);
assertThat(optionalOwner).isPresent();
Owner owner = optionalOwner.get();
Owner owner = this.owners.findById(1);
String oldLastName = owner.getLastName();
String newLastName = oldLastName + "X";
@ -132,9 +129,7 @@ class ClinicServiceTests {
this.owners.save(owner);
// retrieving new name from database
optionalOwner = this.owners.findById(1);
assertThat(optionalOwner).isPresent();
owner = optionalOwner.get();
owner = this.owners.findById(1);
assertThat(owner.getLastName()).isEqualTo(newLastName);
}
@ -151,10 +146,7 @@ class ClinicServiceTests {
@Test
@Transactional
void shouldInsertPetIntoDatabaseAndGenerateId() {
Optional<Owner> optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
Owner owner6 = optionalOwner.get();
Owner owner6 = this.owners.findById(6);
int found = owner6.getPets().size();
Pet pet = new Pet();
@ -167,9 +159,7 @@ class ClinicServiceTests {
this.owners.save(owner6);
optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
owner6 = optionalOwner.get();
owner6 = this.owners.findById(6);
assertThat(owner6.getPets()).hasSize(found + 1);
// checks that id has been generated
pet = owner6.getPet("bowser");
@ -179,10 +169,7 @@ class ClinicServiceTests {
@Test
@Transactional
void shouldUpdatePetName() {
Optional<Owner> optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
Owner owner6 = optionalOwner.get();
Owner owner6 = this.owners.findById(6);
Pet pet7 = owner6.getPet(7);
String oldName = pet7.getName();
@ -190,9 +177,7 @@ class ClinicServiceTests {
pet7.setName(newName);
this.owners.save(owner6);
optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
owner6 = optionalOwner.get();
owner6 = this.owners.findById(6);
pet7 = owner6.getPet(7);
assertThat(pet7.getName()).isEqualTo(newName);
}
@ -201,20 +186,17 @@ class ClinicServiceTests {
void shouldFindVets() {
Collection<Vet> vets = this.vets.findAll();
Vet vet = EntityUtils.getById(vets, Vet.class, 3);
assertThat(vet.getLastName()).isEqualTo("Douglas");
Vet vet = EntityUtils.getById(vets, Vet.class, 2);
assertThat(vet.getLastName()).isEqualTo("Leary");
assertThat(vet.getNrOfSpecialties()).isEqualTo(2);
assertThat(vet.getSpecialties().get(0).getName()).isEqualTo("dentistry");
assertThat(vet.getSpecialties().get(1).getName()).isEqualTo("surgery");
assertThat(vet.getSpecialties().get(0).getName()).isEqualTo("anesthesia");
assertThat(vet.getSpecialties().get(1).getName()).isEqualTo("dentistry");
}
@Test
@Transactional
void shouldAddNewVisitForPet() {
Optional<Owner> optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
Owner owner6 = optionalOwner.get();
Owner owner6 = this.owners.findById(6);
Pet pet7 = owner6.getPet(7);
int found = pet7.getVisits().size();
Visit visit = new Visit();
@ -223,6 +205,8 @@ class ClinicServiceTests {
owner6.addVisit(pet7.getId(), visit);
this.owners.save(owner6);
owner6 = this.owners.findById(6);
assertThat(pet7.getVisits()) //
.hasSize(found + 1) //
.allMatch(value -> value.getId() != null);
@ -230,10 +214,7 @@ class ClinicServiceTests {
@Test
void shouldFindVisitsByPetId() {
Optional<Owner> optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
Owner owner6 = optionalOwner.get();
Owner owner6 = this.owners.findById(6);
Pet pet7 = owner6.getPet(7);
Collection<Visit> visits = pet7.getVisits();

View file

@ -30,7 +30,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
// luck ((plain(st) UNIT test)! :)
class CrashControllerTests {
final CrashController testee = new CrashController();
CrashController testee = new CrashController();
@Test
void testTriggerException() {

View file

@ -22,11 +22,11 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.test.context.aot.DisabledInAotMode;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.ResultActions;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
@ -48,7 +48,7 @@ class VetControllerTests {
@Autowired
private MockMvc mockMvc;
@MockitoBean
@MockBean
private VetRepository vets;
private Vet james() {

View file

@ -156,7 +156,8 @@
<stringProp name="HTTPSampler.response_timeout"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">${CONTEXT_WEB}/webjars/bootstrap/dist/js/bootstrap.bundle.min.js</stringProp>
<stringProp name="HTTPSampler.path">
${CONTEXT_WEB}/webjars/bootstrap/5.3.3/dist/js/bootstrap.bundle.min.js</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
@ -419,7 +420,8 @@
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">${CONTEXT_WEB}/owners/${count}/pets/${petCount}/visits/new</stringProp>
<stringProp name="HTTPSampler.path">
${CONTEXT_WEB}/owners/${count}/pets/${petCount}/visits/new</stringProp>
<stringProp name="HTTPSampler.method">GET</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>
@ -456,7 +458,8 @@
<stringProp name="HTTPSampler.port"></stringProp>
<stringProp name="HTTPSampler.protocol"></stringProp>
<stringProp name="HTTPSampler.contentEncoding"></stringProp>
<stringProp name="HTTPSampler.path">${CONTEXT_WEB}/owners/${count}/pets/${petCount}/visits/new</stringProp>
<stringProp name="HTTPSampler.path">
${CONTEXT_WEB}/owners/${count}/pets/${petCount}/visits/new</stringProp>
<stringProp name="HTTPSampler.method">POST</stringProp>
<boolProp name="HTTPSampler.follow_redirects">true</boolProp>
<boolProp name="HTTPSampler.auto_redirects">false</boolProp>

6
tanzu.yml Normal file
View file

@ -0,0 +1,6 @@
apiVersion: config.tanzu.vmware.com/v1
configuration:
dev:
paths:
- .tanzu/config/
kind: TanzuConfig