From 1878f691e00d35d492723756ea06d55984eda06d Mon Sep 17 00:00:00 2001 From: Moritz Eysholdt Date: Thu, 19 Mar 2026 05:59:01 +0000 Subject: [PATCH] feat(CX-215): add first name search to find owners form Add a first name input field to the find owners form so users can narrow results when multiple owners share a surname. Both fields are optional and use prefix matching. - Add findByFirstNameStartingWithAndLastNameStartingWith to OwnerRepository - Update OwnerController to filter by both first and last name - Add first name input to findOwners.html template - Preserve both query params in ownersList.html pagination links - Add tests for first-name-only, combined, and no-results scenarios Co-authored-by: Ona --- .../petclinic/owner/OwnerController.java | 14 ++++-- .../petclinic/owner/OwnerRepository.java | 12 +++++ .../templates/owners/findOwners.html | 8 ++++ .../templates/owners/ownersList.html | 10 ++-- .../petclinic/owner/OwnerControllerTests.java | 47 +++++++++++++++++-- 5 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java b/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java index 199ca361..f0670f01 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java @@ -99,9 +99,13 @@ public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner if (lastName == null) { lastName = ""; // empty string signifies broadest possible search } + String firstName = owner.getFirstName(); + if (firstName == null) { + firstName = ""; // empty string signifies broadest possible search + } - // find owners by last name - Page ownersResults = findPaginatedForOwnersLastName(page, lastName); + // find owners by first name and last name + Page ownersResults = findPaginatedForOwners(page, firstName, lastName); if (ownersResults.isEmpty()) { // no owners found result.rejectValue("lastName", "notFound", "not found"); @@ -115,6 +119,8 @@ public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner } // multiple owners found + model.addAttribute("firstName", firstName); + model.addAttribute("lastName", lastName); return addPaginationModel(page, model, ownersResults); } @@ -127,10 +133,10 @@ private String addPaginationModel(int page, Model model, Page paginated) return "owners/ownersList"; } - private Page findPaginatedForOwnersLastName(int page, String lastname) { + private Page findPaginatedForOwners(int page, String firstName, String lastName) { int pageSize = 5; Pageable pageable = PageRequest.of(page - 1, pageSize); - return owners.findByLastNameStartingWith(lastname, pageable); + return owners.findByFirstNameStartingWithAndLastNameStartingWith(firstName, lastName, pageable); } @GetMapping("/owners/{ownerId}/edit") diff --git a/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java b/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java index d2b3dde4..d0b7ca85 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java @@ -44,6 +44,18 @@ public interface OwnerRepository extends JpaRepository { */ Page findByLastNameStartingWith(String lastName, Pageable pageable); + /** + * Retrieve {@link Owner}s from the data store by first name and last name, returning + * all owners whose first name starts with the given first name and whose last + * name starts with the given last name. + * @param firstName Value to search for in first name + * @param lastName Value to search for in last name + * @return a Collection of matching {@link Owner}s (or an empty Collection if none + * found) + */ + Page findByFirstNameStartingWithAndLastNameStartingWith(String firstName, String lastName, + Pageable pageable); + /** * Retrieve an {@link Owner} from the data store by id. *

diff --git a/src/main/resources/templates/owners/findOwners.html b/src/main/resources/templates/owners/findOwners.html index 703351c7..5a163912 100644 --- a/src/main/resources/templates/owners/findOwners.html +++ b/src/main/resources/templates/owners/findOwners.html @@ -7,6 +7,14 @@

Find Owners

+
+
+ +
+ +
+
+
diff --git a/src/main/resources/templates/owners/ownersList.html b/src/main/resources/templates/owners/ownersList.html index 01223c1c..880006c8 100644 --- a/src/main/resources/templates/owners/ownersList.html +++ b/src/main/resources/templates/owners/ownersList.html @@ -32,26 +32,26 @@

Owners

Pages: [ - [[${i}]] + [[${i}]] [[${i}]] - + - - - diff --git a/src/test/java/org/springframework/samples/petclinic/owner/OwnerControllerTests.java b/src/test/java/org/springframework/samples/petclinic/owner/OwnerControllerTests.java index bcab1974..5560b4e9 100644 --- a/src/test/java/org/springframework/samples/petclinic/owner/OwnerControllerTests.java +++ b/src/test/java/org/springframework/samples/petclinic/owner/OwnerControllerTests.java @@ -91,7 +91,8 @@ private Owner george() { void setup() { Owner george = george(); - given(this.owners.findByLastNameStartingWith(eq("Franklin"), any(Pageable.class))) + given(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(eq(""), eq("Franklin"), + any(Pageable.class))) .willReturn(new PageImpl<>(List.of(george))); given(this.owners.findById(TEST_OWNER_ID)).willReturn(Optional.of(george)); @@ -142,29 +143,67 @@ void testInitFindForm() throws Exception { @Test void testProcessFindFormSuccess() throws Exception { Page tasks = new PageImpl<>(List.of(george(), new Owner())); - when(this.owners.findByLastNameStartingWith(anyString(), any(Pageable.class))).thenReturn(tasks); + when(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(anyString(), anyString(), + any(Pageable.class))) + .thenReturn(tasks); mockMvc.perform(get("/owners?page=1")).andExpect(status().isOk()).andExpect(view().name("owners/ownersList")); } @Test void testProcessFindFormByLastName() throws Exception { Page tasks = new PageImpl<>(List.of(george())); - when(this.owners.findByLastNameStartingWith(eq("Franklin"), any(Pageable.class))).thenReturn(tasks); + when(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(eq(""), eq("Franklin"), + any(Pageable.class))) + .thenReturn(tasks); mockMvc.perform(get("/owners?page=1").param("lastName", "Franklin")) .andExpect(status().is3xxRedirection()) .andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID)); } + @Test + void testProcessFindFormByFirstName() throws Exception { + Page tasks = new PageImpl<>(List.of(george())); + when(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(eq("George"), eq(""), any(Pageable.class))) + .thenReturn(tasks); + mockMvc.perform(get("/owners?page=1").param("firstName", "George")) + .andExpect(status().is3xxRedirection()) + .andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID)); + } + + @Test + void testProcessFindFormByFirstNameAndLastName() throws Exception { + Page tasks = new PageImpl<>(List.of(george())); + when(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(eq("George"), eq("Franklin"), + any(Pageable.class))) + .thenReturn(tasks); + mockMvc.perform(get("/owners?page=1").param("firstName", "George").param("lastName", "Franklin")) + .andExpect(status().is3xxRedirection()) + .andExpect(view().name("redirect:/owners/" + TEST_OWNER_ID)); + } + @Test void testProcessFindFormNoOwnersFound() throws Exception { Page tasks = new PageImpl<>(List.of()); - when(this.owners.findByLastNameStartingWith(eq("Unknown Surname"), any(Pageable.class))).thenReturn(tasks); + when(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(eq(""), eq("Unknown Surname"), + any(Pageable.class))) + .thenReturn(tasks); mockMvc.perform(get("/owners?page=1").param("lastName", "Unknown Surname")) .andExpect(status().isOk()) .andExpect(model().attributeHasFieldErrors("owner", "lastName")) .andExpect(model().attributeHasFieldErrorCode("owner", "lastName", "notFound")) .andExpect(view().name("owners/findOwners")); + } + @Test + void testProcessFindFormByFirstNameNoOwnersFound() throws Exception { + Page tasks = new PageImpl<>(List.of()); + when(this.owners.findByFirstNameStartingWithAndLastNameStartingWith(eq("Unknown"), eq(""), any(Pageable.class))) + .thenReturn(tasks); + mockMvc.perform(get("/owners?page=1").param("firstName", "Unknown")) + .andExpect(status().isOk()) + .andExpect(model().attributeHasFieldErrors("owner", "lastName")) + .andExpect(model().attributeHasFieldErrorCode("owner", "lastName", "notFound")) + .andExpect(view().name("owners/findOwners")); } @Test