Modified project to support the tasks listed in the case sheet

This commit is contained in:
Stefano Bodini 2024-03-16 17:05:47 +01:00
parent 0328c6ff44
commit ca95107d6c
14 changed files with 192 additions and 40 deletions

View file

@ -69,6 +69,13 @@
<scope>test</scope>
</dependency>
<!-- Java 8 LocalDate Jackson support -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.15.3</version>
</dependency>
<!-- Databases - Uses H2 by default -->
<dependency>
<groupId>com.h2database</groupId>

View file

@ -0,0 +1,25 @@
package org.springframework.samples.petclinic.api;
import org.springframework.samples.petclinic.owner.Owner;
import org.springframework.samples.petclinic.owner.OwnerRepository;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class OwnerApiController {
private final OwnerRepository ownerRepository;
public OwnerApiController(OwnerRepository ownerRepository) {
this.ownerRepository = ownerRepository;
}
@GetMapping("/api/owners")
List<Owner> searchOwnersBySurname(@RequestParam String lastname) {
return ownerRepository.findAllByLastName(lastname);
}
}

View file

@ -59,6 +59,10 @@ public class Owner extends Person {
@Digits(fraction = 0, integer = 10)
private String telephone;
@Column(name = "email")
@NotBlank
private String email;
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinColumn(name = "owner_id")
@OrderBy("name")
@ -92,6 +96,14 @@ public class Owner extends Person {
return this.pets;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public void addPet(Pet pet) {
if (pet.isNew()) {
getPets().add(pet);

View file

@ -15,9 +15,7 @@
*/
package org.springframework.samples.petclinic.owner;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import jakarta.validation.Valid;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
@ -25,17 +23,13 @@ import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import jakarta.validation.Valid;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.util.List;
import java.util.Map;
/**
* @author Juergen Hoeller
* @author Ken Krebs
@ -83,7 +77,9 @@ class OwnerController {
}
@GetMapping("/owners/find")
public String initFindForm() {
public String initFindForm(@RequestParam(defaultValue = "1") int page, Model model) {
Page<Owner> ownersResults = findPaginatedForOwnersLastName(page, "");
addPaginationModel(page, model, ownersResults);
return "owners/findOwners";
}

View file

@ -57,6 +57,10 @@ public interface OwnerRepository extends Repository<Owner, Integer> {
@Transactional(readOnly = true)
Page<Owner> findByLastName(@Param("lastName") String lastName, Pageable pageable);
@Query("SELECT DISTINCT owner FROM Owner owner left join owner.pets WHERE owner.lastName LIKE :lastName% ")
@Transactional(readOnly = true)
List<Owner> findAllByLastName(@Param("lastName") String lastName);
/**
* Retrieve an {@link Owner} from the data store by id.
* @param id the id to search for

View file

@ -15,24 +15,25 @@
*/
package org.springframework.samples.petclinic.owner;
import java.time.LocalDate;
import java.util.Collection;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.validation.Valid;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.util.StringUtils;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import java.time.LocalDate;
import java.util.Collection;
/**
* @author Juergen Hoeller
* @author Ken Krebs
@ -44,6 +45,8 @@ class PetController {
private static final String VIEWS_PETS_CREATE_OR_UPDATE_FORM = "pets/createOrUpdatePetForm";
private static final String POSTMAN_URL = "https://postman-echo.com/post";
private final OwnerRepository owners;
public PetController(OwnerRepository owners) {
@ -117,6 +120,24 @@ class PetController {
}
this.owners.save(owner);
try {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.findAndRegisterModules();
String petAsJson = objectMapper.writeValueAsString(pet);
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<String> request = new HttpEntity<>(petAsJson, headers);
ResponseEntity<String> response = restTemplate.postForEntity(POSTMAN_URL, request, String.class);
if (!response.getStatusCode().is2xxSuccessful()) {
System.out.printf("Unsuccessful POST of new pet to %s", POSTMAN_URL);
}
}
catch (JsonProcessingException e) {
System.out.printf("Unable to map pet to JSON, skipping the POST request to %s", POSTMAN_URL);
}
redirectAttributes.addFlashAttribute("message", "New Pet has been Added");
return "redirect:/owners/{ownerId}";
}

View file

@ -22,16 +22,16 @@ INSERT INTO types VALUES (default, 'snake');
INSERT INTO types VALUES (default, 'bird');
INSERT INTO types VALUES (default, 'hamster');
INSERT INTO owners VALUES (default, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023');
INSERT INTO owners VALUES (default, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749');
INSERT INTO owners VALUES (default, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763');
INSERT INTO owners VALUES (default, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198');
INSERT INTO owners VALUES (default, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765');
INSERT INTO owners VALUES (default, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654');
INSERT INTO owners VALUES (default, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387');
INSERT INTO owners VALUES (default, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683');
INSERT INTO owners VALUES (default, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435');
INSERT INTO owners VALUES (default, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487');
INSERT INTO owners VALUES (default, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023', 'george.franklin@gmail.com');
INSERT INTO owners VALUES (default, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749', 'betty.davis@gmail.com');
INSERT INTO owners VALUES (default, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763', 'eddy.rodriquez@hotmail.com');
INSERT INTO owners VALUES (default, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198', 'hide.the.pain.harold@aon.com');
INSERT INTO owners VALUES (default, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765', 'pistol.pete@gmail.com');
INSERT INTO owners VALUES (default, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654', 'jean.coleman@hotmail.com');
INSERT INTO owners VALUES (default, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387', 'jeff.black@outlook.uk');
INSERT INTO owners VALUES (default, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683', 'maria.escobito@outlook.com');
INSERT INTO owners VALUES (default, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435', 'david.schroeder@gmail.com');
INSERT INTO owners VALUES (default, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487', 'carlos.estaban@aon.com');
INSERT INTO pets VALUES (default, 'Leo', '2010-09-07', 1, 1);
INSERT INTO pets VALUES (default, 'Basil', '2012-08-06', 6, 2);
@ -51,3 +51,9 @@ INSERT INTO visits VALUES (default, 7, '2013-01-01', 'rabies shot');
INSERT INTO visits VALUES (default, 8, '2013-01-02', 'rabies shot');
INSERT INTO visits VALUES (default, 8, '2013-01-03', 'neutered');
INSERT INTO visits VALUES (default, 7, '2013-01-04', 'spayed');
-- This is the command needed to migrate an existing database to add the email address on every owner
-- I assumed it to being not nullable as it might be needed for communication with the owner,
-- but that would require us to insert either a placeholder or an empty string while we wait for all owners to insert the right email address
-- ALTER TABLE owners ADD email CHAR(50) DEFAULT 'Insert valid email address' NOT NULL

View file

@ -39,7 +39,8 @@ CREATE TABLE owners (
last_name VARCHAR_IGNORECASE(30),
address VARCHAR(255),
city VARCHAR(80),
telephone VARCHAR(20)
telephone VARCHAR(20),
email VARCHAR(50)
);
CREATE INDEX owners_last_name ON owners (last_name);

View file

@ -16,6 +16,8 @@
th:replace="~{fragments/inputField :: input ('City', 'city', 'text')}" />
<input
th:replace="~{fragments/inputField :: input ('Telephone', 'telephone', 'text')}" />
<input
th:replace="~{fragments/inputField :: input ('Email (max 50 char)', 'email', 'text')}" />
</div>
<div class="form-group">
<div class="col-sm-offset-2 col-sm-10">

View file

@ -28,6 +28,67 @@
<a class="btn btn-primary" th:href="@{/owners/new}">Add Owner</a>
<div>
<span>&nbsp;</span>
<span>&nbsp;</span>
</div>
<h2>Owners</h2>
<table id="owners" class="table table-striped">
<thead>
<tr>
<th style="width: 150px;">Name</th>
<th style="width: 200px;">Address</th>
<th>City</th>
<th style="width: 120px">Telephone</th>
<th style="width: 200px">Email</th>
<th>Pets</th>
</tr>
</thead>
<tbody>
<tr th:each="owner : ${listOwners}">
<td>
<a th:href="@{/owners/__${owner.id}__}" th:text="${owner.firstName + ' ' + owner.lastName}"/></a>
</td>
<td th:text="${owner.address}"/>
<td th:text="${owner.city}"/>
<td th:text="${owner.telephone}"/>
<td th:text="${owner.email}"/>
<td><span th:text="${#strings.listJoin(owner.pets, ', ')}"/></td>
</tr>
</tbody>
</table>
<div th:if="${totalPages > 1}">
<span>Pages:</span>
<span>[</span>
<span th:each="i: ${#numbers.sequence(1, totalPages)}">
<a th:if="${currentPage != i}" th:href="@{'/owners/find?page=' + ${i}}">[[${i}]]</a>
<span th:unless="${currentPage != i}">[[${i}]]</span>
</span>
<span>]&nbsp;</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/owners/find?page=1'}" title="First"
class="fa fa-fast-backward"></a>
<span th:unless="${currentPage > 1}" title="First" class="fa fa-fast-backward"></span>
</span>
<span>
<a th:if="${currentPage > 1}" th:href="@{'/owners/find?page=__${currentPage - 1}__'}" title="Previous"
class="fa fa-step-backward"></a>
<span th:unless="${currentPage > 1}" title="Previous" class="fa fa-step-backward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/owners/find?page=__${currentPage + 1}__'}" title="Next"
class="fa fa-step-forward"></a>
<span th:unless="${currentPage < totalPages}" title="Next" class="fa fa-step-forward"></span>
</span>
<span>
<a th:if="${currentPage < totalPages}" th:href="@{'/owners/find?page=__${totalPages}__'}" title="Last"
class="fa fa-fast-forward"></a>
<span th:unless="${currentPage < totalPages}" title="Last" class="fa fa-step-forward"></span>
</span>
</div>
</form>
</body>

View file

@ -36,6 +36,10 @@
<th>Telephone</th>
<td th:text="*{telephone}"></td>
</tr>
<tr>
<th>Email</th>
<td th:text="*{email}"></td>
</tr>
</table>
<a th:href="@{__${owner.id}__/edit}" class="btn btn-primary">Edit

View file

@ -13,6 +13,7 @@
<th style="width: 200px;">Address</th>
<th>City</th>
<th style="width: 120px">Telephone</th>
<th style="width: 200px">Email</th>
<th>Pets</th>
</tr>
</thead>
@ -24,6 +25,7 @@
<td th:text="${owner.address}"/>
<td th:text="${owner.city}"/>
<td th:text="${owner.telephone}"/>
<td th:text="${owner.email}"/>
<td><span th:text="${#strings.listJoin(owner.pets, ', ')}"/></td>
</tr>
</tbody>

View file

@ -75,6 +75,7 @@ class OwnerControllerTests {
george.setAddress("110 W. Liberty St.");
george.setCity("Madison");
george.setTelephone("6085551023");
george.setEmail("franklin.george@gmail.com");
Pet max = new Pet();
PetType dog = new PetType();
dog.setName("dog");
@ -117,7 +118,8 @@ class OwnerControllerTests {
.param("lastName", "Bloggs")
.param("address", "123 Caramel Street")
.param("city", "London")
.param("telephone", "01316761638"))
.param("telephone", "01316761638")
.param("email", "joe.bloggs@gmail.com"))
.andExpect(status().is3xxRedirection());
}
@ -129,11 +131,14 @@ class OwnerControllerTests {
.andExpect(model().attributeHasErrors("owner"))
.andExpect(model().attributeHasFieldErrors("owner", "address"))
.andExpect(model().attributeHasFieldErrors("owner", "telephone"))
.andExpect(model().attributeHasFieldErrors("owner", "email"))
.andExpect(view().name("owners/createOrUpdateOwnerForm"));
}
@Test
void testInitFindForm() throws Exception {
Page<Owner> paginatedOwners = new PageImpl<Owner>(Lists.newArrayList(george(), new Owner()));
Mockito.when(this.owners.findByLastName(anyString(), any(Pageable.class))).thenReturn(paginatedOwners);
mockMvc.perform(get("/owners/find"))
.andExpect(status().isOk())
.andExpect(model().attributeExists("owner"))
@ -178,6 +183,7 @@ class OwnerControllerTests {
.andExpect(model().attribute("owner", hasProperty("address", is("110 W. Liberty St."))))
.andExpect(model().attribute("owner", hasProperty("city", is("Madison"))))
.andExpect(model().attribute("owner", hasProperty("telephone", is("6085551023"))))
.andExpect(model().attribute("owner", hasProperty("email", is("franklin.george@gmail.com"))))
.andExpect(view().name("owners/createOrUpdateOwnerForm"));
}
@ -188,7 +194,8 @@ class OwnerControllerTests {
.param("lastName", "Bloggs")
.param("address", "123 Caramel Street")
.param("city", "London")
.param("telephone", "01616291589"))
.param("telephone", "01616291589")
.param("email", "joe.big.bloggs@gmail.com"))
.andExpect(status().is3xxRedirection())
.andExpect(view().name("redirect:/owners/{ownerId}"));
}
@ -206,11 +213,13 @@ class OwnerControllerTests {
.perform(post("/owners/{ownerId}/edit", TEST_OWNER_ID).param("firstName", "Joe")
.param("lastName", "Bloggs")
.param("address", "")
.param("telephone", ""))
.param("telephone", "")
.param("email", ""))
.andExpect(status().isOk())
.andExpect(model().attributeHasErrors("owner"))
.andExpect(model().attributeHasFieldErrors("owner", "address"))
.andExpect(model().attributeHasFieldErrors("owner", "telephone"))
.andExpect(model().attributeHasFieldErrors("owner", "email"))
.andExpect(view().name("owners/createOrUpdateOwnerForm"));
}
@ -223,6 +232,7 @@ class OwnerControllerTests {
.andExpect(model().attribute("owner", hasProperty("address", is("110 W. Liberty St."))))
.andExpect(model().attribute("owner", hasProperty("city", is("Madison"))))
.andExpect(model().attribute("owner", hasProperty("telephone", is("6085551023"))))
.andExpect(model().attribute("owner", hasProperty("email", is("franklin.george@gmail.com"))))
.andExpect(model().attribute("owner", hasProperty("pets", not(empty()))))
.andExpect(model().attribute("owner", hasProperty("pets", new BaseMatcher<List<Pet>>() {

View file

@ -111,6 +111,7 @@ class ClinicServiceTests {
owner.setAddress("4, Evans Street");
owner.setCity("Wollongong");
owner.setTelephone("4444444444");
owner.setEmail("sam.shultz@outlook.de");
this.owners.save(owner);
assertThat(owner.getId().longValue()).isNotEqualTo(0);