initial commit
Some checks failed
CI Pipeline / Build and Test (push) Has been cancelled
Build and Publish TechDocs / build-and-publish (push) Failing after 2s

Change-Id: I0cbba3ab47a624a312bbb0d00caa3e4d67bc2239
This commit is contained in:
Scaffolder
2026-03-24 20:10:07 +00:00
commit 886d7ab743
136 changed files with 27283 additions and 0 deletions

View File

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

View File

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

View File

@@ -0,0 +1,36 @@
/*
* Copyright 2012-2025 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;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportRuntimeHints;
/**
* PetClinic Spring Boot Application.
*
* @author Dave Syer
*/
@SpringBootApplication
@ImportRuntimeHints(PetClinicRuntimeHints.class)
public class PetClinicApplication {
public static void main(String[] args) {
SpringApplication.run(PetClinicApplication.class, args);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 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;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.samples.petclinic.model.BaseEntity;
import org.springframework.samples.petclinic.model.Person;
import org.springframework.samples.petclinic.vet.Vet;
public class PetClinicRuntimeHints implements RuntimeHintsRegistrar {
@Override
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("mysql-default-conf");
hints.serialization().registerType(BaseEntity.class);
hints.serialization().registerType(Person.class);
hints.serialization().registerType(Vet.class);
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2025 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.model;
import java.io.Serializable;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.MappedSuperclass;
/**
* Simple JavaBean domain object with an id property. Used as a base class for objects
* needing this property.
*
* @author Ken Krebs
* @author Juergen Hoeller
*/
@MappedSuperclass
public class BaseEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public boolean isNew() {
return this.id == null;
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2012-2025 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.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
* a base class for objects needing these properties.
*
* @author Ken Krebs
* @author Juergen Hoeller
* @author Wick Dynex
*/
@MappedSuperclass
public class NamedEntity extends BaseEntity {
@Column
@NotBlank
private String name;
public String getName() {
return this.name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
String name = this.getName();
return name != null ? name : "<null>";
}
}

View File

@@ -0,0 +1,54 @@
/*
* Copyright 2012-2025 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.model;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.validation.constraints.NotBlank;
/**
* Simple JavaBean domain object representing an person.
*
* @author Ken Krebs
*/
@MappedSuperclass
public class Person extends BaseEntity {
@Column
@NotBlank
private String firstName;
@Column
@NotBlank
private String lastName;
public String getFirstName() {
return this.firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return this.lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
}

View File

@@ -0,0 +1,20 @@
/*
* Copyright 2012-2025 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.
*/
/**
* The classes in this package represent utilities used by the domain.
*/
package org.springframework.samples.petclinic.model;

View File

@@ -0,0 +1,176 @@
/*
* Copyright 2012-2025 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 java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import org.springframework.core.style.ToStringCreator;
import org.springframework.samples.petclinic.model.Person;
import org.springframework.util.Assert;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.NotBlank;
/**
* Simple JavaBean domain object representing an owner.
*
* @author Ken Krebs
* @author Juergen Hoeller
* @author Sam Brannen
* @author Michael Isvy
* @author Oliver Drotbohm
* @author Wick Dynex
*/
@Entity
@Table(name = "owners")
public class Owner extends Person {
@Column
@NotBlank
private String address;
@Column
@NotBlank
private String city;
@Column
@NotBlank
@Pattern(regexp = "\\d{10}", message = "{telephone.invalid}")
private String telephone;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "owner_id")
@OrderBy("name")
private final List<Pet> pets = new ArrayList<>();
public String getAddress() {
return this.address;
}
public void setAddress(String address) {
this.address = address;
}
public String getCity() {
return this.city;
}
public void setCity(String city) {
this.city = city;
}
public String getTelephone() {
return this.telephone;
}
public void setTelephone(String telephone) {
this.telephone = telephone;
}
public List<Pet> getPets() {
return this.pets;
}
public void addPet(Pet pet) {
if (pet.isNew()) {
getPets().add(pet);
}
}
/**
* 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
*/
public Pet getPet(String name) {
return getPet(name, false);
}
/**
* 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
*/
public Pet getPet(Integer id) {
for (Pet pet : getPets()) {
if (!pet.isNew()) {
Integer compId = pet.getId();
if (Objects.equals(compId, id)) {
return pet;
}
}
}
return null;
}
/**
* 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
*/
public Pet getPet(String name, boolean ignoreNew) {
for (Pet pet : getPets()) {
String compName = pet.getName();
if (compName != null && compName.equalsIgnoreCase(name)) {
if (!ignoreNew || !pet.isNew()) {
return pet;
}
}
}
return null;
}
@Override
public String toString() {
return new ToStringCreator(this).append("id", this.getId())
.append("new", this.isNew())
.append("lastName", this.getLastName())
.append("firstName", this.getFirstName())
.append("address", this.address)
.append("city", this.city)
.append("telephone", this.telephone)
.toString();
}
/**
* Adds the given {@link Visit} to the {@link Pet} with the given identifier.
* @param petId the identifier of the {@link Pet}, must not be {@literal null}.
* @param visit the visit to add, must not be {@literal null}.
*/
public void addVisit(Integer petId, Visit visit) {
Assert.notNull(petId, "Pet identifier must not be null!");
Assert.notNull(visit, "Visit must not be null!");
Pet pet = getPet(petId);
Assert.notNull(pet, "Invalid Pet identifier!");
pet.addVisit(visit);
}
}

View File

@@ -0,0 +1,176 @@
/*
* Copyright 2012-2025 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 java.util.List;
import java.util.Objects;
import java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
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 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 {
private static final String VIEWS_OWNER_CREATE_OR_UPDATE_FORM = "owners/createOrUpdateOwnerForm";
private final OwnerRepository owners;
public OwnerController(OwnerRepository owners) {
this.owners = owners;
}
@InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields("id");
}
@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."));
}
@GetMapping("/owners/new")
public String initCreationForm() {
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
@PostMapping("/owners/new")
public String processCreationForm(@Valid Owner owner, BindingResult result, RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
redirectAttributes.addFlashAttribute("error", "There was an error in creating the owner.");
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
this.owners.save(owner);
redirectAttributes.addFlashAttribute("message", "New Owner Created");
return "redirect:/owners/" + owner.getId();
}
@GetMapping("/owners/find")
public String initFindForm() {
return "owners/findOwners";
}
@GetMapping("/owners")
public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner, BindingResult result,
Model model) {
// allow parameterless GET request for /owners to return all records
String lastName = owner.getLastName();
if (lastName == null) {
lastName = ""; // empty string signifies broadest possible search
}
// find owners by last name
Page<Owner> ownersResults = findPaginatedForOwnersLastName(page, lastName);
if (ownersResults.isEmpty()) {
// no owners found
result.rejectValue("lastName", "notFound", "not found");
return "owners/findOwners";
}
if (ownersResults.getTotalElements() == 1) {
// 1 owner found
owner = ownersResults.iterator().next();
return "redirect:/owners/" + owner.getId();
}
// multiple owners found
return addPaginationModel(page, model, ownersResults);
}
private String addPaginationModel(int page, Model model, Page<Owner> paginated) {
List<Owner> listOwners = paginated.getContent();
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", paginated.getTotalPages());
model.addAttribute("totalItems", paginated.getTotalElements());
model.addAttribute("listOwners", listOwners);
return "owners/ownersList";
}
private Page<Owner> findPaginatedForOwnersLastName(int page, String lastname) {
int pageSize = 5;
Pageable pageable = PageRequest.of(page - 1, pageSize);
return owners.findByLastNameStartingWith(lastname, pageable);
}
@GetMapping("/owners/{ownerId}/edit")
public String initUpdateOwnerForm() {
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
@PostMapping("/owners/{ownerId}/edit")
public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result, @PathVariable("ownerId") int ownerId,
RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
redirectAttributes.addFlashAttribute("error", "There was an error in updating the owner.");
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}
if (!Objects.equals(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");
return "redirect:/owners/{ownerId}";
}
/**
* Custom handler for displaying an owner.
* @param ownerId the ID of the owner to display
* @return a ModelMap with the model attributes for the view
*/
@GetMapping("/owners/{ownerId}")
public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) {
ModelAndView mav = new ModelAndView("owners/ownerDetails");
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 "));
mav.addObject(owner);
return mav;
}
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2012-2025 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 java.util.Optional;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* Repository class for <code>Owner</code> domain objects. All method names are compliant
* with Spring Data naming conventions so this interface can easily be extended for Spring
* Data. See:
* https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation
*
* @author Ken Krebs
* @author Juergen Hoeller
* @author Sam Brannen
* @author Michael Isvy
* @author Wick Dynex
*/
public interface OwnerRepository extends JpaRepository<Owner, Integer> {
/**
* Retrieve {@link Owner}s from the data store by last name, returning all owners
* whose last name <i>starts</i> with the given name.
* @param lastName Value to search for
* @return a Collection of matching {@link Owner}s (or an empty Collection if none
* found)
*/
Page<Owner> findByLastNameStartingWith(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)
*/
Optional<Owner> findById(Integer id);
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2012-2025 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 java.time.LocalDate;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.samples.petclinic.model.NamedEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import jakarta.persistence.OrderBy;
import jakarta.persistence.Table;
/**
* Simple business object representing a pet.
*
* @author Ken Krebs
* @author Juergen Hoeller
* @author Sam Brannen
* @author Wick Dynex
*/
@Entity
@Table(name = "pets")
public class Pet extends NamedEntity {
@Column
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate birthDate;
@ManyToOne
@JoinColumn(name = "type_id")
private PetType type;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "pet_id")
@OrderBy("date ASC")
private final Set<Visit> visits = new LinkedHashSet<>();
public void setBirthDate(LocalDate birthDate) {
this.birthDate = birthDate;
}
public LocalDate getBirthDate() {
return this.birthDate;
}
public PetType getType() {
return this.type;
}
public void setType(PetType type) {
this.type = type;
}
public Collection<Visit> getVisits() {
return this.visits;
}
public void addVisit(Visit visit) {
getVisits().add(visit);
}
}

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2012-2025 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 java.time.LocalDate;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.validation.Valid;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
/**
* @author Juergen Hoeller
* @author Ken Krebs
* @author Arjen Poutsma
* @author Wick Dynex
*/
@Controller
@RequestMapping("/owners/{ownerId}")
class PetController {
private static final String VIEWS_PETS_CREATE_OR_UPDATE_FORM = "pets/createOrUpdatePetForm";
private final OwnerRepository owners;
private final PetTypeRepository types;
public PetController(OwnerRepository owners, PetTypeRepository types) {
this.owners = owners;
this.types = types;
}
@ModelAttribute("types")
public Collection<PetType> populatePetTypes() {
return this.types.findPetTypes();
}
@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 "));
return owner;
}
@ModelAttribute("pet")
public Pet findPet(@PathVariable("ownerId") int ownerId,
@PathVariable(name = "petId", required = false) Integer petId) {
if (petId == null) {
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 "));
return owner.getPet(petId);
}
@InitBinder("owner")
public void initOwnerBinder(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields("id");
}
@InitBinder("pet")
public void initPetBinder(WebDataBinder dataBinder) {
dataBinder.setValidator(new PetValidator());
}
@GetMapping("/pets/new")
public String initCreationForm(Owner owner, ModelMap model) {
Pet pet = new Pet();
owner.addPet(pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
@PostMapping("/pets/new")
public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result,
RedirectAttributes redirectAttributes) {
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");
}
if (result.hasErrors()) {
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() {
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
@PostMapping("/pets/{petId}/edit")
public String processUpdateForm(Owner owner, @Valid Pet pet, BindingResult result,
RedirectAttributes redirectAttributes) {
String petName = pet.getName();
// checking if the pet name already exists for the owner
if (StringUtils.hasText(petName)) {
Pet existingPet = owner.getPet(petName, false);
if (existingPet != null && !Objects.equals(existingPet.getId(), pet.getId())) {
result.rejectValue("name", "duplicate", "already exists");
}
}
LocalDate currentDate = LocalDate.now();
if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(currentDate)) {
result.rejectValue("birthDate", "typeMismatch.birthDate");
}
if (result.hasErrors()) {
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}
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) {
Integer id = pet.getId();
Assert.state(id != null, "'pet.getId()' must not be null");
Pet existingPet = owner.getPet(id);
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

@@ -0,0 +1,30 @@
/*
* Copyright 2012-2025 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.springframework.samples.petclinic.model.NamedEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
/**
* @author Juergen Hoeller Can be Cat, Dog, Hamster...
*/
@Entity
@Table(name = "types")
public class PetType extends NamedEntity {
}

View File

@@ -0,0 +1,62 @@
/*
* Copyright 2012-2025 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.springframework.format.Formatter;
import org.springframework.stereotype.Component;
import java.text.ParseException;
import java.util.Collection;
import java.util.Locale;
import java.util.Objects;
/**
* Instructs Spring MVC on how to parse and print elements of type 'PetType'. Starting
* from Spring 3.0, Formatters have come as an improvement in comparison to legacy
* PropertyEditors. See the following links for more details: - The Spring ref doc:
* https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#format
*
* @author Mark Fisher
* @author Juergen Hoeller
* @author Michael Isvy
*/
@Component
public class PetTypeFormatter implements Formatter<PetType> {
private final PetTypeRepository types;
public PetTypeFormatter(PetTypeRepository types) {
this.types = types;
}
@Override
public String print(PetType petType, Locale locale) {
String name = petType.getName();
return name != null ? name : "<null>";
}
@Override
public PetType parse(String text, Locale locale) throws ParseException {
Collection<PetType> findPetTypes = this.types.findPetTypes();
for (PetType type : findPetTypes) {
if (Objects.equals(type.getName(), text)) {
return type;
}
}
throw new ParseException("type not found: " + text, 0);
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2012-2025 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 java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
/**
* Repository class for <code>PetType</code> domain objects.
*
* @author Patrick Baumgartner
*/
public interface PetTypeRepository extends JpaRepository<PetType, 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")
List<PetType> findPetTypes();
}

View File

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

View File

@@ -0,0 +1,68 @@
/*
* Copyright 2012-2025 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 java.time.LocalDate;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.samples.petclinic.model.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import jakarta.validation.constraints.NotBlank;
/**
* Simple JavaBean domain object representing a visit.
*
* @author Ken Krebs
* @author Dave Syer
*/
@Entity
@Table(name = "visits")
public class Visit extends BaseEntity {
@Column(name = "visit_date")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate date;
@NotBlank
private String description;
/**
* Creates a new instance of Visit for the current date
*/
public Visit() {
this.date = LocalDate.now();
}
public LocalDate getDate() {
return this.date;
}
public void setDate(LocalDate date) {
this.date = date;
}
public String getDescription() {
return this.description;
}
public void setDescription(String description) {
this.description = description;
}
}

View File

@@ -0,0 +1,104 @@
/*
* Copyright 2012-2025 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 java.util.Map;
import java.util.Optional;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
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 Dave Syer
* @author Wick Dynex
*/
@Controller
class VisitController {
private final OwnerRepository owners;
public VisitController(OwnerRepository owners) {
this.owners = owners;
}
@InitBinder
public void setAllowedFields(WebDataBinder dataBinder) {
dataBinder.setDisallowedFields("id");
}
/**
* Called before each and every @RequestMapping annotated method. 2 goals: - Make sure
* we always have fresh data - Since we do not use the session scope, make sure that
* Pet object always has an id (Even though id is not part of the form fields)
* @param petId
* @return Pet
*/
@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 "));
Pet pet = owner.getPet(petId);
if (pet == null) {
throw new IllegalArgumentException(
"Pet with id " + petId + " not found for owner with id " + ownerId + ".");
}
model.put("pet", pet);
model.put("owner", owner);
Visit visit = new Visit();
pet.addVisit(visit);
return visit;
}
// Spring MVC calls method loadPetWithVisit(...) before initNewVisitForm is
// called
@GetMapping("/owners/{ownerId}/pets/{petId}/visits/new")
public String initNewVisitForm() {
return "pets/createOrUpdateVisitForm";
}
// Spring MVC calls method loadPetWithVisit(...) before processNewVisitForm is
// called
@PostMapping("/owners/{ownerId}/pets/{petId}/visits/new")
public String processNewVisitForm(@ModelAttribute Owner owner, @PathVariable int petId, @Valid Visit visit,
BindingResult result, RedirectAttributes redirectAttributes) {
if (result.hasErrors()) {
return "pets/createOrUpdateVisitForm";
}
owner.addVisit(petId, visit);
this.owners.save(owner);
redirectAttributes.addFlashAttribute("message", "Your visit has been booked");
return "redirect:/owners/{ownerId}";
}
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2012-2025 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;

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2012-2025 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;

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2012-2025 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.system;
import org.springframework.boot.cache.autoconfigure.JCacheManagerCustomizer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.cache.configuration.MutableConfiguration;
/**
* Cache configuration intended for caches providing the JCache API. This configuration
* creates the used cache for the application and enables statistics that become
* accessible via JMX.
*/
@Configuration(proxyBeanMethods = false)
@EnableCaching
class CacheConfiguration {
@Bean
public JCacheManagerCustomizer petclinicCacheConfigurationCustomizer() {
return cm -> cm.createCache("vets", cacheConfiguration());
}
/**
* Create a simple configuration that enable statistics via the JCache programmatic
* configuration API.
* <p>
* Within the configuration object that is provided by the JCache API standard, there
* is only a very limited set of configuration options. The really relevant
* configuration options (like the size limit) must be set via a configuration
* mechanism that is provided by the selected JCache implementation.
*/
private javax.cache.configuration.Configuration<Object, Object> cacheConfiguration() {
return new MutableConfiguration<>().setStatisticsEnabled(true);
}
}

View File

@@ -0,0 +1,37 @@
/*
* Copyright 2012-2025 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.system;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
/**
* Controller used to showcase what happens when an exception is thrown
*
* @author Michael Isvy
* <p/>
* Also see how a view that resolves to "error" has been added ("error.html").
*/
@Controller
class CrashController {
@GetMapping("/oups")
public String triggerException() {
throw new RuntimeException(
"Expected: controller used to showcase what " + "happens when an exception is thrown");
}
}

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

@@ -0,0 +1,30 @@
/*
* Copyright 2012-2025 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.system;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
class WelcomeController {
@GetMapping("/")
public String welcome() {
return "welcome";
}
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2012-2025 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.system;

View File

@@ -0,0 +1,32 @@
/*
* Copyright 2012-2025 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.vet;
import org.springframework.samples.petclinic.model.NamedEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
/**
* Models a {@link Vet Vet's} specialty (for example, dentistry).
*
* @author Juergen Hoeller
*/
@Entity
@Table(name = "specialties")
public class Specialty extends NamedEntity {
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2012-2025 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.vet;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import org.springframework.samples.petclinic.model.NamedEntity;
import org.springframework.samples.petclinic.model.Person;
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.
*
* @author Ken Krebs
* @author Juergen Hoeller
* @author Sam Brannen
* @author Arjen Poutsma
*/
@Entity
@Table(name = "vets")
public class Vet extends Person {
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "vet_specialties", joinColumns = @JoinColumn(name = "vet_id"),
inverseJoinColumns = @JoinColumn(name = "specialty_id"))
private Set<Specialty> specialties;
protected Set<Specialty> getSpecialtiesInternal() {
if (this.specialties == null) {
this.specialties = new HashSet<>();
}
return this.specialties;
}
@XmlElement
public List<Specialty> getSpecialties() {
return getSpecialtiesInternal().stream()
.sorted(Comparator.comparing(NamedEntity::getName))
.collect(Collectors.toList());
}
public int getNrOfSpecialties() {
return getSpecialtiesInternal().size();
}
public void addSpecialty(Specialty specialty) {
getSpecialtiesInternal().add(specialty);
}
}

View File

@@ -0,0 +1,78 @@
/*
* Copyright 2012-2025 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.vet;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @author Juergen Hoeller
* @author Mark Fisher
* @author Ken Krebs
* @author Arjen Poutsma
*/
@Controller
class VetController {
private final VetRepository vetRepository;
public VetController(VetRepository vetRepository) {
this.vetRepository = vetRepository;
}
@GetMapping("/vets.html")
public String showVetList(@RequestParam(defaultValue = "1") int page, Model model) {
// Here we are returning an object of type 'Vets' rather than a collection of Vet
// objects so it is simpler for Object-Xml mapping
Vets vets = new Vets();
Page<Vet> paginated = findPaginated(page);
vets.getVetList().addAll(paginated.toList());
return addPaginationModel(page, paginated, model);
}
private String addPaginationModel(int page, Page<Vet> paginated, Model model) {
List<Vet> listVets = paginated.getContent();
model.addAttribute("currentPage", page);
model.addAttribute("totalPages", paginated.getTotalPages());
model.addAttribute("totalItems", paginated.getTotalElements());
model.addAttribute("listVets", listVets);
return "vets/vetList";
}
private Page<Vet> findPaginated(int page) {
int pageSize = 5;
Pageable pageable = PageRequest.of(page - 1, pageSize);
return vetRepository.findAll(pageable);
}
@GetMapping({ "/vets" })
public @ResponseBody Vets showResourcesVetList() {
// Here we are returning an object of type 'Vets' rather than a collection of Vet
// objects so it is simpler for JSon/Object mapping
Vets vets = new Vets();
vets.getVetList().addAll(this.vetRepository.findAll());
return vets;
}
}

View File

@@ -0,0 +1,58 @@
/*
* Copyright 2012-2025 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.vet;
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.repository.Repository;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;
/**
* Repository class for <code>Vet</code> domain objects All method names are compliant
* with Spring Data naming conventions so this interface can easily be extended for Spring
* Data. See:
* https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation
*
* @author Ken Krebs
* @author Juergen Hoeller
* @author Sam Brannen
* @author Michael Isvy
*/
public interface VetRepository extends Repository<Vet, Integer> {
/**
* Retrieve all <code>Vet</code>s from the data store.
* @return a <code>Collection</code> of <code>Vet</code>s
*/
@Transactional(readOnly = true)
@Cacheable("vets")
Collection<Vet> findAll() throws DataAccessException;
/**
* Retrieve all <code>Vet</code>s from data store in Pages
* @param pageable
* @return
* @throws DataAccessException
*/
@Transactional(readOnly = true)
@Cacheable("vets")
Page<Vet> findAll(Pageable pageable) throws DataAccessException;
}

View File

@@ -0,0 +1,43 @@
/*
* Copyright 2012-2025 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.vet;
import java.util.ArrayList;
import java.util.List;
import jakarta.xml.bind.annotation.XmlElement;
import jakarta.xml.bind.annotation.XmlRootElement;
/**
* Simple domain object representing a list of veterinarians. Mostly here to be used for
* the 'vets' {@link org.springframework.web.servlet.view.xml.MarshallingView}.
*
* @author Arjen Poutsma
*/
@XmlRootElement
public class Vets {
private List<Vet> vets;
@XmlElement
public List<Vet> getVetList() {
if (vets == null) {
vets = new ArrayList<>();
}
return vets;
}
}

View File

@@ -0,0 +1,16 @@
/*
* Copyright 2012-2025 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.vet;

View File

@@ -0,0 +1,7 @@
# database init, supports mysql too
database=mysql
spring.datasource.url=${MYSQL_URL:jdbc:mysql://localhost/petclinic}
spring.datasource.username=${MYSQL_USER:petclinic}
spring.datasource.password=${MYSQL_PASS:petclinic}
# SQL is written to be idempotent so this is safe
spring.sql.init.mode=always

View File

@@ -0,0 +1,7 @@
# database init, supports postgres too
database=postgres
spring.datasource.url=${POSTGRES_URL:jdbc:postgresql://localhost/petclinic}
spring.datasource.username=${POSTGRES_USER:petclinic}
spring.datasource.password=${POSTGRES_PASS:petclinic}
# SQL is written to be idempotent so this is safe
spring.sql.init.mode=always

View File

@@ -0,0 +1,27 @@
# 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
# Web
spring.thymeleaf.mode=HTML
# JPA
spring.jpa.hibernate.ddl-auto=none
spring.jpa.open-in-view=false
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategySnakeCaseImpl
spring.jpa.properties.hibernate.default_batch_fetch_size=16
# Internationalization
spring.messages.basename=messages/messages
# Actuator
management.endpoints.web.exposure.include=*
# Logging
logging.level.org.springframework=INFO
# logging.level.org.springframework.web=DEBUG
# logging.level.org.springframework.context.annotation=TRACE
# Maximum time static resources should be cached
spring.web.resources.cache.cachecontrol.max-age=12h

View File

@@ -0,0 +1,15 @@
|\ _,,,--,,_
/,`.-'`' ._ \-;;,_
_______ __|,4- ) )_ .;.(__`'-'__ ___ __ _ ___ _______
| | '---''(_/._)-'(_\_) | | | | | | | | |
| _ | ___|_ _| | | | | |_| | | | __ _ _
| |_| | |___ | | | | | | | | | | \ \ \ \
| ___| ___| | | | _| |___| | _ | | _| \ \ \ \
| | | |___ | | | |_| | | | | | | |_ ) ) ) )
|___| |_______| |___| |_______|_______|___|_| |__|___|_______| / / / /
==================================================================/_/_/_/
:: Built with Spring Boot :: ${spring-boot.version}

View File

@@ -0,0 +1,53 @@
INSERT INTO vets VALUES (default, 'James', 'Carter');
INSERT INTO vets VALUES (default, 'Helen', 'Leary');
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 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);
INSERT INTO types VALUES (default, 'cat');
INSERT INTO types VALUES (default, 'dog');
INSERT INTO types VALUES (default, 'lizard');
INSERT INTO types VALUES (default, 'snake');
INSERT INTO types VALUES (default, 'bird');
INSERT INTO types VALUES (default, 'hamster');
INSERT INTO owners VALUES (default, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023');
INSERT INTO owners VALUES (default, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749');
INSERT INTO owners VALUES (default, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763');
INSERT INTO owners VALUES (default, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198');
INSERT INTO owners VALUES (default, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765');
INSERT INTO owners VALUES (default, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654');
INSERT INTO owners VALUES (default, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387');
INSERT INTO owners VALUES (default, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683');
INSERT INTO owners VALUES (default, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435');
INSERT INTO owners VALUES (default, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487');
INSERT INTO pets VALUES (default, 'Leo', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Basil', '2012-08-06', 6, 2);
INSERT INTO pets VALUES (default, 'Rosy', '2011-04-17', 2, 3);
INSERT INTO pets VALUES (default, 'Jewel', '2010-03-07', 2, 3);
INSERT INTO pets VALUES (default, 'Iggy', '2010-11-30', 3, 4);
INSERT INTO pets VALUES (default, 'George', '2010-01-20', 4, 5);
INSERT INTO pets VALUES (default, 'Samantha', '2012-09-04', 1, 6);
INSERT INTO pets VALUES (default, 'Max', '2012-09-04', 1, 6);
INSERT INTO pets VALUES (default, 'Lucky', '2011-08-06', 5, 7);
INSERT INTO pets VALUES (default, 'Mulligan', '2007-02-24', 2, 8);
INSERT INTO pets VALUES (default, 'Freddy', '2010-03-09', 5, 9);
INSERT INTO pets VALUES (default, 'Lucky', '2010-06-24', 2, 10);
INSERT INTO pets VALUES (default, 'Sly', '2012-06-08', 1, 10);
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,64 @@
DROP TABLE vet_specialties IF EXISTS;
DROP TABLE vets IF EXISTS;
DROP TABLE specialties IF EXISTS;
DROP TABLE visits IF EXISTS;
DROP TABLE pets IF EXISTS;
DROP TABLE types IF EXISTS;
DROP TABLE owners IF EXISTS;
CREATE TABLE vets (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
first_name VARCHAR(30),
last_name VARCHAR(30)
);
CREATE INDEX vets_last_name ON vets (last_name);
CREATE TABLE specialties (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(80)
);
CREATE INDEX specialties_name ON specialties (name);
CREATE TABLE vet_specialties (
vet_id INTEGER NOT NULL,
specialty_id INTEGER NOT NULL
);
ALTER TABLE vet_specialties ADD CONSTRAINT fk_vet_specialties_vets FOREIGN KEY (vet_id) REFERENCES vets (id);
ALTER TABLE vet_specialties ADD CONSTRAINT fk_vet_specialties_specialties FOREIGN KEY (specialty_id) REFERENCES specialties (id);
CREATE TABLE types (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(80)
);
CREATE INDEX types_name ON types (name);
CREATE TABLE owners (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
first_name VARCHAR(30),
last_name VARCHAR_IGNORECASE(30),
address VARCHAR(255),
city VARCHAR(80),
telephone VARCHAR(20)
);
CREATE INDEX owners_last_name ON owners (last_name);
CREATE TABLE pets (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name VARCHAR(30),
birth_date DATE,
type_id INTEGER NOT NULL,
owner_id INTEGER
);
ALTER TABLE pets ADD CONSTRAINT fk_pets_owners FOREIGN KEY (owner_id) REFERENCES owners (id);
ALTER TABLE pets ADD CONSTRAINT fk_pets_types FOREIGN KEY (type_id) REFERENCES types (id);
CREATE INDEX pets_name ON pets (name);
CREATE TABLE visits (
id INTEGER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
pet_id INTEGER,
visit_date DATE,
description VARCHAR(255)
);
ALTER TABLE visits ADD CONSTRAINT fk_visits_pets FOREIGN KEY (pet_id) REFERENCES pets (id);
CREATE INDEX visits_pet_id ON visits (pet_id);

View File

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

View File

@@ -0,0 +1,36 @@
================================================================================
=== Spring PetClinic sample application - MySQL Configuration ===
================================================================================
@author Sam Brannen
@author Costin Leau
@author Dave Syer
--------------------------------------------------------------------------------
1) Download and install the MySQL database (e.g., MySQL Community Server 5.1.x),
which can be found here: https://dev.mysql.com/downloads/. Or run the
"docker-compose.yml" from the root of the project (if you have docker installed
locally):
$ docker-compose up
...
mysql_1_eedb4818d817 | MySQL init process done. Ready for start up.
...
2) (Once only) create the PetClinic database and user by executing the "db/mysql/user.sql"
scripts. You can connect to the database running in the docker container using
`mysql -u root -h localhost --protocol tcp`, but you don't need to run the script there
because the petclinic user is already set up if you use the provided `docker-compose.yml`.
3) Run the app with `spring.profiles.active=mysql` (e.g. as a System property via the command
line, but any way that sets that property in a Spring Boot app should work). For example use
mvn spring-boot:run -Dspring-boot.run.profiles=mysql
To activate the profile on the command line.
N.B. the "petclinic" database has to exist for the app to work with the JDBC URL value
as it is configured by default. This condition is taken care of automatically by the
docker-compose configuration provided, or by the `user.sql` script if you run that as
root.

View File

@@ -0,0 +1,55 @@
CREATE TABLE IF NOT EXISTS vets (
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(30),
last_name VARCHAR(30),
INDEX(last_name)
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS specialties (
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80),
INDEX(name)
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS vet_specialties (
vet_id INT(4) UNSIGNED NOT NULL,
specialty_id INT(4) UNSIGNED NOT NULL,
FOREIGN KEY (vet_id) REFERENCES vets(id),
FOREIGN KEY (specialty_id) REFERENCES specialties(id),
UNIQUE (vet_id,specialty_id)
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS types (
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(80),
INDEX(name)
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS owners (
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
first_name VARCHAR(30),
last_name VARCHAR(30),
address VARCHAR(255),
city VARCHAR(80),
telephone VARCHAR(20),
INDEX(last_name)
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS pets (
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(30),
birth_date DATE,
type_id INT(4) UNSIGNED NOT NULL,
owner_id INT(4) UNSIGNED,
INDEX(name),
FOREIGN KEY (owner_id) REFERENCES owners(id),
FOREIGN KEY (type_id) REFERENCES types(id)
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS visits (
id INT(4) UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
pet_id INT(4) UNSIGNED,
visit_date DATE,
description VARCHAR(255),
FOREIGN KEY (pet_id) REFERENCES pets(id)
) engine=InnoDB;

View File

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

View File

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

View File

@@ -0,0 +1,19 @@
===============================================================================
=== Spring PetClinic sample application - PostgreSQL Configuration ===
===============================================================================
--------------------------------------------------------------------------------
1) Run the "docker-compose.yml" from the root of the project:
$ docker-compose up
...
spring-petclinic-postgres-1 | The files belonging to this database system will be owned by user "postgres".
...
2) Run the app with `spring.profiles.active=postgres` (e.g. as a System property via the command
line, but any way that sets that property in a Spring Boot app should work). For example use
mvn spring-boot:run -Dspring-boot.run.profiles=postgres
To activate the profile on the command line.

View File

@@ -0,0 +1,52 @@
CREATE TABLE IF NOT EXISTS vets (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
first_name TEXT,
last_name TEXT
);
CREATE INDEX ON vets (last_name);
CREATE TABLE IF NOT EXISTS specialties (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name TEXT
);
CREATE INDEX ON specialties (name);
CREATE TABLE IF NOT EXISTS vet_specialties (
vet_id INT NOT NULL REFERENCES vets (id),
specialty_id INT NOT NULL REFERENCES specialties (id),
UNIQUE (vet_id, specialty_id)
);
CREATE TABLE IF NOT EXISTS types (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name TEXT
);
CREATE INDEX ON types (name);
CREATE TABLE IF NOT EXISTS owners (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
first_name TEXT,
last_name TEXT,
address TEXT,
city TEXT,
telephone TEXT
);
CREATE INDEX ON owners (last_name);
CREATE TABLE IF NOT EXISTS pets (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
name TEXT,
birth_date DATE,
type_id INT NOT NULL REFERENCES types (id),
owner_id INT REFERENCES owners (id)
);
CREATE INDEX ON pets (name);
CREATE INDEX ON pets (owner_id);
CREATE TABLE IF NOT EXISTS visits (
id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
pet_id INT REFERENCES pets (id),
visit_date DATE,
description TEXT
);
CREATE INDEX ON visits (pet_id);

View File

@@ -0,0 +1,51 @@
welcome=Welcome
required=is required
notFound=has not been found
duplicate=is already in use
nonNumeric=must be all numeric
duplicateFormSubmission=Duplicate form submission is not allowed
typeMismatch.date=invalid date
typeMismatch.birthDate=invalid date
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
error.404=The requested page was not found.
error.500=An internal server error occurred.
error.general=An unexpected error occurred.

View File

@@ -0,0 +1,51 @@
welcome=Willkommen
required=muss angegeben werden
notFound=wurde nicht gefunden
duplicate=ist bereits vergeben
nonNumeric=darf nur numerisch sein
duplicateFormSubmission=Wiederholtes Absenden des Formulars ist nicht erlaubt
typeMismatch.date=ung<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
error.404=Die angeforderte Seite wurde nicht gefunden.
error.500=Ein interner Serverfehler ist aufgetreten.
error.general=Ein unerwarteter Fehler ist aufgetreten.

View File

@@ -0,0 +1 @@
# This file is intentionally empty. Message look-ups will fall back to the default "messages.properties" file.

View File

@@ -0,0 +1,51 @@
welcome=Bienvenido
required=Es requerido
notFound=No ha sido encontrado
duplicate=Ya se encuentra en uso
nonNumeric=Sólo debe contener numeros
duplicateFormSubmission=No se permite el envío de formularios duplicados
typeMismatch.date=Fecha invalida
typeMismatch.birthDate=Fecha invalida
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
error.404=La página solicitada no fue encontrada.
error.500=Ocurrió un error interno del servidor.
error.general=Ocurrió un error inesperado.

View File

@@ -0,0 +1,51 @@
welcome=خوش آمدید
required=الزامی
notFound=یافت نشد
duplicate=قبلا استفاده شده
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=حیوانات و ویزیت‌ها
error.404=صفحه درخواستی پیدا نشد.
error.500=خطای داخلی سرور رخ داد.
error.general=خطای غیرمنتظره‌ای رخ داد.

View File

@@ -0,0 +1,51 @@
welcome=환영합니다
required=입력이 필요합니다
notFound=찾을 수 없습니다
duplicate=이미 존재합니다
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=반려동물 및 방문
error.404=요청하신 페이지를 찾을 수 없습니다.
error.500=서버 내부 오류가 발생했습니다.
error.general=알 수 없는 오류가 발생했습니다.

View File

@@ -0,0 +1,51 @@
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
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
error.404=A página solicitada não foi encontrada.
error.500=Ocorreu um erro interno no servidor.
error.general=Ocorreu um erro inesperado.

View File

@@ -0,0 +1,51 @@
welcome=Добро пожаловать
required=необходимо
notFound=не найдено
duplicate=уже используется
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=Питомцы и визиты
error.404=Запрашиваемая страница не найдена.
error.500=Произошла внутренняя ошибка сервера.
error.general=Произошла непредвиденная ошибка.

View File

@@ -0,0 +1,51 @@
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
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
error.404=İstenen sayfa bulunamadı.
error.500=Sunucuda dahili bir hata oluştu.
error.general=Beklenmeyen bir hata oluştu.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 83 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 362 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

@@ -0,0 +1,66 @@
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" viewBox="0 0 2674.9 417">
<style>
.st0, .st1 {
fill: #6db33f;
}
.st3 {
fill: #fff;
}
</style>
<g>
<path class="st0"
d="M366.9,29c-5.8,14.1-13.3,26.6-21.6,37.8c-36.6-37.3-87.8-61-144.3-61C90,5.8-0.7,96-0.7,207.4 c0,58.2,24.9,110.6,64.4,147.6l7.5,6.7c34.9,29.5,80.3,47.4,129.7,47.4c106,0,193.3-82.7,200.8-187.1 C407.7,171.3,392.3,106.4,366.9,29z M92.9,356.7c-5.8,7.5-16.6,8.3-24.1,2.5s-8.3-16.6-2.5-24.1s16.6-8.3,24.1-2.5 C97.5,338.4,98.7,349.2,92.9,356.7z M365.7,296.4c-49.5,66.1-155.9,43.7-223.7,47c0,0-12.1,0.8-24.1,2.5c0,0,4.6-2.1,10.4-4.2 c47.8-16.6,70.3-20,99.4-34.9c54.5-27.9,108.9-89,119.8-152.2c-20.8,60.7-84,113.1-141.4,134.3c-39.5,14.6-110.6,28.7-110.6,28.7 l-2.9-1.7c-48.2-23.7-49.9-128.5,38.3-162.2c38.7-15,75.3-6.7,117.3-16.6c44.5-10.4,96.1-43.7,116.8-87.3 C388.1,120.1,416.4,229,365.7,296.4z"></path>
<g>
<path class="st1"
d="M516.2,286.4c-5-2.5-8.3-8.3-8.3-15.4c0-10,7.9-18.3,18.3-18.3c3.7,0,7.1,1.2,9.6,2.5 c18.7,12.5,38.7,18.7,56.1,18.7c19.1,0,30.4-8.3,30.4-21.2v-0.8c0-15.4-20.8-20.4-43.7-27.4c-28.7-8.3-61.1-20-61.1-57.4v-0.8 c0-37,30.8-59.5,69.4-59.5c20.8,0,42.4,5.8,61.5,15.8c6.2,3.3,10.8,9.1,10.8,17c0,10.4-8.3,18.3-18.7,18.3c-3.7,0-5.8-0.8-8.7-2.1 c-15.8-8.3-32-13.3-45.7-13.3c-17.5,0-27.4,8.3-27.4,19.1v0.8c0,14.6,21.2,20.4,44.1,27.9c28.7,8.7,60.7,22,60.7,57v0.8 c0,41.2-32,61.5-72.3,61.5C565.7,309.7,538.6,301.8,516.2,286.4z"></path>
<path class="st1"
d="M680,129.7c0-12.5,9.6-22.5,22-22.5s22.5,10,22.5,22.5V143c14.6-20.4,34.9-36.6,66.5-36.6 c45.7,0,90.6,36.2,90.6,101.5v0.8c0,64.9-44.5,101.5-90.6,101.5c-32.4,0-52.8-16.2-66.5-34.5v69c0,12.5-10,22.5-22.5,22.5 c-12.1,0-22-9.6-22-22.5V129.7z M836.8,208.7v-0.8c0-37.8-25.4-62.4-55.7-62.4c-30.4,0-57,25.4-57,62.4v0.8 c0,37.4,26.6,62.4,57,62.4C811.4,271.5,836.8,247.3,836.8,208.7z"></path>
<path class="st1"
d="M899.1,129.7c0-12.5,9.6-22.5,22-22.5s22.5,10,22.5,22.5v10.8c2.1-16.6,29.5-33.3,49.1-33.3 c14.1,0,22,9.1,22,22c0,11.6-7.9,19.5-17.9,21.6c-32,5.4-53.6,33.3-53.6,71.9v64.4c0,12.1-10,22-22.5,22c-12.1,0-22-9.6-22-22 V129.7H899.1z"></path>
<path class="st1"
d="M1032.6,130.1c0-12.5,9.6-22.5,22-22.5s22.5,10,22.5,22.5v157.6c0,12.5-10,22-22.5,22c-12.1,0-22-9.6-22-22 V130.1z"></path>
<path class="st1"
d="M1100,130.1c0-12.5,9.6-22.5,22-22.5s22.5,10,22.5,22.5v9.1c12.5-18.3,30.8-32,61.1-32 c44.1,0,69.4,29.5,69.4,74.8v105.2c0,12.5-9.6,22-22,22s-22.5-9.6-22.5-22v-91.5c0-30.4-15-47.8-42-47.8 c-25.8,0-44.1,18.3-44.1,48.6v91.1c0,12.5-10,22-22.5,22c-12.1,0-22-9.6-22-22L1100,130.1L1100,130.1z"></path>
<path class="st1"
d="M1472.1,106.8c-12.5,0-22.5,10-22.5,22.5v13.3c-14.6-20.4-34.9-36.6-66.5-36.6c-45.7,0-90.6,36.2-90.6,101.5 v0.8c0,64.9,44.5,101.5,90.6,101.5c32.4,0,52.8-16.2,66.5-34.1c-2.1,35.3-23.7,53.6-61.5,53.6c-22.5,0-42-5.4-59.9-15.4 c-2.1-1.2-5-1.7-7.9-1.7c-10.4,0-19.1,8.3-19.1,18.3c0,8.7,5,15,12.5,17.9c23.7,11.6,48.2,17.5,75.7,17.5 c35.3,0,62.8-8.3,80.3-26.2c16.2-16.2,24.9-40.7,24.9-73.6V129.7C1494.6,116.8,1484.6,106.8,1472.1,106.8z M1393.5,271 c-30.8,0-55.7-24.1-55.7-62.8v-0.8c0-37.8,25.4-62.4,55.7-62.4s57,25.4,57,62.4v0.8C1450.9,245.7,1424.3,271,1393.5,271z"></path>
<path class="st1"
d="M1077.5,53.6c0,12.5-10,22.5-22.5,22.5s-22.5-10-22.5-22.5s10-22.5,22.5-22.5 C1067.1,30.7,1077.5,40.7,1077.5,53.6z"></path>
</g>
<g>
<path class="st1"
d="M1545.7,153.8c-12.5,0-22.9-10.4-22.9-22.9c0-12.9,10.4-22.9,22.9-22.9c12.9,0,22.9,10.4,22.9,22.9 S1558.6,153.8,1545.7,153.8z M1545.7,111.4c-10.8,0-19.5,8.7-19.5,19.5s8.7,19.5,19.5,19.5s19.5-8.7,19.5-19.5 C1565.2,119.7,1556.5,111.4,1545.7,111.4z M1551.9,143.8l-6.7-10.4h-4.6v10.4h-3.7v-26.2h10.8c4.6,0,8.7,3.3,8.7,7.9 c0,5.8-5.4,7.9-6.7,7.9l7.1,10.4H1551.9L1551.9,143.8z M1547.4,120.9h-6.7v9.1h7.1c2.1,0,4.6-1.7,4.6-4.6 C1552.4,122.6,1549.9,120.9,1547.4,120.9z"></path>
</g>
<g>
<path class="st1"
d="M1652.3,260.1c0,20-12.8,34.1-30,34.1c-10.4,0-18.8-5.2-23.7-13.7v12.2h-9.4v-88h9.4v35.1 c4.9-8.6,13.2-13.7,23.7-13.7C1639.4,226.1,1652.3,240.2,1652.3,260.1z M1642.3,260.1c0-15-9.1-25.6-21.8-25.6 c-12.8,0-21.9,10.6-21.9,25.6s9.1,25.7,21.9,25.7C1633.3,285.8,1642.3,275.1,1642.3,260.1z"></path>
<path class="st1"
d="M1668,313.3l2.3-8.1c2.3,1.1,4.9,1.6,7.7,1.6c4.2,0,6.9-1.4,9.4-5.8l3.8-8.1l-28.3-65.3h10.4l22.8,54.1 l21.5-54.1h10.2l-29.7,72.4c-4.5,11.1-10.8,15.2-19.8,15.4C1674.6,315.4,1671,314.6,1668,313.3z"></path>
<path class="st1"
d="M1861.3,206.2l-38.1,86.5h-10.7l-38.1-86.5h10.7l32.9,74.4l32.7-74.4L1861.3,206.2L1861.3,206.2z"></path>
<path class="st1"
d="M1877.1,206.2h9.8l32.1,60.3l32.1-60.3h9.8v86.5h-9.8v-65.3l-32.1,60.3l-32.1-60.3v65.3h-9.8V206.2z"></path>
<path class="st1"
d="M1977.2,227.6h10.1l16.2,52.5l17.8-52.5h8.2l17.8,52.5l16.2-52.5h10.1l-21.9,65.1h-8.9l-17.2-52l-17.5,52 h-8.9L1977.2,227.6z"></path>
<path class="st1"
d="M2140.1,253.1v39.7h-9.4v-10.2c-4.7,7.7-14.4,11.7-22.9,11.7c-13.7,0-23.8-7.9-23.8-20.8 c0-13,11.6-21.7,25.6-21.7c7,0,14.5,1.5,21.2,3.7v-2.4c0-8.8-3.5-18.9-17.6-18.9c-6.5,0-13.1,2.9-19,5.9l-3.8-7.7 c9.3-4.7,17.1-6.3,23.4-6.3C2131.4,226.1,2140.1,237.3,2140.1,253.1z M2130.7,272.3v-9.9c-5.9-1.6-12.6-2.6-19.6-2.6 c-9.7,0-17.9,5.5-17.9,13.3c0,8.1,7,12.8,16.2,12.8C2118,285.9,2128.2,281.5,2130.7,272.3z"></path>
<path class="st1"
d="M2194.9,226.1v8.4c-14.7,0-25.2,9.1-25.2,21.8v36.4h-9.4v-65.1h9.4v12.7 C2173.6,231.7,2182.9,226.1,2194.9,226.1z"></path>
<path class="st1"
d="M2257.8,278l5.8,6.2c-5.7,6.2-17.1,10.1-26.2,10.1c-17.4,0-33-14.2-33-34.2c0-19.4,14.6-33.9,32.1-33.9 c19.6,0,30.8,14.9,30.8,37.6H2214c1.4,12.7,10.3,22,23.2,22C2245,285.8,2253.8,282.4,2257.8,278z M2214.3,255.3h43.8 c-1.3-11.7-8.2-20.8-21.2-20.8C2225.4,234.6,2216.1,242.7,2214.3,255.3z"></path>
<path class="st1"
d="M2344.6,215.6h-29.1v-9.3h68v9.3h-29.1v77.2h-9.8L2344.6,215.6L2344.6,215.6z"></path>
<path class="st1"
d="M2451.6,253.1v39.7h-9.4v-10.2c-4.7,7.7-14.4,11.7-22.9,11.7c-13.7,0-23.8-7.9-23.8-20.8 c0-13,11.6-21.7,25.6-21.7c7,0,14.5,1.5,21.2,3.7v-2.4c0-8.8-3.5-18.9-17.6-18.9c-6.5,0-13.1,2.9-19,5.9l-3.8-7.7 c9.3-4.7,17.1-6.3,23.4-6.3C2442.9,226.1,2451.6,237.3,2451.6,253.1z M2442.2,272.3v-9.9c-5.9-1.6-12.6-2.6-19.6-2.6 c-9.7,0-17.9,5.5-17.9,13.3c0,8.1,7,12.8,16.2,12.8C2429.4,285.9,2439.6,281.5,2442.2,272.3z"></path>
<path class="st1"
d="M2526.3,251.3v41.4h-9.4v-40.2c0-10.6-6.7-18-16.2-18c-11,0-20.1,7.7-20.1,16.7v41.4h-9.4v-65.1h9.4v10.4 c3.8-6.9,12.2-12,21.4-12C2516.2,226.1,2526.3,236.6,2526.3,251.3z"></path>
<path class="st1"
d="M2542.6,285.3l38.4-48.7h-37.6v-8.9h50.4v7.4l-38.5,48.7h38.8v8.9h-51.4L2542.6,285.3L2542.6,285.3z"></path>
<path class="st1"
d="M2665.4,227.6v65.1h-9.4v-10.4c-3.8,6.9-12.2,12-21.4,12c-14.4,0-24.4-10.4-24.4-25.2v-41.4h9.4v40.2 c0,10.6,6.7,18,16.2,18c11,0,20.1-7.7,20.1-16.7v-41.4h9.5V227.6z"></path>
</g>
</g>
<path class="st3"
d="M92.9,356.7c-5.8,7.5-16.6,8.3-24.1,2.5s-8.3-16.6-2.5-24.1s16.6-8.3,24.1-2.5 C97.5,338.4,98.7,349.2,92.9,356.7z"></path>
<path class="st3"
d="M365.7,296.4c-49.5,66.1-155.9,43.7-223.7,47c0,0-12.1,0.8-24.1,2.5c0,0,4.6-2.1,10.4-4.2 c47.8-16.6,70.3-20,99.4-34.9c54.5-27.9,108.9-89,119.8-152.2c-20.8,60.7-84,113.1-141.4,134.3c-39.5,14.6-110.6,28.7-110.6,28.7 l-2.9-1.7c-48.2-23.7-49.9-128.5,38.3-162.2c38.7-15,75.3-6.7,117.3-16.6c44.5-10.4,96.1-43.7,116.8-87.3 C388.1,120.1,416.4,229,365.7,296.4z"></path>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'error')}">
<body>
<img src="../static/resources/images/pets.png" th:src="@{/resources/images/pets.png}" />
<!-- Title: Something happened -->
<h2 th:text="#{somethingHappened}">Something happened...</h2>
<!-- Status-specific error message -->
<p th:switch="${status}">
<span th:case="404" th:text="#{error.404}">The requested page was not found.</span>
<span th:case="500" th:text="#{error.500}">An internal server error occurred.</span>
<span th:case="*" th:text="#{error.general}">An unexpected error occurred.</span>
</p>
<!-- Exception message (for developers) -->
<p th:text="${message}">Exception message</p>
</body>
</html>

View File

@@ -0,0 +1,27 @@
<!DOCTYPE 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 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}__}" />
<input th:case="'date'" class="form-control" type="date" th:field="*{__${name}__}" />
</div>
<span th:if="${valid}" class="fa fa-ok form-control-feedback" aria-hidden="true"></span>
<th:block th:if="${!valid}">
<span class="fa fa-remove form-control-feedback" aria-hidden="true"></span>
<span class="help-inline" th:errors="*{__${name}__}" th:text="#{error}">Error</span>
</th:block>
</div>
</div>
</th:block>
</form>
</body>
</html>

View File

@@ -0,0 +1,88 @@
<!DOCTYPE html>
<html th:fragment="layout (template, menu)" xmlns:th="https://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" type="image/x-icon" th:href="@{/resources/images/favicon.png}">
<title th:text="#{layoutTitle}">PetClinic :: a Spring Framework demonstration</title>
<link th:href="@{/webjars/font-awesome/css/font-awesome.min.css}" rel="stylesheet">
<link rel="stylesheet" th:href="@{/resources/css/petclinic.css}" />
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark" role="navigation">
<div class="container-fluid">
<a class="navbar-brand" th:href="@{/}"><span></span></a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#main-navbar">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="main-navbar" style>
<ul class="navbar-nav me-auto mb-2 mb-lg-0" th:remove="all">
<li th:fragment="menuItem (link,active,title,glyph,text)" th:class="nav-item">
<a th:class="${active==menu ? 'nav-link active' : 'nav-link'}" th:href="@{__${link}__}" th:title="${title}">
<span th:class="'fa fa-'+${glyph}" class="fa fa-home"></span>
<span th:text="${text}">Template</span>
</a>
</li>
</ul>
<ul class="nav navbar-nav me-auto">
<li th:replace="~{::menuItem ('/','home','home page','home',#{home})}">
<span class="fa fa-home" aria-hidden="true"></span>
<span th:text="#{home}">Home</span>
</li>
<li th:replace="~{::menuItem ('/owners/find','owners','find owners','search',#{findOwners})}">
<span class="fa fa-search" aria-hidden="true"></span>
<span th:text="#{findOwners}">Find owners</span>
</li>
<li th:replace="~{::menuItem ('/vets.html','vets','veterinarians','th-list',#{vets})}">
<span class="fa fa-th-list" aria-hidden="true"></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})}">
<span class="fa exclamation-triangle" aria-hidden="true"></span>
<span th:text="#{error}">Error</span>
</li>
</ul>
</div>
</div>
</nav>
<div class="container-fluid">
<div class="container xd-container">
<th:block th:insert="${template}" />
<br />
<br />
<div class="container">
<div class="row">
<div class="col-12 text-center">
<img src="../static/images/spring-logo.svg" th:src="@{/resources/images/spring-logo.svg}"
alt="VMware Tanzu Logo" class="logo">
</div>
</div>
</div>
</div>
</div>
<script th:src="@{/webjars/bootstrap/dist/js/bootstrap.bundle.min.js}"></script>
</body>
</html>

View File

@@ -0,0 +1,27 @@
<!DOCTYPE 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 th:for="${name}" class="col-sm-2 control-label" th:text="${label}">Label</label>
<div class="col-sm-10">
<select th:field="*{__${name}__}">
<option th:each="item : ${items}" th:value="${item}" th:text="${item}">dog</option>
</select>
<span th:if="${valid}" class="fa fa-ok form-control-feedback" aria-hidden="true"></span>
<th:block th:if="${!valid}">
<span class="fa fa-remove form-control-feedback" aria-hidden="true"></span>
<span class="help-inline" th:errors="*{__${name}__}" th:text="#{error}">Error</span>
</th:block>
</div>
</div>
</th:block>
</form>
</body>
</html>

View File

@@ -0,0 +1,25 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'owners')}">
<body>
<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 (#{firstName}, 'firstName', 'text')}" />
<input th:replace="~{fragments/inputField :: input (#{lastName}, 'lastName', 'text')}" />
<input th:replace="~{fragments/inputField :: input (#{address}, 'address', 'text')}" />
<input th:replace="~{fragments/inputField :: input (#{city}, 'city', 'text')}" />
<input 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']} ? #{addOwner} : #{updateOwner}" class="btn btn-primary" type="submit"
th:text="${text}">Add Owner</button>
</div>
</div>
</form>
</body>
</html>

View File

@@ -0,0 +1,35 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'owners')}">
<body>
<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" 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>
</div>
</div>
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">
<button type="submit" class="btn btn-primary" th:text="#{findOwner}">Find Owner</button>
</div>
</div>
<a class="btn btn-primary" th:href="@{/owners/new}" th:text="#{addOwner}">Add Owner</a>
</form>
</body>
</html>

View File

@@ -0,0 +1,96 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'owners')}">
<body>
<h2 th:text="#{ownerInformation}">Owner Information</h2>
<div th:if="${message}" class="alert alert-success" id="success-message">
<span th:text="${message}"></span>
</div>
<div th:if="${error}" class="alert alert-danger" id="error-message">
<span th:text="${error}"></span>
</div>
<table class="table table-striped" th:object="${owner}">
<tr>
<th th:text="#{name}">Name</th>
<td><b th:text="*{firstName + ' ' + lastName}"></b></td>
</tr>
<tr>
<th th:text="#{address}">Address</th>
<td th:text="*{address}"></td>
</tr>
<tr>
<th th:text="#{city}">City</th>
<td th:text="*{city}"></td>
</tr>
<tr>
<th th:text="#{telephone}">Telephone</th>
<td th:text="*{telephone}"></td>
</tr>
</table>
<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" th:text="#{addNewPet}">Add
New Pet</a>
<br />
<br />
<br />
<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 th:text="#{name}">Name</dt>
<dd th:text="${pet.name}"></dd>
<dt th:text="#{birthDate}">Birth Date</dt>
<dd th:text="${#temporals.format(pet.birthDate, 'yyyy-MM-dd')}"></dd>
<dt th:text="#{type}">Type</dt>
<dd th:text="${pet.type}"></dd>
</dl>
</td>
<td valign="top">
<table class="table-condensed">
<thead>
<tr>
<th th:text="#{visitDate}">Visit Date</th>
<th th:text="#{description}">Description</th>
</tr>
</thead>
<tr th:each="visit : ${pet.visits}">
<td th:text="${#temporals.format(visit.date, 'yyyy-MM-dd')}"></td>
<td th:text="${visit?.description}"></td>
</tr>
<tr>
<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>
</tr>
</table>
<script>
// Function to hide the success and error messages after 3 seconds
function hideMessages() {
setTimeout(function () {
document.getElementById("success-message").style.display = "none";
document.getElementById("error-message").style.display = "none";
}, 3000); // 3000 milliseconds (3 seconds)
}
// Call the function to hide messages
hideMessages();
</script>
</body>
</html>

View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'owners')}">
<body>
<h2 th:text="#{owners}">Owners</h2>
<table id="owners" class="table table-striped">
<thead>
<tr>
<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>
<tr th:each="owner : ${listOwners}">
<td>
<a th:href="@{/owners/__${owner.id}__}" th:text="${owner.firstName + ' ' + owner.lastName}" /></a>
</td>
<td th:text="${owner.address}" />
<td th:text="${owner.city}" />
<td th:text="${owner.telephone}" />
<td><span th:text="${#strings.listJoin(owner.pets, ', ')}" /></td>
</tr>
</tbody>
</table>
<div th:if="${totalPages > 1}">
<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>
<span th:unless="${currentPage != i}">[[${i}]]</span>
</span>
<span>]&nbsp;</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/owners?page=1'}" th:title="#{first}" class="fa fa-fast-backward"></a>
<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}__'}" th:title="#{previous}"
class="fa fa-step-backward"></a>
<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}__'}" th:title="#{next}"
class="fa fa-step-forward"></a>
<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}__'}" th:title="#{last}"
class="fa fa-fast-forward"></a>
<span th:unless="${currentPage < totalPages}" th:title="#{last}" class="fa fa-fast-forward"></span>
</span>
</div>
</body>
</html>

View File

@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'owners')}">
<body>
<h2>
<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" th:text="#{owner}">Owner</label>
<div class="col-sm-10">
<span th:text="${owner?.firstName + ' ' + owner?.lastName}" />
</div>
</div>
<input th:replace="~{fragments/inputField :: input ('Name', 'name', 'text')}" />
<input th:replace="~{fragments/inputField :: input ('Birth Date', 'birthDate', 'date')}" />
<input th:replace="~{fragments/selectField :: select ('Type', 'type', ${types})}" />
</div>
<div class="form-group">
<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>
</div>
</div>
</form>
</body>
</html>

View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'owners')}">
<body>
<h2>
<th:block th:if="${visit['new']}" th:text="#{new}">New </th:block>
Visit
</h2>
<b th:text="#{pet}">Pet</b>
<table class="table table-striped">
<thead>
<tr>
<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>
<td th:text="${pet.name}"></td>
<td th:text="${#temporals.format(pet.birthDate, 'yyyy-MM-dd')}"></td>
<td th:text="${pet.type}"></td>
<td th:text="${owner?.firstName + ' ' + owner?.lastName}"></td>
</tr>
</table>
<form th:object="${visit}" class="form-horizontal" method="post">
<div class="form-group has-feedback">
<input th:replace="~{fragments/inputField :: input ('Date', 'date', 'date')}" />
<input th:replace="~{fragments/inputField :: input ('Description', 'description', 'text')}" />
</div>
<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" th:text="#{addVisit}">Add Visit</button>
</div>
</div>
</form>
<br />
<b th:text="#{previousVisits}">Previous Visits</b>
<table class="table table-striped">
<tr>
<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>
<td th:text=" ${visit.description}"></td>
</tr>
</table>
</body>
</html>

View File

@@ -0,0 +1,57 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'vets')}">
<body>
<h2 th:text="#{vets}">Veterinarians</h2>
<table id="vets" class="table table-striped">
<thead>
<tr>
<th th:text="#{name}">Name</th>
<th th:text="#{specialties}">Specialties</th>
</tr>
</thead>
<tbody>
<tr th:each="vet : ${listVets}">
<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}" th:text="#{none}">none</span>
</td>
</tr>
</tbody>
</table>
<div th:if="${totalPages > 1}">
<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>
<span th:unless="${currentPage != i}">[[${i}]]</span>
</span>
<span>]&nbsp;</span>
<span>
<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}" th:title="#{first}" class="fa fa-fast-backward"></span>
</span>
<span>
<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}" th:title="#{previous}" class="fa fa-step-backward"></span>
</span>
<span>
<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}" th:title="#{next}" class="fa fa-step-forward"></span>
</span>
<span>
<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}" th:title="#{last}" class="fa fa-fast-forward"></span>
</span>
</div>
</body>
</html>

View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html xmlns:th="https://www.thymeleaf.org" th:replace="~{fragments/layout :: layout (~{::body},'home')}">
<body>
<h2 th:text="#{welcome}">Welcome</h2>
<div class="row">
<div class="col-md-12">
<img class="img-responsive" src="../static/resources/images/pets.png" th:src="@{/resources/images/pets.png}" />
</div>
</div>
</body>
</html>

73
src/main/scss/header.scss Normal file
View File

@@ -0,0 +1,73 @@
.navbar {
border-top: 4px solid #6db33f;
background-color: #34302d;
margin-bottom: 0px;
border-bottom: 0;
border-left: 0;
border-right: 0;
}
.navbar a.navbar-brand {
background: url("../images/spring-logo-dataflow.png") -1px -1px no-repeat;
margin: 12px 0 6px;
width: 229px;
height: 46px;
display: inline-block;
text-decoration: none;
padding: 0;
}
.navbar a.navbar-brand span {
display: block;
width: 229px;
height: 46px;
background: url("../images/spring-logo-dataflow.png") -1px -48px no-repeat;
opacity: 0;
-moz-transition: opacity 0.12s ease-in-out;
-webkit-transition: opacity 0.12s ease-in-out;
-o-transition: opacity 0.12s ease-in-out;
}
.navbar a:hover.navbar-brand span {
opacity: 1;
}
.navbar li > a, .navbar-text {
font-family: "montserratregular", sans-serif;
text-shadow: none;
font-size: 14px;
/* line-height: 14px; */
padding: 28px 20px;
transition: all 0.15s;
-webkit-transition: all 0.15s;
-moz-transition: all 0.15s;
-o-transition: all 0.15s;
-ms-transition: all 0.15s;
}
.navbar li > a {
text-transform: uppercase;
}
.navbar .navbar-text {
margin-top: 0;
margin-bottom: 0;
}
.navbar li:hover > a {
color: #eeeeee;
background-color: #6db33f;
}
.navbar-toggle {
border-width: 0;
.icon-bar + .icon-bar {
margin-top: 3px;
}
.icon-bar {
width: 19px;
height: 3px;
}
}

View File

@@ -0,0 +1,214 @@
/*
* Copyright 2016 the original author or authors.
*
* 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.
*/
@import "bootstrap";
$icon-font-path: "../../webjars/bootstrap/fonts/";
$spring-green: #6db33f;
$spring-dark-green: #5fa134;
$spring-brown: #34302D;
$spring-grey: #838789;
$spring-light-grey: #f1f1f1;
$body-bg: $spring-light-grey;
$text-color: $spring-brown;
$link-color: $spring-dark-green;
$link-hover-color: $spring-dark-green;
$navbar-default-link-color: $spring-light-grey;
$navbar-default-link-active-color: $spring-light-grey;
$navbar-default-link-hover-color: $spring-light-grey;
$navbar-default-link-hover-bg: $spring-green;
$navbar-default-toggle-icon-bar-bg: $spring-light-grey;
$navbar-default-toggle-hover-bg: transparent;
$navbar-default-link-active-bg: $spring-green;
$border-radius-base: 0;
$border-radius-large: 0;
$border-radius-small: 0;
$nav-tabs-active-link-hover-color: $spring-light-grey;
$nav-tabs-active-link-hover-bg: $spring-brown;
$nav-tabs-active-link-hover-border-color: $spring-brown;
$nav-tabs-border-color: $spring-brown;
$pagination-active-bg: $spring-brown;
$pagination-active-border: $spring-green;
$table-border-color: $spring-brown;
.table > thead > tr > th {
background-color: lighten($spring-brown, 3%);
color: $spring-light-grey;
}
.table-filter {
background-color: $spring-brown;
padding: 9px 12px;
}
.nav > li > a {
color: $spring-grey;
}
.btn-primary {
margin-top: 12px;
border-width: 2px;
transition: border 0.15s;
color: $spring-light-grey;
background: $spring-brown;
border-color: $spring-green;
-webkit-transition: border 0.15s;
-moz-transition: border 0.15s;
-o-transition: border 0.15s;
-ms-transition: border 0.15s;
&:hover,
&:focus,
&:active,
&.active,
.open .dropdown-toggle {
background-color: $spring-brown;
border-color: $spring-brown;
}
}
.container .text-muted {
margin: 20px 0;
}
code {
font-size: 80%;
}
.xd-container {
margin-top: 40px;
margin-bottom: 100px;
padding-left: 5px;
padding-right: 5px;
}
h1 {
margin-bottom: 15px
}
.index-page--subtitle {
font-size: 16px;
line-height: 24px;
margin: 0 0 30px;
}
.form-horizontal button.btn-inverse {
margin-left: 32px;
}
#job-params-modal .modal-dialog {
width: 90%;
margin-left:auto;
margin-right:auto;
}
[ng-cloak].splash {
display: block !important;
}
[ng-cloak] {
display: none;
}
.splash {
background: $spring-green;
color: $spring-brown;
display: none;
}
.error-page {
margin-top: 100px;
text-align: center;
}
.error-page .error-title {
font-size: 24px;
line-height: 24px;
margin: 30px 0 0;
}
table td {
vertical-align: middle;
}
table td .progress {
margin-bottom: 0;
}
table td.action-column {
width: 1px;
}
.help-block {
color: lighten($text-color, 50%); // lighten the text some for contrast
}
.xd-containers {
font-size: 15px;
}
.cluster-view > table td {
vertical-align: top;
}
.cluster-view .label, .cluster-view .column-block {
display: block;
}
.cluster-view .input-group-addon {
width: 0%;
}
.cluster-view {
margin-bottom: 0;
}
.container-details-table th {
background-color: lighten($spring-brown, 3%);
color: $spring-light-grey;
}
.status-help-content-table td {
color: $spring-brown;
}
.logo {
width: 200px;
}
.myspinner {
animation-name: spinner;
animation-duration: 2s;
animation-iteration-count: infinite;
animation-timing-function: linear;
-webkit-transform-origin: 49% 50%;
-webkit-animation-name: spinner;
-webkit-animation-duration: 2s;
-webkit-animation-iteration-count: infinite;
-webkit-animation-timing-function: linear;
}
hr {
border-top: 1px dotted $spring-brown;
}
@import "typography.scss";
@import "header.scss";
@import "responsive.scss";

View File

@@ -0,0 +1,41 @@
@media (max-width: 768px) {
.navbar-toggle {
position:absolute;
z-index: 9999;
left:0px;
top:0px;
}
.navbar a.navbar-brand {
display: block;
margin: 0 auto 0 auto;
width: 148px;
height: 50px;
float: none;
background: url("../images/spring-logo-dataflow-mobile.png") 0 center no-repeat;
}
.homepage-billboard .homepage-subtitle {
font-size: 21px;
line-height: 21px;
}
.navbar a.navbar-brand span {
display: none;
}
.navbar {
border-top-width: 0;
}
.xd-container {
margin-top: 20px;
margin-bottom: 30px;
}
.index-page--subtitle {
margin-top: 10px;
margin-bottom: 30px;
}
}

View File

@@ -0,0 +1,60 @@
@font-face {
font-family: 'varela_roundregular';
src: url('../fonts/varela_round-webfont.eot');
src: url('../fonts/varela_round-webfont.eot?#iefix') format('embedded-opentype'),
url('../fonts/varela_round-webfont.woff') format('woff'),
url('../fonts/varela_round-webfont.ttf') format('truetype'),
url('../fonts/varela_round-webfont.svg#varela_roundregular') format('svg');
font-weight: normal;
font-style: normal;
}
@font-face {
font-family: 'montserratregular';
src: url('../fonts/montserrat-webfont.eot');
src: url('../fonts/montserrat-webfont.eot?#iefix') format('embedded-opentype'),
url('../fonts/montserrat-webfont.woff') format('woff'),
url('../fonts/montserrat-webfont.ttf') format('truetype'),
url('../fonts/montserrat-webfont.svg#montserratregular') format('svg');
font-weight: normal;
font-style: normal;
}
body, h1, h2, h3, p, input {
margin: 0;
font-weight: 400;
font-family: "varela_roundregular", sans-serif;
color: #34302d;
}
h1 {
font-size: 24px;
line-height: 30px;
font-family: "montserratregular", sans-serif;
}
h2 {
font-size: 18px;
font-weight: 700;
line-height: 24px;
margin-bottom: 10px;
font-family: "montserratregular", sans-serif;
}
h3 {
font-size: 16px;
line-height: 24px;
margin-bottom: 10px;
font-weight: 700;
}
p {
//font-size: 15px;
//line-height: 24px;
}
strong {
font-weight: 700;
font-family: "montserratregular", sans-serif;
}

View File

@@ -0,0 +1,74 @@
/*
* Copyright 2012-2025 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;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.samples.petclinic.vet.VetRepository;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.aot.DisabledInAotMode;
import org.springframework.web.client.RestTemplate;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
import org.testcontainers.mysql.MySQLContainer;
import org.testcontainers.utility.DockerImageName;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@ActiveProfiles("mysql")
@Testcontainers(disabledWithoutDocker = true)
@DisabledInNativeImage
@DisabledInAotMode
class MySqlIntegrationTests {
@ServiceConnection
@Container
static MySQLContainer container = new MySQLContainer(DockerImageName.parse("mysql:9.6"));
@LocalServerPort
int port;
@Autowired
private VetRepository vets;
@Autowired
private RestTemplateBuilder builder;
@Test
void findAll() {
vets.findAll();
vets.findAll(); // served from cache
}
@Test
void ownerDetails() {
RestTemplate template = builder.rootUri("http://localhost:" + port).build();
ResponseEntity<String> result = template.exchange(RequestEntity.get("/owners/1").build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
}
}

View File

@@ -0,0 +1,47 @@
/*
* Copyright 2012-2025 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;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.testcontainers.mysql.MySQLContainer;
import org.testcontainers.utility.DockerImageName;
/**
* PetClinic Spring Boot Application.
*
* @author Dave Syer
*/
@Configuration
public class MysqlTestApplication {
@ServiceConnection
@Profile("mysql")
@Bean
static MySQLContainer container() {
return new MySQLContainer(DockerImageName.parse("mysql:9.6"));
}
public static void main(String[] args) {
SpringApplication.run(PetClinicApplication.class, "--spring.profiles.active=mysql",
"--spring.docker.compose.enabled=false");
}
}

View File

@@ -0,0 +1,70 @@
/*
* Copyright 2012-2025 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;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.samples.petclinic.vet.VetRepository;
import org.springframework.web.client.RestTemplate;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = "logging.level.sql=DEBUG")
public class PetClinicIntegrationTests {
@LocalServerPort
int port;
@Autowired
private VetRepository vets;
@Autowired
private RestTemplateBuilder builder;
@Test
void findAll() {
vets.findAll();
vets.findAll(); // served from cache
}
@Test
void ownerDetails() {
RestTemplate template = builder.rootUri("http://localhost:" + port).build();
ResponseEntity<String> result = template.exchange(RequestEntity.get("/owners/1").build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
}
@Test
void ownerList() {
RestTemplate template = builder.rootUri("http://localhost:" + port).build();
ResponseEntity<String> result = template.exchange(RequestEntity.get("/owners?lastName=").build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
}
public static void main(String[] args) {
SpringApplication.run(PetClinicApplication.class, "--spring.docker.compose.lifecycle-management=NONE");
}
}

View File

@@ -0,0 +1,150 @@
/*
* Copyright 2012-2025 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;
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;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.event.ApplicationPreparedEvent;
import org.springframework.boot.restclient.RestTemplateBuilder;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.test.web.server.LocalServerPort;
import org.springframework.context.ApplicationListener;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.EnumerablePropertySource;
import org.springframework.core.env.PropertySource;
import org.springframework.http.HttpStatus;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
import org.springframework.samples.petclinic.vet.VetRepository;
import org.springframework.test.context.ActiveProfiles;
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" })
@ActiveProfiles("postgres")
@DisabledInNativeImage
public class PostgresIntegrationTests {
@LocalServerPort
int port;
@Autowired
private VetRepository vets;
@Autowired
private RestTemplateBuilder builder;
@BeforeAll
static void available() {
assumeTrue(DockerClientFactory.instance().isDockerAvailable(), "Docker not available");
}
public static void main(String[] args) {
new SpringApplicationBuilder(PetClinicApplication.class) //
.profiles("postgres") //
.properties( //
"spring.docker.compose.start.arguments=postgres" //
) //
.listeners(new PropertiesLogger()) //
.run(args);
}
@Test
void findAll() throws Exception {
vets.findAll();
vets.findAll(); // served from cache
}
@Test
void ownerDetails() {
RestTemplate template = builder.rootUri("http://localhost:" + port).build();
ResponseEntity<String> result = template.exchange(RequestEntity.get("/owners/1").build(), String.class);
assertThat(result.getStatusCode()).isEqualTo(HttpStatus.OK);
}
static class PropertiesLogger implements ApplicationListener<ApplicationPreparedEvent> {
private static final Log log = LogFactory.getLog(PropertiesLogger.class);
private ConfigurableEnvironment environment;
private boolean isFirstRun = true;
@Override
public void onApplicationEvent(ApplicationPreparedEvent event) {
if (isFirstRun) {
environment = event.getApplicationContext().getEnvironment();
printProperties();
}
isFirstRun = false;
}
public void printProperties() {
for (EnumerablePropertySource<?> source : findPropertiesPropertySources()) {
log.info("PropertySource: " + source.getName());
String[] names = source.getPropertyNames();
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();
if (resolved.equals(value)) {
log.info(name + "=" + resolved);
}
else {
log.info(name + "=" + value + " OVERRIDDEN to " + resolved);
}
}
}
}
private List<EnumerablePropertySource<?>> findPropertiesPropertySources() {
List<EnumerablePropertySource<?>> sources = new LinkedList<>();
for (PropertySource<?> source : environment.getPropertySources()) {
if (source instanceof EnumerablePropertySource enumerable) {
sources.add(enumerable);
}
}
return sources;
}
}
}

View File

@@ -0,0 +1,60 @@
/*
* Copyright 2012-2025 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.model;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.Locale;
import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
/**
* @author Michael Isvy Simple test to make sure that Bean Validation is working (useful
* when upgrading to a new version of Hibernate Validator/ Bean Validation)
*/
class ValidatorTests {
private Validator createValidator() {
LocalValidatorFactoryBean localValidatorFactoryBean = new LocalValidatorFactoryBean();
localValidatorFactoryBean.afterPropertiesSet();
return localValidatorFactoryBean;
}
@Test
void shouldNotValidateWhenFirstNameEmpty() {
LocaleContextHolder.setLocale(Locale.ENGLISH);
Person person = new Person();
person.setFirstName("");
person.setLastName("smith");
Validator validator = createValidator();
Set<ConstraintViolation<Person>> constraintViolations = validator.validate(person);
assertThat(constraintViolations).hasSize(1);
ConstraintViolation<Person> violation = constraintViolations.iterator().next();
assertThat(violation.getPropertyPath()).hasToString("firstName");
assertThat(violation.getMessage()).isEqualTo("must not be blank");
}
}

View File

@@ -0,0 +1,251 @@
/*
* Copyright 2012-2025 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.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
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.List;
import java.util.Optional;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasItem;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.not;
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.*;
/**
* Test class for {@link OwnerController}
*
* @author Colin But
* @author Wick Dynex
*/
@WebMvcTest(OwnerController.class)
@DisabledInNativeImage
@DisabledInAotMode
class OwnerControllerTests {
private static final int TEST_OWNER_ID = 1;
@Autowired
private MockMvc mockMvc;
@MockitoBean
private OwnerRepository owners;
private Owner george() {
Owner george = new Owner();
george.setId(TEST_OWNER_ID);
george.setFirstName("George");
george.setLastName("Franklin");
george.setAddress("110 W. Liberty St.");
george.setCity("Madison");
george.setTelephone("6085551023");
Pet max = new Pet();
PetType dog = new PetType();
dog.setName("dog");
max.setType(dog);
max.setName("Max");
max.setBirthDate(LocalDate.now());
george.addPet(max);
max.setId(1);
return george;
}
@BeforeEach
void setup() {
Owner george = george();
given(this.owners.findByLastNameStartingWith(eq("Franklin"), any(Pageable.class)))
.willReturn(new PageImpl<>(List.of(george)));
given(this.owners.findById(TEST_OWNER_ID)).willReturn(Optional.of(george));
Visit visit = new Visit();
visit.setDate(LocalDate.now());
george.getPet("Max").getVisits().add(visit);
}
@Test
void initCreationForm() throws Exception {
mockMvc.perform(get("/owners/new"))
.andExpect(status().isOk())
.andExpect(model().attributeExists("owner"))
.andExpect(view().name("owners/createOrUpdateOwnerForm"));
}
@Test
void processCreationFormSuccess() throws Exception {
mockMvc
.perform(post("/owners/new").param("firstName", "Joe")
.param("lastName", "Bloggs")
.param("address", "123 Caramel Street")
.param("city", "London")
.param("telephone", "1316761638"))
.andExpect(status().is3xxRedirection());
}
@Test
void processCreationFormHasErrors() throws Exception {
mockMvc
.perform(post("/owners/new").param("firstName", "Joe").param("lastName", "Bloggs").param("city", "London"))
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("owner"))
.andExpect(model().attributeHasFieldErrors("owner", "address"))
.andExpect(model().attributeHasFieldErrors("owner", "telephone"))
.andExpect(view().name("owners/createOrUpdateOwnerForm"));
}
@Test
void initFindForm() throws Exception {
mockMvc.perform(get("/owners/find"))
.andExpect(status().isOk())
.andExpect(model().attributeExists("owner"))
.andExpect(view().name("owners/findOwners"));
}
@Test
void processFindFormSuccess() throws Exception {
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 processFindFormByLastName() throws Exception {
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())
.andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID));
}
@Test
void processFindFormNoOwnersFound() throws Exception {
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())
.andExpect(model().attributeHasFieldErrors("owner", "lastName"))
.andExpect(model().attributeHasFieldErrorCode("owner", "lastName", "notFound"))
.andExpect(view().name("owners/findOwners"));
}
@Test
void initUpdateOwnerForm() throws Exception {
mockMvc.perform(get("/owners/{ownerId}/edit", TEST_OWNER_ID))
.andExpect(status().isOk())
.andExpect(model().attributeExists("owner"))
.andExpect(model().attribute("owner", hasProperty("lastName", is("Franklin"))))
.andExpect(model().attribute("owner", hasProperty("firstName", is("George"))))
.andExpect(model().attribute("owner", hasProperty("address", is("110 W. Liberty St."))))
.andExpect(model().attribute("owner", hasProperty("city", is("Madison"))))
.andExpect(model().attribute("owner", hasProperty("telephone", is("6085551023"))))
.andExpect(view().name("owners/createOrUpdateOwnerForm"));
}
@Test
void processUpdateOwnerFormSuccess() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/edit", TEST_OWNER_ID).param("firstName", "Joe")
.param("lastName", "Bloggs")
.param("address", "123 Caramel Street")
.param("city", "London")
.param("telephone", "1616291589"))
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Test
void processUpdateOwnerFormUnchangedSuccess() throws Exception {
mockMvc.perform(post("/owners/{ownerId}/edit", TEST_OWNER_ID))
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Test
void processUpdateOwnerFormHasErrors() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/edit", TEST_OWNER_ID).param("firstName", "Joe")
.param("lastName", "Bloggs")
.param("address", "")
.param("telephone", ""))
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("owner"))
.andExpect(model().attributeHasFieldErrors("owner", "address"))
.andExpect(model().attributeHasFieldErrors("owner", "telephone"))
.andExpect(view().name("owners/createOrUpdateOwnerForm"));
}
@Test
void showOwner() throws Exception {
mockMvc.perform(get("/owners/{ownerId}", TEST_OWNER_ID))
.andExpect(status().isOk())
.andExpect(model().attribute("owner", hasProperty("lastName", is("Franklin"))))
.andExpect(model().attribute("owner", hasProperty("firstName", is("George"))))
.andExpect(model().attribute("owner", hasProperty("address", is("110 W. Liberty St."))))
.andExpect(model().attribute("owner", hasProperty("city", is("Madison"))))
.andExpect(model().attribute("owner", hasProperty("telephone", is("6085551023"))))
.andExpect(model().attribute("owner", hasProperty("pets", not(empty()))))
.andExpect(model().attribute("owner",
hasProperty("pets", hasItem(hasProperty("visits", hasSize(greaterThan(0)))))))
.andExpect(view().name("owners/ownerDetails"));
}
@Test
void processUpdateOwnerFormWithIdMismatch() 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

@@ -0,0 +1,211 @@
/*
* Copyright 2012-2025 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.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
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.List;
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;
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 the {@link PetController}
*
* @author Colin But
* @author Wick Dynex
*/
@WebMvcTest(value = PetController.class,
includeFilters = @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE))
@DisabledInNativeImage
@DisabledInAotMode
class PetControllerTests {
private static final int TEST_OWNER_ID = 1;
private static final int TEST_PET_ID = 1;
@Autowired
private MockMvc mockMvc;
@MockitoBean
private OwnerRepository owners;
@MockitoBean
private PetTypeRepository types;
@BeforeEach
void setup() {
PetType cat = new PetType();
cat.setId(3);
cat.setName("hamster");
given(this.types.findPetTypes()).willReturn(List.of(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));
}
@Test
void initCreationForm() throws Exception {
mockMvc.perform(get("/owners/{ownerId}/pets/new", TEST_OWNER_ID))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdatePetForm"))
.andExpect(model().attributeExists("pet"));
}
@Test
void processCreationFormSuccess() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty")
.param("type", "hamster")
.param("birthDate", "2015-02-12"))
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Nested
class ProcessCreationFormHasErrors {
@Test
void processCreationFormWithBlankName() 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 processCreationFormWithDuplicateName() 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 processCreationFormWithMissingPetType() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/new", TEST_OWNER_ID).param("name", "Betty")
.param("birthDate", "2015-02-12"))
.andExpect(model().attributeHasNoErrors("owner"))
.andExpect(model().attributeHasErrors("pet"))
.andExpect(model().attributeHasFieldErrors("pet", "type"))
.andExpect(model().attributeHasFieldErrorCode("pet", "type", "required"))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
@Test
void processCreationFormWithInvalidBirthDate() 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 initUpdateForm() throws Exception {
mockMvc.perform(get("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID))
.andExpect(status().isOk())
.andExpect(model().attributeExists("pet"))
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
}
@Test
void processUpdateFormSuccess() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/{petId}/edit", TEST_OWNER_ID, TEST_PET_ID).param("name", "Betty")
.param("type", "hamster")
.param("birthDate", "2015-02-12"))
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Nested
class ProcessUpdateFormHasErrors {
@Test
void processUpdateFormWithInvalidBirthDate() 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", "birthDate"))
.andExpect(model().attributeHasFieldErrorCode("pet", "birthDate", "typeMismatch"))
.andExpect(view().name("pets/createOrUpdatePetForm"));
}
@Test
void processUpdateFormWithBlankName() 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

@@ -0,0 +1,97 @@
/*
* Copyright 2012-2025 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 static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Locale;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
/**
* Test class for {@link PetTypeFormatter}
*
* @author Colin But
*/
@ExtendWith(MockitoExtension.class)
@DisabledInNativeImage
class PetTypeFormatterTests {
@Mock
private PetTypeRepository types;
private PetTypeFormatter petTypeFormatter;
@BeforeEach
void setup() {
this.petTypeFormatter = new PetTypeFormatter(types);
}
@Test
void testPrint() {
PetType petType = new PetType();
petType.setName("Hamster");
String petTypeName = this.petTypeFormatter.print(petType, Locale.ENGLISH);
assertThat(petTypeName).isEqualTo("Hamster");
}
@Test
void shouldParse() throws ParseException {
given(types.findPetTypes()).willReturn(makePetTypes());
PetType petType = petTypeFormatter.parse("Bird", Locale.ENGLISH);
assertThat(petType.getName()).isEqualTo("Bird");
}
@Test
void shouldThrowParseException() {
given(types.findPetTypes()).willReturn(makePetTypes());
Assertions.assertThrows(ParseException.class, () -> {
petTypeFormatter.parse("Fish", Locale.ENGLISH);
});
}
/**
* Helper method to produce some sample pet types just for test purpose
* @return {@link Collection} of {@link PetType}
*/
private List<PetType> makePetTypes() {
List<PetType> petTypes = new ArrayList<>();
petTypes.add(new PetType() {
{
setName("Dog");
}
});
petTypes.add(new PetType() {
{
setName("Bird");
}
});
return petTypes;
}
}

View File

@@ -0,0 +1,117 @@
/*
* 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
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 validate() {
petType.setName(petTypeName);
pet.setName(petName);
pet.setType(petType);
pet.setBirthDate(petBirthDate);
petValidator.validate(pet, errors);
assertFalse(errors.hasErrors());
}
@Nested
class ValidateHasErrors {
@Test
void validateWithInvalidPetName() {
petType.setName(petTypeName);
pet.setName("");
pet.setType(petType);
pet.setBirthDate(petBirthDate);
petValidator.validate(pet, errors);
assertTrue(errors.hasFieldErrors("name"));
}
@Test
void validateWithInvalidPetType() {
pet.setName(petName);
pet.setType(null);
pet.setBirthDate(petBirthDate);
petValidator.validate(pet, errors);
assertTrue(errors.hasFieldErrors("type"));
}
@Test
void validateWithInvalidBirthDate() {
petType.setName(petTypeName);
pet.setName(petName);
pet.setType(petType);
pet.setBirthDate(null);
petValidator.validate(pet, errors);
assertTrue(errors.hasFieldErrors("birthDate"));
}
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2012-2025 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 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;
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;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
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
@DisabledInAotMode
class VisitControllerTests {
private static final int TEST_OWNER_ID = 1;
private static final int TEST_PET_ID = 1;
@Autowired
private MockMvc mockMvc;
@MockitoBean
private OwnerRepository owners;
@BeforeEach
void init() {
Owner owner = new Owner();
Pet pet = new Pet();
owner.addPet(pet);
pet.setId(TEST_PET_ID);
given(this.owners.findById(TEST_OWNER_ID)).willReturn(Optional.of(owner));
}
@Test
void initNewVisitForm() throws Exception {
mockMvc.perform(get("/owners/{ownerId}/pets/{petId}/visits/new", TEST_OWNER_ID, TEST_PET_ID))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdateVisitForm"));
}
@Test
void processNewVisitFormSuccess() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/{petId}/visits/new", TEST_OWNER_ID, TEST_PET_ID)
.param("name", "George")
.param("description", "Visit Description"))
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@Test
void processNewVisitFormHasErrors() throws Exception {
mockMvc
.perform(post("/owners/{ownerId}/pets/{petId}/visits/new", TEST_OWNER_ID, TEST_PET_ID).param("name",
"George"))
.andExpect(model().attributeHasErrors("visit"))
.andExpect(status().isOk())
.andExpect(view().name("pets/createOrUpdateVisitForm"));
}
}

View File

@@ -0,0 +1,251 @@
/*
* Copyright 2012-2025 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.service;
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.data.jpa.test.autoconfigure.DataJpaTest;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase;
import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase.Replace;
import org.springframework.data.domain.Page;
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.owner.Pet;
import org.springframework.samples.petclinic.owner.PetType;
import org.springframework.samples.petclinic.owner.PetTypeRepository;
import org.springframework.samples.petclinic.owner.Visit;
import org.springframework.samples.petclinic.vet.Vet;
import org.springframework.samples.petclinic.vet.VetRepository;
import org.springframework.transaction.annotation.Transactional;
/**
* Integration test of the Service and the Repository layer.
* <p>
* ClinicServiceSpringDataJpaTests subclasses benefit from the following services provided
* by the Spring TestContext Framework:
* </p>
* <ul>
* <li><strong>Spring IoC container caching</strong> which spares us unnecessary set up
* time between test execution.</li>
* <li><strong>Dependency Injection</strong> of test fixture instances, meaning that we
* don't need to perform application context lookups. See the use of
* {@link Autowired @Autowired} on the <code> </code> instance variable, which uses
* autowiring <em>by type</em>.
* <li><strong>Transaction management</strong>, meaning each test method is executed in
* its own transaction, which is automatically rolled back by default. Thus, even if tests
* insert or otherwise change database state, there is no need for a teardown or cleanup
* script.
* <li>An {@link org.springframework.context.ApplicationContext ApplicationContext} is
* also inherited and can be used for explicit bean lookup if necessary.</li>
* </ul>
*
* @author Ken Krebs
* @author Rod Johnson
* @author Juergen Hoeller
* @author Sam Brannen
* @author Michael Isvy
* @author Dave Syer
*/
@DataJpaTest
// Ensure that if the mysql profile is active we connect to the real database:
@AutoConfigureTestDatabase(replace = Replace.NONE)
// @TestPropertySource("/application-postgres.properties")
class ClinicServiceTests {
@Autowired
protected OwnerRepository owners;
@Autowired
protected PetTypeRepository types;
@Autowired
protected VetRepository vets;
private final Pageable pageable = Pageable.unpaged();
@Test
void shouldFindOwnersByLastName() {
Page<Owner> owners = this.owners.findByLastNameStartingWith("Davis", pageable);
assertThat(owners).hasSize(2);
owners = this.owners.findByLastNameStartingWith("Daviss", pageable);
assertThat(owners).isEmpty();
}
@Test
void shouldFindSingleOwnerWithPet() {
Optional<Owner> optionalOwner = this.owners.findById(1);
assertThat(optionalOwner).isPresent();
Owner owner = optionalOwner.get();
assertThat(owner.getLastName()).startsWith("Franklin");
assertThat(owner.getPets()).hasSize(1);
assertThat(owner.getPets().get(0).getType()).isNotNull();
assertThat(owner.getPets().get(0).getType().getName()).isEqualTo("cat");
}
@Test
@Transactional
void shouldInsertOwner() {
Page<Owner> owners = this.owners.findByLastNameStartingWith("Schultz", pageable);
int found = (int) owners.getTotalElements();
Owner owner = new Owner();
owner.setFirstName("Sam");
owner.setLastName("Schultz");
owner.setAddress("4, Evans Street");
owner.setCity("Wollongong");
owner.setTelephone("4444444444");
this.owners.save(owner);
assertThat(owner.getId()).isNotZero();
owners = this.owners.findByLastNameStartingWith("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();
String oldLastName = owner.getLastName();
String newLastName = oldLastName + "X";
owner.setLastName(newLastName);
this.owners.save(owner);
// retrieving new name from database
optionalOwner = this.owners.findById(1);
assertThat(optionalOwner).isPresent();
owner = optionalOwner.get();
assertThat(owner.getLastName()).isEqualTo(newLastName);
}
@Test
void shouldFindAllPetTypes() {
Collection<PetType> petTypes = this.types.findPetTypes();
PetType petType1 = EntityUtils.getById(petTypes, PetType.class, 1);
assertThat(petType1.getName()).isEqualTo("cat");
PetType petType4 = EntityUtils.getById(petTypes, PetType.class, 4);
assertThat(petType4.getName()).isEqualTo("snake");
}
@Test
@Transactional
void shouldInsertPetIntoDatabaseAndGenerateId() {
Optional<Owner> optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
Owner owner6 = optionalOwner.get();
int found = owner6.getPets().size();
Pet pet = new Pet();
pet.setName("bowser");
Collection<PetType> types = this.types.findPetTypes();
pet.setType(EntityUtils.getById(types, PetType.class, 2));
pet.setBirthDate(LocalDate.now());
owner6.addPet(pet);
assertThat(owner6.getPets()).hasSize(found + 1);
this.owners.save(owner6);
optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
owner6 = optionalOwner.get();
assertThat(owner6.getPets()).hasSize(found + 1);
// checks that id has been generated
pet = owner6.getPet("bowser");
assertThat(pet.getId()).isNotNull();
}
@Test
@Transactional
void shouldUpdatePetName() {
Optional<Owner> optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
Owner owner6 = optionalOwner.get();
Pet pet7 = owner6.getPet(7);
String oldName = pet7.getName();
String newName = oldName + "X";
pet7.setName(newName);
this.owners.save(owner6);
optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
owner6 = optionalOwner.get();
pet7 = owner6.getPet(7);
assertThat(pet7.getName()).isEqualTo(newName);
}
@Test
void shouldFindVets() {
Collection<Vet> vets = this.vets.findAll();
Vet vet = EntityUtils.getById(vets, Vet.class, 3);
assertThat(vet.getLastName()).isEqualTo("Douglas");
assertThat(vet.getNrOfSpecialties()).isEqualTo(2);
assertThat(vet.getSpecialties().get(0).getName()).isEqualTo("dentistry");
assertThat(vet.getSpecialties().get(1).getName()).isEqualTo("surgery");
}
@Test
@Transactional
void shouldAddNewVisitForPet() {
Optional<Owner> optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
Owner owner6 = optionalOwner.get();
Pet pet7 = owner6.getPet(7);
int found = pet7.getVisits().size();
Visit visit = new Visit();
visit.setDescription("test");
owner6.addVisit(pet7.getId(), visit);
this.owners.save(owner6);
assertThat(pet7.getVisits()) //
.hasSize(found + 1) //
.allMatch(value -> value.getId() != null);
}
@Test
void shouldFindVisitsByPetId() {
Optional<Owner> optionalOwner = this.owners.findById(6);
assertThat(optionalOwner).isPresent();
Owner owner6 = optionalOwner.get();
Pet pet7 = owner6.getPet(7);
Collection<Visit> visits = pet7.getVisits();
assertThat(visits) //
.hasSize(2) //
.element(0)
.extracting(Visit::getDate)
.isNotNull();
}
}

View File

@@ -0,0 +1,53 @@
/*
* Copyright 2012-2025 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.service;
import org.springframework.orm.ObjectRetrievalFailureException;
import org.springframework.samples.petclinic.model.BaseEntity;
import java.util.Collection;
/**
* Utility methods for handling entities. Separate from the BaseEntity class mainly
* because of dependency on the ORM-associated ObjectRetrievalFailureException.
*
* @author Juergen Hoeller
* @author Sam Brannen
* @see org.springframework.samples.petclinic.model.BaseEntity
* @since 29.10.2003
*/
public abstract class EntityUtils {
/**
* Look up the entity of the given class with the given id in the given collection.
* @param entities the collection to search
* @param entityClass the entity class to look up
* @param entityId the entity id to look up
* @return the found entity
* @throws ObjectRetrievalFailureException if the entity was not found
*/
public static <T extends BaseEntity> T getById(Collection<T> entities, Class<T> entityClass, int entityId)
throws ObjectRetrievalFailureException {
for (T entity : entities) {
if (entity.getId() != null && entity.getId() == entityId && entityClass.isInstance(entity)) {
return entity;
}
}
throw new ObjectRetrievalFailureException(entityClass, entityId);
}
}

View File

@@ -0,0 +1,101 @@
/*
* Copyright 2012-2025 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.system;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration;
import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration;
import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.boot.resttestclient.TestRestTemplate;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
/**
* Integration Test for {@link CrashController}.
*
* @author Alex Lutz
*/
// NOT Waiting https://github.com/spring-projects/spring-boot/issues/5574
@SpringBootTest(webEnvironment = RANDOM_PORT,
properties = { "spring.web.error.include-message=ALWAYS", "management.endpoints.access.default=none" })
@AutoConfigureTestRestTemplate
class CrashControllerIntegrationTests {
@Value("${local.server.port}")
private int port;
@Autowired
private TestRestTemplate rest;
@Test
void triggerExceptionJson() {
ResponseEntity<Map<String, Object>> resp = rest.exchange(
RequestEntity.get("http://localhost:" + port + "/oups").build(),
new ParameterizedTypeReference<Map<String, Object>>() {
});
assertThat(resp).isNotNull();
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(resp.getBody()).containsKey("timestamp");
assertThat(resp.getBody()).containsKey("status");
assertThat(resp.getBody()).containsKey("error");
assertThat(resp.getBody()).containsEntry("message",
"Expected: controller used to showcase what happens when an exception is thrown");
assertThat(resp.getBody()).containsEntry("path", "/oups");
}
@Test
void triggerExceptionHtml() {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(List.of(MediaType.TEXT_HTML));
ResponseEntity<String> resp = rest.exchange("http://localhost:" + port + "/oups", HttpMethod.GET,
new HttpEntity<>(headers), String.class);
assertThat(resp).isNotNull();
assertThat(resp.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
assertThat(resp.getBody()).isNotNull();
// html:
assertThat(resp.getBody()).containsSubsequence("<body>", "<h2>", "Something happened...", "</h2>", "<p>",
"Expected:", "controller", "used", "to", "showcase", "what", "happens", "when", "an", "exception", "is",
"thrown", "</p>", "</body>");
// Not the whitelabel error page:
assertThat(resp.getBody()).doesNotContain("Whitelabel Error Page",
"This application has no explicit mapping for");
}
@SpringBootApplication(exclude = { DataSourceAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class, HibernateJpaAutoConfiguration.class })
static class TestConfiguration {
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2012-2025 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.system;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
/**
* Test class for {@link CrashController}
*
* @author Colin But
* @author Alex Lutz
*/
// Waiting https://github.com/spring-projects/spring-boot/issues/5574 ..good
// luck ((plain(st) UNIT test)! :)
class CrashControllerTests {
final CrashController testee = new CrashController();
@Test
void triggerException() {
assertThatExceptionOfType(RuntimeException.class).isThrownBy(() -> testee.triggerException())
.withMessageContaining("Expected: controller used to showcase what happens when an exception is thrown");
}
}

View File

@@ -0,0 +1,138 @@
package org.springframework.samples.petclinic.system;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
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
void checkNonInternationalizedStrings() throws Exception {
Path root = Path.of("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
void checkI18nPropertyFilesAreInSync() throws Exception {
List<Path> propertyFiles;
try (Stream<Path> stream = Files.walk(Path.of(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) || "messages_en.properties".equals(fileName)) {
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);
}
}
}

View File

@@ -0,0 +1,100 @@
/*
* Copyright 2012-2025 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.vet;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
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;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
/**
* Test class for the {@link VetController}
*/
@WebMvcTest(VetController.class)
@DisabledInNativeImage
@DisabledInAotMode
class VetControllerTests {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private VetRepository vets;
private Vet james() {
Vet james = new Vet();
james.setFirstName("James");
james.setLastName("Carter");
james.setId(1);
return james;
}
private Vet helen() {
Vet helen = new Vet();
helen.setFirstName("Helen");
helen.setLastName("Leary");
helen.setId(2);
Specialty radiology = new Specialty();
radiology.setId(1);
radiology.setName("radiology");
helen.addSpecialty(radiology);
return helen;
}
@BeforeEach
void setup() {
given(this.vets.findAll()).willReturn(Lists.newArrayList(james(), helen()));
given(this.vets.findAll(any(Pageable.class)))
.willReturn(new PageImpl<Vet>(Lists.newArrayList(james(), helen())));
}
@Test
void showVetListHtml() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/vets.html?page=1"))
.andExpect(status().isOk())
.andExpect(model().attributeExists("listVets"))
.andExpect(view().name("vets/vetList"));
}
@Test
void showResourcesVetList() throws Exception {
ResultActions actions = mockMvc.perform(get("/vets").accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
actions.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.vetList[0].id").value(1));
}
}

Some files were not shown because too many files have changed in this diff Show More