Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit d7c40a4

Browse files
committedMar 4, 2025··
Polishing.
Introduce ReactiveValidatingEntityCallback, extract BeanValidationDelegate. Document Bean Validation callbacks. See #4901 Original pull request: #4910
1 parent 54c7d8f commit d7c40a4

File tree

7 files changed

+302
-57
lines changed

7 files changed

+302
-57
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.mapping.event;
17+
18+
import jakarta.validation.ConstraintViolation;
19+
import jakarta.validation.Validator;
20+
21+
import java.util.Set;
22+
23+
import org.apache.commons.logging.Log;
24+
import org.apache.commons.logging.LogFactory;
25+
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* Delegate to handle common calls to Bean {@link Validator Validation}.
30+
*
31+
* @author Mark Paluch
32+
* @since 4.5
33+
*/
34+
class BeanValidationDelegate {
35+
36+
private static final Log LOG = LogFactory.getLog(BeanValidationDelegate.class);
37+
38+
private final Validator validator;
39+
40+
/**
41+
* Creates a new {@link BeanValidationDelegate} using the given {@link Validator}.
42+
*
43+
* @param validator must not be {@literal null}.
44+
*/
45+
public BeanValidationDelegate(Validator validator) {
46+
Assert.notNull(validator, "Validator must not be null");
47+
this.validator = validator;
48+
}
49+
50+
/**
51+
* Validate the given object.
52+
*
53+
* @param object
54+
* @return set of constraint violations.
55+
*/
56+
public Set<ConstraintViolation<Object>> validate(Object object) {
57+
58+
if (LOG.isDebugEnabled()) {
59+
LOG.debug(String.format("Validating object: %s", object));
60+
}
61+
62+
Set<ConstraintViolation<Object>> violations = validator.validate(object);
63+
64+
if (!violations.isEmpty()) {
65+
if (LOG.isDebugEnabled()) {
66+
LOG.info(String.format("During object: %s validation violations found: %s", object, violations));
67+
}
68+
}
69+
70+
return violations;
71+
}
72+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.mapping.event;
17+
18+
import jakarta.validation.ConstraintViolation;
19+
import jakarta.validation.ConstraintViolationException;
20+
import jakarta.validation.Validator;
21+
import reactor.core.publisher.Mono;
22+
23+
import java.util.Set;
24+
25+
import org.bson.Document;
26+
27+
import org.springframework.core.Ordered;
28+
29+
/**
30+
* Reactive variant of JSR-303 dependant entities validator.
31+
* <p>
32+
* When it is registered as Spring component its automatically invoked after object to {@link Document} conversion and
33+
* before entities are saved to the database.
34+
*
35+
* @author Mark Paluch
36+
* @author Rene Felgenträger
37+
* @since 4.5
38+
*/
39+
public class ReactiveValidatingEntityCallback implements ReactiveBeforeSaveCallback<Object>, Ordered {
40+
41+
private final BeanValidationDelegate delegate;
42+
43+
/**
44+
* Creates a new {@link ReactiveValidatingEntityCallback} using the given {@link Validator}.
45+
*
46+
* @param validator must not be {@literal null}.
47+
*/
48+
public ReactiveValidatingEntityCallback(Validator validator) {
49+
this.delegate = new BeanValidationDelegate(validator);
50+
}
51+
52+
@Override
53+
public Mono<Object> onBeforeSave(Object entity, Document document, String collection) {
54+
55+
Set<ConstraintViolation<Object>> violations = delegate.validate(entity);
56+
57+
if (!violations.isEmpty()) {
58+
return Mono.error(new ConstraintViolationException(violations));
59+
}
60+
61+
return Mono.just(entity);
62+
}
63+
64+
@Override
65+
public int getOrder() {
66+
return 100;
67+
}
68+
69+
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2025 the original author or authors.
2+
* Copyright 2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,60 +18,51 @@
1818
import jakarta.validation.ConstraintViolation;
1919
import jakarta.validation.ConstraintViolationException;
2020
import jakarta.validation.Validator;
21+
2122
import java.util.Set;
22-
import org.apache.commons.logging.Log;
23-
import org.apache.commons.logging.LogFactory;
23+
2424
import org.bson.Document;
25+
2526
import org.springframework.core.Ordered;
26-
import org.springframework.util.Assert;
2727

2828
/**
2929
* JSR-303 dependant entities validator.
3030
* <p>
31-
* When it is registered as Spring component its automatically invoked after any {@link AbstractMongoEventListener} and
32-
* before entities are saved in database.
31+
* When it is registered as Spring component its automatically invoked after object to {@link Document} conversion and
32+
* before entities are saved to the database.
3333
*
34-
* @author original authors of {@link ValidatingMongoEventListener}
3534
* @author Rene Felgenträger
36-
* @see {@link ValidatingMongoEventListener}
35+
* @author Mark Paluch
36+
* @since 4.5
3737
*/
3838
public class ValidatingEntityCallback implements BeforeSaveCallback<Object>, Ordered {
3939

40-
private static final Log LOG = LogFactory.getLog(ValidatingEntityCallback.class);
41-
42-
// TODO: create a validation handler (similar to "AuditingHandler") an reference it from "ValidatingMongoEventListener" and "ValidatingMongoEventListener"
43-
private final Validator validator;
40+
private final BeanValidationDelegate delegate;
4441

4542
/**
4643
* Creates a new {@link ValidatingEntityCallback} using the given {@link Validator}.
4744
*
4845
* @param validator must not be {@literal null}.
4946
*/
5047
public ValidatingEntityCallback(Validator validator) {
51-
Assert.notNull(validator, "Validator must not be null");
52-
this.validator = validator;
48+
this.delegate = new BeanValidationDelegate(validator);
5349
}
5450

55-
// TODO: alternatively implement the "BeforeConvertCallback" interface and set the order to highest value ?
5651
@Override
5752
public Object onBeforeSave(Object entity, Document document, String collection) {
5853

59-
if (LOG.isDebugEnabled()) {
60-
LOG.debug(String.format("Validating object: %s", entity));
61-
}
62-
Set<ConstraintViolation<Object>> violations = validator.validate(entity);
54+
Set<ConstraintViolation<Object>> violations = delegate.validate(entity);
6355

6456
if (!violations.isEmpty()) {
65-
if (LOG.isDebugEnabled()) {
66-
LOG.info(String.format("During object: %s validation violations found: %s", entity, violations));
67-
}
6857
throw new ConstraintViolationException(violations);
6958
}
59+
7060
return entity;
7161
}
7262

7363
@Override
7464
public int getOrder() {
7565
return 100;
7666
}
67+
7768
}

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

Lines changed: 14 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -15,58 +15,48 @@
1515
*/
1616
package org.springframework.data.mongodb.core.mapping.event;
1717

18-
import java.util.Set;
19-
18+
import jakarta.validation.ConstraintViolation;
2019
import jakarta.validation.ConstraintViolationException;
2120
import jakarta.validation.Validator;
2221

23-
import org.apache.commons.logging.Log;
24-
import org.apache.commons.logging.LogFactory;
22+
import java.util.Set;
2523

26-
import org.springframework.util.Assert;
24+
import org.bson.Document;
2725

2826
/**
29-
* javax.validation dependant entities validator. When it is registered as Spring component its automatically invoked
30-
* before entities are saved in database.
27+
* JSR-303 dependant entities validator.
28+
* <p>
29+
* When it is registered as Spring component its automatically invoked after object to {@link Document} conversion and
30+
* before entities are saved to the database.
3131
*
3232
* @author Maciej Walkowiak
3333
* @author Oliver Gierke
3434
* @author Christoph Strobl
35-
*
36-
* @see {@link ValidatingEntityCallback}
35+
* @deprecated since 4.5, use {@link ValidatingEntityCallback} respectively {@link ReactiveValidatingEntityCallback}
36+
* instead to ensure ordering and interruption of saving when encountering validation constraint violations.
3737
*/
38+
@Deprecated(since = "4.5")
3839
public class ValidatingMongoEventListener extends AbstractMongoEventListener<Object> {
3940

40-
private static final Log LOG = LogFactory.getLog(ValidatingMongoEventListener.class);
41-
42-
private final Validator validator;
41+
private final BeanValidationDelegate delegate;
4342

4443
/**
4544
* Creates a new {@link ValidatingMongoEventListener} using the given {@link Validator}.
4645
*
4746
* @param validator must not be {@literal null}.
4847
*/
4948
public ValidatingMongoEventListener(Validator validator) {
50-
51-
Assert.notNull(validator, "Validator must not be null");
52-
this.validator = validator;
49+
this.delegate = new BeanValidationDelegate(validator);
5350
}
5451

55-
@SuppressWarnings({ "rawtypes", "unchecked" })
5652
@Override
5753
public void onBeforeSave(BeforeSaveEvent<Object> event) {
5854

59-
if (LOG.isDebugEnabled()) {
60-
LOG.debug(String.format("Validating object: %s", event.getSource()));
61-
}
62-
Set violations = validator.validate(event.getSource());
55+
Set<ConstraintViolation<Object>> violations = delegate.validate(event.getSource());
6356

6457
if (!violations.isEmpty()) {
65-
66-
if (LOG.isDebugEnabled()) {
67-
LOG.info(String.format("During object: %s validation violations found: %s", event.getSource(), violations));
68-
}
6958
throw new ConstraintViolationException(violations);
7059
}
7160
}
61+
7262
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2025 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.data.mongodb.core.mapping.event;
17+
18+
import jakarta.validation.ConstraintViolationException;
19+
import jakarta.validation.Validation;
20+
import jakarta.validation.ValidatorFactory;
21+
import jakarta.validation.constraints.Min;
22+
import jakarta.validation.constraints.NotNull;
23+
import reactor.test.StepVerifier;
24+
25+
import org.bson.Document;
26+
import org.junit.jupiter.api.BeforeEach;
27+
import org.junit.jupiter.api.Test;
28+
29+
/**
30+
* Unit tests for {@link ReactiveValidatingEntityCallback}.
31+
*
32+
* @author Mark Paluch
33+
* @author Rene Felgenträger
34+
*/
35+
class ReactiveValidatingEntityCallbackUnitTests {
36+
37+
private ReactiveValidatingEntityCallback callback;
38+
39+
@BeforeEach
40+
void setUp() {
41+
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
42+
callback = new ReactiveValidatingEntityCallback(factory.getValidator());
43+
}
44+
}
45+
46+
@Test // GH-4910
47+
void validationThrowsException() {
48+
49+
Coordinates coordinates = new Coordinates(-1, -1);
50+
51+
callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates") //
52+
.as(StepVerifier::create) //
53+
.verifyError(ConstraintViolationException.class);
54+
}
55+
56+
@Test // GH-4910
57+
void validateSuccessful() {
58+
59+
Coordinates coordinates = new Coordinates(0, 0);
60+
61+
callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates") //
62+
.as(StepVerifier::create) //
63+
.expectNext(coordinates) //
64+
.verifyComplete();
65+
}
66+
67+
record Coordinates(@NotNull @Min(0) Integer x, @NotNull @Min(0) Integer y) {
68+
69+
Document toDocument() {
70+
return Document.parse("""
71+
{
72+
"x": %d,
73+
"y": %d
74+
}
75+
""".formatted(x, y));
76+
}
77+
}
78+
79+
}

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

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2025 the original author or authors.
2+
* Copyright 2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,49 +15,51 @@
1515
*/
1616
package org.springframework.data.mongodb.core.mapping.event;
1717

18-
import static org.assertj.core.api.Assertions.assertThat;
19-
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
18+
import static org.assertj.core.api.Assertions.*;
2019

2120
import jakarta.validation.ConstraintViolationException;
2221
import jakarta.validation.Validation;
2322
import jakarta.validation.ValidatorFactory;
2423
import jakarta.validation.constraints.Min;
2524
import jakarta.validation.constraints.NotNull;
25+
2626
import org.bson.Document;
2727
import org.junit.jupiter.api.BeforeEach;
2828
import org.junit.jupiter.api.Test;
2929

3030
/**
31-
* Unit test for {@link ValidatingEntityCallback}.
31+
* Unit tests for {@link ValidatingEntityCallback}.
3232
*
3333
* @author Rene Felgenträger
34+
* @author Mark Paluch
3435
*/
3536
class ValidatingEntityCallbackUnitTests {
3637

3738
private ValidatingEntityCallback callback;
3839

3940
@BeforeEach
40-
public void setUp() {
41+
void setUp() {
4142
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
4243
callback = new ValidatingEntityCallback(factory.getValidator());
4344
}
4445
}
4546

46-
@Test
47-
// GH-4910
48-
void invalidModel_throwsException() {
47+
@Test // GH-4910
48+
void validationThrowsException() {
49+
4950
Coordinates coordinates = new Coordinates(-1, -1);
5051

5152
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(
5253
() -> callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates"))
5354
.satisfies(e -> assertThat(e.getConstraintViolations()).hasSize(2));
5455
}
5556

56-
@Test
57-
// GH-4910
58-
void validModel_noExceptionThrown() {
57+
@Test // GH-4910
58+
void validateSuccessful() {
59+
5960
Coordinates coordinates = new Coordinates(0, 0);
6061
Object entity = callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates");
62+
6163
assertThat(entity).isEqualTo(coordinates);
6264
}
6365

@@ -72,4 +74,5 @@ Document toDocument() {
7274
""".formatted(x, y));
7375
}
7476
}
77+
7578
}

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

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,44 @@ Can modify the domain object, to be returned after save, `Document` containing a
104104

105105
|===
106106

107+
=== Bean Validation
108+
109+
Spring Data MongoDB supports Bean Validation for MongoDB entities annotated with https://beanvalidation.org/[https://xxx][Jakarta Validation annotations].
110+
111+
You can enable Bean Validation by registering `ValidatingEntityCallback` respectively `ReactiveValidatingEntityCallback` for reactive driver usage in your Spring `ApplicationContext` as shown in the following example:
112+
113+
[tabs]
114+
======
115+
Imperative::
116+
+
117+
[source,java,indent=0,subs="verbatim,quotes",role="primary"]
118+
----
119+
@Configuration
120+
class Config {
121+
122+
@Bean
123+
public ValidatingEntityCallback validatingEntityCallback(Validator validator) {
124+
return new ValidatingEntityCallback(validator);
125+
}
126+
}
127+
----
128+
129+
Reactive::
130+
+
131+
[source,java,indent=0,subs="verbatim,quotes",role="secondary"]
132+
----
133+
@Configuration
134+
class Config {
135+
136+
@Bean
137+
public ReactiveValidatingEntityCallback validatingEntityCallback(Validator validator) {
138+
return new ReactiveValidatingEntityCallback(validator);
139+
}
140+
}
141+
----
142+
======
143+
144+
If you're using both, imperative and reactive, then you can enable also both callbacks.
145+
146+
NOTE: When using XML-based configuration, historically, `ValidatingMongoEventListener` is registered through our namespace handlers when configuring `<mongo:mapping-converter>`.
147+
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.

0 commit comments

Comments
 (0)
Please sign in to comment.