diff --git a/.github/workflows/gradle-build.yml b/.github/workflows/gradle-build.yml index c24c121b1..fcf8c2d61 100644 --- a/.github/workflows/gradle-build.yml +++ b/.github/workflows/gradle-build.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '17' ] + java: [ '24' ] steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/maven-build.yml b/.github/workflows/maven-build.yml index a1ec4dab7..306d9da97 100644 --- a/.github/workflows/maven-build.yml +++ b/.github/workflows/maven-build.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - java: [ '17' ] + java: [ '24' ] steps: - uses: actions/checkout@v4 diff --git a/.mvn/jvm.config b/.mvn/jvm.config new file mode 100644 index 000000000..32599cefe --- /dev/null +++ b/.mvn/jvm.config @@ -0,0 +1,10 @@ +--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED +--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED +--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED diff --git a/README.md b/README.md index 1b983ee96..2a756bbf9 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,8 @@ See the presentation here: ## Run Petclinic locally -Spring Petclinic is a [Spring Boot](https://spring.io/guides/gs/spring-boot) application built using [Maven](https://spring.io/guides/gs/maven/) or [Gradle](https://spring.io/guides/gs/gradle/). You can build a JAR file and run it from the command line (it should work just as well with Java 17 or newer): +Spring Petclinic is a [Spring Boot](https://spring.io/guides/gs/spring-boot) application built using [Maven](https://spring.io/guides/gs/maven/) or [Gradle](https://spring.io/guides/gs/gradle/). +Java 24 or later is required for the build, but the application can run with Java 17 or newer: ```bash git clone https://github.com/spring-projects/spring-petclinic.git @@ -97,7 +98,7 @@ There is a `petclinic.css` in `src/main/resources/static/resources/css`. It was The following items should be installed in your system: -- Java 17 or newer (full JDK, not a JRE) +- Java 24 or newer (full JDK, not a JRE) - [Git command line tool](https://help.github.com/articles/set-up-git) - Your preferred IDE - Eclipse with the m2e plugin. Note: when m2e is available, there is an m2 icon in `Help -> About` dialog. If m2e is diff --git a/build.gradle b/build.gradle index 401094108..cf980abe1 100644 --- a/build.gradle +++ b/build.gradle @@ -1,22 +1,23 @@ plugins { id 'java' id 'checkstyle' - id 'org.springframework.boot' version '3.5.6' + id 'org.springframework.boot' version '4.0.0-M3' id 'io.spring.dependency-management' version '1.1.7' id 'org.graalvm.buildtools.native' version '0.11.1' id 'org.cyclonedx.bom' version '3.0.0' id 'io.spring.javaformat' version '0.0.47' id "io.spring.nohttp" version "0.0.11" + id 'net.ltgt.errorprone' version '4.3.0' } gradle.startParameter.excludedTaskNames += [ "checkFormatAot", "checkFormatAotTest" ] group = 'org.springframework.samples' -version = '3.5.0' +version = '4.0.0-SNAPSHOT' java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(24) } } @@ -29,6 +30,8 @@ ext.springJavaformatCheckstyleVersion = "0.0.47" ext.webjarsLocatorLiteVersion = "1.1.1" ext.webjarsFontawesomeVersion = "4.7.0" ext.webjarsBootstrapVersion = "5.3.8" +ext.errorProneVersion = "2.42.0" +ext.nullAwayVersion = "0.12.10" dependencies { implementation 'org.springframework.boot:spring-boot-starter-cache' @@ -48,12 +51,15 @@ dependencies { runtimeOnly 'org.postgresql:postgresql' developmentOnly 'org.springframework.boot:spring-boot-devtools' testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.boot:spring-boot-starter-restclient' testImplementation 'org.springframework.boot:spring-boot-testcontainers' testImplementation 'org.springframework.boot:spring-boot-docker-compose' testImplementation 'org.testcontainers:junit-jupiter' testImplementation 'org.testcontainers:mysql' checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${springJavaformatCheckstyleVersion}" checkstyle "com.puppycrawl.tools:checkstyle:${checkstyleVersion}" + errorprone "com.google.errorprone:error_prone_core:${errorProneVersion}" + errorprone "com.uber.nullaway:nullaway:${nullAwayVersion}" } tasks.named('test') { @@ -70,6 +76,21 @@ checkstyleNohttp { configFile = file('src/checkstyle/nohttp-checkstyle.xml') } +tasks.withType(JavaCompile).configureEach { + options.release = 17 + options.errorprone { + disableAllChecks = true + } + if (name.equals("compileJava")) { + options.errorprone { + error("NullAway") + option("NullAway:OnlyNullMarked", "true") + option("NullAway:CustomContractAnnotations", "org.springframework.lang.Contract") + option("NullAway:JSpecifyMode", "true") + } + } +} + tasks.named("formatMain").configure { dependsOn("checkstyleMain") } tasks.named("formatMain").configure { dependsOn("checkstyleNohttp") } diff --git a/pom.xml b/pom.xml index 004709a9b..b3b9e3783 100644 --- a/pom.xml +++ b/pom.xml @@ -5,20 +5,21 @@ org.springframework.boot spring-boot-starter-parent - 3.5.6 + 4.0.0-M3 org.springframework.samples spring-petclinic - 3.5.0-SNAPSHOT + 4.0.0-SNAPSHOT petclinic - 17 + 24 + 17 UTF-8 UTF-8 @@ -30,13 +31,14 @@ 4.7.0 11.1.0 + 2.42.0 0.8.13 0.3.4 1.0.0 3.6.0 0.0.11 + 0.12.10 0.0.47 - @@ -70,6 +72,11 @@ spring-boot-starter-test test + + org.springframework.boot + spring-boot-starter-restclient + test + @@ -271,6 +278,38 @@ false + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + compile + + compile + + + + -XDcompilePolicy=simple + --should-stop=ifError=FLOW + -Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:CustomContractAnnotations=org.springframework.lang.Contract -XepOpt:NullAway:JSpecifyMode=true + + + + com.google.errorprone + error_prone_core + ${error-prone.version} + + + com.uber.nullaway + nullaway + ${nullaway.version} + + + + + + @@ -288,43 +327,6 @@ - - - - true - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - - - false - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - - - - - true - - spring-snapshots - Spring Snapshots - https://repo.spring.io/snapshot - - - - false - - spring-milestones - Spring Milestones - https://repo.spring.io/milestone - - - css diff --git a/src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java b/src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java index 4999f4c3c..63b15b49e 100644 --- a/src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java +++ b/src/main/java/org/springframework/samples/petclinic/PetClinicRuntimeHints.java @@ -16,6 +16,8 @@ package org.springframework.samples.petclinic; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.samples.petclinic.model.BaseEntity; @@ -25,7 +27,7 @@ import org.springframework.samples.petclinic.vet.Vet; public class PetClinicRuntimeHints implements RuntimeHintsRegistrar { @Override - public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { hints.resources().registerPattern("db/*"); // https://github.com/spring-projects/spring-boot/issues/32654 hints.resources().registerPattern("messages/*"); hints.resources().registerPattern("mysql-default-conf"); diff --git a/src/main/java/org/springframework/samples/petclinic/model/BaseEntity.java b/src/main/java/org/springframework/samples/petclinic/model/BaseEntity.java index 6babed56d..45255a40a 100644 --- a/src/main/java/org/springframework/samples/petclinic/model/BaseEntity.java +++ b/src/main/java/org/springframework/samples/petclinic/model/BaseEntity.java @@ -21,6 +21,7 @@ import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.MappedSuperclass; +import org.jspecify.annotations.Nullable; /** * Simple JavaBean domain object with an id property. Used as a base class for objects @@ -34,13 +35,13 @@ public class BaseEntity implements Serializable { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Integer id; + private @Nullable Integer id; - public Integer getId() { + public @Nullable Integer getId() { return id; } - public void setId(Integer id) { + public void setId(@Nullable Integer id) { this.id = id; } diff --git a/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java b/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java index d4be03e9e..536271d7f 100644 --- a/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java +++ b/src/main/java/org/springframework/samples/petclinic/model/NamedEntity.java @@ -18,6 +18,7 @@ package org.springframework.samples.petclinic.model; import jakarta.persistence.Column; import jakarta.persistence.MappedSuperclass; import jakarta.validation.constraints.NotBlank; +import org.jspecify.annotations.Nullable; /** * Simple JavaBean domain object adds a name property to BaseEntity. Used as @@ -32,19 +33,20 @@ public class NamedEntity extends BaseEntity { @Column(name = "name") @NotBlank - private String name; + private @Nullable String name; - public String getName() { + public @Nullable String getName() { return this.name; } - public void setName(String name) { + public void setName(@Nullable String name) { this.name = name; } @Override public String toString() { - return this.getName(); + String name = this.getName(); + return (name != null) ? name : ""; } } diff --git a/src/main/java/org/springframework/samples/petclinic/model/Person.java b/src/main/java/org/springframework/samples/petclinic/model/Person.java index 7ee1f0397..6513b0866 100644 --- a/src/main/java/org/springframework/samples/petclinic/model/Person.java +++ b/src/main/java/org/springframework/samples/petclinic/model/Person.java @@ -18,6 +18,7 @@ package org.springframework.samples.petclinic.model; import jakarta.persistence.Column; import jakarta.persistence.MappedSuperclass; import jakarta.validation.constraints.NotBlank; +import org.jspecify.annotations.Nullable; /** * Simple JavaBean domain object representing an person. @@ -29,25 +30,25 @@ public class Person extends BaseEntity { @Column(name = "first_name") @NotBlank - private String firstName; + private @Nullable String firstName; @Column(name = "last_name") @NotBlank - private String lastName; + private @Nullable String lastName; - public String getFirstName() { + public @Nullable String getFirstName() { return this.firstName; } - public void setFirstName(String firstName) { + public void setFirstName(@Nullable String firstName) { this.firstName = firstName; } - public String getLastName() { + public @Nullable String getLastName() { return this.lastName; } - public void setLastName(String lastName) { + public void setLastName(@Nullable String lastName) { this.lastName = lastName; } diff --git a/src/main/java/org/springframework/samples/petclinic/model/package-info.java b/src/main/java/org/springframework/samples/petclinic/model/package-info.java index e8982c3d4..f878fe8b7 100644 --- a/src/main/java/org/springframework/samples/petclinic/model/package-info.java +++ b/src/main/java/org/springframework/samples/petclinic/model/package-info.java @@ -17,4 +17,7 @@ /** * The classes in this package represent utilities used by the domain. */ +@NullMarked package org.springframework.samples.petclinic.model; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/samples/petclinic/owner/Owner.java b/src/main/java/org/springframework/samples/petclinic/owner/Owner.java index 63e3acc7a..000c06292 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/Owner.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/Owner.java @@ -17,6 +17,7 @@ package org.springframework.samples.petclinic.owner; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import org.springframework.core.style.ToStringCreator; import org.springframework.samples.petclinic.model.Person; @@ -32,6 +33,7 @@ import jakarta.persistence.OrderBy; import jakarta.persistence.Table; import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.NotBlank; +import org.jspecify.annotations.Nullable; /** * Simple JavaBean domain object representing an owner. @@ -49,43 +51,43 @@ public class Owner extends Person { @Column(name = "address") @NotBlank - private String address; + private @Nullable String address; @Column(name = "city") @NotBlank - private String city; + private @Nullable String city; @Column(name = "telephone") @NotBlank @Pattern(regexp = "\\d{10}", message = "{telephone.invalid}") - private String telephone; + private @Nullable String telephone; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinColumn(name = "owner_id") @OrderBy("name") private final List pets = new ArrayList<>(); - public String getAddress() { + public @Nullable String getAddress() { return this.address; } - public void setAddress(String address) { + public void setAddress(@Nullable String address) { this.address = address; } - public String getCity() { + public @Nullable String getCity() { return this.city; } - public void setCity(String city) { + public void setCity(@Nullable String city) { this.city = city; } - public String getTelephone() { + public @Nullable String getTelephone() { return this.telephone; } - public void setTelephone(String telephone) { + public void setTelephone(@Nullable String telephone) { this.telephone = telephone; } @@ -104,7 +106,7 @@ public class Owner extends Person { * @param name to test * @return the Pet with the given name, or null if no such Pet exists for this Owner */ - public Pet getPet(String name) { + public @Nullable Pet getPet(String name) { return getPet(name, false); } @@ -113,11 +115,11 @@ public class Owner extends Person { * @param id to test * @return the Pet with the given id, or null if no such Pet exists for this Owner */ - public Pet getPet(Integer id) { + public @Nullable Pet getPet(Integer id) { for (Pet pet : getPets()) { if (!pet.isNew()) { Integer compId = pet.getId(); - if (compId.equals(id)) { + if (Objects.equals(compId, id)) { return pet; } } @@ -131,7 +133,7 @@ public class Owner extends Person { * @param ignoreNew whether to ignore new pets (pets that are not saved yet) * @return the Pet with the given name, or null if no such Pet exists for this Owner */ - public Pet getPet(String name, boolean ignoreNew) { + public @Nullable Pet getPet(String name, boolean ignoreNew) { for (Pet pet : getPets()) { String compName = pet.getName(); if (compName != null && compName.equalsIgnoreCase(name)) { 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 1348457ee..41b99619e 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java @@ -16,6 +16,7 @@ package org.springframework.samples.petclinic.owner; import java.util.List; +import java.util.Objects; import java.util.Optional; import org.springframework.data.domain.Page; @@ -34,6 +35,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.servlet.ModelAndView; import jakarta.validation.Valid; +import org.jspecify.annotations.Nullable; + import org.springframework.web.servlet.mvc.support.RedirectAttributes; /** @@ -60,7 +63,7 @@ class OwnerController { } @ModelAttribute("owner") - public Owner findOwner(@PathVariable(name = "ownerId", required = false) Integer ownerId) { + public Owner findOwner(@PathVariable(name = "ownerId", required = false) @Nullable Integer ownerId) { return ownerId == null ? new Owner() : this.owners.findById(ownerId) .orElseThrow(() -> new IllegalArgumentException("Owner not found with id: " + ownerId @@ -93,12 +96,13 @@ class OwnerController { public String processFindForm(@RequestParam(defaultValue = "1") int page, Owner owner, BindingResult result, Model model) { // allow parameterless GET request for /owners to return all records - if (owner.getLastName() == null) { - owner.setLastName(""); // empty string signifies broadest possible search + String lastName = owner.getLastName(); + if (lastName == null) { + lastName = ""; // empty string signifies broadest possible search } // find owners by last name - Page ownersResults = findPaginatedForOwnersLastName(page, owner.getLastName()); + Page ownersResults = findPaginatedForOwnersLastName(page, lastName); if (ownersResults.isEmpty()) { // no owners found result.rejectValue("lastName", "notFound", "not found"); @@ -143,7 +147,7 @@ class OwnerController { return VIEWS_OWNER_CREATE_OR_UPDATE_FORM; } - if (owner.getId() != ownerId) { + if (!Objects.equals(owner.getId(), ownerId)) { result.rejectValue("id", "mismatch", "The owner ID in the form does not match the URL."); redirectAttributes.addFlashAttribute("error", "Owner ID mismatch. Please try again."); return "redirect:/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 d26bdb62f..e0e9b2f0b 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/OwnerRepository.java @@ -58,6 +58,6 @@ public interface OwnerRepository extends JpaRepository { * @throws IllegalArgumentException if the id is null (assuming null is not a valid * input for id) */ - Optional findById(@Nonnull Integer id); + Optional findById(Integer id); } diff --git a/src/main/java/org/springframework/samples/petclinic/owner/Pet.java b/src/main/java/org/springframework/samples/petclinic/owner/Pet.java index 1945f9b67..7af9e5b4f 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/Pet.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/Pet.java @@ -32,6 +32,7 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; import jakarta.persistence.OrderBy; import jakarta.persistence.Table; +import org.jspecify.annotations.Nullable; /** * Simple business object representing a pet. @@ -47,30 +48,30 @@ public class Pet extends NamedEntity { @Column(name = "birth_date") @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDate birthDate; + private @Nullable LocalDate birthDate; @ManyToOne @JoinColumn(name = "type_id") - private PetType type; + private @Nullable PetType type; @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) @JoinColumn(name = "pet_id") @OrderBy("date ASC") private final Set visits = new LinkedHashSet<>(); - public void setBirthDate(LocalDate birthDate) { + public void setBirthDate(@Nullable LocalDate birthDate) { this.birthDate = birthDate; } - public LocalDate getBirthDate() { + public @Nullable LocalDate getBirthDate() { return this.birthDate; } - public PetType getType() { + public @Nullable PetType getType() { return this.type; } - public void setType(PetType type) { + public void setType(@Nullable PetType type) { this.type = type; } 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 56707d99f..b7274080e 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/PetController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/PetController.java @@ -17,10 +17,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; @@ -32,6 +34,8 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import jakarta.validation.Valid; +import org.jspecify.annotations.Nullable; + import org.springframework.web.servlet.mvc.support.RedirectAttributes; /** @@ -69,8 +73,8 @@ class PetController { } @ModelAttribute("pet") - public Pet findPet(@PathVariable("ownerId") int ownerId, - @PathVariable(name = "petId", required = false) Integer petId) { + public @Nullable Pet findPet(@PathVariable("ownerId") int ownerId, + @PathVariable(name = "petId", required = false) @Nullable Integer petId) { if (petId == null) { return new Pet(); @@ -135,7 +139,7 @@ class PetController { // checking if the pet name already exists for the owner if (StringUtils.hasText(petName)) { Pet existingPet = owner.getPet(petName, false); - if (existingPet != null && !existingPet.getId().equals(pet.getId())) { + if (existingPet != null && !Objects.equals(existingPet.getId(), pet.getId())) { result.rejectValue("name", "duplicate", "already exists"); } } @@ -160,7 +164,9 @@ class PetController { * @param pet The pet with updated details */ private void updatePetDetails(Owner owner, Pet pet) { - Pet existingPet = owner.getPet(pet.getId()); + Integer id = pet.getId(); + Assert.state(id != null, "'pet.getId()' must not be null"); + Pet existingPet = owner.getPet(id); if (existingPet != null) { // Update existing pet's properties existingPet.setName(pet.getName()); diff --git a/src/main/java/org/springframework/samples/petclinic/owner/PetTypeFormatter.java b/src/main/java/org/springframework/samples/petclinic/owner/PetTypeFormatter.java index 7ddc6e923..b62751c97 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/PetTypeFormatter.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/PetTypeFormatter.java @@ -21,6 +21,7 @@ import org.springframework.stereotype.Component; import java.text.ParseException; import java.util.Collection; import java.util.Locale; +import java.util.Objects; /** * Instructs Spring MVC on how to parse and print elements of type 'PetType'. Starting @@ -43,14 +44,15 @@ public class PetTypeFormatter implements Formatter { @Override public String print(PetType petType, Locale locale) { - return petType.getName(); + String name = petType.getName(); + return (name != null) ? name : ""; } @Override public PetType parse(String text, Locale locale) throws ParseException { Collection findPetTypes = this.types.findPetTypes(); for (PetType type : findPetTypes) { - if (type.getName().equals(text)) { + if (Objects.equals(type.getName(), text)) { return type; } } diff --git a/src/main/java/org/springframework/samples/petclinic/owner/Visit.java b/src/main/java/org/springframework/samples/petclinic/owner/Visit.java index 085cd2849..7259fab9f 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/Visit.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/Visit.java @@ -24,6 +24,7 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.Table; import jakarta.validation.constraints.NotBlank; +import org.jspecify.annotations.Nullable; /** * Simple JavaBean domain object representing a visit. @@ -37,10 +38,10 @@ public class Visit extends BaseEntity { @Column(name = "visit_date") @DateTimeFormat(pattern = "yyyy-MM-dd") - private LocalDate date; + private @Nullable LocalDate date; @NotBlank - private String description; + private @Nullable String description; /** * Creates a new instance of Visit for the current date @@ -49,19 +50,19 @@ public class Visit extends BaseEntity { this.date = LocalDate.now(); } - public LocalDate getDate() { + public @Nullable LocalDate getDate() { return this.date; } - public void setDate(LocalDate date) { + public void setDate(@Nullable LocalDate date) { this.date = date; } - public String getDescription() { + public @Nullable String getDescription() { return this.description; } - public void setDescription(String description) { + public void setDescription(@Nullable String description) { this.description = description; } diff --git a/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java b/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java index 21c1823a5..cc3e3ce1a 100644 --- a/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java +++ b/src/main/java/org/springframework/samples/petclinic/owner/VisitController.java @@ -67,6 +67,10 @@ class VisitController { "Owner not found with id: " + ownerId + ". Please ensure the ID is correct ")); Pet pet = owner.getPet(petId); + if (pet == null) { + throw new IllegalArgumentException( + "Pet with id " + petId + " not found for owner with id " + ownerId + "."); + } model.put("pet", pet); model.put("owner", owner); diff --git a/src/main/java/org/springframework/samples/petclinic/owner/package-info.java b/src/main/java/org/springframework/samples/petclinic/owner/package-info.java new file mode 100644 index 000000000..ef080a0d6 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/owner/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.samples.petclinic.owner; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/samples/petclinic/package-info.java b/src/main/java/org/springframework/samples/petclinic/package-info.java new file mode 100644 index 000000000..c8261d8f0 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.samples.petclinic; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java b/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java index 1382f3aea..13cb74301 100644 --- a/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java +++ b/src/main/java/org/springframework/samples/petclinic/system/CacheConfiguration.java @@ -16,7 +16,7 @@ package org.springframework.samples.petclinic.system; -import org.springframework.boot.autoconfigure.cache.JCacheManagerCustomizer; +import org.springframework.boot.cache.autoconfigure.JCacheManagerCustomizer; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/src/main/java/org/springframework/samples/petclinic/system/package-info.java b/src/main/java/org/springframework/samples/petclinic/system/package-info.java new file mode 100644 index 000000000..25da176d0 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/system/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.samples.petclinic.system; + +import org.jspecify.annotations.NullMarked; diff --git a/src/main/java/org/springframework/samples/petclinic/vet/Vet.java b/src/main/java/org/springframework/samples/petclinic/vet/Vet.java index fb2bd71ee..c8bd549d5 100644 --- a/src/main/java/org/springframework/samples/petclinic/vet/Vet.java +++ b/src/main/java/org/springframework/samples/petclinic/vet/Vet.java @@ -31,6 +31,7 @@ import jakarta.persistence.JoinTable; import jakarta.persistence.ManyToMany; import jakarta.persistence.Table; import jakarta.xml.bind.annotation.XmlElement; +import org.jspecify.annotations.Nullable; /** * Simple JavaBean domain object representing a veterinarian. @@ -47,7 +48,7 @@ public class Vet extends Person { @ManyToMany(fetch = FetchType.EAGER) @JoinTable(name = "vet_specialties", joinColumns = @JoinColumn(name = "vet_id"), inverseJoinColumns = @JoinColumn(name = "specialty_id")) - private Set specialties; + private @Nullable Set specialties; protected Set getSpecialtiesInternal() { if (this.specialties == null) { diff --git a/src/main/java/org/springframework/samples/petclinic/vet/Vets.java b/src/main/java/org/springframework/samples/petclinic/vet/Vets.java index 634cad773..a3292927a 100644 --- a/src/main/java/org/springframework/samples/petclinic/vet/Vets.java +++ b/src/main/java/org/springframework/samples/petclinic/vet/Vets.java @@ -20,6 +20,7 @@ import java.util.List; import jakarta.xml.bind.annotation.XmlElement; import jakarta.xml.bind.annotation.XmlRootElement; +import org.jspecify.annotations.Nullable; /** * Simple domain object representing a list of veterinarians. Mostly here to be used for @@ -30,7 +31,7 @@ import jakarta.xml.bind.annotation.XmlRootElement; @XmlRootElement public class Vets { - private List vets; + private @Nullable List vets; @XmlElement public List getVetList() { diff --git a/src/main/java/org/springframework/samples/petclinic/vet/package-info.java b/src/main/java/org/springframework/samples/petclinic/vet/package-info.java new file mode 100644 index 000000000..6fbc63698 --- /dev/null +++ b/src/main/java/org/springframework/samples/petclinic/vet/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.samples.petclinic.vet; + +import org.jspecify.annotations.NullMarked; diff --git a/src/test/java/org/springframework/samples/petclinic/MySqlIntegrationTests.java b/src/test/java/org/springframework/samples/petclinic/MySqlIntegrationTests.java index a78e2e6e1..92fbf1d6d 100644 --- a/src/test/java/org/springframework/samples/petclinic/MySqlIntegrationTests.java +++ b/src/test/java/org/springframework/samples/petclinic/MySqlIntegrationTests.java @@ -21,11 +21,11 @@ import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledInNativeImage; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.server.LocalServerPort; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; -import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.server.test.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; diff --git a/src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java b/src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java index 617af9652..002822ee6 100644 --- a/src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java +++ b/src/test/java/org/springframework/samples/petclinic/PetClinicIntegrationTests.java @@ -21,10 +21,10 @@ import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; +import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.server.test.LocalServerPort; import org.springframework.http.HttpStatus; import org.springframework.http.RequestEntity; import org.springframework.http.ResponseEntity; diff --git a/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java b/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java index 709d33e66..a9b51c07b 100644 --- a/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java +++ b/src/test/java/org/springframework/samples/petclinic/PostgresIntegrationTests.java @@ -32,10 +32,10 @@ import org.junit.jupiter.api.condition.DisabledInNativeImage; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.context.event.ApplicationPreparedEvent; +import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.boot.test.web.server.LocalServerPort; -import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.web.server.test.LocalServerPort; import org.springframework.context.ApplicationListener; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.EnumerablePropertySource; diff --git a/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java b/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java index 4c78ab317..f1cabaf3c 100644 --- a/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java +++ b/src/test/java/org/springframework/samples/petclinic/service/ClinicServiceTests.java @@ -82,7 +82,7 @@ class ClinicServiceTests { @Autowired protected VetRepository vets; - Pageable pageable; + private final Pageable pageable = Pageable.unpaged(); @Test void shouldFindOwnersByLastName() { diff --git a/src/test/java/org/springframework/samples/petclinic/service/EntityUtils.java b/src/test/java/org/springframework/samples/petclinic/service/EntityUtils.java index 180ef07f1..d1ce9d74f 100644 --- a/src/test/java/org/springframework/samples/petclinic/service/EntityUtils.java +++ b/src/test/java/org/springframework/samples/petclinic/service/EntityUtils.java @@ -43,7 +43,7 @@ public abstract class EntityUtils { public static T getById(Collection entities, Class entityClass, int entityId) throws ObjectRetrievalFailureException { for (T entity : entities) { - if (entity.getId() == entityId && entityClass.isInstance(entity)) { + if (entity.getId() != null && entity.getId() == entityId && entityClass.isInstance(entity)) { return entity; } } diff --git a/src/test/java/org/springframework/samples/petclinic/system/CrashControllerIntegrationTests.java b/src/test/java/org/springframework/samples/petclinic/system/CrashControllerIntegrationTests.java index ed8b0819a..2d1ac0dd0 100644 --- a/src/test/java/org/springframework/samples/petclinic/system/CrashControllerIntegrationTests.java +++ b/src/test/java/org/springframework/samples/petclinic/system/CrashControllerIntegrationTests.java @@ -26,11 +26,11 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; -import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration; -import org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration; +import org.springframework.boot.hibernate.autoconfigure.HibernateJpaAutoConfiguration; +import org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration; +import org.springframework.boot.jdbc.autoconfigure.DataSourceTransactionManagerAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.web.client.TestRestTemplate; +import org.springframework.boot.web.server.test.client.TestRestTemplate; import org.springframework.core.ParameterizedTypeReference; import org.springframework.http.HttpEntity; import org.springframework.http.HttpHeaders;