diff --git a/src/main/java/org/springframework/samples/petclinic/owner/PetController.java b/src/main/java/org/springframework/samples/petclinic/owner/PetController.java index 8398e4f13..74acda684 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/PetController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/PetController.java @@ -15,15 +15,12 @@ */ package org.springframework.samples.petclinic.owner; -import java.time.LocalDate; import java.util.Collection; -import java.util.Objects; import java.util.Optional; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.util.Assert; -import org.springframework.util.StringUtils; import org.springframework.validation.BindingResult; import org.springframework.web.bind.WebDataBinder; import org.springframework.web.bind.annotation.GetMapping; @@ -53,9 +50,12 @@ class PetController { private final PetTypeRepository types; - public PetController(OwnerRepository owners, PetTypeRepository types) { + private final PetValidationService petValidationService; + + public PetController(OwnerRepository owners, PetTypeRepository types, PetValidationService petValidationService) { this.owners = owners; this.types = types; + this.petValidationService = petValidationService; } @ModelAttribute("types") @@ -106,13 +106,7 @@ class PetController { public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result, RedirectAttributes redirectAttributes) { - if (StringUtils.hasText(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null) - result.rejectValue("name", "duplicate", "already exists"); - - LocalDate currentDate = LocalDate.now(); - if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(currentDate)) { - result.rejectValue("birthDate", "typeMismatch.birthDate"); - } + this.petValidationService.validateForCreation(pet, owner, result); if (result.hasErrors()) { return VIEWS_PETS_CREATE_OR_UPDATE_FORM; @@ -133,20 +127,8 @@ class PetController { public String processUpdateForm(Owner owner, @Valid Pet pet, BindingResult result, RedirectAttributes redirectAttributes) { - String petName = pet.getName(); - - // checking if the pet name already exists for the owner - if (StringUtils.hasText(petName)) { - Pet existingPet = owner.getPet(petName, false); - if (existingPet != null && !Objects.equals(existingPet.getId(), pet.getId())) { - result.rejectValue("name", "duplicate", "already exists"); - } - } - - LocalDate currentDate = LocalDate.now(); - if (pet.getBirthDate() != null && pet.getBirthDate().isAfter(currentDate)) { - result.rejectValue("birthDate", "typeMismatch.birthDate"); - } + // Delegated validation to service - CCN reduced from 7 to 3 + this.petValidationService.validateForUpdate(pet, owner, result); if (result.hasErrors()) { return VIEWS_PETS_CREATE_OR_UPDATE_FORM; diff --git a/src/main/java/org/springframework/samples/petclinic/owner/PetValidationService.java b/src/main/java/org/springframework/samples/petclinic/owner/PetValidationService.java new file mode 100644 index 000000000..ab6e7447f --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/owner/PetValidationService.java @@ -0,0 +1,124 @@ +/* + * Copyright 2012-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.samples.petclinic.owner; + +import java.time.LocalDate; +import java.util.Objects; + +import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; +import org.springframework.validation.Errors; + +/** + * Service responsible for Pet validation logic. Extracts complex validation rules from + * the controller layer. Follows Single Responsibility Principle (SRP) from SOLID. + * + * @author Nathan Dalbert + * @author Paulo Henrique + * @author Mickael de Albuquerque + * @author Igor Rego + */ +@Service +public class PetValidationService { + + /** + * Validates pet name for duplicates during creation. + * @param pet the pet being created + * @param owner the owner of the pet + * @param errors validation errors container + * @return true if validation passes, false otherwise + */ + public boolean validatePetNameForCreation(Pet pet, Owner owner, Errors errors) { + if (!StringUtils.hasText(pet.getName())) { + return true; // Let @Valid handle blank names + } + + if (pet.isNew() && owner.getPet(pet.getName(), true) != null) { + errors.rejectValue("name", "duplicate", "already exists"); + return false; + } + + return true; + } + + /** + * Validates pet name for duplicates during update. + * @param pet the pet being updated + * @param owner the owner of the pet + * @param errors validation errors container + * @return true if validation passes, false otherwise + */ + public boolean validatePetNameForUpdate(Pet pet, Owner owner, Errors errors) { + if (!StringUtils.hasText(pet.getName())) { + return true; // Let @Valid handle blank names + } + + Pet existingPet = owner.getPet(pet.getName(), false); + if (existingPet != null && !Objects.equals(existingPet.getId(), pet.getId())) { + errors.rejectValue("name", "duplicate", "already exists"); + return false; + } + + return true; + } + + /** + * Validates that birth date is not in the future. + * @param pet the pet being validated + * @param errors validation errors container + * @return true if validation passes, false otherwise + */ + public boolean validateBirthDate(Pet pet, Errors errors) { + if (pet.getBirthDate() == null) { + return true; + } + + LocalDate currentDate = LocalDate.now(); + if (pet.getBirthDate().isAfter(currentDate)) { + errors.rejectValue("birthDate", "typeMismatch.birthDate"); + return false; + } + + return true; + } + + /** + * Performs all validations for pet creation. + * @param pet the pet being created + * @param owner the owner of the pet + * @param errors validation errors container + * @return true if all validations pass, false otherwise + */ + public boolean validateForCreation(Pet pet, Owner owner, Errors errors) { + boolean isNameValid = validatePetNameForCreation(pet, owner, errors); + boolean isBirthDateValid = validateBirthDate(pet, errors); + return isNameValid && isBirthDateValid; + } + + /** + * Performs all validations for pet update. + * @param pet the pet being updated + * @param owner the owner of the pet + * @param errors validation errors container + * @return true if all validations pass, false otherwise + */ + public boolean validateForUpdate(Pet pet, Owner owner, Errors errors) { + boolean isNameValid = validatePetNameForUpdate(pet, owner, errors); + boolean isBirthDateValid = validateBirthDate(pet, errors); + return isNameValid && isBirthDateValid; + } + +} diff --git a/src/test/java/org/springframework/samples/petclinic/owner/PetControllerTests.java b/src/test/java/org/springframework/samples/petclinic/owner/PetControllerTests.java index 391bb3dbb..fee75bff7 100644 --- a/src/test/java/org/springframework/samples/petclinic/owner/PetControllerTests.java +++ b/src/test/java/org/springframework/samples/petclinic/owner/PetControllerTests.java @@ -46,7 +46,8 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers. * @author Wick Dynex */ @WebMvcTest(value = PetController.class, - includeFilters = @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE)) + includeFilters = { @ComponentScan.Filter(value = PetTypeFormatter.class, type = FilterType.ASSIGNABLE_TYPE), + @ComponentScan.Filter(value = PetValidationService.class, type = FilterType.ASSIGNABLE_TYPE) }) @DisabledInNativeImage @DisabledInAotMode class PetControllerTests {