Merge pull request #279 from spring-projects/main

updating
This commit is contained in:
Amro Hendawi 2025-05-14 16:13:57 +02:00 committed by GitHub
commit 6178b702b6
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 654 additions and 129 deletions

2
.github/dco.yml vendored Normal file
View file

@ -0,0 +1,2 @@
require:
members: false

View file

@ -17,6 +17,8 @@ cd spring-petclinic
java -jar target/*.jar
```
(On Windows, or if your shell doesn't expand the glob, you might need to specify the JAR file name explicitly on the command line at the end there.)
You can then access the Petclinic at <http://localhost:8080/>.
<img width="1042" alt="petclinic-screenshot" src="https://cloud.githubusercontent.com/assets/838318/19727082/2aee6d6c-9b8e-11e6-81fe-e889a5ddfded.png">
@ -155,7 +157,8 @@ Here is a list of them:
The [issue tracker](https://github.com/spring-projects/spring-petclinic/issues) is the preferred channel for bug reports, feature requests and submitting pull requests.
For pull requests, editor preferences are available in the [editor config](.editorconfig) for easy use in common text editors. Read more and download plugins at <https://editorconfig.org>. If you have not previously done so, please fill out and submit the [Contributor License Agreement](https://cla.pivotal.io/sign/spring).
For pull requests, editor preferences are available in the [editor config](.editorconfig) for easy use in common text editors. Read more and download plugins at <https://editorconfig.org>. All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin.
For additional details, please refer to the blog post [Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring).
## License

View file

@ -1,6 +1,6 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.0'
id 'org.springframework.boot' version '3.4.2'
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'

View file

@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
<version>3.4.2</version>
<relativePath></relativePath>
</parent>

View file

@ -57,7 +57,7 @@ public class Owner extends Person {
@Column(name = "telephone")
@NotBlank
@Pattern(regexp = "\\d{10}", message = "Telephone must be a 10-digit number")
@Pattern(regexp = "\\d{10}", message = "{telephone.invalid}")
private String telephone;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)

View file

@ -23,7 +23,6 @@ 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.transaction.annotation.Transactional;
/**
* Repository class for <code>Owner</code> domain objects All method names are compliant
@ -70,9 +69,4 @@ public interface OwnerRepository extends JpaRepository<Owner, Integer> {
*/
Optional<Owner> findById(@Nonnull Integer id);
/**
* Returns all the owners from data store
**/
Page<Owner> findAll(Pageable pageable);
}

View file

@ -129,7 +129,7 @@ class PetController {
String petName = pet.getName();
// checking if the pet name already exist for the owner
// checking if the pet name already exists for the owner
if (StringUtils.hasText(petName)) {
Pet existingPet = owner.getPet(petName, false);
if (existingPet != null && !existingPet.getId().equals(pet.getId())) {
@ -146,10 +146,28 @@ class PetController {
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
owner.addPet(pet);
this.owners.save(owner);
updatePetDetails(owner, pet);
redirectAttributes.addFlashAttribute("message", "Pet details has been edited");
return "redirect:/owners/{ownerId}";
}
/**
* Updates the pet details if it exists or adds a new pet to the owner.
* @param owner The owner of the pet
* @param pet The pet with updated details
*/
private void updatePetDetails(Owner owner, Pet pet) {
Pet existingPet = owner.getPet(pet.getId());
if (existingPet != null) {
// Update existing pet's properties
existingPet.setName(pet.getName());
existingPet.setBirthDate(pet.getBirthDate());
existingPet.setType(pet.getType());
}
else {
owner.addPet(pet);
}
this.owners.save(owner);
}
}

View file

@ -15,7 +15,6 @@
*/
package org.springframework.samples.petclinic.owner;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.Formatter;
import org.springframework.stereotype.Component;
@ -38,7 +37,6 @@ public class PetTypeFormatter implements Formatter<PetType> {
private final OwnerRepository owners;
@Autowired
public PetTypeFormatter(OwnerRepository owners) {
this.owners = owners;
}

View file

@ -0,0 +1,60 @@
package org.springframework.samples.petclinic.system;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.LocaleResolver;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
import org.springframework.web.servlet.i18n.SessionLocaleResolver;
import java.util.Locale;
/**
* Configures internationalization (i18n) support for the application.
*
* <p>
* Handles loading language-specific messages, tracking the user's language, and allowing
* language changes via the URL parameter (e.g., <code>?lang=de</code>).
* </p>
*
* @author Anuj Ashok Potdar
*/
@Configuration
@SuppressWarnings("unused")
public class WebConfiguration implements WebMvcConfigurer {
/**
* Uses session storage to remember the users language setting across requests.
* Defaults to English if nothing is specified.
* @return session-based {@link LocaleResolver}
*/
@Bean
public LocaleResolver localeResolver() {
SessionLocaleResolver resolver = new SessionLocaleResolver();
resolver.setDefaultLocale(Locale.ENGLISH);
return resolver;
}
/**
* Allows the app to switch languages using a URL parameter like
* <code>?lang=es</code>.
* @return a {@link LocaleChangeInterceptor} that handles the change
*/
@Bean
public LocaleChangeInterceptor localeChangeInterceptor() {
LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();
interceptor.setParamName("lang");
return interceptor;
}
/**
* Registers the locale change interceptor so it can run on each request.
* @param registry where interceptors are added
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(localeChangeInterceptor());
}
}

View file

@ -15,14 +15,13 @@
*/
package org.springframework.samples.petclinic.vet;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.beans.support.MutableSortDefinition;
import org.springframework.beans.support.PropertyComparator;
import org.springframework.samples.petclinic.model.NamedEntity;
import org.springframework.samples.petclinic.model.Person;
import jakarta.persistence.Entity;
@ -59,9 +58,9 @@ public class Vet extends Person {
@XmlElement
public List<Specialty> getSpecialties() {
List<Specialty> sortedSpecs = new ArrayList<>(getSpecialtiesInternal());
PropertyComparator.sort(sortedSpecs, new MutableSortDefinition("name", true, true));
return Collections.unmodifiableList(sortedSpecs);
return getSpecialtiesInternal().stream()
.sorted(Comparator.comparing(NamedEntity::getName))
.collect(Collectors.toList());
}
public int getNrOfSpecialties() {

View file

@ -6,3 +6,43 @@ nonNumeric=must be all numeric
duplicateFormSubmission=Duplicate form submission is not allowed
typeMismatch.date=invalid date
typeMismatch.birthDate=invalid date
owner=Owner
firstName=First Name
lastName=Last Name
address=Address
city=City
telephone=Telephone
owners=Owners
addOwner=Add Owner
findOwner=Find Owner
findOwners=Find Owners
updateOwner=Update Owner
vets=Veterinarians
name=Name
specialties=Specialties
none=none
pages=pages
first=First
next=Next
previous=Previous
last=Last
somethingHappened=Something happened...
pets=Pets
home=Home
error=Error
telephone.invalid=Telephone must be a 10-digit number
layoutTitle=PetClinic :: a Spring Framework demonstration
pet=Pet
birthDate=Birth Date
type=Type
previousVisits=Previous Visits
date=Date
description=Description
new=New
addVisit=Add Visit
editPet=Edit Pet
ownerInformation=Owner Information
visitDate=Visit Date
editOwner=Edit Owner
addNewPet=Add New Pet
petsAndVisits=Pets and Visits

View file

@ -6,4 +6,43 @@ nonNumeric=darf nur numerisch sein
duplicateFormSubmission=Wiederholtes Absenden des Formulars ist nicht erlaubt
typeMismatch.date=ung<EFBFBD>ltiges Datum
typeMismatch.birthDate=ung<EFBFBD>ltiges Datum
owner=Besitzer
firstName=Vorname
lastName=Nachname
address=Adresse
city=Stadt
telephone=Telefon
owners=Besitzer
addOwner=Besitzer hinzufügen
findOwner=Besitzer finden
findOwners=Besitzer suchen
updateOwner=Besitzer aktualisieren
vets=Tierärzte
name=Name
specialties=Fachgebiete
none=keine
pages=Seiten
first=Erste
next=Nächste
previous=Vorherige
last=Letzte
somethingHappened=Etwas ist passiert...
pets=Haustiere
home=Startseite
error=Fehler
telephone.invalid=Telefonnummer muss aus 10 Ziffern bestehen
layoutTitle=PetClinic :: eine Demonstration des Spring Frameworks
pet=Haustier
birthDate=Geburtsdatum
type=Typ
previousVisits=Frühere Besuche
date=Datum
description=Beschreibung
new=Neu
addVisit=Besuch hinzufügen
editPet=Haustier bearbeiten
ownerInformation=Besitzerinformationen
visitDate=Besuchsdatum
editOwner=Besitzer bearbeiten
addNewPet=Neues Haustier hinzufügen
petsAndVisits=Haustiere und Besuche

View file

@ -6,4 +6,43 @@ nonNumeric=Sólo debe contener numeros
duplicateFormSubmission=No se permite el envío de formularios duplicados
typeMismatch.date=Fecha invalida
typeMismatch.birthDate=Fecha invalida
owner=Propietario
firstName=Nombre
lastName=Apellido
address=Dirección
city=Ciudad
telephone=Teléfono
owners=Propietarios
addOwner=Añadir propietario
findOwner=Buscar propietario
findOwners=Buscar propietarios
updateOwner=Actualizar propietario
vets=Veterinarios
name=Nombre
specialties=Especialidades
none=ninguno
pages=páginas
first=Primero
next=Siguiente
previous=Anterior
last=Último
somethingHappened=Algo pasó...
pets=Mascotas
home=Inicio
error=Error
telephone.invalid=El número de teléfono debe tener 10 dígitos
layoutTitle=PetClinic :: una demostración de Spring Framework
pet=Mascota
birthDate=Fecha de nacimiento
type=Tipo
previousVisits=Visitas anteriores
date=Fecha
description=Descripción
new=Nuevo
addVisit=Agregar visita
editPet=Editar mascota
ownerInformation=Información del propietario
visitDate=Fecha de visita
editOwner=Editar propietario
addNewPet=Agregar nueva mascota
petsAndVisits=Mascotas y visitas

View file

@ -6,4 +6,43 @@ nonNumeric=باید عددی باشد
duplicateFormSubmission=ارسال تکراری فرم مجاز نیست
typeMismatch.date=تاریخ نامعتبر
typeMismatch.birthDate=تاریخ تولد نامعتبر
owner=مالک
firstName=نام
lastName=نام خانوادگی
address=آدرس
city=شهر
telephone=تلفن
owners=مالکان
addOwner=افزودن مالک
findOwner=یافتن مالک
findOwners=یافتن مالکان
updateOwner=ویرایش مالک
vets=دامپزشکان
name=نام
specialties=تخصص‌ها
none=هیچ‌کدام
pages=صفحات
first=اول
next=بعدی
previous=قبلی
last=آخر
somethingHappened=مشکلی پیش آمد...
pets=حیوانات خانگی
home=خانه
error=خطا
telephone.invalid=شماره تلفن باید ۱۰ رقمی باشد
layoutTitle=PetClinic :: یک نمایش از Spring Framework
pet=حیوان خانگی
birthDate=تاریخ تولد
type=نوع
previousVisits=ویزیت‌های قبلی
date=تاریخ
description=توضیحات
new=جدید
addVisit=افزودن ویزیت
editPet=ویرایش حیوان خانگی
ownerInformation=اطلاعات مالک
visitDate=تاریخ ویزیت
editOwner=ویرایش مالک
addNewPet=افزودن حیوان خانگی جدید
petsAndVisits=حیوانات و ویزیت‌ها

View file

@ -6,3 +6,43 @@ nonNumeric=모두 숫자로 입력해야 합니다
duplicateFormSubmission=중복 제출은 허용되지 않습니다
typeMismatch.date=잘못된 날짜입니다
typeMismatch.birthDate=잘못된 날짜입니다
owner=소유자
firstName=이름
lastName=
address=주소
city=도시
telephone=전화번호
owners=소유자 목록
addOwner=소유자 추가
findOwner=소유자 찾기
findOwners=소유자들 찾기
updateOwner=소유자 수정
vets=수의사
name=이름
specialties=전문 분야
none=없음
pages=페이지
first=첫 번째
next=다음
previous=이전
last=마지막
somethingHappened=문제가 발생했습니다...
pets=반려동물
home=
error=오류
telephone.invalid=전화번호는 10자리 숫자여야 합니다
layoutTitle=PetClinic :: Spring Framework 데모
pet=반려동물
birthDate=생년월일
type=종류
previousVisits=이전 방문
date=날짜
description=설명
new=새로운
addVisit=방문 추가
editPet=반려동물 수정
ownerInformation=소유자 정보
visitDate=방문 날짜
editOwner=소유자 수정
addNewPet=새 반려동물 추가
petsAndVisits=반려동물 및 방문

View file

@ -6,3 +6,43 @@ nonNumeric=Deve ser tudo numerico
duplicateFormSubmission=O envio duplicado de formulario nao e permitido
typeMismatch.date=Data invalida
typeMismatch.birthDate=Data de nascimento invalida
owner=Proprietário
firstName=Primeiro Nome
lastName=Sobrenome
address=Endereço
city=Cidade
telephone=Telefone
owners=Proprietários
addOwner=Adicionar proprietário
findOwner=Encontrar proprietário
findOwners=Encontrar proprietários
updateOwner=Atualizar proprietário
vets=Veterinários
name=Nome
specialties=Especialidades
none=nenhum
pages=páginas
first=Primeiro
next=Próximo
previous=Anterior
last=Último
somethingHappened=Algo aconteceu...
pets=Animais de estimação
home=Início
error=Erro
telephone.invalid=O número de telefone deve conter 10 dígitos
layoutTitle=PetClinic :: uma demonstração do Spring Framework
pet=Animal de estimação
birthDate=Data de nascimento
type=Tipo
previousVisits=Visitas anteriores
date=Data
description=Descrição
new=Novo
addVisit=Adicionar visita
editPet=Editar animal
ownerInformation=Informações do proprietário
visitDate=Data da visita
editOwner=Editar proprietário
addNewPet=Adicionar novo animal
petsAndVisits=Animais e visitas

View file

@ -6,4 +6,43 @@ nonNumeric=должно быть все числовое значение
duplicateFormSubmission=Дублирование формы не допускается
typeMismatch.date=неправильная даные
typeMismatch.birthDate=неправильная дата
owner=Владелец
firstName=Имя
lastName=Фамилия
address=Адрес
city=Город
telephone=Телефон
owners=Владельцы
addOwner=Добавить владельца
findOwner=Найти владельца
findOwners=Найти владельцев
updateOwner=Обновить владельца
vets=Ветеринары
name=Имя
specialties=Специальности
none=нет
pages=страницы
first=Первый
next=Следующий
previous=Предыдущий
last=Последний
somethingHappened=Что-то пошло не так...
pets=Питомцы
home=Главная
error=Ошибка
telephone.invalid=Телефон должен содержать 10 цифр
layoutTitle=PetClinic :: демонстрация Spring Framework
pet=Питомец
birthDate=Дата рождения
type=Тип
previousVisits=Предыдущие визиты
date=Дата
description=Описание
new=Новый
addVisit=Добавить визит
editPet=Редактировать питомца
ownerInformation=Информация о владельце
visitDate=Дата визита
editOwner=Редактировать владельца
addNewPet=Добавить нового питомца
petsAndVisits=Питомцы и визиты

View file

@ -6,4 +6,43 @@ nonNumeric=sadece sayısal olmalıdır
duplicateFormSubmission=Formun tekrar gönderilmesine izin verilmez
typeMismatch.date=geçersiz tarih
typeMismatch.birthDate=geçersiz tarih
owner=Sahip
firstName=Ad
lastName=Soyad
address=Adres
city=Şehir
telephone=Telefon
owners=Sahipler
addOwner=Sahip Ekle
findOwner=Sahip Bul
findOwners=Sahipleri Bul
updateOwner=Sahip Güncelle
vets=Veterinerler
name=İsim
specialties=Uzmanlıklar
none=yok
pages=sayfalar
first=İlk
next=Sonraki
previous=Önceki
last=Son
somethingHappened=Bir şey oldu...
pets=Evcil Hayvanlar
home=Ana Sayfa
error=Hata
telephone.invalid=Telefon numarası 10 basamaklı olmalıdır
layoutTitle=PetClinic :: bir Spring Framework demosu
pet=Evcil Hayvan
birthDate=Doğum Tarihi
type=Tür
previousVisits=Önceki Ziyaretler
date=Tarih
description=ıklama
new=Yeni
addVisit=Ziyaret Ekle
editPet=Evcil Hayvanı Düzenle
ownerInformation=Sahip Bilgileri
visitDate=Ziyaret Tarihi
editOwner=Sahibi Düzenle
addNewPet=Yeni Evcil Hayvan Ekle
petsAndVisits=Evcil Hayvanlar ve Ziyaretler

View file

@ -4,8 +4,8 @@
<body>
<img src="../static/resources/images/pets.png" th:src="@{/resources/images/pets.png}"/>
<h2>Something happened...</h2>
<h2 th:text="#{somethingHappened}">Something happened...</h2>
<p th:text="${message}">Exception message</p>
</body>
</html>
</html>

View file

@ -1,11 +1,11 @@
<html>
<html xmlns:th="https://www.thymeleaf.org">
<body>
<form>
<th:block th:fragment="input (label, name, type)">
<div th:with="valid=${!#fields.hasErrors(name)}"
th:class="${'form-group' + (valid ? '' : ' has-error')}"
class="form-group">
<label class="col-sm-2 control-label" th:text="${label}">Label</label>
<label th:for="${name}" class="col-sm-2 control-label" th:text="${label}">Label</label>
<div class="col-sm-10">
<div th:switch="${type}">
<input th:case="'text'" class="form-control" type="text" th:field="*{__${name}__}" />
@ -18,7 +18,7 @@
<span
class="fa fa-remove form-control-feedback"
aria-hidden="true"></span>
<span class="help-inline" th:errors="*{__${name}__}">Error</span>
<span class="help-inline" th:errors="*{__${name}__}" th:text="#{error}">Error</span>
</th:block>
</div>
</div>

View file

@ -1,5 +1,6 @@
<!doctype html>
<html th:fragment="layout (template, menu)">
<html th:fragment="layout (template, menu)"
xmlns:th="https://www.thymeleaf.org">
<head>
@ -10,7 +11,7 @@
<link rel="shortcut icon" type="image/x-icon" th:href="@{/resources/images/favicon.png}">
<title>PetClinic :: a Spring Framework demonstration</title>
<title th:text="#{layoutTitle}">PetClinic :: a Spring Framework demonstration</title>
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.2/html5shiv.min.js"></script>
@ -45,25 +46,25 @@
<ul class="nav navbar-nav me-auto">
<li th:replace="~{::menuItem ('/','home','home page','home','Home')}">
<li th:replace="~{::menuItem ('/','home','home page','home',#{home})}">
<span class="fa fa-home" aria-hidden="true"></span>
<span>Home</span>
<span th:text="#{home}">Home</span>
</li>
<li th:replace="~{::menuItem ('/owners/find','owners','find owners','search','Find owners')}">
<li th:replace="~{::menuItem ('/owners/find','owners','find owners','search',#{findOwners})}">
<span class="fa fa-search" aria-hidden="true"></span>
<span>Find owners</span>
<span th:text="#{findOwners}">Find owners</span>
</li>
<li th:replace="~{::menuItem ('/vets.html','vets','veterinarians','th-list','Veterinarians')}">
<li th:replace="~{::menuItem ('/vets.html','vets','veterinarians','th-list',#{vets})}">
<span class="fa fa-th-list" aria-hidden="true"></span>
<span>Veterinarians</span>
<span th:text="#{vets}">Veterinarians</span>
</li>
<li
th:replace="~{::menuItem ('/oups','error','trigger a RuntimeException to see how it is handled','exclamation-triangle','Error')}">
th:replace="~{::menuItem ('/oups','error','trigger a RuntimeException to see how it is handled','exclamation-triangle',#{error})}">
<span class="fa exclamation-triangle" aria-hidden="true"></span>
<span>Error</span>
<span th:text="#{error}">Error</span>
</li>
</ul>

View file

@ -1,11 +1,11 @@
<html>
<html xmlns:th="https://www.thymeleaf.org">
<body>
<form>
<th:block th:fragment="select (label, name, items)">
<div th:with="valid=${!#fields.hasErrors(name)}"
th:class="${'form-group' + (valid ? '' : ' has-error')}"
class="form-group">
<label class="col-sm-2 control-label" th:text="${label}">Label</label>
<label th:for="${name}" class="col-sm-2 control-label" th:text="${label}">Label</label>
<div class="col-sm-10">
<select th:field="*{__${name}__}">
@ -19,7 +19,7 @@
<span
class="fa fa-remove form-control-feedback"
aria-hidden="true"></span>
<span class="help-inline" th:errors="*{__${name}__}">Error</span>
<span class="help-inline" th:errors="*{__${name}__}" th:text="#{error}">Error</span>
</th:block>
</div>
</div>

View file

@ -3,24 +3,24 @@
<body>
<h2>Owner</h2>
<h2 th:text="#{owner}">Owner</h2>
<form th:object="${owner}" class="form-horizontal" id="add-owner-form" method="post">
<div class="form-group has-feedback">
<input
th:replace="~{fragments/inputField :: input ('First Name', 'firstName', 'text')}" />
th:replace="~{fragments/inputField :: input (#{firstName}, 'firstName', 'text')}" />
<input
th:replace="~{fragments/inputField :: input ('Last Name', 'lastName', 'text')}" />
th:replace="~{fragments/inputField :: input (#{lastName}, 'lastName', 'text')}" />
<input
th:replace="~{fragments/inputField :: input ('Address', 'address', 'text')}" />
th:replace="~{fragments/inputField :: input (#{address}, 'address', 'text')}" />
<input
th:replace="~{fragments/inputField :: input ('City', 'city', 'text')}" />
th:replace="~{fragments/inputField :: input (#{city}, 'city', 'text')}" />
<input
th:replace="~{fragments/inputField :: input ('Telephone', 'telephone', 'text')}" />
th:replace="~{fragments/inputField :: input (#{telephone}, 'telephone', 'text')}" />
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button
th:with="text=${owner['new']} ? 'Add Owner' : 'Update Owner'"
th:with="text=${owner['new']} ? #{addOwner} : #{updateOwner}"
class="btn btn-primary" type="submit" th:text="${text}">Add
Owner</button>
</div>

View file

@ -3,30 +3,31 @@
<body>
<h2>Find Owners</h2>
<h2 th:text="#{findOwners}">Find Owners</h2>
<form th:object="${owner}" th:action="@{/owners}" method="get"
class="form-horizontal" id="search-owner-form">
<div class="form-group">
<div class="control-group" id="lastNameGroup">
<label class="col-sm-2 control-label">Last name </label>
<label class="col-sm-2 control-label" th:text="#{lastName}">Last name </label>
<div class="col-sm-10">
<input class="form-control" th:field="*{lastName}" size="30"
maxlength="80" /> <span class="help-inline"><div
th:if="${#fields.hasAnyErrors()}">
<p th:each="err : ${#fields.allErrors()}" th:text="${err}">Error</p>
</div></span>
maxlength="80" />
<span class="help-inline">
<div th:if="${#fields.hasAnyErrors()}">
<p th:each="err : ${#fields.allErrors()}" th:text="${err}">Error</p>
</div>
</span>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary">Find
Owner</button>
<button type="submit" class="btn btn-primary" th:text="#{findOwner}">Find Owner</button>
</div>
</div>
<a class="btn btn-primary" th:href="@{/owners/new}">Add Owner</a>
<a class="btn btn-primary" th:href="@{/owners/new}" th:text="#{addOwner}">Add Owner</a>
</form>

View file

@ -6,7 +6,7 @@
<body>
<h2>Owner Information</h2>
<h2 th:text="#{ownerInformation}">Owner Information</h2>
<div th:if="${message}" class="alert alert-success" id="success-message">
<span th:text="${message}"></span>
@ -21,44 +21,44 @@
<table class="table table-striped" th:object="${owner}">
<tr>
<th>Name</th>
<th th:text="#{name}">Name</th>
<td><b th:text="*{firstName + ' ' + lastName}"></b></td>
</tr>
<tr>
<th>Address</th>
<th th:text="#{address}">Address</th>
<td th:text="*{address}"></td>
</tr>
<tr>
<th>City</th>
<th th:text="#{city}">City</th>
<td th:text="*{city}"></td>
</tr>
<tr>
<th>Telephone</th>
<th th:text="#{telephone}">Telephone</th>
<td th:text="*{telephone}"></td>
</tr>
</table>
<a th:href="@{__${owner.id}__/edit}" class="btn btn-primary">Edit
<a th:href="@{__${owner.id}__/edit}" class="btn btn-primary" th:text="#{editOwner}">Edit
Owner</a>
<a th:href="@{__${owner.id}__/pets/new}" class="btn btn-primary">Add
<a th:href="@{__${owner.id}__/pets/new}" class="btn btn-primary" th:text="#{addNewPet}">Add
New Pet</a>
<br />
<br />
<br />
<h2>Pets and Visits</h2>
<h2 th:text="#{petsAndVisits}">Pets and Visits</h2>
<table class="table table-striped">
<tr th:each="pet : ${owner.pets}">
<td valign="top">
<dl class="dl-horizontal">
<dt>Name</dt>
<dt th:text="#{name}">Name</dt>
<dd th:text="${pet.name}"></dd>
<dt>Birth Date</dt>
<dt th:text="#{birthDate}">Birth Date</dt>
<dd
th:text="${#temporals.format(pet.birthDate, 'yyyy-MM-dd')}"></dd>
<dt>Type</dt>
<dt th:text="#{type}">Type</dt>
<dd th:text="${pet.type}"></dd>
</dl>
</td>
@ -66,8 +66,8 @@
<table class="table-condensed">
<thead>
<tr>
<th>Visit Date</th>
<th>Description</th>
<th th:text="#{visitDate}">Visit Date</th>
<th th:text="#{description}">Description</th>
</tr>
</thead>
<tr th:each="visit : ${pet.visits}">
@ -75,8 +75,8 @@
<td th:text="${visit?.description}"></td>
</tr>
<tr>
<td><a th:href="@{__${owner.id}__/pets/__${pet.id}__/edit}">Edit Pet</a></td>
<td><a th:href="@{__${owner.id}__/pets/__${pet.id}__/visits/new}">Add Visit</a></td>
<td><a th:href="@{__${owner.id}__/pets/__${pet.id}__/edit}" th:text="#{editPet}">Edit Pet</a></td>
<td><a th:href="@{__${owner.id}__/pets/__${pet.id}__/visits/new}" th:text="#{addVisit}">Add Visit</a></td>
</tr>
</table>
</td>

View file

@ -4,16 +4,16 @@
<body>
<h2>Owners</h2>
<h2 th:text="#{owners}">Owners</h2>
<table id="owners" class="table table-striped">
<thead>
<tr>
<th style="width: 150px;">Name</th>
<th style="width: 200px;">Address</th>
<th>City</th>
<th style="width: 120px">Telephone</th>
<th>Pets</th>
<th th:text="#{name}" style="width: 150px;">Name</th>
<th th:text="#{address}" style="width: 200px;">Address</th>
<th th:text="#{city}">City</th>
<th th:text="#{telephone}" style="width: 120px">Telephone</th>
<th th:text="#{pets}">Pets</th>
</tr>
</thead>
<tbody>
@ -29,7 +29,7 @@
</tbody>
</table>
<div th:if="${totalPages > 1}">
<span>Pages:</span>
<span th:text="#{pages}">Pages:</span>
<span>[</span>
<span th:each="i: ${#numbers.sequence(1, totalPages)}">
<a th:if="${currentPage != i}" th:href="@{'/owners?page=' + ${i}}">[[${i}]]</a>
@ -37,24 +37,24 @@
</span>
<span>]&nbsp;</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=1'}" title="First"
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=1'}" th:title="#{first}"
class="fa fa-fast-backward"></a>
<span th:unless="${currentPage > 1}" title="First" class="fa fa-fast-backward"></span>
<span th:unless="${currentPage > 1}" th:title="#{first}" class="fa fa-fast-backward"></span>
</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=__${currentPage - 1}__'}" title="Previous"
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=__${currentPage - 1}__'}" th:title="#{previous}"
class="fa fa-step-backward"></a>
<span th:unless="${currentPage > 1}" title="Previous" class="fa fa-step-backward"></span>
<span th:unless="${currentPage > 1}" th:title="#{previous}" class="fa fa-step-backward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/owners?page=__${currentPage + 1}__'}" title="Next"
<a th:if="${currentPage < totalPages}" th:href="@{'/owners?page=__${currentPage + 1}__'}" th:title="#{next}"
class="fa fa-step-forward"></a>
<span th:unless="${currentPage < totalPages}" title="Next" class="fa fa-step-forward"></span>
<span th:unless="${currentPage < totalPages}" th:title="#{next}" class="fa fa-step-forward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/owners?page=__${totalPages}__'}" title="Last"
<a th:if="${currentPage < totalPages}" th:href="@{'/owners?page=__${totalPages}__'}" th:title="#{last}"
class="fa fa-fast-forward"></a>
<span th:unless="${currentPage < totalPages}" title="Last" class="fa fa-step-forward"></span>
<span th:unless="${currentPage < totalPages}" th:title="#{last}" class="fa fa-step-forward"></span>
</span>
</div>
</body>

View file

@ -4,14 +4,14 @@
<body>
<h2>
<th:block th:if="${pet['new']}">New </th:block>
Pet
<th:block th:if="${pet['new']}" th:text="#{new}">New </th:block>
<span th:text="#{pet}">Pet</span>
</h2>
<form th:object="${pet}" class="form-horizontal" method="post">
<input type="hidden" name="id" th:value="*{id}" />
<div class="form-group has-feedback">
<div class="form-group">
<label class="col-sm-2 control-label">Owner</label>
<label class="col-sm-2 control-label" th:text="#{owner}">Owner</label>
<div class="col-sm-10">
<span th:text="${owner?.firstName + ' ' + owner?.lastName}" />
</div>
@ -27,8 +27,7 @@
<div class="col-sm-offset-2 col-sm-10">
<button
th:with="text=${pet['new']} ? 'Add Pet' : 'Update Pet'"
class="btn btn-primary" type="submit" th:text="${text}">Add
Pet</button>
class="btn btn-primary" type="submit" th:text="${text}">Add Pet</button>
</div>
</div>
</form>

View file

@ -4,18 +4,18 @@
<body>
<h2>
<th:block th:if="${visit['new']}">New </th:block>
<th:block th:if="${visit['new']}" th:text="#{new}">New </th:block>
Visit
</h2>
<b>Pet</b>
<b th:text="#{pet}">Pet</b>
<table class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Birth Date</th>
<th>Type</th>
<th>Owner</th>
<th th:text="#{name}">Name</th>
<th th:text="#{birthDate}">Birth Date</th>
<th th:text="#{type}">Type</th>
<th th:text="#{owner}">Owner</th>
</tr>
</thead>
<tr>
@ -39,17 +39,17 @@
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<input type="hidden" name="petId" th:value="${pet.id}" />
<button class="btn btn-primary" type="submit">Add Visit</button>
<button class="btn btn-primary" type="submit" th:text="${addVisit}">Add Visit</button>
</div>
</div>
</form>
<br />
<b>Previous Visits</b>
<b th:text="#{previousVisits}">Previous Visits</b>
<table class="table table-striped">
<tr>
<th>Date</th>
<th>Description</th>
<th th:text="#{date}">Date</th>
<th th:text="#{description}">Description</th>
</tr>
<tr th:if="${!visit['new']}" th:each="visit : ${pet.visits}">
<td th:text="${#temporals.format(visit.date, 'yyyy-MM-dd')}"></td>

View file

@ -5,13 +5,13 @@
<body>
<h2>Veterinarians</h2>
<h2 th:text="#{vets}">Veterinarians</h2>
<table id="vets" class="table table-striped">
<thead>
<tr>
<th>Name</th>
<th>Specialties</th>
<th th:text="#{name}">Name</th>
<th th:text="#{specialties}">Specialties</th>
</tr>
</thead>
<tbody>
@ -19,13 +19,13 @@
<td th:text="${vet.firstName + ' ' + vet.lastName}"></td>
<td><span th:each="specialty : ${vet.specialties}"
th:text="${specialty.name + ' '}"/> <span
th:if="${vet.nrOfSpecialties == 0}">none</span></td>
th:if="${vet.nrOfSpecialties == 0}" th:text="#{none}">none</span></td>
</tr>
</tbody>
</table>
<div th:if="${totalPages > 1}">
<span>Pages:</span>
<span th:text="#{pages}">Pages:</span>
<span>[</span>
<span th:each="i: ${#numbers.sequence(1, totalPages)}">
<a th:if="${currentPage != i}" th:href="@{'/vets.html?page=__${i}__'}">[[${i}]]</a>
@ -33,24 +33,24 @@
</span>
<span>]&nbsp;</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/vets.html?page=1'}" title="First"
<a th:if="${currentPage > 1}" th:href="@{'/vets.html?page=1'}" th:title="#{first}"
class="fa fa-fast-backward"></a>
<span th:unless="${currentPage > 1}" title="First" class="fa fa-fast-backward"></span>
<span th:unless="${currentPage > 1}" th:text="#{first}" th:title="#{first}" class="fa fa-fast-backward"></span>
</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/vets.html?page=__${currentPage - 1}__'}" title="Previous"
<a th:if="${currentPage > 1}" th:href="@{'/vets.html?page=__${currentPage - 1}__'}" th:title="#{previous}"
class="fa fa-step-backward"></a>
<span th:unless="${currentPage > 1}" title="Previous" class="fa fa-step-backward"></span>
<span th:unless="${currentPage > 1}" th:text="#{previous}" th:title="#{previous}" class="fa fa-step-backward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/vets.html?page=__${currentPage + 1}__'}" title="Next"
<a th:if="${currentPage < totalPages}" th:href="@{'/vets.html?page=__${currentPage + 1}__'}" th:title="#{next}"
class="fa fa-step-forward"></a>
<span th:unless="${currentPage < totalPages}" title="Next" class="fa fa-step-forward"></span>
<span th:unless="${currentPage < totalPages}" th:text="#{next}" th:title="#{next}" class="fa fa-step-forward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/vets.html?page=__${totalPages}__'}" title="Last"
<a th:if="${currentPage < totalPages}" th:href="@{'/vets.html?page=__${totalPages}__'}" th:title="#{last}"
class="fa fa-fast-forward"></a>
<span th:unless="${currentPage < totalPages}" title="Last" class="fa fa-fast-forward"></span>
<span th:unless="${currentPage < totalPages}" th:text="#{last}" class="fa fa-fast-forward"></span>
</span>
</div>
</body>

View file

@ -40,7 +40,8 @@ public class MysqlTestApplication {
}
public static void main(String[] args) {
SpringApplication.run(PetClinicApplication.class, "--spring.profiles.active=mysql");
SpringApplication.run(PetClinicApplication.class, "--spring.profiles.active=mysql",
"--spring.docker.compose.enabled=false");
}
}

View file

@ -16,7 +16,6 @@
package org.springframework.samples.petclinic.owner;
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;
@ -31,6 +30,7 @@ import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import static org.hamcrest.Matchers.empty;
@ -92,9 +92,7 @@ class OwnerControllerTests {
Owner george = george();
given(this.owners.findByLastNameStartingWith(eq("Franklin"), any(Pageable.class)))
.willReturn(new PageImpl<>(Lists.newArrayList(george)));
given(this.owners.findAll(any(Pageable.class))).willReturn(new PageImpl<>(Lists.newArrayList(george)));
.willReturn(new PageImpl<>(List.of(george)));
given(this.owners.findById(TEST_OWNER_ID)).willReturn(Optional.of(george));
Visit visit = new Visit();
@ -143,14 +141,14 @@ class OwnerControllerTests {
@Test
void testProcessFindFormSuccess() throws Exception {
Page<Owner> tasks = new PageImpl<>(Lists.newArrayList(george(), new Owner()));
Page<Owner> tasks = new PageImpl<>(List.of(george(), new Owner()));
when(this.owners.findByLastNameStartingWith(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()));
Page<Owner> tasks = new PageImpl<>(List.of(george()));
when(this.owners.findByLastNameStartingWith(eq("Franklin"), any(Pageable.class))).thenReturn(tasks);
mockMvc.perform(get("/owners?page=1").param("lastName", "Franklin"))
.andExpect(status().is3xxRedirection())
@ -159,7 +157,7 @@ class OwnerControllerTests {
@Test
void testProcessFindFormNoOwnersFound() throws Exception {
Page<Owner> tasks = new PageImpl<>(Lists.newArrayList());
Page<Owner> tasks = new PageImpl<>(List.of());
when(this.owners.findByLastNameStartingWith(eq("Unknown Surname"), any(Pageable.class))).thenReturn(tasks);
mockMvc.perform(get("/owners?page=1").param("lastName", "Unknown Surname"))
.andExpect(status().isOk())

View file

@ -16,7 +16,6 @@
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;
@ -30,6 +29,7 @@ import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
import static org.mockito.BDDMockito.given;
@ -66,7 +66,7 @@ class PetControllerTests {
PetType cat = new PetType();
cat.setId(3);
cat.setName("hamster");
given(this.owners.findPetTypes()).willReturn(Lists.newArrayList(cat));
given(this.owners.findPetTypes()).willReturn(List.of(cat));
Owner owner = new Owner();
Pet pet = new Pet();

View file

@ -0,0 +1,136 @@
package org.springframework.samples.petclinic.system;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.fail;
/**
* This test ensures that there are no hard-coded strings without internationalization in
* any HTML files. Also ensures that a string is translated in every language to avoid
* partial translations.
*
* @author Anuj Ashok Potdar
*/
public class I18nPropertiesSyncTest {
private static final String I18N_DIR = "src/main/resources";
private static final String BASE_NAME = "messages";
public static final String PROPERTIES = ".properties";
private static final Pattern HTML_TEXT_LITERAL = Pattern.compile(">([^<>{}]+)<");
private static final Pattern BRACKET_ONLY = Pattern.compile("<[^>]*>\\s*[\\[\\]](?:&nbsp;)?\\s*</[^>]*>");
private static final Pattern HAS_TH_TEXT_ATTRIBUTE = Pattern.compile("th:(u)?text\\s*=\\s*\"[^\"]+\"");
@Test
public void checkNonInternationalizedStrings() throws IOException {
Path root = Paths.get("src/main");
List<Path> files;
try (Stream<Path> stream = Files.walk(root)) {
files = stream.filter(p -> p.toString().endsWith(".java") || p.toString().endsWith(".html"))
.filter(p -> !p.toString().contains("/test/"))
.filter(p -> !p.getFileName().toString().endsWith("Test.java"))
.toList();
}
StringBuilder report = new StringBuilder();
for (Path file : files) {
List<String> lines = Files.readAllLines(file);
for (int i = 0; i < lines.size(); i++) {
String line = lines.get(i).trim();
if (line.startsWith("//") || line.startsWith("@") || line.contains("log.")
|| line.contains("System.out"))
continue;
if (file.toString().endsWith(".html")) {
boolean hasLiteralText = HTML_TEXT_LITERAL.matcher(line).find();
boolean hasThTextAttribute = HAS_TH_TEXT_ATTRIBUTE.matcher(line).find();
boolean isBracketOnly = BRACKET_ONLY.matcher(line).find();
if (hasLiteralText && !line.contains("#{") && !hasThTextAttribute && !isBracketOnly) {
report.append("HTML: ")
.append(file)
.append(" Line ")
.append(i + 1)
.append(": ")
.append(line)
.append("\n");
}
}
}
}
if (!report.isEmpty()) {
fail("Hardcoded (non-internationalized) strings found:\n" + report);
}
}
@Test
public void checkI18nPropertyFilesAreInSync() throws IOException {
List<Path> propertyFiles;
try (Stream<Path> stream = Files.walk(Paths.get(I18N_DIR))) {
propertyFiles = stream.filter(p -> p.getFileName().toString().startsWith(BASE_NAME))
.filter(p -> p.getFileName().toString().endsWith(PROPERTIES))
.toList();
}
Map<String, Properties> localeToProps = new HashMap<>();
for (Path path : propertyFiles) {
Properties props = new Properties();
try (var reader = Files.newBufferedReader(path)) {
props.load(reader);
localeToProps.put(path.getFileName().toString(), props);
}
}
String baseFile = BASE_NAME + PROPERTIES;
Properties baseProps = localeToProps.get(baseFile);
if (baseProps == null) {
fail("Base properties file '" + baseFile + "' not found.");
return;
}
Set<String> baseKeys = baseProps.stringPropertyNames();
StringBuilder report = new StringBuilder();
for (Map.Entry<String, Properties> entry : localeToProps.entrySet()) {
String fileName = entry.getKey();
// We use fallback logic to include english strings, hence messages_en is not
// populated.
if (fileName.equals(baseFile) || fileName.equals("messages_en.properties"))
continue;
Properties props = entry.getValue();
Set<String> missingKeys = new TreeSet<>(baseKeys);
missingKeys.removeAll(props.stringPropertyNames());
if (!missingKeys.isEmpty()) {
report.append("Missing keys in ").append(fileName).append(":\n");
missingKeys.forEach(k -> report.append(" ").append(k).append("\n"));
}
}
if (!report.isEmpty()) {
fail("Translation files are not in sync:\n" + report);
}
}
}