From 47b53b4a1fecacb6a816420dcdebb89f1fbba008 Mon Sep 17 00:00:00 2001 From: Vitaliy Baschlykoff Date: Mon, 13 Apr 2026 15:54:30 +1000 Subject: [PATCH 1/2] Add tests that fail with current entities implementation based on lombok data annotation --- .../model/VehicleInspectionTest.java | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 memex/src/test/java/com/johnlpage/memex/VehicleInspection/model/VehicleInspectionTest.java diff --git a/memex/src/test/java/com/johnlpage/memex/VehicleInspection/model/VehicleInspectionTest.java b/memex/src/test/java/com/johnlpage/memex/VehicleInspection/model/VehicleInspectionTest.java new file mode 100644 index 0000000..18902ef --- /dev/null +++ b/memex/src/test/java/com/johnlpage/memex/VehicleInspection/model/VehicleInspectionTest.java @@ -0,0 +1,58 @@ +package com.johnlpage.memex.VehicleInspection.model; + +import org.junit.jupiter.api.Test; +import java.util.List; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +class VehicleInspectionTest { + + @Test + void toString_noStackOverflow_whenVehicleHasInspections() { + VehicleInspection inspection = new VehicleInspection(); + inspection.setTestid(1L); + + Vehicle vehicle = new Vehicle(); + vehicle.setVehicleid(100L); + vehicle.setInspections(List.of(inspection)); + + inspection.setVehicle(vehicle); + + assertDoesNotThrow(() -> inspection.toString()); + } + + @Test + void equals_noStackOverflow_whenVehicleHasInspections() { + VehicleInspection a = new VehicleInspection(); + a.setTestid(1L); + VehicleInspection b = new VehicleInspection(); + b.setTestid(1L); + + // Separate Vehicle instances so equals() can't short-circuit on reference equality + Vehicle vehicleA = new Vehicle(); + vehicleA.setVehicleid(100L); + vehicleA.setInspections(List.of(a)); + + Vehicle vehicleB = new Vehicle(); + vehicleB.setVehicleid(100L); + vehicleB.setInspections(List.of(b)); + + a.setVehicle(vehicleA); + b.setVehicle(vehicleB); + + assertDoesNotThrow(() -> a.equals(b)); + } + + @Test + void hashCode_noStackOverflow_whenVehicleHasInspections() { + VehicleInspection inspection = new VehicleInspection(); + inspection.setTestid(1L); + + Vehicle vehicle = new Vehicle(); + vehicle.setVehicleid(100L); + vehicle.setInspections(List.of(inspection)); + + inspection.setVehicle(vehicle); + + assertDoesNotThrow(() -> inspection.hashCode()); + } +} From 0d535bdf084f543150c3cf030346b39535a39239 Mon Sep 17 00:00:00 2001 From: Vitaliy Baschlykoff Date: Mon, 13 Apr 2026 16:47:18 +1000 Subject: [PATCH 2/2] Replace `@Data` lombok annotation with `@Getter`/`@Setter` for JPA and record for DTO --- .../memex/VehicleInspection/model/Vehicle.java | 9 +++++++-- .../model/VehicleInspection.java | 11 ++++++++--- .../generics/service/DocumentHistory.java | 11 ++++++++--- .../MongoDbJsonStreamingLoaderService.java | 18 +++++++----------- memex/templates/model/Model.java.template | 9 +++++++-- .../scripts/generate-models-from-json.groovy | 9 +++++++-- 6 files changed, 44 insertions(+), 23 deletions(-) diff --git a/memex/src/main/java/com/johnlpage/memex/VehicleInspection/model/Vehicle.java b/memex/src/main/java/com/johnlpage/memex/VehicleInspection/model/Vehicle.java index 24880b0..f5b7c1c 100644 --- a/memex/src/main/java/com/johnlpage/memex/VehicleInspection/model/Vehicle.java +++ b/memex/src/main/java/com/johnlpage/memex/VehicleInspection/model/Vehicle.java @@ -5,13 +5,18 @@ import java.util.List; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; import org.springframework.data.annotation.ReadOnlyProperty; -@Data +@Getter +@Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) public class Vehicle { + @EqualsAndHashCode.Include Long vehicleid; String make; String model; diff --git a/memex/src/main/java/com/johnlpage/memex/VehicleInspection/model/VehicleInspection.java b/memex/src/main/java/com/johnlpage/memex/VehicleInspection/model/VehicleInspection.java index f130af4..1d70dfc 100644 --- a/memex/src/main/java/com/johnlpage/memex/VehicleInspection/model/VehicleInspection.java +++ b/memex/src/main/java/com/johnlpage/memex/VehicleInspection/model/VehicleInspection.java @@ -7,7 +7,9 @@ import com.johnlpage.memex.util.DeleteFlag; import com.johnlpage.memex.util.ObjectConverter; import jakarta.validation.constraints.Min; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.Version; @@ -18,7 +20,7 @@ import java.util.HashMap; import java.util.Map; -/* Replace @Data with this to make an Immutable model +/* Replace @Getter/@Setter with this to make an Immutable model * which is a little more efficient but no setters just a builder * This also impact the controller and fuzzer and JsonLoaderService - * changes there are commented @@ -28,12 +30,15 @@ * @Value */ -@Data +@Getter +@Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @Document(collection = "vehicleinspection") public class VehicleInspection { @Id + @EqualsAndHashCode.Include Long testid; @Field("lock_version") diff --git a/memex/src/main/java/com/johnlpage/memex/generics/service/DocumentHistory.java b/memex/src/main/java/com/johnlpage/memex/generics/service/DocumentHistory.java index ab7709c..3084467 100644 --- a/memex/src/main/java/com/johnlpage/memex/generics/service/DocumentHistory.java +++ b/memex/src/main/java/com/johnlpage/memex/generics/service/DocumentHistory.java @@ -5,12 +5,14 @@ import java.util.Date; import java.util.Map; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; import org.bson.types.ObjectId; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; -/* Replace @Data with this to make an Immutable model +/* Replace @Getter/@Setter with this to make an Immutable model * which is a little more efficient but no setters just a builder * This also impacts the controller and PreWriteTrigger and * JsonLoaderService changes there are commented @@ -20,12 +22,15 @@ * @Value */ -@Data +@Getter +@Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) @JsonIgnoreProperties(ignoreUnknown = true) @JsonInclude(JsonInclude.Include.NON_NULL) @Document() public class DocumentHistory { @Id + @EqualsAndHashCode.Include ObjectId historyId; Object recordId; Date timestamp; diff --git a/memex/src/main/java/com/johnlpage/memex/generics/service/MongoDbJsonStreamingLoaderService.java b/memex/src/main/java/com/johnlpage/memex/generics/service/MongoDbJsonStreamingLoaderService.java index bea1f38..715295c 100644 --- a/memex/src/main/java/com/johnlpage/memex/generics/service/MongoDbJsonStreamingLoaderService.java +++ b/memex/src/main/java/com/johnlpage/memex/generics/service/MongoDbJsonStreamingLoaderService.java @@ -9,8 +9,6 @@ import com.johnlpage.memex.util.UpdateStrategy; import com.mongodb.bulk.BulkWriteResult; import jakarta.annotation.Nullable; -import lombok.AllArgsConstructor; -import lombok.Data; import lombok.RequiredArgsConstructor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -131,13 +129,11 @@ public JsonStreamingLoadResponse loadFromJsonStream( } } - @Data - @AllArgsConstructor - public static class JsonStreamingLoadResponse { - long updates; - long deletes; - long inserts; - boolean success; - String message; - } + public record JsonStreamingLoadResponse( + long updates, + long deletes, + long inserts, + boolean success, + String message + ) {} } diff --git a/memex/templates/model/Model.java.template b/memex/templates/model/Model.java.template index d651896..ca78a47 100644 --- a/memex/templates/model/Model.java.template +++ b/memex/templates/model/Model.java.template @@ -5,7 +5,9 @@ import com.fasterxml.jackson.annotation.JsonAnyGetter; import com.fasterxml.jackson.annotation.JsonAnySetter; import com.johnlpage.memex.util.DeleteFlag; import com.johnlpage.memex.util.ObjectConverter; -import lombok.Data; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; import lombok.NoArgsConstructor; import lombok.AllArgsConstructor; import org.springframework.data.annotation.Id; @@ -19,13 +21,16 @@ import java.util.Map; __idImport__ -@Data +@Getter +@Setter +@EqualsAndHashCode(onlyExplicitlyIncluded = true) @NoArgsConstructor @AllArgsConstructor @Document(collection = "__collectionName__") public class __className__ { @Id + @EqualsAndHashCode.Include private __idType__ __idFieldName__; @Field("lock_version") diff --git a/memex/templates/scripts/generate-models-from-json.groovy b/memex/templates/scripts/generate-models-from-json.groovy index 6ce811f..0ae83fd 100644 --- a/memex/templates/scripts/generate-models-from-json.groovy +++ b/memex/templates/scripts/generate-models-from-json.groovy @@ -457,7 +457,9 @@ processClass = { Map classes, String className, Map fields, boolean isRoot, Stri isRoot : isRoot, fields : [], imports : new HashSet([ - 'lombok.Data', + 'lombok.Getter', + 'lombok.Setter', + 'lombok.EqualsAndHashCode', 'org.springframework.data.mongodb.core.mapping.Field', 'com.fasterxml.jackson.annotation.JsonAnySetter', 'com.fasterxml.jackson.annotation.JsonAnyGetter', @@ -556,7 +558,9 @@ def generateClassContent = { Map classInfo, String pkgName, String collectionNam sb.append(" */\n") // Annotations - sb.append("@Data\n") + sb.append("@Getter\n"); + sb.append("@Setter\n"); + sb.append("@EqualsAndHashCode(onlyExplicitlyIncluded = true)\n"); if (classInfo.isRoot) { sb.append("@Document(collection = \"${collectionName}\")\n") } @@ -567,6 +571,7 @@ def generateClassContent = { Map classInfo, String pkgName, String collectionNam if (classInfo.isRoot) { // Add ID field first sb.append(" @Id\n") + sb.append(" @EqualsAndHashCode.Include\n") sb.append(" private String ${classInfo.idFieldName};\n\n") // Add version field