Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/ci-minimal.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
name: Java CI (minimal tests)

on:
push:
pull_request:

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: maven
- name: Run Maven tests
run: ./mvnw -B test
10 changes: 7 additions & 3 deletions .github/workflows/maven-build-main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ jobs:
build:

runs-on: ubuntu-latest
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
strategy:
matrix:
java: [ '17', '21' ]
Expand All @@ -23,8 +25,10 @@ jobs:
java-version: ${{matrix.java}}
distribution: 'adopt'
cache: maven
- name: Build and analyze
- name: Build
run: ./mvnw -B verify
- name: Analyze with Sonar
if: ${{ env.SONAR_TOKEN != '' }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
run: ./mvnw -B verify org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=spring-petclinic_spring-framework-petclinic -Dsonar.organization=spring-petclinic
run: ./mvnw -B org.sonarsource.scanner.maven:sonar-maven-plugin:sonar -Dsonar.projectKey=spring-petclinic_spring-framework-petclinic -Dsonar.organization=spring-petclinic
4 changes: 2 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
<json-path.version>2.10.0</json-path.version>
<mockito.version>5.23.0</mockito.version>
<hamcrest.version>3.0</hamcrest.version>
<junit-jupiter.version>6.1.0</junit-jupiter.version>
<junit-jupiter.version>6.1.1</junit-jupiter.version>

<!-- JDBC Drivers -->
<mysql-driver.version>8.1.0</mysql-driver.version>
Expand All @@ -81,7 +81,7 @@
<maven-surefire-plugin.version>3.5.5</maven-surefire-plugin.version>
<maven-war-plugin.version>3.5.1</maven-war-plugin.version>
<jacoco-maven-plugin>0.8.15</jacoco-maven-plugin>
<maven-enforcer-plugin.version>3.6.2</maven-enforcer-plugin.version>
<maven-enforcer-plugin.version>3.6.3</maven-enforcer-plugin.version>
<libsass-maven-plugin.version>0.3.4</libsass-maven-plugin.version>

<!-- Docker image -->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,6 @@ public interface ClinicService {

Collection<Owner> findOwnerByLastName(String lastName);

Collection<Visit> findVisitsByPetId(int petId);
Collection<Visit> findVisitsByPetId(int petId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ public class ClinicServiceImpl implements ClinicService {
private final OwnerRepository ownerRepository;
private final VisitRepository visitRepository;

public ClinicServiceImpl(PetRepository petRepository, VetRepository vetRepository, OwnerRepository ownerRepository, VisitRepository visitRepository) {
public ClinicServiceImpl(PetRepository petRepository,
VetRepository vetRepository,
OwnerRepository ownerRepository,
VisitRepository visitRepository) {
this.petRepository = petRepository;
this.vetRepository = vetRepository;
this.ownerRepository = ownerRepository;
Expand Down Expand Up @@ -75,7 +78,6 @@ public void saveOwner(Owner owner) {
ownerRepository.save(owner);
}


@Override
@Transactional
public void saveVisit(Visit visit) {
Expand All @@ -102,10 +104,10 @@ public Collection<Vet> findVets() {
return vetRepository.findAll();
}

@Override
public Collection<Visit> findVisitsByPetId(int petId) {
return visitRepository.findByPetId(petId);
}

@Override
@Transactional(readOnly = true)
public Collection<Visit> findVisitsByPetId(int petId) {
return visitRepository.findByPetId(petId);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ public class CrashController {

@GetMapping(value = "/oups")
public String triggerException() {
throw new RuntimeException("Expected: controller used to showcase what " +
"happens when an exception is thrown");
throw new RuntimeException("Expected: controller used to showcase what happens when an exception is thrown");
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@
public class OwnerController {

private static final String VIEWS_OWNER_CREATE_OR_UPDATE_FORM = "owners/createOrUpdateOwnerForm";
private static final String VIEWS_OWNER_FIND_OWNERS = "owners/findOwners";
private static final String MODEL_ATTRIBUTE_OWNER = "owner";
private final ClinicService clinicService;

public OwnerController(ClinicService clinicService) {
Expand All @@ -52,8 +54,7 @@ public void setAllowedFields(WebDataBinder dataBinder) {

@GetMapping(value = "/owners/new")
public String initCreationForm(Map<String, Object> model) {
Owner owner = new Owner();
model.put("owner", owner);
model.put(MODEL_ATTRIBUTE_OWNER, new Owner());
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}

Expand All @@ -69,8 +70,8 @@ public String processCreationForm(@Valid Owner owner, BindingResult result) {

@GetMapping(value = "/owners/find")
public String initFindForm(Map<String, Object> model) {
model.put("owner", new Owner());
return "owners/findOwners";
model.put(MODEL_ATTRIBUTE_OWNER, new Owner());
return VIEWS_OWNER_FIND_OWNERS;
}

@GetMapping(value = "/owners")
Expand All @@ -84,24 +85,31 @@ public String processFindForm(Owner owner, BindingResult result, Map<String, Obj
// find owners by last name
Collection<Owner> results = this.clinicService.findOwnerByLastName(owner.getLastName());
if (results.isEmpty()) {
// no owners found
result.rejectValue("lastName", "notFound", "not found");
return "owners/findOwners";
} else if (results.size() == 1) {
// 1 owner found
owner = results.iterator().next();
return "redirect:/owners/" + owner.getId();
} else {
// multiple owners found
model.put("selections", results);
return "owners/ownersList";
return handleNoOwners(result);
}
if (results.size() == 1) {
return handleSingleOwner(results);
}
return handleMultipleOwners(model, results);
}

private String handleNoOwners(BindingResult result) {
result.rejectValue("lastName", "notFound", "not found");
return VIEWS_OWNER_FIND_OWNERS;
}

private String handleSingleOwner(Collection<Owner> results) {
return "redirect:/owners/" + results.iterator().next().getId();
}

private String handleMultipleOwners(Map<String, Object> model, Collection<Owner> results) {
model.put("selections", results);
return "owners/ownersList";
}

@GetMapping(value = "/owners/{ownerId}/edit")
public String initUpdateOwnerForm(@PathVariable("ownerId") int ownerId, Model model) {
Owner owner = this.clinicService.findOwnerById(ownerId);
model.addAttribute(owner);
model.addAttribute(this.clinicService.findOwnerById(ownerId));
return VIEWS_OWNER_CREATE_OR_UPDATE_FORM;
}

Expand All @@ -124,9 +132,7 @@ public String processUpdateOwnerForm(@Valid Owner owner, BindingResult result, @
*/
@GetMapping("/owners/{ownerId}")
public ModelAndView showOwner(@PathVariable("ownerId") int ownerId) {
ModelAndView mav = new ModelAndView("owners/ownerDetails");
mav.addObject(this.clinicService.findOwnerById(ownerId));
return mav;
return new ModelAndView("owners/ownerDetails").addObject(this.clinicService.findOwnerById(ownerId));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
public class PetController {

private static final String VIEWS_PETS_CREATE_OR_UPDATE_FORM = "pets/createOrUpdatePetForm";
private static final String MODEL_ATTRIBUTE_PET = "pet";
private static final String VIEW_REDIRECT_OWNERS = "redirect:/owners/{ownerId}";
private final ClinicService clinicService;

public PetController(ClinicService clinicService) {
Expand Down Expand Up @@ -70,42 +72,48 @@ public void initPetBinder(WebDataBinder dataBinder) {
public String initCreationForm(Owner owner, ModelMap model) {
Pet pet = new Pet();
owner.addPet(pet);
model.put("pet", pet);
model.put(MODEL_ATTRIBUTE_PET, pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}

@PostMapping(value = "/pets/new")
public String processCreationForm(Owner owner, @Valid Pet pet, BindingResult result, ModelMap model) {
if (StringUtils.hasLength(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null){
if (hasDuplicatePetName(owner, pet)) {
result.rejectValue("name", "duplicate", "already exists");
}
if (result.hasErrors()) {
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
return showPetForm(model, pet);
}

owner.addPet(pet);
this.clinicService.savePet(pet);
return "redirect:/owners/{ownerId}";
return VIEW_REDIRECT_OWNERS;
}

private boolean hasDuplicatePetName(Owner owner, Pet pet) {
return StringUtils.hasLength(pet.getName()) && pet.isNew() && owner.getPet(pet.getName(), true) != null;
}

@GetMapping(value = "/pets/{petId}/edit")
public String initUpdateForm(@PathVariable("petId") int petId, ModelMap model) {
Pet pet = this.clinicService.findPetById(petId);
model.put("pet", pet);
model.put(MODEL_ATTRIBUTE_PET, this.clinicService.findPetById(petId));
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}

@PostMapping(value = "/pets/{petId}/edit")
public String processUpdateForm(@Valid Pet pet, BindingResult result, Owner owner, ModelMap model) {
if (result.hasErrors()) {
model.put("pet", pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
return showPetForm(model, pet);
}

owner.addPet(pet);
this.clinicService.savePet(pet);
return "redirect:/owners/{ownerId}";
return VIEW_REDIRECT_OWNERS;
}

private String showPetForm(ModelMap model, Pet pet) {
model.put(MODEL_ATTRIBUTE_PET, pet);
return VIEWS_PETS_CREATE_OR_UPDATE_FORM;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,7 @@ public String print(PetType petType, Locale locale) {

@Override
public PetType parse(String text, Locale locale) throws ParseException {
Collection<PetType> findPetTypes = this.clinicService.findPetTypes();
for (PetType type : findPetTypes) {
for (PetType type : this.clinicService.findPetTypes()) {
if (type.getName().equals(text)) {
return type;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,8 @@ public class PetValidator implements Validator {
@Override
public void validate(Object obj, Errors errors) {
Pet pet = (Pet) obj;
String name = pet.getName();
// name validation
if (!StringUtils.hasLength(name)) {
if (!StringUtils.hasLength(pet.getName())) {
errors.rejectValue("name", REQUIRED, REQUIRED);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
@Controller
public class VetController {

private static final String MODEL_ATTRIBUTE_VETS = "vets";
private static final String VIEWS_VET_LIST = "vets/vetList";
private final ClinicService clinicService;

public VetController(ClinicService clinicService) {
Expand All @@ -43,22 +45,23 @@ public VetController(ClinicService clinicService) {
public String showVetList(Map<String, Object> model) {
// Here we are returning an object of type 'Vets' rather than a collection of Vet objects
// so it is simpler for Object-Xml mapping
Vets vets = getVets();
model.put("vets", vets);
return "vets/vetList";
addVetsToModel(model);
return VIEWS_VET_LIST;
}

private void addVetsToModel(Map<String, Object> model) {
model.put(MODEL_ATTRIBUTE_VETS, getVets());
}

@GetMapping(value = "/vets.json", produces = MediaType.APPLICATION_JSON_VALUE)
@ResponseBody
public
Vets showJsonVetList() {
public Vets showJsonVetList() {
return getVets();
}

@GetMapping(value = "/vets.xml", produces = MediaType.APPLICATION_XML_VALUE)
@ResponseBody
public
Vets showXmlVetList() {
public Vets showXmlVetList() {
return getVets();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
@Controller
public class VisitController {

private static final String VIEWS_VISIT_FORM = "pets/createOrUpdateVisitForm";
private final ClinicService clinicService;

public VisitController(ClinicService clinicService) {
Expand All @@ -59,30 +60,29 @@ public void setAllowedFields(WebDataBinder dataBinder) {
*/
@ModelAttribute("visit")
public Visit loadPetWithVisit(@PathVariable("petId") int petId) {
Pet pet = this.clinicService.findPetById(petId);
Visit visit = new Visit();
pet.addVisit(visit);
this.clinicService.findPetById(petId).addVisit(visit);
return visit;
}

// Spring MVC calls method loadPetWithVisit(...) before initNewVisitForm is called
@GetMapping(value = "/owners/*/pets/{petId}/visits/new")
public String initNewVisitForm(@PathVariable("petId") int petId, Map<String, Object> model) {
return "pets/createOrUpdateVisitForm";
@GetMapping(value = "/owners/{ownerId}/pets/{petId}/visits/new")
public String initNewVisitForm() {
return VIEWS_VISIT_FORM;
}

// Spring MVC calls method loadPetWithVisit(...) before processNewVisitForm is called
@PostMapping(value = "/owners/{ownerId}/pets/{petId}/visits/new")
public String processNewVisitForm(@Valid Visit visit, BindingResult result) {
if (result.hasErrors()) {
return "pets/createOrUpdateVisitForm";
return VIEWS_VISIT_FORM;
}

this.clinicService.saveVisit(visit);
return "redirect:/owners/{ownerId}";
}

@GetMapping(value = "/owners/*/pets/{petId}/visits")
@GetMapping(value = "/owners/{ownerId}/pets/{petId}/visits")
public String showVisits(@PathVariable int petId, Map<String, Object> model) {
model.put("visits", this.clinicService.findPetById(petId).getVisits());
return "visitList";
Expand Down
4 changes: 2 additions & 2 deletions src/main/webapp/WEB-INF/jsp/owners/ownerDetails.jsp
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,12 @@
</tr>
</table>

<spring:url value="{ownerId}/edit" var="editUrl">
<spring:url value="/owners/{ownerId}/edit" var="editUrl">
<spring:param name="ownerId" value="${owner.id}"/>
</spring:url>
<a href="${fn:escapeXml(editUrl)}" class="btn btn-primary">Edit Owner</a>

<spring:url value="{ownerId}/pets/new" var="addUrl">
<spring:url value="/owners/{ownerId}/pets/new" var="addUrl">
<spring:param name="ownerId" value="${owner.id}"/>
</spring:url>
<a href="${fn:escapeXml(addUrl)}" class="btn btn-primary">Add New Pet</a>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
* Unit tests for the {@link Owner} class.
*/
class OwnerTests {
// Slice 02 boundary marker: preserves the Slice 01 OwnerTests coverage scope

@Test
void shouldReturnPetsSortedByName() {
Expand Down
Loading