Save pet attributes and retrieve them feature completed.

This commit is contained in:
Shrirang 2025-05-20 17:24:42 +05:30
parent e76ed81718
commit 8c8509b46e
17 changed files with 621 additions and 5 deletions

BIN
src.zip

Binary file not shown.

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

@ -72,7 +72,7 @@ public class PetController {
@ModelAttribute("pet")
public Pet findPet(@PathVariable("ownerId") int ownerId,
@PathVariable(name = "petId", required = false) Integer petId) {
@PathVariable(name = "petId", required = false) Integer petId) {
if (petId == null) {
return new Pet();

View file

@ -65,7 +65,7 @@ public class VisitController {
*/
@ModelAttribute("visit")
public Visit loadPetWithVisit(@PathVariable("ownerId") int ownerId, @PathVariable("petId") int petId,
Map<String, Object> model) {
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 "));

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

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

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

@ -8,7 +8,7 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
database=mysql
spring.datasource.url=${MYSQL_URL:jdbc:mysql://localhost/petclinic}
spring.datasource.username=root
spring.datasource.password=testA@123
spring.datasource.password=your-password
# SQL is written to be idempotent so this is safe
spring.sql.init.mode=always

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

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

@ -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 org.springframework.samples.petclinic.system.controller.CrashController}.
* Integration Test for
* {@link org.springframework.samples.petclinic.system.controller.CrashController}.
*
* @author Alex Lutz
*/

View file

@ -22,7 +22,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import org.springframework.samples.petclinic.system.controller.CrashController;
/**
* Test class for {@link org.springframework.samples.petclinic.system.controller.CrashController}
* Test class for
* {@link org.springframework.samples.petclinic.system.controller.CrashController}
*
* @author Colin But
* @author Alex Lutz