Skip to content

Commit 54c7d8f

Browse files
rfelgentmp911de
authored andcommitted
Introduce ValidatingEntityCallback.
Closes #4901 Original pull request: #4910 Signed-off-by: felgentraeger <[email protected]>
1 parent ceac2b8 commit 54c7d8f

File tree

3 files changed

+154
-0
lines changed

3 files changed

+154
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
* Copyright 2012-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 java.util.Set;
22+
import org.apache.commons.logging.Log;
23+
import org.apache.commons.logging.LogFactory;
24+
import org.bson.Document;
25+
import org.springframework.core.Ordered;
26+
import org.springframework.util.Assert;
27+
28+
/**
29+
* JSR-303 dependant entities validator.
30+
* <p>
31+
* When it is registered as Spring component its automatically invoked after any {@link AbstractMongoEventListener} and
32+
* before entities are saved in database.
33+
*
34+
* @author original authors of {@link ValidatingMongoEventListener}
35+
* @author Rene Felgenträger
36+
* @see {@link ValidatingMongoEventListener}
37+
*/
38+
public class ValidatingEntityCallback implements BeforeSaveCallback<Object>, Ordered {
39+
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;
44+
45+
/**
46+
* Creates a new {@link ValidatingEntityCallback} using the given {@link Validator}.
47+
*
48+
* @param validator must not be {@literal null}.
49+
*/
50+
public ValidatingEntityCallback(Validator validator) {
51+
Assert.notNull(validator, "Validator must not be null");
52+
this.validator = validator;
53+
}
54+
55+
// TODO: alternatively implement the "BeforeConvertCallback" interface and set the order to highest value ?
56+
@Override
57+
public Object onBeforeSave(Object entity, Document document, String collection) {
58+
59+
if (LOG.isDebugEnabled()) {
60+
LOG.debug(String.format("Validating object: %s", entity));
61+
}
62+
Set<ConstraintViolation<Object>> violations = validator.validate(entity);
63+
64+
if (!violations.isEmpty()) {
65+
if (LOG.isDebugEnabled()) {
66+
LOG.info(String.format("During object: %s validation violations found: %s", entity, violations));
67+
}
68+
throw new ConstraintViolationException(violations);
69+
}
70+
return entity;
71+
}
72+
73+
@Override
74+
public int getOrder() {
75+
return 100;
76+
}
77+
}

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

+2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
* @author Maciej Walkowiak
3333
* @author Oliver Gierke
3434
* @author Christoph Strobl
35+
*
36+
* @see {@link ValidatingEntityCallback}
3537
*/
3638
public class ValidatingMongoEventListener extends AbstractMongoEventListener<Object> {
3739

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Copyright 2012-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 static org.assertj.core.api.Assertions.assertThat;
19+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
20+
21+
import jakarta.validation.ConstraintViolationException;
22+
import jakarta.validation.Validation;
23+
import jakarta.validation.ValidatorFactory;
24+
import jakarta.validation.constraints.Min;
25+
import jakarta.validation.constraints.NotNull;
26+
import org.bson.Document;
27+
import org.junit.jupiter.api.BeforeEach;
28+
import org.junit.jupiter.api.Test;
29+
30+
/**
31+
* Unit test for {@link ValidatingEntityCallback}.
32+
*
33+
* @author Rene Felgenträger
34+
*/
35+
class ValidatingEntityCallbackUnitTests {
36+
37+
private ValidatingEntityCallback callback;
38+
39+
@BeforeEach
40+
public void setUp() {
41+
try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) {
42+
callback = new ValidatingEntityCallback(factory.getValidator());
43+
}
44+
}
45+
46+
@Test
47+
// GH-4910
48+
void invalidModel_throwsException() {
49+
Coordinates coordinates = new Coordinates(-1, -1);
50+
51+
assertThatExceptionOfType(ConstraintViolationException.class).isThrownBy(
52+
() -> callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates"))
53+
.satisfies(e -> assertThat(e.getConstraintViolations()).hasSize(2));
54+
}
55+
56+
@Test
57+
// GH-4910
58+
void validModel_noExceptionThrown() {
59+
Coordinates coordinates = new Coordinates(0, 0);
60+
Object entity = callback.onBeforeSave(coordinates, coordinates.toDocument(), "coordinates");
61+
assertThat(entity).isEqualTo(coordinates);
62+
}
63+
64+
record Coordinates(@NotNull @Min(0) Integer x, @NotNull @Min(0) Integer y) {
65+
66+
Document toDocument() {
67+
return Document.parse("""
68+
{
69+
"x": %d,
70+
"y": %d
71+
}
72+
""".formatted(x, y));
73+
}
74+
}
75+
}

0 commit comments

Comments
 (0)