This commit is contained in:
shreyasprayag 2025-06-05 18:08:38 +05:30 committed by GitHub
commit f29b91c3bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 396 additions and 2 deletions

13
pom.xml
View file

@ -151,6 +151,19 @@
<artifactId>jakarta.xml.bind-api</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
</dependencies>
<build>

View file

@ -0,0 +1,101 @@
package org.springframework.samples.petclinic.owner;
import jakarta.annotation.Nonnull;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
/**
* Simple business object representing a pet.
*
* @author Shreyas Prayag
*/
public class NewPet {
/**
* The unique identifier for the pet. This field is mapped to the "id" column in the
* "new_pet" table. It is automatically generated by the database when a new pet is
* created.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;
/**
* The name of the pet. This field is mapped to the "name" column in the "new_pet"
* table. It cannot be null and should not be empty.
*/
@NotNull
private String name;
/**
* The date of birth of the pet. This field is mapped to the "birth_date" column in
* the "new_pet" table. It cannot be null and should not be empty.
*/
private Date birthDate;
/**
* The owner of the pet.
* <p>
* This field represents a many-to-one relationship between pets and their owners
* each pet is associated with one owner, but an owner can have multiple pets.
* </p>
* <p>
* The relationship is mapped via the {@code owner_id} foreign key in the
* {@code new_pet} table.
* </p>
*
* @see Owner
*/
@ManyToOne
@JoinColumn(name = "owner_id")
private Owner owner;
/**
* The temperament of the pet.
* <p>
* This field represents a many-to-one relationship between pets and their
* temperaments each pet has one temperament, there can be more than one pets with
* one temperament.
* </p>
* <p>
* The relationship is mapped via the {@code temperament_id} foreign key in the
* {@code new_pet} table.
* </p>
*
* @see PetTemperament
*/
@ManyToOne()
@JoinColumn(name = "temperament_id")
private PetTemperament temperament;
/**
* The type of the pet.
* <p>
* This field represents a many-to-one relationship between pets and their owners
* each pet is associated with one type, but there can be multiple pets of a type.
* </p>
* <p>
* The relationship is mapped via the {@code type_id} foreign key in the
* {@code new_pet} table.
* </p>
*
* @see Owner
*/
@ManyToOne
@JoinColumn(name = "type_id")
private PetType type;
}

View file

@ -0,0 +1,92 @@
package org.springframework.samples.petclinic.owner;
import org.springframework.hateoas.EntityModel;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.linkTo;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.methodOn;
/**
* new Controller to handle requests relating pet
*
* The existing controller is not a REST API controller. So this controller is created to
* cater Rest endpoints relating Pet.
*
* @author Shreyas Prayag
* @since 1.0
*/
@RestController
@RequestMapping("owners/{ownerId}")
public class NewPetController {
private final NewPetRepository petRepository;
NewPetController(NewPetRepository petRepository) {
this.petRepository = petRepository;
}
/**
* Retrieves a pet by its ID.
* <p>
* This endpoint returns a pet based on the provided pet ID. If the pet with the given
* ID exists, it returns the pet details in the response body with a 200 OK status. If
* the pet is not found, a 404 Not Found status is returned.
* </p>
* @param petId The ID of the pet to retrieve. It is expected to be a positive
* integer.
* @return A {@link ResponseEntity} containing the {@link Pet} object if found, or a
* 404 response if not found.
*
* this endpoint is hateoas compliant
*/
@GetMapping("/pet/{petId}")
public ResponseEntity<EntityModel<NewPet>> getPetById(@PathVariable int ownerId, @PathVariable int petId) {
NewPet pet = petRepository.findById(petId);
if (pet == null) {
return ResponseEntity.notFound().build();
}
EntityModel<NewPet> petResource = EntityModel.of(pet);
// Add self link
petResource.add(linkTo(methodOn(NewPetController.class).getPetById(ownerId, petId)).withSelfRel());
// Add link to create new pet
petResource.add(linkTo(methodOn(NewPetController.class).createNewPet(ownerId, null)).withRel("create-new-pet"));
return ResponseEntity.ok(petResource);
}
/**
* Creates a new pet.
* <p>
* This endpoint accepts a {@link Pet} object in the request body and saves it to the
* database. It returns the created {@link Pet} object along with a 201 Created HTTP
* status code. If the pet data is invalid, a 400 Bad Request status is returned.
* </p>
* @param newPet The pet object to create. This is expected to contain the pet details
* in the request body.
* @return A {@link ResponseEntity} containing the created {@link Pet} object and a
* 201 Created status.
*
* this endpoint is hateoas compliant
*/
@PostMapping("/pet")
public ResponseEntity<EntityModel<NewPet>> createNewPet(@PathVariable int ownerId, @RequestBody NewPet newPet) {
NewPet savedPet = petRepository.save(newPet);
EntityModel<NewPet> petResource = EntityModel.of(savedPet);
// Add self link
petResource.add(linkTo(methodOn(NewPetController.class).getPetById(ownerId, savedPet.getId())).withSelfRel());
return ResponseEntity.status(HttpStatus.CREATED).body(petResource);
}
}

View file

@ -0,0 +1,33 @@
package org.springframework.samples.petclinic.owner;
import org.springframework.data.jpa.repository.JpaRepository;
/**
* Repository interface for managing {@link NewPet} entities.
* <p>
* Extends {@link JpaRepository} to provide CRUD operations, pagination, and JPA-specific
* functionality.
* </p>
*
* @author You
* @since 1.0
*/
public interface NewPetRepository extends JpaRepository<NewPet, Integer> {
/**
* Finds the pet with the given id.
* @param id The id of the pet to search for.
* @return a pet with the specified id.
*
*/
NewPet findById(int id);
/**
* Saves the given pet
* @param newPet The pet object to be saved.
* @return pet object being saved.
*
*/
NewPet save(NewPet newPet);
}

View file

@ -0,0 +1,46 @@
package org.springframework.samples.petclinic.owner;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
/**
* Simple business object representing pet temperament.
*
* @author Shreyas Prayag
*/
public class PetTemperament {
/**
* The unique identifier for the temperament. This field is mapped to the "id" column
* in the "pet_temperament" table. It is automatically generated by the database when
* a new temperament is created.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
/**
* The Temperament of pet. This field is mapped to the "temperament" column in the
* "pet_temperament" table. It cannot be null and should not be empty.
*/
@NotNull
private String temperament;
public PetTemperament(String temperament) {
this.temperament = temperament;
}
}

View file

@ -1,13 +1,23 @@
# database init, supports mysql too
database=h2
database=mysql
spring.sql.init.schema-locations=classpath*:db/${database}/schema.sql
spring.sql.init.data-locations=classpath*:db/${database}/data.sql
# Data Source properties
spring.datasource.url=jdbc:mysql://localhost:3306/petclinic
spring.datasource.username=petclinic
spring.datasource.password=petclinic
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# Logging level for Hibernate SQL
logging.level.org.hibernate.SQL=DEBUG
# Web
spring.thymeleaf.mode=HTML
# JPA
spring.jpa.hibernate.ddl-auto=none
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
spring.jpa.open-in-view=false
# Internationalization

View file

@ -0,0 +1,99 @@
package org.springframework.samples.petclinic.owner;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
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.hateoas.EntityModel;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@WebMvcTest(NewPetController.class)
class NewPetControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private NewPetRepository petRepository;
private NewPet testPet;
private PetType testPetType;
@BeforeEach
void setup() {
testPetType = new PetType();
testPetType.setId(1);
testPetType.setName("Test");
testPet = new NewPet();
testPet.setId(1);
testPet.setName("Buddy");
testPet.setType(testPetType);
}
@Test
void getPetById_found() throws Exception {
when(petRepository.findById(1)).thenReturn(testPet);
mockMvc.perform(get("/owners/1/pet/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id", is(1)))
.andExpect(jsonPath("$.name", is("Buddy")))
.andExpect(jsonPath("$.type.name", is("Test")))
.andExpect(jsonPath("$._links.self.href").exists())
.andExpect(jsonPath("$._links.create-new-pet.href").exists());
}
@Test
void getPetById_notFound() throws Exception {
when(petRepository.findById(99)).thenReturn(null);
mockMvc.perform(get("/owners/1/pet/99")).andExpect(status().isNotFound());
}
@Test
void createNewPet_success() throws Exception {
testPetType = new PetType();
testPetType.setId(1);
testPetType.setName("Test1");
NewPet newPet = new NewPet();
newPet.setId(2);
newPet.setName("Whiskers");
newPet.setType(testPetType);
when(petRepository.save(any(NewPet.class))).thenReturn(newPet);
mockMvc.perform(post("/owners/1/pet").contentType(MediaType.APPLICATION_JSON).content(asJsonString(newPet)))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.id", is(2)))
.andExpect(jsonPath("$.name", is("Whiskers")))
.andExpect(jsonPath("$.type.name", is("Test1")))
.andExpect(jsonPath("$._links.self.href").exists());
}
// Utility method to convert objects to JSON string
private static String asJsonString(Object obj) {
try {
return new ObjectMapper().writeValueAsString(obj);
}
catch (Exception e) {
throw new RuntimeException("Failed to convert object to JSON string", e);
}
}
}