Browse Source

Polishing.

Introduce ReactiveValidatingEntityCallback, extract BeanValidationDelegate. Document Bean Validation callbacks.

See #4901
Original pull request: #4910
pull/4915/head
Mark Paluch 10 months ago
parent
commit
d7c40a4e6f
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 72
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeanValidationDelegate.java
  2. 69
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveValidatingEntityCallback.java
  3. 35
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java
  4. 38
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListener.java
  5. 79
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ReactiveValidatingEntityCallbackUnitTests.java
  6. 25
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java
  7. 41
      src/main/antora/modules/ROOT/pages/mongodb/lifecycle-events.adoc

72
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeanValidationDelegate.java

@ -0,0 +1,72 @@ @@ -0,0 +1,72 @@
/*
* Copyright 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.Validator;
import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert;
/**
* Delegate to handle common calls to Bean {@link Validator Validation}.
*
* @author Mark Paluch
* @since 4.5
*/
class BeanValidationDelegate {
private static final Log LOG = LogFactory.getLog(BeanValidationDelegate.class);
private final Validator validator;
/**
* Creates a new {@link BeanValidationDelegate} using the given {@link Validator}.
*
* @param validator must not be {@literal null}.
*/
public BeanValidationDelegate(Validator validator) {
Assert.notNull(validator, "Validator must not be null");
this.validator = validator;
}
/**
* Validate the given object.
*
* @param object
* @return set of constraint violations.
*/
public Set<ConstraintViolation<Object>> validate(Object object) {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Validating object: %s", object));
}
Set<ConstraintViolation<Object>> violations = validator.validate(object);
if (!violations.isEmpty()) {
if (LOG.isDebugEnabled()) {
LOG.info(String.format("During object: %s validation violations found: %s", object, violations));
}
}
return violations;
}
}

69
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ReactiveValidatingEntityCallback.java

@ -0,0 +1,69 @@ @@ -0,0 +1,69 @@
/*
* Copyright 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 reactor.core.publisher.Mono;
import java.util.Set;
import org.bson.Document;
import org.springframework.core.Ordered;
/**
* Reactive variant of JSR-303 dependant entities validator.
* <p>
* When it is registered as Spring component its automatically invoked after object to {@link Document} conversion and
* before entities are saved to the database.
*
* @author Mark Paluch
* @author Rene Felgenträger
* @since 4.5
*/
public class ReactiveValidatingEntityCallback implements ReactiveBeforeSaveCallback<Object>, Ordered {
private final BeanValidationDelegate delegate;
/**
* Creates a new {@link ReactiveValidatingEntityCallback} using the given {@link Validator}.
*
* @param validator must not be {@literal null}.
*/
public ReactiveValidatingEntityCallback(Validator validator) {
this.delegate = new BeanValidationDelegate(validator);
}
@Override
public Mono<Object> onBeforeSave(Object entity, Document document, String collection) {
Set<ConstraintViolation<Object>> violations = delegate.validate(entity);
if (!violations.isEmpty()) {
return Mono.error(new ConstraintViolationException(violations));
}
return Mono.just(entity);
}
@Override
public int getOrder() {
return 100;
}
}

35
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallback.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2025 the original author or authors.
* Copyright 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.
@ -18,29 +18,26 @@ package org.springframework.data.mongodb.core.mapping.event; @@ -18,29 +18,26 @@ 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.
* <p>
* 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 object to {@link Document} conversion and
* before entities are saved to the database.
*
* @author original authors of {@link ValidatingMongoEventListener}
* @author Rene Felgenträger
* @see {@link ValidatingMongoEventListener}
* @author Mark Paluch
* @since 4.5
*/
public class ValidatingEntityCallback implements BeforeSaveCallback<Object>, 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;
private final BeanValidationDelegate delegate;
/**
* Creates a new {@link ValidatingEntityCallback} using the given {@link Validator}.
@ -48,25 +45,18 @@ public class ValidatingEntityCallback implements BeforeSaveCallback<Object>, Ord @@ -48,25 +45,18 @@ public class ValidatingEntityCallback implements BeforeSaveCallback<Object>, Ord
* @param validator must not be {@literal null}.
*/
public ValidatingEntityCallback(Validator validator) {
Assert.notNull(validator, "Validator must not be null");
this.validator = validator;
this.delegate = new BeanValidationDelegate(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<ConstraintViolation<Object>> violations = validator.validate(entity);
Set<ConstraintViolation<Object>> violations = delegate.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;
}
@ -74,4 +64,5 @@ public class ValidatingEntityCallback implements BeforeSaveCallback<Object>, Ord @@ -74,4 +64,5 @@ public class ValidatingEntityCallback implements BeforeSaveCallback<Object>, Ord
public int getOrder() {
return 100;
}
}

38
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/ValidatingMongoEventListener.java

@ -15,31 +15,30 @@ @@ -15,31 +15,30 @@
*/
package org.springframework.data.mongodb.core.mapping.event;
import java.util.Set;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import java.util.Set;
import org.springframework.util.Assert;
import org.bson.Document;
/**
* javax.validation dependant entities validator. When it is registered as Spring component its automatically invoked
* before entities are saved in database.
* JSR-303 dependant entities validator.
* <p>
* When it is registered as Spring component its automatically invoked after object to {@link Document} conversion and
* before entities are saved to the database.
*
* @author Maciej Walkowiak
* @author Oliver Gierke
* @author Christoph Strobl
*
* @see {@link ValidatingEntityCallback}
* @deprecated since 4.5, use {@link ValidatingEntityCallback} respectively {@link ReactiveValidatingEntityCallback}
* instead to ensure ordering and interruption of saving when encountering validation constraint violations.
*/
@Deprecated(since = "4.5")
public class ValidatingMongoEventListener extends AbstractMongoEventListener<Object> {
private static final Log LOG = LogFactory.getLog(ValidatingMongoEventListener.class);
private final Validator validator;
private final BeanValidationDelegate delegate;
/**
* Creates a new {@link ValidatingMongoEventListener} using the given {@link Validator}.
@ -47,26 +46,17 @@ public class ValidatingMongoEventListener extends AbstractMongoEventListener<Obj @@ -47,26 +46,17 @@ public class ValidatingMongoEventListener extends AbstractMongoEventListener<Obj
* @param validator must not be {@literal null}.
*/
public ValidatingMongoEventListener(Validator validator) {
Assert.notNull(validator, "Validator must not be null");
this.validator = validator;
this.delegate = new BeanValidationDelegate(validator);
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override
public void onBeforeSave(BeforeSaveEvent<Object> event) {
if (LOG.isDebugEnabled()) {
LOG.debug(String.format("Validating object: %s", event.getSource()));
}
Set violations = validator.validate(event.getSource());
Set<ConstraintViolation<Object>> violations = delegate.validate(event.getSource());
if (!violations.isEmpty()) {
if (LOG.isDebugEnabled()) {
LOG.info(String.format("During object: %s validation violations found: %s", event.getSource(), violations));
}
throw new ConstraintViolationException(violations);
}
}
}

79
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ReactiveValidatingEntityCallbackUnitTests.java

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
/*
* Copyright 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.Validation;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import reactor.test.StepVerifier;
import org.bson.Document;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link ReactiveValidatingEntityCallback}.
*
* @author Mark Paluch
* @author Rene Felgenträger
*/
class ReactiveValidatingEntityCallbackUnitTests {
private ReactiveValidatingEntityCallback callback;
@BeforeEach
void setUp() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
callback = new ReactiveValidatingEntityCallback(factory.getValidator());
}
}
@Test // GH-4910
void validationThrowsException() {
Coordinates coordinates = new Coordinates(-1, -1);
callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates") //
.as(StepVerifier::create) //
.verifyError(ConstraintViolationException.class);
}
@Test // GH-4910
void validateSuccessful() {
Coordinates coordinates = new Coordinates(0, 0);
callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates") //
.as(StepVerifier::create) //
.expectNext(coordinates) //
.verifyComplete();
}
record Coordinates(@NotNull @Min(0) Integer x, @NotNull @Min(0) Integer y) {
Document toDocument() {
return Document.parse("""
{
"x": %d,
"y": %d
}
""".formatted(x, y));
}
}
}

25
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ValidatingEntityCallbackUnitTests.java

@ -1,5 +1,5 @@ @@ -1,5 +1,5 @@
/*
* Copyright 2012-2025 the original author or authors.
* Copyright 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.
@ -15,37 +15,38 @@ @@ -15,37 +15,38 @@
*/
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 static org.assertj.core.api.Assertions.*;
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}.
* Unit tests for {@link ValidatingEntityCallback}.
*
* @author Rene Felgenträger
* @author Mark Paluch
*/
class ValidatingEntityCallbackUnitTests {
private ValidatingEntityCallback callback;
@BeforeEach
public void setUp() {
void setUp() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
callback = new ValidatingEntityCallback(factory.getValidator());
}
}
@Test
// GH-4910
void invalidModel_throwsException() {
@Test // GH-4910
void validationThrowsException() {
Coordinates coordinates = new Coordinates(-1, -1);
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(
@ -53,11 +54,12 @@ class ValidatingEntityCallbackUnitTests { @@ -53,11 +54,12 @@ class ValidatingEntityCallbackUnitTests {
.satisfies(e -> assertThat(e.getConstraintViolations()).hasSize(2));
}
@Test
// GH-4910
void validModel_noExceptionThrown() {
@Test // GH-4910
void validateSuccessful() {
Coordinates coordinates = new Coordinates(0, 0);
Object entity = callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates");
assertThat(entity).isEqualTo(coordinates);
}
@ -72,4 +74,5 @@ class ValidatingEntityCallbackUnitTests { @@ -72,4 +74,5 @@ class ValidatingEntityCallbackUnitTests {
""".formatted(x, y));
}
}
}

41
src/main/antora/modules/ROOT/pages/mongodb/lifecycle-events.adoc

@ -104,3 +104,44 @@ Can modify the domain object, to be returned after save, `Document` containing a @@ -104,3 +104,44 @@ Can modify the domain object, to be returned after save, `Document` containing a
|===
=== Bean Validation
Spring Data MongoDB supports Bean Validation for MongoDB entities annotated with https://beanvalidation.org/[https://xxx][Jakarta Validation annotations].
You can enable Bean Validation by registering `ValidatingEntityCallback` respectively `ReactiveValidatingEntityCallback` for reactive driver usage in your Spring `ApplicationContext` as shown in the following example:
[tabs]
======
Imperative::
+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
----
@Configuration
class Config {
@Bean
public ValidatingEntityCallback validatingEntityCallback(Validator validator) {
return new ValidatingEntityCallback(validator);
}
}
----
Reactive::
+
[source,java,indent=0,subs="verbatim,quotes",role="secondary"]
----
@Configuration
class Config {
@Bean
public ReactiveValidatingEntityCallback validatingEntityCallback(Validator validator) {
return new ReactiveValidatingEntityCallback(validator);
}
}
----
======
If you're using both, imperative and reactive, then you can enable also both callbacks.
NOTE: When using XML-based configuration, historically, `ValidatingMongoEventListener` is registered through our namespace handlers when configuring `<mongo:mapping-converter>`.
If you want to use the newer Entity Callback variant, make sure to not use `<mongo:mapping-converter>`, otherwise you'll end up with both, the `ValidatingMongoEventListener` and the `ValidatingEntityCallback` being registered.

Loading…
Cancel
Save