This commit is contained in:
Shrirang Joshi 2025-05-20 21:49:26 +00:00 committed by GitHub
commit 913ab6dd61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 774 additions and 204 deletions

2
.github/dco.yml vendored
View file

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

View file

@ -1,31 +0,0 @@
name: Deploy and Test Cluster
on:
push:
branches: [main]
paths:
- 'k8s/**'
pull_request:
branches: [main]
paths:
- 'k8s/**'
jobs:
deploy-and-test-cluster:
runs-on: ubuntu-latest
steps:
- name: Check out the repository
uses: actions/checkout@v2
- name: Create k8s Kind Cluster
uses: helm/kind-action@v1
- name: Deploy application
run: |
kubectl apply -f k8s/
- name: Wait for Pods to be ready
run: |
kubectl wait --for=condition=ready pod -l app=demo-db --timeout=180s
kubectl wait --for=condition=ready pod -l app=petclinic --timeout=180s

View file

@ -1,31 +0,0 @@
# This workflow will build a Java project with Gradle, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-java-with-gradle
name: Java CI with Gradle
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '17' ]
steps:
- uses: actions/checkout@v4
- name: Set up JDK ${{matrix.java}}
uses: actions/setup-java@v4
with:
java-version: ${{matrix.java}}
distribution: 'adopt'
cache: maven
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
- name: Build with Gradle
run: ./gradlew build

View file

@ -1,29 +0,0 @@
# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time
# For more information see: https://docs.github.com/en/actions/use-cases-and-examples/building-and-testing/building-and-testing-java-with-maven
name: Java CI with Maven
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '17' ]
steps:
- uses: actions/checkout@v4
- name: Set up JDK ${{matrix.java}}
uses: actions/setup-java@v4
with:
java-version: ${{matrix.java}}
distribution: 'adopt'
cache: maven
- name: Build with Maven Wrapper
run: ./mvnw -B verify

View file

@ -1,3 +1,42 @@
## Pet Attributes API Persistent Assignment
## Project Updates
- **Refactored / Rearranged Project Structure**
[Commit Link](https://github.com/shrirangjoshi94/spring-petclinic/commit/569cc89c3ae4ebe50a432022cbf71e568ef5b4d5)
- **Save and Fetch Pet Attributes Feature**
[Pull Request Link](https://github.com/shrirangjoshi94/spring-petclinic/pull/2)
---
### Development Notes
What I did was first restructured the project and merged the refactoring PR.
Then I created a new PR to implement the save and fetch Pet Attributes feature.
### 1. Save Attributes for a Pet
**POST** `/pets/{petId}/attributes`
Example request:
`POST localhost:8080/pets/2/attributes`
**Request Body:**
```json
{
"temperament": "angry",
"lengthCm": 401,
"weightKg": 100
}
```
### 2. GET Attributes for a Pet
**GET** `/pets/{petId}/attributes`
Example request:
`GET localhost:8080/pets/2/attributes`
----------------------------------------------------------
# Spring PetClinic Sample Application [![Build Status](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml/badge.svg)](https://github.com/spring-projects/spring-petclinic/actions/workflows/maven-build.yml)[![Build Status](https://github.com/spring-projects/spring-petclinic/actions/workflows/gradle-build.yml/badge.svg)](https://github.com/spring-projects/spring-petclinic/actions/workflows/gradle-build.yml)
[![Open in Gitpod](https://gitpod.io/button/open-in-gitpod.svg)](https://gitpod.io/#https://github.com/spring-projects/spring-petclinic) [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://github.com/codespaces/new?hide_repo_select=true&ref=main&repo=7517918)
@ -126,40 +165,3 @@ The following items should be installed in your system:
1. Navigate to the Petclinic
Visit [http://localhost:8080](http://localhost:8080) in your browser.
## Looking for something in particular?
|Spring Boot Configuration | Class or Java property files |
|--------------------------|---|
|The Main Class | [PetClinicApplication](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java) |
|Properties Files | [application.properties](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/resources) |
|Caching | [CacheConfiguration](https://github.com/spring-projects/spring-petclinic/blob/main/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java) |
## Interesting Spring Petclinic branches and forks
The Spring Petclinic "main" branch in the [spring-projects](https://github.com/spring-projects/spring-petclinic)
GitHub org is the "canonical" implementation based on Spring Boot and Thymeleaf. There are
[quite a few forks](https://spring-petclinic.github.io/docs/forks.html) in the GitHub org
[spring-petclinic](https://github.com/spring-petclinic). If you are interested in using a different technology stack to implement the Pet Clinic, please join the community there.
## Interaction with other open-source projects
One of the best parts about working on the Spring Petclinic application is that we have the opportunity to work in direct contact with many Open Source projects. We found bugs/suggested improvements on various topics such as Spring, Spring Data, Bean Validation and even Eclipse! In many cases, they've been fixed/implemented in just a few days.
Here is a list of them:
| Name | Issue |
|------|-------|
| Spring JDBC: simplify usage of NamedParameterJdbcTemplate | [SPR-10256](https://github.com/spring-projects/spring-framework/issues/14889) and [SPR-10257](https://github.com/spring-projects/spring-framework/issues/14890) |
| Bean Validation / Hibernate Validator: simplify Maven dependencies and backward compatibility |[HV-790](https://hibernate.atlassian.net/browse/HV-790) and [HV-792](https://hibernate.atlassian.net/browse/HV-792) |
| Spring Data: provide more flexibility when working with JPQL queries | [DATAJPA-292](https://github.com/spring-projects/spring-data-jpa/issues/704) |
## Contributing
The [issue tracker](https://github.com/spring-projects/spring-petclinic/issues) is the preferred channel for bug reports, feature requests and submitting pull requests.
For pull requests, editor preferences are available in the [editor config](.editorconfig) for easy use in common text editors. Read more and download plugins at <https://editorconfig.org>. All commits must include a __Signed-off-by__ trailer at the end of each commit message to indicate that the contributor agrees to the Developer Certificate of Origin.
For additional details, please refer to the blog post [Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring).
## License
The Spring PetClinic sample application is released under version 2.0 of the [Apache License](https://www.apache.org/licenses/LICENSE-2.0).

View file

@ -18,9 +18,9 @@ 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;
import org.springframework.samples.petclinic.common.model.BaseEntity;
import org.springframework.samples.petclinic.common.model.Person;
import org.springframework.samples.petclinic.vet.model.Vet;
public class PetClinicRuntimeHints implements RuntimeHintsRegistrar {

View file

@ -13,9 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.owner;
package org.springframework.samples.petclinic.common.formatter;
import org.springframework.format.Formatter;
import org.springframework.samples.petclinic.owner.model.PetType;
import org.springframework.samples.petclinic.owner.repository.OwnerRepository;
import org.springframework.stereotype.Component;
import java.text.ParseException;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.model;
package org.springframework.samples.petclinic.common.model;
import java.io.Serializable;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.model;
package org.springframework.samples.petclinic.common.model;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.model;
package org.springframework.samples.petclinic.common.model;
import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;

View file

@ -1,20 +0,0 @@
/*
* Copyright 2012-2019 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

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.owner;
package org.springframework.samples.petclinic.owner.controller;
import java.util.List;
import java.util.Optional;
@ -21,6 +21,8 @@ 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.samples.petclinic.owner.model.Owner;
import org.springframework.samples.petclinic.owner.repository.OwnerRepository;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
@ -44,7 +46,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
* @author Wick Dynex
*/
@Controller
class OwnerController {
public class OwnerController {
private static final String VIEWS_OWNER_CREATE_OR_UPDATE_FORM = "owners/createOrUpdateOwnerForm";

View file

@ -0,0 +1,67 @@
package org.springframework.samples.petclinic.owner.controller;
import jakarta.validation.Valid;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.samples.petclinic.owner.dto.PetAttributesDTO;
import org.springframework.samples.petclinic.owner.expection.PetNotFoundException;
import org.springframework.samples.petclinic.owner.model.PetAttributes;
import org.springframework.samples.petclinic.owner.repository.PetRepository;
import org.springframework.samples.petclinic.owner.service.PetAttributesService;
import org.springframework.samples.petclinic.owner.validation.PetIdExistsValidator;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Optional;
@RestController
@RequestMapping("/pets/{petId}/attributes")
public class PetAttributesController {
private final PetAttributesService petAttributesService;
private PetIdExistsValidator petIdExistsValidator;
private PetRepository petRepository;
public PetAttributesController(PetIdExistsValidator petIdExistsValidator, PetAttributesService petAttributesService,
PetRepository petRepository) {
this.petIdExistsValidator = petIdExistsValidator;
this.petAttributesService = petAttributesService;
this.petRepository = petRepository;
}
@GetMapping
public ResponseEntity<?> getPetAttributes(@PathVariable("petId") int petId) {
if (!petRepository.existsById(petId)) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body("Pet not found");
}
Optional<PetAttributes> optionalAttributes = petAttributesService.findByPetId(petId);
return optionalAttributes.<ResponseEntity<?>>map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.status(HttpStatus.NOT_FOUND).body("Attributes not found"));
}
@PostMapping
public ResponseEntity savePetAttributes(@PathVariable int petId, @Valid @RequestBody PetAttributesDTO dto,
BindingResult bindingResult) {
// Validate petId exists
petIdExistsValidator.validate(petId, bindingResult);
if (bindingResult.hasErrors()) {
return ResponseEntity.badRequest().body(bindingResult.getAllErrors());
}
dto.setPetId(petId);
try {
petAttributesService.savePetAttributes(dto);
return ResponseEntity.status(HttpStatus.CREATED).body("Pet attributes saved");
} catch (PetNotFoundException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(e.getMessage());
}
}
}

View file

@ -13,12 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.owner;
package org.springframework.samples.petclinic.owner.controller;
import java.time.LocalDate;
import java.util.Collection;
import java.util.Optional;
import org.springframework.samples.petclinic.owner.model.Owner;
import org.springframework.samples.petclinic.owner.model.Pet;
import org.springframework.samples.petclinic.owner.model.PetType;
import org.springframework.samples.petclinic.owner.repository.OwnerRepository;
import org.springframework.samples.petclinic.owner.validation.PetValidator;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
@ -42,7 +47,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
*/
@Controller
@RequestMapping("/owners/{ownerId}")
class PetController {
public class PetController {
private static final String VIEWS_PETS_CREATE_OR_UPDATE_FORM = "pets/createOrUpdatePetForm";

View file

@ -13,11 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.owner;
package org.springframework.samples.petclinic.owner.controller;
import java.util.Map;
import java.util.Optional;
import org.springframework.samples.petclinic.owner.model.Owner;
import org.springframework.samples.petclinic.owner.repository.OwnerRepository;
import org.springframework.samples.petclinic.owner.model.Pet;
import org.springframework.samples.petclinic.owner.model.Visit;
import org.springframework.stereotype.Controller;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
@ -39,7 +43,7 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
* @author Wick Dynex
*/
@Controller
class VisitController {
public class VisitController {
private final OwnerRepository owners;

View file

@ -0,0 +1,60 @@
package org.springframework.samples.petclinic.owner.dto;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
public class PetAttributesDTO {
@NotNull(message = "Pet ID is required")
private int petId;
@NotBlank(message = "Temperament is required")
@Size(max = 100, message = "Temperament must not exceed 100 characters")
private String temperament;
@NotNull
@DecimalMin(value = "0.1", inclusive = true, message = "Length must be at least 0.1 cm")
@DecimalMax(value = "500.0", inclusive = true, message = "Length must be less than or equal to 500 cm")
private BigDecimal lengthCm;
@NotNull
@DecimalMin(value = "0.1", inclusive = true, message = "Weight must be at least 0.1 kg")
@DecimalMax(value = "500.0", inclusive = true, message = "Weight must be less than or equal to 500 kg")
private BigDecimal weightKg;
// Getters and setters
public int getPetId() {
return petId;
}
public void setPetId(int petId) {
this.petId = petId;
}
public String getTemperament() {
return temperament;
}
public void setTemperament(String temperament) {
this.temperament = temperament;
}
public BigDecimal getLengthCm() {
return lengthCm;
}
public void setLengthCm(BigDecimal lengthCm) {
this.lengthCm = lengthCm;
}
public BigDecimal getWeightKg() {
return weightKg;
}
public void setWeightKg(BigDecimal weightKg) {
this.weightKg = weightKg;
}
}

View file

@ -0,0 +1,9 @@
package org.springframework.samples.petclinic.owner.expection;
public class PetNotFoundException extends RuntimeException {
public PetNotFoundException(String message) {
super(message);
}
}

View file

@ -13,13 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.owner;
package org.springframework.samples.petclinic.owner.model;
import java.util.ArrayList;
import java.util.List;
import org.springframework.core.style.ToStringCreator;
import org.springframework.samples.petclinic.model.Person;
import org.springframework.samples.petclinic.common.model.Person;
import org.springframework.util.Assert;
import jakarta.persistence.CascadeType;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.owner;
package org.springframework.samples.petclinic.owner.model;
import java.time.LocalDate;
import java.util.Collection;
@ -21,7 +21,7 @@ import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.samples.petclinic.model.NamedEntity;
import org.springframework.samples.petclinic.common.model.NamedEntity;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;

View file

@ -0,0 +1,82 @@
package org.springframework.samples.petclinic.owner.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import java.math.BigDecimal;
@Entity
@Table(name = "pet_attributes")
public class PetAttributes {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "pet_id", nullable = false)
@JsonIgnore
private Pet pet;
private String temperament;
@Column(name = "length_cm", precision = 5, scale = 2)
private BigDecimal lengthCm;
@Column(name = "weight_kg", precision = 5, scale = 2)
private BigDecimal weightKg;
@Column(name = "additional_attributes", columnDefinition = "json")
private String additionalAttributes;
// --- Getters and Setters ---
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Pet getPet() {
return pet;
}
public void setPet(Pet pet) {
this.pet = pet;
}
public String getTemperament() {
return temperament;
}
public void setTemperament(String temperament) {
this.temperament = temperament;
}
public BigDecimal getLengthCm() {
return lengthCm;
}
public void setLengthCm(BigDecimal lengthCm) {
this.lengthCm = lengthCm;
}
public BigDecimal getWeightKg() {
return weightKg;
}
public void setWeightKg(BigDecimal weightKg) {
this.weightKg = weightKg;
}
public String getAdditionalAttributes() {
return additionalAttributes;
}
public void setAdditionalAttributes(String additionalAttributes) {
this.additionalAttributes = additionalAttributes;
}
}

View file

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.owner;
package org.springframework.samples.petclinic.owner.model;
import org.springframework.samples.petclinic.model.NamedEntity;
import org.springframework.samples.petclinic.common.model.NamedEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

View file

@ -13,12 +13,12 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.owner;
package org.springframework.samples.petclinic.owner.model;
import java.time.LocalDate;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.samples.petclinic.model.BaseEntity;
import org.springframework.samples.petclinic.common.model.BaseEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.owner;
package org.springframework.samples.petclinic.owner.repository;
import java.util.List;
import java.util.Optional;
@ -23,6 +23,8 @@ import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.samples.petclinic.owner.model.Owner;
import org.springframework.samples.petclinic.owner.model.PetType;
/**
* Repository class for <code>Owner</code> domain objects All method names are compliant

View file

@ -0,0 +1,24 @@
package org.springframework.samples.petclinic.owner.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.samples.petclinic.owner.model.PetAttributes;
/**
* Repository class for <code>PetAttributes</code> domain objects. Provides CRUD
* operations and custom query methods related to a pet's attributes.
*
* @see org.springframework.samples.petclinic.owner.model.PetAttributes
*/
public interface PetAttributesRepository extends JpaRepository<PetAttributes, Integer> {
/**
* Find pet attributes by pet ID.
*
* @param petId the ID of the pet
* @return an Optional containing the PetAttributes if found
*/
Optional<PetAttributes> findByPetId(Integer petId);
}

View file

@ -0,0 +1,8 @@
package org.springframework.samples.petclinic.owner.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.samples.petclinic.owner.model.Pet;
public interface PetRepository extends JpaRepository<Pet, Integer> {
}

View file

@ -0,0 +1,47 @@
package org.springframework.samples.petclinic.owner.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.samples.petclinic.owner.dto.PetAttributesDTO;
import org.springframework.samples.petclinic.owner.expection.PetNotFoundException;
import org.springframework.samples.petclinic.owner.model.Pet;
import org.springframework.samples.petclinic.owner.model.PetAttributes;
import org.springframework.samples.petclinic.owner.repository.PetAttributesRepository;
import org.springframework.samples.petclinic.owner.repository.PetRepository;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class PetAttributesService {
private final PetAttributesRepository petAttributesRepository;
private final PetRepository petRepository;
@Autowired
public PetAttributesService(PetAttributesRepository petAttributesRepository, PetRepository petRepository) {
this.petAttributesRepository = petAttributesRepository;
this.petRepository = petRepository;
}
public Optional<PetAttributes> findByPetId(Integer petId) {
return petAttributesRepository.findByPetId(petId);
}
public void savePetAttributes(PetAttributesDTO dto) {
Optional<Pet> pet = petRepository.findById(dto.getPetId());
if (pet.isEmpty()) {
throw new PetNotFoundException("Pet not found");
}
PetAttributes attributes = petAttributesRepository.findByPetId(dto.getPetId()).orElse(new PetAttributes());
attributes.setPet(pet.get());
attributes.setTemperament(dto.getTemperament());
attributes.setLengthCm(dto.getLengthCm());
attributes.setWeightKg(dto.getWeightKg());
petAttributesRepository.save(attributes);
}
}

View file

@ -0,0 +1,49 @@
package org.springframework.samples.petclinic.owner.validation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.samples.petclinic.owner.repository.PetRepository;
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;
@Component
public class PetIdExistsValidator implements Validator {
private final PetRepository petRepository;
@Autowired
public PetIdExistsValidator(PetRepository petRepository) {
this.petRepository = petRepository;
}
@Override
public boolean supports(Class<?> clazz) {
// This validator supports Integer class, for validating petId field
return Integer.class.equals(clazz);
}
@Override
public void validate(Object target, Errors errors) {
if (target == null) {
errors.reject("petId.null", "Pet ID is required");
return;
}
if (!(target instanceof Integer)) {
errors.reject("petId.type", "Pet ID must be an integer");
return;
}
Integer petId = (Integer) target;
if (petId <= 0) {
errors.rejectValue("", "petId.positive", "Pet ID must be a positive number");
return;
}
if (!petRepository.existsById(petId)) {
errors.rejectValue("", "petId.notFound", "Pet with ID " + petId + " does not exist");
}
}
}

View file

@ -13,8 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.owner;
package org.springframework.samples.petclinic.owner.validation;
import org.springframework.samples.petclinic.owner.model.Pet;
import org.springframework.util.StringUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.samples.petclinic.system;
package org.springframework.samples.petclinic.system.config;
import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer;
import org.springframework.cache.annotation.EnableCaching;

View file

@ -1,4 +1,4 @@
package org.springframework.samples.petclinic.system;
package org.springframework.samples.petclinic.system.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.system;
package org.springframework.samples.petclinic.system.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@ -26,7 +26,7 @@ import org.springframework.web.bind.annotation.GetMapping;
* Also see how a view that resolves to "error" has been added ("error.html").
*/
@Controller
class CrashController {
public class CrashController {
@GetMapping("/oups")
public String triggerException() {

View file

@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.samples.petclinic.system;
package org.springframework.samples.petclinic.system.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

View file

@ -13,13 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.vet;
package org.springframework.samples.petclinic.vet.controller;
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.samples.petclinic.vet.model.Vet;
import org.springframework.samples.petclinic.vet.repository.VetRepository;
import org.springframework.samples.petclinic.vet.model.Vets;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
@ -33,7 +36,7 @@ import org.springframework.web.bind.annotation.ResponseBody;
* @author Arjen Poutsma
*/
@Controller
class VetController {
public class VetController {
private final VetRepository vetRepository;

View file

@ -13,9 +13,9 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.vet;
package org.springframework.samples.petclinic.vet.model;
import org.springframework.samples.petclinic.model.NamedEntity;
import org.springframework.samples.petclinic.common.model.NamedEntity;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.vet;
package org.springframework.samples.petclinic.vet.model;
import java.util.Comparator;
import java.util.HashSet;
@ -21,8 +21,8 @@ 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 org.springframework.samples.petclinic.common.model.NamedEntity;
import org.springframework.samples.petclinic.common.model.Person;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;

View file

@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.vet;
package org.springframework.samples.petclinic.vet.model;
import java.util.ArrayList;
import java.util.List;

View file

@ -13,13 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.samples.petclinic.vet;
package org.springframework.samples.petclinic.vet.repository;
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.samples.petclinic.vet.model.Vet;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collection;

View file

@ -1,7 +1,22 @@
# 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
#database=jdbc:mysql://localhost:3306/petclinic?useSSL=false&serverTimezone=UTC
#spring.datasource.username=root
#spring.datasource.password=testA@123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# database init, supports mysql too
database=mysql
spring.datasource.url=${MYSQL_URL:jdbc:mysql://localhost/petclinic}
spring.datasource.username=root
spring.datasource.password=your-password
# SQL is written to be idempotent so this is safe
spring.sql.init.mode=always
# Hibernate Dialect (important for compatibility)
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.sql.init.schema-locations=classpath*:db/mysql/schema.sql
spring.sql.init.data-locations=classpath*:db/mysql/data.sql
# Web
spring.thymeleaf.mode=HTML

View file

@ -53,3 +53,13 @@ CREATE TABLE IF NOT EXISTS visits (
description VARCHAR(255),
FOREIGN KEY (pet_id) REFERENCES pets(id)
) engine=InnoDB;
CREATE TABLE IF NOT EXISTS pet_attributes (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
pet_id INT UNSIGNED NOT NULL,
temperament VARCHAR(100),
length_cm DECIMAL(5,2),
weight_kg DECIMAL(5,2),
additional_attributes JSON,
FOREIGN KEY (pet_id) REFERENCES pets(id)
);

View file

@ -29,7 +29,7 @@ import org.springframework.boot.web.client.RestTemplateBuilder;
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.samples.petclinic.vet.repository.VetRepository;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.aot.DisabledInAotMode;
import org.springframework.web.client.RestTemplate;

View file

@ -28,7 +28,7 @@ import org.springframework.boot.web.client.RestTemplateBuilder;
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.samples.petclinic.vet.repository.VetRepository;
import org.springframework.web.client.RestTemplate;
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)

View file

@ -43,7 +43,7 @@ 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.samples.petclinic.vet.repository.VetRepository;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.client.RestTemplate;
import org.testcontainers.DockerClientFactory;

View file

@ -23,6 +23,7 @@ import java.util.Set;
import org.junit.jupiter.api.Test;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.samples.petclinic.common.model.Person;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import jakarta.validation.ConstraintViolation;

View file

@ -24,6 +24,12 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.samples.petclinic.owner.controller.OwnerController;
import org.springframework.samples.petclinic.owner.model.Owner;
import org.springframework.samples.petclinic.owner.model.Pet;
import org.springframework.samples.petclinic.owner.model.PetType;
import org.springframework.samples.petclinic.owner.model.Visit;
import org.springframework.samples.petclinic.owner.repository.OwnerRepository;
import org.springframework.test.context.aot.DisabledInAotMode;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

View file

@ -0,0 +1,139 @@
package org.springframework.samples.petclinic.owner;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.samples.petclinic.owner.controller.PetAttributesController;
import org.springframework.samples.petclinic.owner.dto.PetAttributesDTO;
import org.springframework.samples.petclinic.owner.expection.PetNotFoundException;
import org.springframework.samples.petclinic.owner.model.PetAttributes;
import org.springframework.samples.petclinic.owner.repository.PetRepository;
import org.springframework.samples.petclinic.owner.service.PetAttributesService;
import org.springframework.samples.petclinic.owner.validation.PetIdExistsValidator;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.validation.BindingResult;
import java.math.BigDecimal;
import java.util.Optional;
import static org.mockito.ArgumentMatchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(PetAttributesController.class)
public class PetAttributesControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private PetAttributesService petAttributesService;
@MockBean
private PetIdExistsValidator petIdExistsValidator;
@MockBean
private PetRepository petRepository;
@Autowired
private ObjectMapper objectMapper;
@Test
void testGetPetAttributes_Found() throws Exception {
int petId = 1;
PetAttributes attributes = new PetAttributes();
Mockito.when(petRepository.existsById(petId)).thenReturn(true);
Mockito.when(petAttributesService.findByPetId(petId)).thenReturn(Optional.of(attributes));
mockMvc.perform(get("/pets/{petId}/attributes", petId)).andExpect(status().isOk());
}
@Test
void testGetPetAttributes_PetNotFound() throws Exception {
int petId = 1;
Mockito.when(petRepository.existsById(petId)).thenReturn(false);
mockMvc.perform(get("/pets/{petId}/attributes", petId))
.andExpect(status().isNotFound())
.andExpect(content().string("Pet not found"));
}
@Test
void testGetPetAttributes_AttributesNotFound() throws Exception {
int petId = 1;
Mockito.when(petRepository.existsById(petId)).thenReturn(true);
Mockito.when(petAttributesService.findByPetId(petId)).thenReturn(Optional.empty());
mockMvc.perform(get("/pets/{petId}/attributes", petId))
.andExpect(status().isNotFound())
.andExpect(content().string("Attributes not found"));
}
@Test
void testSavePetAttributes_Success() throws Exception {
int petId = 1;
PetAttributesDTO dto = new PetAttributesDTO();
dto.setTemperament("Calm");
dto.setWeightKg(BigDecimal.valueOf(12.5));
dto.setLengthCm(BigDecimal.valueOf(60.0));
Mockito.doNothing().when(petIdExistsValidator).validate(eq(petId), any(BindingResult.class));
Mockito.doNothing().when(petAttributesService).savePetAttributes(any(PetAttributesDTO.class));
mockMvc
.perform(post("/pets/{petId}/attributes", petId).contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isCreated())
.andExpect(content().string("Pet attributes saved"));
}
@Test
void testSavePetAttributes_PetNotFoundException() throws Exception {
int petId = 1;
PetAttributesDTO dto = new PetAttributesDTO();
dto.setTemperament("Aggressive");
dto.setWeightKg(BigDecimal.valueOf(12.5));
dto.setLengthCm(BigDecimal.valueOf(60.0));
Mockito.doNothing().when(petIdExistsValidator).validate(eq(petId), any(BindingResult.class));
Mockito.doThrow(new PetNotFoundException("Pet not found"))
.when(petAttributesService)
.savePetAttributes(any(PetAttributesDTO.class));
mockMvc
.perform(post("/pets/{petId}/attributes", petId).contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isNotFound())
.andExpect(content().string("Pet not found"));
}
@Test
void testSavePetAttributes_ValidationErrors() throws Exception {
int petId = 1;
PetAttributesDTO dto = new PetAttributesDTO();
dto.setTemperament("Friendly");
dto.setWeightKg(BigDecimal.valueOf(10.0));
dto.setLengthCm(BigDecimal.valueOf(50.0));
// Simulate petId validation failure
Mockito.doAnswer(invocation -> {
BindingResult result = invocation.getArgument(1);
result.reject("petId", "Validation failed");
return null;
}).when(petIdExistsValidator).validate(eq(petId), any(BindingResult.class));
mockMvc
.perform(post("/pets/{petId}/attributes", petId).contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(dto)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$[0].code").value("petId"));
}
}

View file

@ -24,6 +24,13 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.samples.petclinic.common.formatter.PetTypeFormatter;
import org.springframework.samples.petclinic.owner.controller.PetController;
import org.springframework.samples.petclinic.owner.controller.PetController;
import org.springframework.samples.petclinic.owner.model.Owner;
import org.springframework.samples.petclinic.owner.model.Pet;
import org.springframework.samples.petclinic.owner.model.PetType;
import org.springframework.samples.petclinic.owner.repository.OwnerRepository;
import org.springframework.test.context.aot.DisabledInAotMode;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

View file

@ -32,6 +32,9 @@ import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.samples.petclinic.common.formatter.PetTypeFormatter;
import org.springframework.samples.petclinic.owner.model.PetType;
import org.springframework.samples.petclinic.owner.repository.OwnerRepository;
/**
* Test class for {@link PetTypeFormatter}

View file

@ -22,6 +22,9 @@ 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.samples.petclinic.owner.model.Pet;
import org.springframework.samples.petclinic.owner.model.PetType;
import org.springframework.samples.petclinic.owner.validation.PetValidator;
import org.springframework.validation.Errors;
import org.springframework.validation.MapBindingResult;

View file

@ -28,6 +28,10 @@ import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledInNativeImage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.samples.petclinic.owner.controller.VisitController;
import org.springframework.samples.petclinic.owner.model.Owner;
import org.springframework.samples.petclinic.owner.model.Pet;
import org.springframework.samples.petclinic.owner.repository.OwnerRepository;
import org.springframework.test.context.aot.DisabledInAotMode;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

View file

@ -29,13 +29,13 @@ import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabas
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
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.Visit;
import org.springframework.samples.petclinic.vet.Vet;
import org.springframework.samples.petclinic.vet.VetRepository;
import org.springframework.samples.petclinic.owner.model.Owner;
import org.springframework.samples.petclinic.owner.repository.OwnerRepository;
import org.springframework.samples.petclinic.owner.model.Pet;
import org.springframework.samples.petclinic.owner.model.PetType;
import org.springframework.samples.petclinic.owner.model.Visit;
import org.springframework.samples.petclinic.vet.model.Vet;
import org.springframework.samples.petclinic.vet.repository.VetRepository;
import org.springframework.transaction.annotation.Transactional;
/**

View file

@ -17,7 +17,7 @@
package org.springframework.samples.petclinic.service;
import org.springframework.orm.ObjectRetrievalFailureException;
import org.springframework.samples.petclinic.model.BaseEntity;
import org.springframework.samples.petclinic.common.model.BaseEntity;
import java.util.Collection;
@ -27,7 +27,7 @@ import java.util.Collection;
*
* @author Juergen Hoeller
* @author Sam Brannen
* @see org.springframework.samples.petclinic.model.BaseEntity
* @see BaseEntity
* @since 29.10.2003
*/
public abstract class EntityUtils {

View file

@ -0,0 +1,119 @@
package org.springframework.samples.petclinic.service;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.*;
import org.springframework.samples.petclinic.owner.dto.PetAttributesDTO;
import org.springframework.samples.petclinic.owner.expection.PetNotFoundException;
import org.springframework.samples.petclinic.owner.model.Pet;
import org.springframework.samples.petclinic.owner.model.PetAttributes;
import org.springframework.samples.petclinic.owner.repository.PetAttributesRepository;
import org.springframework.samples.petclinic.owner.repository.PetRepository;
import org.springframework.samples.petclinic.owner.service.PetAttributesService;
import java.math.BigDecimal;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
class PetAttributesServiceTest {
@Mock
private PetAttributesRepository petAttributesRepository;
@Mock
private PetRepository petRepository;
@InjectMocks
private PetAttributesService petAttributesService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
@Test
void testFindByPetId_ReturnsAttributes() {
PetAttributes attributes = new PetAttributes();
when(petAttributesRepository.findByPetId(1)).thenReturn(Optional.of(attributes));
Optional<PetAttributes> result = petAttributesService.findByPetId(1);
assertTrue(result.isPresent());
assertEquals(attributes, result.get());
}
@Test
void testFindByPetId_ReturnsEmpty() {
when(petAttributesRepository.findByPetId(1)).thenReturn(Optional.empty());
Optional<PetAttributes> result = petAttributesService.findByPetId(1);
assertFalse(result.isPresent());
}
@Test
void testSavePetAttributes_Success_NewRecord() {
Pet pet = new Pet();
pet.setId(1);
PetAttributesDTO dto = new PetAttributesDTO();
dto.setPetId(1);
dto.setTemperament("Calm");
dto.setLengthCm(BigDecimal.valueOf(40.0));
dto.setWeightKg(BigDecimal.valueOf(8.5));
when(petRepository.findById(1)).thenReturn(Optional.of(pet));
when(petAttributesRepository.findByPetId(1)).thenReturn(Optional.empty());
petAttributesService.savePetAttributes(dto);
// Expect a new PetAttributes to be created and saved
ArgumentCaptor<PetAttributes> captor = ArgumentCaptor.forClass(PetAttributes.class);
verify(petAttributesRepository).save(captor.capture());
PetAttributes saved = captor.getValue();
assertEquals("Calm", saved.getTemperament());
assertEquals(BigDecimal.valueOf(40.0), saved.getLengthCm());
assertEquals(BigDecimal.valueOf(8.5), saved.getWeightKg());
assertEquals(pet, saved.getPet());
}
@Test
void testSavePetAttributes_Success_UpdateExisting() {
Pet pet = new Pet();
pet.setId(1);
PetAttributes existing = new PetAttributes();
existing.setPet(pet);
PetAttributesDTO dto = new PetAttributesDTO();
dto.setPetId(1);
dto.setTemperament("Playful");
dto.setLengthCm(BigDecimal.valueOf(45.0));
dto.setWeightKg(BigDecimal.valueOf(9.0));
when(petRepository.findById(1)).thenReturn(Optional.of(pet));
when(petAttributesRepository.findByPetId(1)).thenReturn(Optional.of(existing));
petAttributesService.savePetAttributes(dto);
verify(petAttributesRepository).save(existing);
assertEquals("Playful", existing.getTemperament());
assertEquals(BigDecimal.valueOf(45.0), existing.getLengthCm());
assertEquals(BigDecimal.valueOf(9.0), existing.getWeightKg());
}
@Test
void testSavePetAttributes_PetNotFound_ThrowsException() {
PetAttributesDTO dto = new PetAttributesDTO();
dto.setPetId(1);
when(petRepository.findById(1)).thenReturn(Optional.empty());
PetNotFoundException ex = assertThrows(PetNotFoundException.class, () -> {
petAttributesService.savePetAttributes(dto);
});
assertEquals("Pet not found", ex.getMessage());
verify(petAttributesRepository, never()).save(any());
}
}

View file

@ -41,7 +41,8 @@ import org.springframework.http.RequestEntity;
import org.springframework.http.ResponseEntity;
/**
* Integration Test for {@link CrashController}.
* Integration Test for
* {@link org.springframework.samples.petclinic.system.controller.CrashController}.
*
* @author Alex Lutz
*/

View file

@ -19,9 +19,11 @@ package org.springframework.samples.petclinic.system;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import org.springframework.samples.petclinic.system.controller.CrashController;
/**
* Test class for {@link CrashController}
* Test class for
* {@link org.springframework.samples.petclinic.system.controller.CrashController}
*
* @author Colin But
* @author Alex Lutz

View file

@ -25,6 +25,10 @@ import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.MediaType;
import org.springframework.samples.petclinic.vet.controller.VetController;
import org.springframework.samples.petclinic.vet.model.Specialty;
import org.springframework.samples.petclinic.vet.model.Vet;
import org.springframework.samples.petclinic.vet.repository.VetRepository;
import org.springframework.test.context.aot.DisabledInAotMode;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

View file

@ -16,6 +16,7 @@
package org.springframework.samples.petclinic.vet;
import org.junit.jupiter.api.Test;
import org.springframework.samples.petclinic.vet.model.Vet;
import org.springframework.util.SerializationUtils;
import static org.assertj.core.api.Assertions.assertThat;