Skip to content

Commit d7c40a4

Browse files
committed
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

+14-24
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+
}

0 commit comments

Comments
 (0)