From 9264c8b083029da06d01226100ce52326a98e69c Mon Sep 17 00:00:00 2001 From: "Ashok Kale, Anuja [External]" Date: Tue, 8 Jul 2025 11:06:23 +0530 Subject: [PATCH] petDetails-enhancement changes --- pom.xml | 13 +++- .../petclinic/PetClinicApplication.java | 2 + .../samples/petclinic/config/CacheConfig.java | 17 +++++ .../controller/PetDetailController.java | 73 +++++++++++++++++++ .../samples/petclinic/dto/PetDetailDto.java | 8 ++ .../exception/GlobalExceptionHandler.java | 25 +++++++ .../exception/ResourceNotFoundException.java | 9 +++ .../samples/petclinic/model/PetDetail.java | 27 +++++++ .../repository/PetDetailRepository.java | 14 ++++ .../petclinic/service/PetDetailService.java | 13 ++++ .../service/PetDetailServiceImpl.java | 61 ++++++++++++++++ .../db/h2/create_petDetails_table.sql | 30 ++++++++ .../controller/PetDetailControllerTest.java | 24 ++++++ .../service/PetDetailServiceTest.java | 38 ++++++++++ 14 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/springframework/samples/petclinic/config/CacheConfig.java create mode 100644 src/main/java/org/springframework/samples/petclinic/controller/PetDetailController.java create mode 100644 src/main/java/org/springframework/samples/petclinic/dto/PetDetailDto.java create mode 100644 src/main/java/org/springframework/samples/petclinic/exception/GlobalExceptionHandler.java create mode 100644 src/main/java/org/springframework/samples/petclinic/exception/ResourceNotFoundException.java create mode 100644 src/main/java/org/springframework/samples/petclinic/model/PetDetail.java create mode 100644 src/main/java/org/springframework/samples/petclinic/repository/PetDetailRepository.java create mode 100644 src/main/java/org/springframework/samples/petclinic/service/PetDetailService.java create mode 100644 src/main/java/org/springframework/samples/petclinic/service/PetDetailServiceImpl.java create mode 100644 src/main/resources/db/h2/create_petDetails_table.sql create mode 100644 src/test/java/org/springframework/samples/petclinic/controller/PetDetailControllerTest.java create mode 100644 src/test/java/org/springframework/samples/petclinic/service/PetDetailServiceTest.java diff --git a/pom.xml b/pom.xml index 8576c22ba..cd60034b8 100644 --- a/pom.xml +++ b/pom.xml @@ -36,6 +36,7 @@ 3.6.0 0.0.11 0.0.46 + 2.5.0 @@ -135,6 +136,7 @@ junit-jupiter test + org.testcontainers mysql @@ -145,7 +147,16 @@ jakarta.xml.bind jakarta.xml.bind-api - + + org.projectlombok + lombok + provided + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + ${spring-doc-version} + diff --git a/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java b/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java index fa0630995..7b4570e38 100644 --- a/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java +++ b/src/main/java/org/springframework/samples/petclinic/PetClinicApplication.java @@ -18,6 +18,7 @@ package org.springframework.samples.petclinic; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.ImportRuntimeHints; /** @@ -26,6 +27,7 @@ import org.springframework.context.annotation.ImportRuntimeHints; * @author Dave Syer */ @SpringBootApplication +@EnableCaching @ImportRuntimeHints(PetClinicRuntimeHints.class) public class PetClinicApplication { diff --git a/src/main/java/org/springframework/samples/petclinic/config/CacheConfig.java b/src/main/java/org/springframework/samples/petclinic/config/CacheConfig.java new file mode 100644 index 000000000..5e0557c18 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/config/CacheConfig.java @@ -0,0 +1,17 @@ +package org.springframework.samples.petclinic.config; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +@EnableCaching +public class CacheConfig { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager("petDetails"); + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/controller/PetDetailController.java b/src/main/java/org/springframework/samples/petclinic/controller/PetDetailController.java new file mode 100644 index 000000000..ffb57ed13 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/controller/PetDetailController.java @@ -0,0 +1,73 @@ +package org.springframework.samples.petclinic.controller; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.samples.petclinic.dto.PetDetailDto; +import org.springframework.samples.petclinic.exception.ResourceNotFoundException; +import org.springframework.samples.petclinic.model.PetDetail; +import org.springframework.samples.petclinic.owner.Pet; +import org.springframework.samples.petclinic.repository.PetDetailRepository; +import org.springframework.samples.petclinic.service.PetDetailService; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/pet-details") +@Tag(name = "PetDetails" , description = "Pet details apis") +public class PetDetailController { + + private PetDetailService service; + private PetDetailRepository petRepo; + + @Autowired + public PetDetailController(PetDetailRepository petRepo, PetDetailService service) { + this.petRepo = petRepo; + this.service = service; + } + + @PostMapping("/{petId}") + @Operation(summary = "create pet details") + public ResponseEntity createDetail(@PathVariable int petId, + @RequestBody PetDetailDto dto) { + Pet pet = petRepo.findByPetId(petId).get().getPet(); + PetDetail detail = PetDetail.builder() + .pet(pet) + .temperament(dto.temperament()) + .weight(dto.weight()) + .length(dto.length()) + .build(); + return ResponseEntity.ok(service.savePetDetail(detail)); + } + + @GetMapping("/{petId}") + @Operation(summary = "Get pet details by per id") + public ResponseEntity getDetail(@PathVariable int petId) { + var detail = service.getPetDetailByPetId(petId); + return detail != null ? ResponseEntity.ok(detail) : ResponseEntity.notFound().build(); + } + + @PutMapping("/{petId}") + @Operation(summary = "Update pet detail") + public ResponseEntity updateDetail(@PathVariable int petId, + @RequestBody PetDetailDto dto) { + Pet pet = petRepo.findById(petId).orElseThrow(() -> + new ResourceNotFoundException("Pet not found with id: " + petId)).getPet(); + + PetDetail detail = PetDetail.builder() + .pet(pet) + .temperament(dto.temperament()) + .weight(dto.weight()) + .length(dto.length()) + .build(); + + return ResponseEntity.ok(service.updatePetDetail(petId, detail)); + } + + @DeleteMapping("/{petId}") + @Operation(summary = "Delete pet detail by pet id") + public ResponseEntity deleteDetail(@PathVariable int petId) { + service.deletePetDetail(petId); + return ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/dto/PetDetailDto.java b/src/main/java/org/springframework/samples/petclinic/dto/PetDetailDto.java new file mode 100644 index 000000000..b06c6b9ed --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/dto/PetDetailDto.java @@ -0,0 +1,8 @@ +package org.springframework.samples.petclinic.dto; + +public record PetDetailDto( + String temperament, + Double weight, + Double length +) { +} diff --git a/src/main/java/org/springframework/samples/petclinic/exception/GlobalExceptionHandler.java b/src/main/java/org/springframework/samples/petclinic/exception/GlobalExceptionHandler.java new file mode 100644 index 000000000..db49a69d8 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/exception/GlobalExceptionHandler.java @@ -0,0 +1,25 @@ +package org.springframework.samples.petclinic.exception; + + +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; + +import java.util.HashMap; +import java.util.Map; + +public class GlobalExceptionHandler { + @ExceptionHandler(ResourceNotFoundException.class) + public ResponseEntity> handleNotFound(ResourceNotFoundException ex) { + Map error = new HashMap<>(); + error.put("message", ex.getMessage()); + return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(Exception.class) + public ResponseEntity> handleGeneric(Exception ex) { + Map error = new HashMap<>(); + error.put("message", "internal server error"); + return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR); + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/exception/ResourceNotFoundException.java b/src/main/java/org/springframework/samples/petclinic/exception/ResourceNotFoundException.java new file mode 100644 index 000000000..16499862d --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/exception/ResourceNotFoundException.java @@ -0,0 +1,9 @@ +package org.springframework.samples.petclinic.exception; + + + +public class ResourceNotFoundException extends RuntimeException { + public ResourceNotFoundException(String message) { + super(message); + } +} diff --git a/src/main/java/org/springframework/samples/petclinic/model/PetDetail.java b/src/main/java/org/springframework/samples/petclinic/model/PetDetail.java new file mode 100644 index 000000000..339b2cd0d --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/model/PetDetail.java @@ -0,0 +1,27 @@ +package org.springframework.samples.petclinic.model; + +import jakarta.persistence.*; +import lombok.Builder; +import lombok.Data; +import org.springframework.samples.petclinic.owner.Pet; +import java.time.LocalDateTime; + +@Entity +@Table(name = "pet_details") +@Data +@Builder +public class PetDetail { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + @OneToOne + @JoinColumn(name = "pet_id", nullable = false) + private Pet pet; + private String temperament; + private Double weight; + private Double length; + @Column(name = "created_at") + private LocalDateTime createdAt = LocalDateTime.now(); +} + diff --git a/src/main/java/org/springframework/samples/petclinic/repository/PetDetailRepository.java b/src/main/java/org/springframework/samples/petclinic/repository/PetDetailRepository.java new file mode 100644 index 000000000..82ab00886 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/repository/PetDetailRepository.java @@ -0,0 +1,14 @@ +package org.springframework.samples.petclinic.repository; + +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.expression.spel.ast.OpAnd; +import org.springframework.samples.petclinic.model.PetDetail; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface PetDetailRepository extends JpaRepository { + Optional findByPetId(int petId); +} + diff --git a/src/main/java/org/springframework/samples/petclinic/service/PetDetailService.java b/src/main/java/org/springframework/samples/petclinic/service/PetDetailService.java new file mode 100644 index 000000000..04de71f0e --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/service/PetDetailService.java @@ -0,0 +1,13 @@ +package org.springframework.samples.petclinic.service; + +import org.springframework.samples.petclinic.dto.PetDetailDto; +import org.springframework.samples.petclinic.model.PetDetail; +import org.springframework.stereotype.Service; + +@Service +public interface PetDetailService { + PetDetail savePetDetail(PetDetail petDetail); + PetDetail getPetDetailByPetId(Integer petId); + PetDetail updatePetDetail(Integer petId, PetDetail petDetail); + void deletePetDetail(Integer petId); +} diff --git a/src/main/java/org/springframework/samples/petclinic/service/PetDetailServiceImpl.java b/src/main/java/org/springframework/samples/petclinic/service/PetDetailServiceImpl.java new file mode 100644 index 000000000..866c5dee0 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/service/PetDetailServiceImpl.java @@ -0,0 +1,61 @@ +package org.springframework.samples.petclinic.service; + + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.cache.annotation.CachePut; +import org.springframework.cache.annotation.Cacheable; + +import org.springframework.samples.petclinic.exception.ResourceNotFoundException; +import org.springframework.samples.petclinic.model.PetDetail; +import org.springframework.samples.petclinic.repository.PetDetailRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +@Service +public class PetDetailServiceImpl implements PetDetailService { + + private final PetDetailRepository petRepo; + + @Autowired + public PetDetailServiceImpl(PetDetailRepository petRepo) { + this.petRepo = petRepo; + } + + @Override + @Transactional + public PetDetail savePetDetail(PetDetail petDetail) { + return petRepo.save(petDetail); + } + + @Override + @Cacheable(value = "petDetails", key = "#petId") + @Transactional + public PetDetail getPetDetailByPetId(Integer petId) { + return petRepo.findByPetId(petId) + .orElseThrow(() -> new ResourceNotFoundException("Pet details not found for the pet id : " +petId)); + } + + @Override + @CachePut(value = "petDetails", key = "#petId") + @Transactional + public PetDetail updatePetDetail(Integer petId, PetDetail updatedDetail) { + PetDetail existing = petRepo.findByPetId(petId) + .orElseThrow(() -> new ResourceNotFoundException("Pet detail not found for the pet id: " + petId)); + + existing.setTemperament(updatedDetail.getTemperament()); + existing.setWeight(updatedDetail.getWeight()); + existing.setLength(updatedDetail.getLength()); + return petRepo.save(existing); + } + + @Override + @CacheEvict(value = "petDetails", key = "#petId") + @Transactional + public void deletePetDetail(Integer petId) { + PetDetail detail = petRepo.findByPetId(petId) + .orElseThrow(() -> new ResourceNotFoundException("Pet details not found for the for pet id: " + petId)); + petRepo.delete(detail); + } +} diff --git a/src/main/resources/db/h2/create_petDetails_table.sql b/src/main/resources/db/h2/create_petDetails_table.sql new file mode 100644 index 000000000..845f41024 --- /dev/null +++ b/src/main/resources/db/h2/create_petDetails_table.sql @@ -0,0 +1,30 @@ +CREATE TABLE pet_types ( + id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(80) NOT NULL +); + +CREATE TABLE pets ( + id INT PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(80) NOT NULL, + birth_date DATE, + type_id INT, + FOREIGN KEY (type_id) REFERENCES pet_types(id) +); + +CREATE TABLE visits ( + id INT PRIMARY KEY AUTO_INCREMENT, + pet_id INT NOT NULL, + date DATE NOT NULL, + description VARCHAR(255), + FOREIGN KEY (pet_id) REFERENCES pets(id) +); + +CREATE TABLE pet_details ( + id INT PRIMARY KEY AUTO_INCREMENT, + pet_id INT NOT NULL UNIQUE, + temperament VARCHAR(100), + weight DOUBLE, + length DOUBLE, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (pet_id) REFERENCES pets(id) +); diff --git a/src/test/java/org/springframework/samples/petclinic/controller/PetDetailControllerTest.java b/src/test/java/org/springframework/samples/petclinic/controller/PetDetailControllerTest.java new file mode 100644 index 000000000..6e0dcea89 --- /dev/null +++ b/src/test/java/org/springframework/samples/petclinic/controller/PetDetailControllerTest.java @@ -0,0 +1,24 @@ +package org.springframework.samples.petclinic.controller; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.web.servlet.MockMvc; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@AutoConfigureMockMvc +class PetDetailControllerTest { + + @Autowired + private MockMvc mockMvc; + + @Test + void testGetPetDetail() throws Exception { + mockMvc.perform(get("/api/pet-details/1")) + .andExpect(status().isOk()); + } +} + diff --git a/src/test/java/org/springframework/samples/petclinic/service/PetDetailServiceTest.java b/src/test/java/org/springframework/samples/petclinic/service/PetDetailServiceTest.java new file mode 100644 index 000000000..ef1aa08c9 --- /dev/null +++ b/src/test/java/org/springframework/samples/petclinic/service/PetDetailServiceTest.java @@ -0,0 +1,38 @@ +package org.springframework.samples.petclinic.service; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.samples.petclinic.model.PetDetail; +import org.springframework.samples.petclinic.owner.Pet; +import org.springframework.samples.petclinic.repository.PetDetailRepository; + +@SpringBootTest + class PetDetailServiceTest { + + @Autowired + private PetDetailService service; + + @Autowired + private PetDetailRepository petRepo; + + @Test + void testSaveAndFetchPetDetail() { + Pet pet = petRepo.findAll().get(0).getPet(); + + PetDetail detail = PetDetail.builder() + .pet(pet) + .temperament("Aggressive") + .weight(20.0) + .length(30.0) + .build(); + + PetDetail saved = service.savePetDetail(detail); + Assertions.assertNotNull(saved.getId()); + + PetDetail fetched = service.getPetDetailByPetId(pet.getId()); + Assertions.assertEquals("Aggressive", fetched.getTemperament()); + } +} +