petDetails-enhancement changes

This commit is contained in:
Ashok Kale, Anuja [External] 2025-07-08 11:06:23 +05:30
parent 30aab0ae76
commit 9264c8b083
14 changed files with 353 additions and 1 deletions

13
pom.xml
View file

@ -36,6 +36,7 @@
<maven-checkstyle.version>3.6.0</maven-checkstyle.version>
<nohttp-checkstyle.version>0.0.11</nohttp-checkstyle.version>
<spring-format.version>0.0.46</spring-format.version>
<spring-doc-version>2.5.0</spring-doc-version>
</properties>
@ -135,6 +136,7 @@
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
@ -145,7 +147,16 @@
<groupId>jakarta.xml.bind</groupId>
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${spring-doc-version}</version>
</dependency>
</dependencies>
<build>

View file

@ -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 {

View file

@ -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");
}
}

View file

@ -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<PetDetail> 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<PetDetail> 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<PetDetail> 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<Void> deleteDetail(@PathVariable int petId) {
service.deletePetDetail(petId);
return ResponseEntity.noContent().build();
}
}

View file

@ -0,0 +1,8 @@
package org.springframework.samples.petclinic.dto;
public record PetDetailDto(
String temperament,
Double weight,
Double length
) {
}

View file

@ -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<Map<String, String>> handleNotFound(ResourceNotFoundException ex) {
Map<String, String> error = new HashMap<>();
error.put("message", ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Map<String, String>> handleGeneric(Exception ex) {
Map<String, String> error = new HashMap<>();
error.put("message", "internal server error");
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

View file

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

View file

@ -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();
}

View file

@ -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<PetDetail, Integer> {
Optional<PetDetail> findByPetId(int petId);
}

View file

@ -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);
}

View file

@ -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);
}
}

View file

@ -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)
);

View file

@ -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());
}
}

View file

@ -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());
}
}