From 364e258ea3a0b9cda8ab9a32ca03cd9d0f844cac Mon Sep 17 00:00:00 2001 From: felgentraeger Date: Sat, 1 Mar 2025 17:21:29 +0100 Subject: [PATCH 1/6] gh-4901: introduce ValidatingEntityCallback Signed-off-by: felgentraeger --- .../event/ValidatingEntityCallback.java | 79 +++++++++++++++++++ .../event/ValidatingMongoEventListener.java | 2 + 2 files changed, 81 insertions(+) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java new file mode 100644 index 0000000000..55e6d6dc57 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java @@ -0,0 +1,79 @@ +/* + * 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.data.mongodb.core.mapping.event; + +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; +import java.util.Set; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.bson.Document; +import org.springframework.core.Ordered; +import org.springframework.util.Assert; + +/** + * javax.validation dependant entities validator. + *

+ * When it is registered as Spring component its automatically invoked + * after any {@link AbstractMongoEventListener} and before entities are saved in database. + * + * @author original authors of {@link ValidatingMongoEventListener} + * @author Rene Felgenträger + * + * @see {@link ValidatingMongoEventListener} + */ +public class ValidatingEntityCallback implements BeforeSaveCallback, Ordered { + + private static final Log LOG = LogFactory.getLog(ValidatingEntityCallback.class); + + // TODO: discuss with spring team, if a handler encapsulating validation logic makes more sense (similar to "AuditingHandler") + private final Validator validator; + + /** + * Creates a new {@link ValidatingEntityCallback} using the given {@link Validator}. + * + * @param validator must not be {@literal null}. + */ + public ValidatingEntityCallback(Validator validator) { + + Assert.notNull(validator, "Validator must not be null"); + this.validator = validator; + } + + // TODO: alternatively implement the "BeforeConvertCallback" interface and set the order to highest value ? + @Override + public Object onBeforeSave(Object entity, Document document, String collection) { + + if (LOG.isDebugEnabled()) { + LOG.debug(String.format("Validating object: %s", entity)); + } + Set violations = validator.validate(entity); + + if (!violations.isEmpty()) { + + if (LOG.isDebugEnabled()) { + LOG.info(String.format("During object: %s validation violations found: %s", event.getSource(), violations)); + } + throw new ConstraintViolationException(violations); + } + return entity; + } + + @Override + public int getOrder() { + return 100; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListener.java index ba58efe661..1242b361b7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListener.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListener.java @@ -32,6 +32,8 @@ * @author Maciej Walkowiak * @author Oliver Gierke * @author Christoph Strobl + * + * @see {@link ValidatingEntityCallback} */ public class ValidatingMongoEventListener extends AbstractMongoEventListener { From e901ed3903868611400634b0a22749197fc3ddd1 Mon Sep 17 00:00:00 2001 From: felgentraeger Date: Sun, 2 Mar 2025 08:04:23 +0100 Subject: [PATCH 2/6] gh-4901: introduce ValidatingEntityCallback Signed-off-by: felgentraeger --- .../event/ValidatingEntityCallback.java | 11 ++- .../event/ValidatingEntityCallbackTests.java | 73 +++++++++++++++++++ 2 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java index 55e6d6dc57..763706c1cd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.mapping.event; +import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.validation.Validator; import java.util.Set; @@ -25,7 +26,7 @@ import org.springframework.util.Assert; /** - * javax.validation dependant entities validator. + * JSR-303 dependant entities validator. *

* When it is registered as Spring component its automatically invoked * after any {@link AbstractMongoEventListener} and before entities are saved in database. @@ -39,7 +40,7 @@ public class ValidatingEntityCallback implements BeforeSaveCallback, Ord private static final Log LOG = LogFactory.getLog(ValidatingEntityCallback.class); - // TODO: discuss with spring team, if a handler encapsulating validation logic makes more sense (similar to "AuditingHandler") + // TODO: create a validation handler (similar to "AuditingHandler") an reference it from "ValidatingMongoEventListener" and "ValidatingMongoEventListener" private final Validator validator; /** @@ -48,7 +49,6 @@ public class ValidatingEntityCallback implements BeforeSaveCallback, Ord * @param validator must not be {@literal null}. */ public ValidatingEntityCallback(Validator validator) { - Assert.notNull(validator, "Validator must not be null"); this.validator = validator; } @@ -60,12 +60,11 @@ public Object onBeforeSave(Object entity, Document document, String collection) if (LOG.isDebugEnabled()) { LOG.debug(String.format("Validating object: %s", entity)); } - Set violations = validator.validate(entity); + Set> violations = validator.validate(entity); if (!violations.isEmpty()) { - if (LOG.isDebugEnabled()) { - LOG.info(String.format("During object: %s validation violations found: %s", event.getSource(), violations)); + LOG.info(String.format("During object: %s validation violations found: %s", entity, violations)); } throw new ConstraintViolationException(violations); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackTests.java new file mode 100644 index 0000000000..e1d2ef097d --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackTests.java @@ -0,0 +1,73 @@ +/* + * 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.data.mongodb.core.mapping.event; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validation; +import jakarta.validation.ValidatorFactory; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * Unit test for {@link ValidatingEntityCallback}. + * + * @author Rene Felgenträger + */ +class ValidatingEntityCallbackTests { + + private ValidatingEntityCallback callback; + + @BeforeEach + public void setUp() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + callback = new ValidatingEntityCallback(factory.getValidator()); + } + } + + @Test + void invalidModel_throwsException() { + Coordinates coordinates = new Coordinates(-1, -1); + + assertThatExceptionOfType(ConstraintViolationException.class) + .isThrownBy(() -> callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates")) + .satisfies(e -> assertThat(e.getConstraintViolations()).hasSize(2)); + } + + @Test + void validModel_noException() { + Coordinates coordinates = new Coordinates(0, 0); + Object entity = callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates"); + assertThat(entity).isEqualTo(coordinates); + } + + record Coordinates(@NotNull @Min(0) Integer x, @NotNull @Min(0) Integer y) { + + Document toDocument() { + return Document.parse(""" + { + "x": %d, + "y": %d + } + """.formatted(x, y)); + } + } +} From 3cde56bf91bb978a76c38ac4994ad929823d85f0 Mon Sep 17 00:00:00 2001 From: felgentraeger Date: Sun, 2 Mar 2025 08:11:49 +0100 Subject: [PATCH 3/6] gh-4901: introduce ValidatingEntityCallback Signed-off-by: felgentraeger --- ...allbackTests.java => ValidatingEntityCallbackUnitTests.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/{ValidatingEntityCallbackTests.java => ValidatingEntityCallbackUnitTests.java} (98%) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java similarity index 98% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackTests.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java index e1d2ef097d..f868d0a404 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java @@ -32,7 +32,7 @@ * * @author Rene Felgenträger */ -class ValidatingEntityCallbackTests { +class ValidatingEntityCallbackUnitTests { private ValidatingEntityCallback callback; From 2e1bdcd40a6918f2d97d9ab22015a5713e02531a Mon Sep 17 00:00:00 2001 From: felgentraeger Date: Sun, 2 Mar 2025 08:12:26 +0100 Subject: [PATCH 4/6] gh-4901: introduce ValidatingEntityCallback Signed-off-by: felgentraeger --- .../core/mapping/event/ValidatingEntityCallbackUnitTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java index f868d0a404..56e4d007a6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java @@ -53,7 +53,7 @@ void invalidModel_throwsException() { } @Test - void validModel_noException() { + void validModel_noExceptionThrown() { Coordinates coordinates = new Coordinates(0, 0); Object entity = callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates"); assertThat(entity).isEqualTo(coordinates); From c0e7bfb4dbbf9665c0d27bfce09b2265feb72cd2 Mon Sep 17 00:00:00 2001 From: felgentraeger Date: Sun, 2 Mar 2025 08:22:26 +0100 Subject: [PATCH 5/6] gh-4901: introduce ValidatingEntityCallback Signed-off-by: felgentraeger --- .../core/mapping/event/ValidatingEntityCallbackUnitTests.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java index 56e4d007a6..7cb8ca2d7d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java @@ -43,7 +43,7 @@ public void setUp() { } } - @Test + @Test // GH-4910 void invalidModel_throwsException() { Coordinates coordinates = new Coordinates(-1, -1); @@ -52,7 +52,7 @@ void invalidModel_throwsException() { .satisfies(e -> assertThat(e.getConstraintViolations()).hasSize(2)); } - @Test + @Test // GH-4910 void validModel_noExceptionThrown() { Coordinates coordinates = new Coordinates(0, 0); Object entity = callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates"); From 51538dd184a75d23e5dbd9e619df18c85d26e895 Mon Sep 17 00:00:00 2001 From: felgentraeger Date: Mon, 3 Mar 2025 11:38:39 +0100 Subject: [PATCH 6/6] gh-4901: introduce ValidatingEntityCallback --- .../event/ValidatingEntityCallback.java | 5 +- .../ValidatingEntityCallbackUnitTests.java | 62 ++++++++++--------- 2 files changed, 34 insertions(+), 33 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java index 763706c1cd..bd453e9810 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java @@ -28,12 +28,11 @@ /** * JSR-303 dependant entities validator. *

- * When it is registered as Spring component its automatically invoked - * after any {@link AbstractMongoEventListener} and before entities are saved in database. + * When it is registered as Spring component its automatically invoked after any {@link AbstractMongoEventListener} and + * before entities are saved in database. * * @author original authors of {@link ValidatingMongoEventListener} * @author Rene Felgenträger - * * @see {@link ValidatingMongoEventListener} */ public class ValidatingEntityCallback implements BeforeSaveCallback, Ordered { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java index 7cb8ca2d7d..05e5feef59 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java @@ -34,40 +34,42 @@ */ class ValidatingEntityCallbackUnitTests { - private ValidatingEntityCallback callback; + private ValidatingEntityCallback callback; - @BeforeEach - public void setUp() { - try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { - callback = new ValidatingEntityCallback(factory.getValidator()); - } - } + @BeforeEach + public void setUp() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + callback = new ValidatingEntityCallback(factory.getValidator()); + } + } - @Test // GH-4910 - void invalidModel_throwsException() { - Coordinates coordinates = new Coordinates(-1, -1); + @Test + // GH-4910 + void invalidModel_throwsException() { + Coordinates coordinates = new Coordinates(-1, -1); - assertThatExceptionOfType(ConstraintViolationException.class) - .isThrownBy(() -> callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates")) - .satisfies(e -> assertThat(e.getConstraintViolations()).hasSize(2)); - } + assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy( + () -> callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates")) + .satisfies(e -> assertThat(e.getConstraintViolations()).hasSize(2)); + } - @Test // GH-4910 - void validModel_noExceptionThrown() { - Coordinates coordinates = new Coordinates(0, 0); - Object entity = callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates"); - assertThat(entity).isEqualTo(coordinates); - } + @Test + // GH-4910 + void validModel_noExceptionThrown() { + Coordinates coordinates = new Coordinates(0, 0); + Object entity = callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates"); + assertThat(entity).isEqualTo(coordinates); + } - record Coordinates(@NotNull @Min(0) Integer x, @NotNull @Min(0) Integer y) { + record Coordinates(@NotNull @Min(0) Integer x, @NotNull @Min(0) Integer y) { - Document toDocument() { - return Document.parse(""" - { - "x": %d, - "y": %d - } - """.formatted(x, y)); - } - } + Document toDocument() { + return Document.parse(""" + { + "x": %d, + "y": %d + } + """.formatted(x, y)); + } + } }