Skip to content

Commit 13e1c7c

Browse files
authored
Merge pull request #3596 from jooby-project/3595
hibernate-validator: better support for custom ConstraintValidatorFactory
2 parents ea9a3f7 + 59cc4ce commit 13e1c7c

File tree

6 files changed

+231
-88
lines changed

6 files changed

+231
-88
lines changed

docs/asciidoc/modules/hibernate-validator.adoc

Lines changed: 32 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,8 @@ import jakarta.validation.Validator;
224224
225225
{
226226
post("/validate", ctx -> {
227-
Validator validator = require(Validator.class);
228-
Set<ConstraintViolation<Bean>> violations = validator.validate(ctx.body(Bean.class));
227+
var validator = require(Validator.class);
228+
var violations = validator.validate(ctx.body(Bean.class));
229229
if (!violations.isEmpty()) {
230230
...
231231
}
@@ -281,72 +281,23 @@ As you know, `Hibernate Validator` allows you to build fully custom `ConstraintV
281281
In some scenarios, you may need access not only to the bean but also to services, repositories, or other resources
282282
to perform more complex validations required by business rules.
283283

284-
In this case you need to implement a custom `ConstraintValidatorFactory` that will rely on your DI framework
285-
instantiating your custom `ConstraintValidator`
286-
287-
1) Implement custom `ConstraintValidatorFactory`:
284+
In this case you need to implement a custom `ConstraintValidator` that will rely on your DI framework.
288285

286+
.Custom Annotation and Validator
289287
[source, java]
290288
----
291-
public class MyConstraintValidatorFactory implements ConstraintValidatorFactory {
292-
293-
private final Function<Class<?>, ?> require;
294-
private final ConstraintValidatorFactory defaultFactory;
295289
296-
public MyConstraintValidatorFactory(Function<Class<?>, ?> require) {
297-
this.require = require;
298-
try (ValidatorFactory factory = Validation.byDefaultProvider()
299-
.configure().buildValidatorFactory()) {
300-
this.defaultFactory = factory.getConstraintValidatorFactory();
301-
}
302-
}
303-
304-
@Override
305-
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
306-
if (isBuiltIn(key)) {
307-
// use default factory for built-in constraint validators
308-
return defaultFactory.getInstance(key);
309-
} else {
310-
// use DI to instantiate custom constraint validator
311-
return (T) require.apply(key);
312-
}
313-
}
314-
315-
@Override
316-
public void releaseInstance(ConstraintValidator<?, ?> instance) {
317-
if(isBuiltIn(instance.getClass())) {
318-
defaultFactory.releaseInstance(instance);
319-
} else {
320-
// No-op: lifecycle usually handled by DI framework
321-
}
322-
}
323-
324-
private boolean isBuiltIn(Class<?> key) {
325-
return key.getName().startsWith("org.hibernate.validator");
326-
}
327-
}
328-
----
290+
@Constraint(validatedBy = MyCustomValidator.class)
291+
@Target({TYPE, ANNOTATION_TYPE})
292+
@Retention(RUNTIME)
293+
public @interface MyCustomAnnotation {
294+
String message() default "My custom message";
329295
330-
2) Register your custom `ConstraintValidatorFactory`:
296+
Class<?>[] groups() default {};
331297
332-
[source, java]
333-
----
334-
{
335-
install(new HibernateValidatorModule().doWith(cfg -> {
336-
cfg.constraintValidatorFactory(new MyConstraintValidatorFactory(this::require)); // <1>
337-
}));
298+
Class<? extends Payload>[] payload() default {};
338299
}
339-
----
340-
341-
<1> This approach using `require` will work with `Guice` or `Avaje`. For `Dagger`, a bit more effort is required,
342-
but the concept is the same, and the same result can be achieved. Both `Avaje` and `Dagger` require additional
343-
configuration due to their build-time nature.
344-
345300
346-
3) Implement your custom `ConstraintValidator`
347-
348-
[source, java]
349-
----
350301
public class MyCustomValidator implements ConstraintValidator<MyCustomAnnotation, Bean> {
351302
352303
// This is the service you want to inject
@@ -381,8 +332,26 @@ Or programmatically:
381332
import io.jooby.hibernate.validator.HibernateValidatorModule;
382333
383334
{
384-
install(new HibernateValidatorModule().doWith(cfg -> {
385-
cfg.failFast(true);
386-
}));
335+
var cfg = byProvider(HibernateValidator.class).configure();
336+
cfg.failFast(true);
337+
install(new HibernateValidatorModule(cfg));
387338
}
388339
----
340+
341+
=== Hibernate integration
342+
343+
Just install `HibernateValidatorModule` before `HibernateModule`, like:
344+
345+
[source, java]
346+
----
347+
import io.jooby.hibernate.validator.HibernateValidatorModule;
348+
349+
{
350+
install(new HibernateValidatorModule());
351+
352+
install(new HibernateModule());
353+
}
354+
----
355+
356+
The `HibernateModule` will detect the constraint validator factory and setup. This avoid creating
357+
a new instance of constraint validator factory.

modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,18 @@
77

88
import static jakarta.validation.Validation.byProvider;
99

10+
import java.util.ArrayList;
11+
import java.util.List;
1012
import java.util.function.Consumer;
1113

1214
import org.hibernate.validator.HibernateValidator;
1315
import org.hibernate.validator.HibernateValidatorConfiguration;
1416

1517
import edu.umd.cs.findbugs.annotations.NonNull;
16-
import io.jooby.Context;
17-
import io.jooby.Extension;
18-
import io.jooby.Jooby;
19-
import io.jooby.StatusCode;
18+
import io.jooby.*;
19+
import io.jooby.internal.hibernate.validator.CompositeConstraintValidatorFactory;
2020
import io.jooby.validation.BeanValidator;
21+
import jakarta.validation.ConstraintValidatorFactory;
2122
import jakarta.validation.ConstraintViolationException;
2223
import jakarta.validation.Validator;
2324

@@ -53,18 +54,32 @@
5354
*/
5455
public class HibernateValidatorModule implements Extension {
5556
private static final String CONFIG_ROOT_PATH = "hibernate.validator";
57+
// TODO: remove it on next major
5658
private Consumer<HibernateValidatorConfiguration> configurer;
5759
private StatusCode statusCode = StatusCode.UNPROCESSABLE_ENTITY;
5860
private String title = "Validation failed";
5961
private boolean disableDefaultViolationHandler = false;
6062
private boolean logException = false;
63+
private List<ConstraintValidatorFactory> factories;
64+
private final HibernateValidatorConfiguration configuration;
65+
66+
public HibernateValidatorModule(@NonNull HibernateValidatorConfiguration configuration) {
67+
this.configuration = configuration;
68+
}
69+
70+
public HibernateValidatorModule() {
71+
this(byProvider(HibernateValidator.class).configure());
72+
}
6173

6274
/**
6375
* Setups a configurer callback.
6476
*
6577
* @param configurer Configurer callback.
6678
* @return This module.
79+
* @deprecated Use {@link
80+
* HibernateValidatorModule#HibernateValidatorModule(HibernateValidatorConfiguration)}
6781
*/
82+
@Deprecated
6883
public HibernateValidatorModule doWith(
6984
@NonNull final Consumer<HibernateValidatorConfiguration> configurer) {
7085
this.configurer = configurer;
@@ -118,28 +133,53 @@ public HibernateValidatorModule disableViolationHandler() {
118133
return this;
119134
}
120135

136+
/**
137+
* Add a custom {@link ConstraintValidatorFactory}. This factory is allowed to returns <code>null
138+
* </code> allowing next factory to create an instance (default or one provided by DI).
139+
*
140+
* @param factory Factory.
141+
* @return This module.
142+
*/
143+
public HibernateValidatorModule with(ConstraintValidatorFactory factory) {
144+
if (factories == null) {
145+
factories = new ArrayList<>();
146+
}
147+
this.factories.add(factory);
148+
return this;
149+
}
150+
121151
@Override
122152
public void install(@NonNull Jooby app) throws Exception {
123153
var config = app.getConfig();
124-
var hbvConfig = byProvider(HibernateValidator.class).configure();
125-
126154
if (config.hasPath(CONFIG_ROOT_PATH)) {
127155
config
128156
.getConfig(CONFIG_ROOT_PATH)
129157
.root()
130158
.forEach(
131159
(k, v) ->
132-
hbvConfig.addProperty(CONFIG_ROOT_PATH + "." + k, v.unwrapped().toString()));
160+
configuration.addProperty(CONFIG_ROOT_PATH + "." + k, v.unwrapped().toString()));
133161
}
134162

163+
// Set default constraint validator factory.
164+
var delegateFactory =
165+
new CompositeConstraintValidatorFactory(
166+
app, configuration.getDefaultConstraintValidatorFactory());
167+
if (this.factories != null) {
168+
this.factories.forEach(delegateFactory::add);
169+
this.factories.clear();
170+
}
171+
configuration.constraintValidatorFactory(delegateFactory);
135172
if (configurer != null) {
136-
configurer.accept(hbvConfig);
173+
configurer.accept(configuration);
137174
}
138-
139-
try (var factory = hbvConfig.buildValidatorFactory()) {
140-
Validator validator = factory.getValidator();
141-
app.getServices().put(Validator.class, validator);
142-
app.getServices().put(BeanValidator.class, new BeanValidatorImpl(validator));
175+
var services = app.getServices();
176+
try (var factory = configuration.buildValidatorFactory()) {
177+
var validator = factory.getValidator();
178+
services.put(Validator.class, validator);
179+
services.put(BeanValidator.class, new BeanValidatorImpl(validator));
180+
// Allow to access validator factory so hibernate can access later
181+
var constraintValidatorFactory = factory.getConstraintValidatorFactory();
182+
services.put(ConstraintValidatorFactory.class, constraintValidatorFactory);
143183

144184
if (!disableDefaultViolationHandler) {
145185
app.error(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.hibernate.validator;
7+
8+
import java.util.Deque;
9+
import java.util.LinkedList;
10+
11+
import org.slf4j.Logger;
12+
13+
import io.jooby.Jooby;
14+
import jakarta.validation.ConstraintValidator;
15+
import jakarta.validation.ConstraintValidatorFactory;
16+
17+
public class CompositeConstraintValidatorFactory implements ConstraintValidatorFactory {
18+
private final Logger log;
19+
private final ConstraintValidatorFactory defaultFactory;
20+
private final Deque<ConstraintValidatorFactory> factories = new LinkedList<>();
21+
22+
public CompositeConstraintValidatorFactory(
23+
Jooby registry, ConstraintValidatorFactory defaultFactory) {
24+
this.log = registry.getLog();
25+
this.defaultFactory = defaultFactory;
26+
this.factories.addLast(new RegistryConstraintValidatorFactory(registry));
27+
}
28+
29+
public ConstraintValidatorFactory add(ConstraintValidatorFactory factory) {
30+
this.factories.addFirst(factory);
31+
return this;
32+
}
33+
34+
@Override
35+
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
36+
if (isBuiltIn(key)) {
37+
// use default factory for built-in constraint validators
38+
return defaultFactory.getInstance(key);
39+
} else {
40+
for (var factory : factories) {
41+
var instance = factory.getInstance(key);
42+
if (instance != null) {
43+
return instance;
44+
}
45+
}
46+
// fallback or fail
47+
return defaultFactory.getInstance(key);
48+
}
49+
}
50+
51+
@Override
52+
public void releaseInstance(ConstraintValidator<?, ?> instance) {
53+
if (isBuiltIn(instance.getClass())) {
54+
defaultFactory.releaseInstance(instance);
55+
} else {
56+
if (instance instanceof AutoCloseable closeable) {
57+
try {
58+
closeable.close();
59+
} catch (Exception e) {
60+
log.debug("Failed to release constraint", e);
61+
}
62+
}
63+
}
64+
}
65+
66+
private boolean isBuiltIn(Class<?> key) {
67+
var name = key.getName();
68+
return name.startsWith("org.hibernate.validator") || name.startsWith("jakarta.validation");
69+
}
70+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Jooby https://jooby.io
3+
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
4+
* Copyright 2014 Edgar Espina
5+
*/
6+
package io.jooby.internal.hibernate.validator;
7+
8+
import org.slf4j.Logger;
9+
import org.slf4j.LoggerFactory;
10+
11+
import io.jooby.Registry;
12+
import io.jooby.exception.RegistryException;
13+
import jakarta.validation.ConstraintValidator;
14+
import jakarta.validation.ConstraintValidatorFactory;
15+
16+
public class RegistryConstraintValidatorFactory implements ConstraintValidatorFactory {
17+
private final Logger log = LoggerFactory.getLogger(getClass());
18+
private final Registry registry;
19+
20+
public RegistryConstraintValidatorFactory(Registry registry) {
21+
this.registry = registry;
22+
}
23+
24+
@Override
25+
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
26+
try {
27+
return registry.require(key);
28+
} catch (RegistryException notfound) {
29+
return null;
30+
}
31+
}
32+
33+
@Override
34+
public void releaseInstance(ConstraintValidator<?, ?> instance) {
35+
if (instance instanceof AutoCloseable closeable) {
36+
try {
37+
closeable.close();
38+
} catch (Exception e) {
39+
log.debug("Failed to release constraint", e);
40+
}
41+
}
42+
}
43+
}

0 commit comments

Comments
 (0)