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 @@
/*
* 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 @@
/*
* 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 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with 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;
import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException; import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator; import jakarta.validation.Validator;
import java.util.Set; import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.bson.Document; import org.bson.Document;
import org.springframework.core.Ordered; import org.springframework.core.Ordered;
import org.springframework.util.Assert;
/** /**
* JSR-303 dependant entities validator. * JSR-303 dependant entities validator.
* <p> * <p>
* When it is registered as Spring component its automatically invoked after any {@link AbstractMongoEventListener} and * When it is registered as Spring component its automatically invoked after object to {@link Document} conversion and
* before entities are saved in database. * before entities are saved to the database.
* *
* @author original authors of {@link ValidatingMongoEventListener}
* @author Rene Felgenträger * @author Rene Felgenträger
* @see {@link ValidatingMongoEventListener} * @author Mark Paluch
* @since 4.5
*/ */
public class ValidatingEntityCallback implements BeforeSaveCallback<Object>, Ordered { public class ValidatingEntityCallback implements BeforeSaveCallback<Object>, Ordered {
private static final Log LOG = LogFactory.getLog(ValidatingEntityCallback.class); private final BeanValidationDelegate delegate;
// 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}. * Creates a new {@link ValidatingEntityCallback} using the given {@link Validator}.
@ -48,25 +45,18 @@ public class ValidatingEntityCallback implements BeforeSaveCallback<Object>, Ord
* @param validator must not be {@literal null}. * @param validator must not be {@literal null}.
*/ */
public ValidatingEntityCallback(Validator validator) { public ValidatingEntityCallback(Validator validator) {
Assert.notNull(validator, "Validator must not be null"); this.delegate = new BeanValidationDelegate(validator);
this.validator = validator;
} }
// TODO: alternatively implement the "BeforeConvertCallback" interface and set the order to highest value ?
@Override @Override
public Object onBeforeSave(Object entity, Document document, String collection) { public Object onBeforeSave(Object entity, Document document, String collection) {
if (LOG.isDebugEnabled()) { Set<ConstraintViolation<Object>> violations = delegate.validate(entity);
LOG.debug(String.format("Validating object: %s", entity));
}
Set<ConstraintViolation<Object>> violations = validator.validate(entity);
if (!violations.isEmpty()) { if (!violations.isEmpty()) {
if (LOG.isDebugEnabled()) {
LOG.info(String.format("During object: %s validation violations found: %s", entity, violations));
}
throw new ConstraintViolationException(violations); throw new ConstraintViolationException(violations);
} }
return entity; return entity;
} }
@ -74,4 +64,5 @@ public class ValidatingEntityCallback implements BeforeSaveCallback<Object>, Ord
public int getOrder() { public int getOrder() {
return 100; return 100;
} }
} }

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

@ -15,31 +15,30 @@
*/ */
package org.springframework.data.mongodb.core.mapping.event; package org.springframework.data.mongodb.core.mapping.event;
import java.util.Set; import jakarta.validation.ConstraintViolation;
import jakarta.validation.ConstraintViolationException; import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator; import jakarta.validation.Validator;
import org.apache.commons.logging.Log; import java.util.Set;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.Assert; import org.bson.Document;
/** /**
* javax.validation dependant entities validator. When it is registered as Spring component its automatically invoked * JSR-303 dependant entities validator.
* before entities are saved in database. * <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 Maciej Walkowiak
* @author Oliver Gierke * @author Oliver Gierke
* @author Christoph Strobl * @author Christoph Strobl
* * @deprecated since 4.5, use {@link ValidatingEntityCallback} respectively {@link ReactiveValidatingEntityCallback}
* @see {@link ValidatingEntityCallback} * instead to ensure ordering and interruption of saving when encountering validation constraint violations.
*/ */
@Deprecated(since = "4.5")
public class ValidatingMongoEventListener extends AbstractMongoEventListener<Object> { public class ValidatingMongoEventListener extends AbstractMongoEventListener<Object> {
private static final Log LOG = LogFactory.getLog(ValidatingMongoEventListener.class); private final BeanValidationDelegate delegate;
private final Validator validator;
/** /**
* Creates a new {@link ValidatingMongoEventListener} using the given {@link Validator}. * Creates a new {@link ValidatingMongoEventListener} using the given {@link Validator}.
@ -47,26 +46,17 @@ public class ValidatingMongoEventListener extends AbstractMongoEventListener<Obj
* @param validator must not be {@literal null}. * @param validator must not be {@literal null}.
*/ */
public ValidatingMongoEventListener(Validator validator) { public ValidatingMongoEventListener(Validator validator) {
this.delegate = new BeanValidationDelegate(validator);
Assert.notNull(validator, "Validator must not be null");
this.validator = validator;
} }
@SuppressWarnings({ "rawtypes", "unchecked" })
@Override @Override
public void onBeforeSave(BeforeSaveEvent<Object> event) { public void onBeforeSave(BeforeSaveEvent<Object> event) {
if (LOG.isDebugEnabled()) { Set<ConstraintViolation<Object>> violations = delegate.validate(event.getSource());
LOG.debug(String.format("Validating object: %s", event.getSource()));
}
Set violations = validator.validate(event.getSource());
if (!violations.isEmpty()) { if (!violations.isEmpty()) {
if (LOG.isDebugEnabled()) {
LOG.info(String.format("During object: %s validation violations found: %s", event.getSource(), violations));
}
throw new ConstraintViolationException(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 @@
/*
* 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 @@
/* /*
* 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -15,37 +15,38 @@
*/ */
package org.springframework.data.mongodb.core.mapping.event; package org.springframework.data.mongodb.core.mapping.event;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.*;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import jakarta.validation.ConstraintViolationException; import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validation; import jakarta.validation.Validation;
import jakarta.validation.ValidatorFactory; import jakarta.validation.ValidatorFactory;
import jakarta.validation.constraints.Min; import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import org.bson.Document; import org.bson.Document;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
/** /**
* Unit test for {@link ValidatingEntityCallback}. * Unit tests for {@link ValidatingEntityCallback}.
* *
* @author Rene Felgenträger * @author Rene Felgenträger
* @author Mark Paluch
*/ */
class ValidatingEntityCallbackUnitTests { class ValidatingEntityCallbackUnitTests {
private ValidatingEntityCallback callback; private ValidatingEntityCallback callback;
@BeforeEach @BeforeEach
public void setUp() { void setUp() {
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
callback = new ValidatingEntityCallback(factory.getValidator()); callback = new ValidatingEntityCallback(factory.getValidator());
} }
} }
@Test @Test // GH-4910
// GH-4910 void validationThrowsException() {
void invalidModel_throwsException() {
Coordinates coordinates = new Coordinates(-1, -1); Coordinates coordinates = new Coordinates(-1, -1);
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy( assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(
@ -53,11 +54,12 @@ class ValidatingEntityCallbackUnitTests {
.satisfies(e -> assertThat(e.getConstraintViolations()).hasSize(2)); .satisfies(e -> assertThat(e.getConstraintViolations()).hasSize(2));
} }
@Test @Test // GH-4910
// GH-4910 void validateSuccessful() {
void validModel_noExceptionThrown() {
Coordinates coordinates = new Coordinates(0, 0); Coordinates coordinates = new Coordinates(0, 0);
Object entity = callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates"); Object entity = callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates");
assertThat(entity).isEqualTo(coordinates); assertThat(entity).isEqualTo(coordinates);
} }
@ -72,4 +74,5 @@ class ValidatingEntityCallbackUnitTests {
""".formatted(x, y)); """.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
|=== |===
=== 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