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..bd453e9810 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java @@ -0,0 +1,77 @@ +/* + * 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.ConstraintViolation; +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; + +/** + * 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. + * + * @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: create a validation handler (similar to "AuditingHandler") an reference it from "ValidatingMongoEventListener" and "ValidatingMongoEventListener" + 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", entity, 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 { 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 new file mode 100644 index 0000000000..05e5feef59 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java @@ -0,0 +1,75 @@ +/* + * 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 ValidatingEntityCallbackUnitTests { + + private ValidatingEntityCallback callback; + + @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); + + 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); + } + + record Coordinates(@NotNull @Min(0) Integer x, @NotNull @Min(0) Integer y) { + + Document toDocument() { + return Document.parse(""" + { + "x": %d, + "y": %d + } + """.formatted(x, y)); + } + } +}