Skip to content

Commit b8e805e

Browse files
committed
hibernate-validator: better support for custom ConstraintValidatorFactory
- simplify custom ConstraintValidatorFactory based on DI as well as manually created factories - integrates Validator factory into hibernate - fix #3595
1 parent 2998385 commit b8e805e

File tree

6 files changed

+200
-72
lines changed

6 files changed

+200
-72
lines changed

docs/asciidoc/modules/hibernate-validator.adoc

Lines changed: 21 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,10 @@ Bean validation via https://hibernate.org/validator/[Hibernate Validator].
1717
import io.jooby.hibernate.validator.HibernateValidatorModule;
1818
1919
{
20-
install(new HibernateValidatorModule());
20+
install(new HibernateValidatorModule()); <1>
21+
22+
// Optional
23+
install(new HibernateModule()); <2>
2124
}
2225
----
2326

@@ -27,10 +30,17 @@ import io.jooby.hibernate.validator.HibernateValidatorModule;
2730
import io.jooby.hibernate.validator.HibernateValidatorModule
2831
2932
{
30-
install(HibernateValidatorModule())
33+
install(HibernateValidatorModule()) <1>
34+
35+
// Optional
36+
install(new HibernateModule()) <2>
3137
}
3238
----
3339

40+
<1> Install Hibernate Validator
41+
<2> HibernateModule must be installed after HibernateValidatorModule. This step is optional, but
42+
required if you choose Hibernate as persistence provider.
43+
3444
3) Usage in MVC routes
3545

3646
.Java
@@ -281,72 +291,25 @@ As you know, `Hibernate Validator` allows you to build fully custom `ConstraintV
281291
In some scenarios, you may need access not only to the bean but also to services, repositories, or other resources
282292
to perform more complex validations required by business rules.
283293

284-
In this case you need to implement a custom `ConstraintValidatorFactory` that will rely on your DI framework
294+
In this case you need to implement a custom `ConstraintValidator` that will rely on your DI framework
285295
instantiating your custom `ConstraintValidator`
286296

287297
1) Implement custom `ConstraintValidatorFactory`:
288298

289299
[source, java]
290300
----
291-
public class MyConstraintValidatorFactory implements ConstraintValidatorFactory {
292301
293-
private final Function<Class<?>, ?> require;
294-
private final ConstraintValidatorFactory defaultFactory;
302+
@Constraint(validatedBy = MyCustomValidator.class)
303+
@Target({TYPE, ANNOTATION_TYPE})
304+
@Retention(RUNTIME)
305+
public @interface MyCustomAnnotation {
306+
String message() default "My custom message";
295307
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-
}
308+
Class<?>[] groups() default {};
303309
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-
}
310+
Class<? extends Payload>[] payload() default {};
327311
}
328-
----
329312
330-
2) Register your custom `ConstraintValidatorFactory`:
331-
332-
[source, java]
333-
----
334-
{
335-
install(new HibernateValidatorModule().doWith(cfg -> {
336-
cfg.constraintValidatorFactory(new MyConstraintValidatorFactory(this::require)); // <1>
337-
}));
338-
}
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-
345-
346-
3) Implement your custom `ConstraintValidator`
347-
348-
[source, java]
349-
----
350313
public class MyCustomValidator implements ConstraintValidator<MyCustomAnnotation, Bean> {
351314
352315
// This is the service you want to inject
@@ -365,6 +328,7 @@ public class MyCustomValidator implements ConstraintValidator<MyCustomAnnotation
365328
}
366329
----
367330

331+
368332
=== Configuration
369333
Any property defined at `hibernate.validator` will be added automatically:
370334

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

Lines changed: 37 additions & 8 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

@@ -58,6 +59,7 @@ public class HibernateValidatorModule implements Extension {
5859
private String title = "Validation failed";
5960
private boolean disableDefaultViolationHandler = false;
6061
private boolean logException = false;
62+
private List<ConstraintValidatorFactory> factories;
6163

6264
/**
6365
* Setups a configurer callback.
@@ -118,6 +120,21 @@ public HibernateValidatorModule disableViolationHandler() {
118120
return this;
119121
}
120122

123+
/**
124+
* Add a custom {@link ConstraintValidatorFactory}. This factory is allowed to returns <code>null
125+
* </code> allowing next factory to create an instance (default or one provided by DI).
126+
*
127+
* @param factory Factory.
128+
* @return This module.
129+
*/
130+
public HibernateValidatorModule with(ConstraintValidatorFactory factory) {
131+
if (factories == null) {
132+
factories = new ArrayList<>();
133+
}
134+
this.factories.add(factory);
135+
return this;
136+
}
137+
121138
@Override
122139
public void install(@NonNull Jooby app) throws Exception {
123140
var config = app.getConfig();
@@ -132,14 +149,26 @@ public void install(@NonNull Jooby app) throws Exception {
132149
hbvConfig.addProperty(CONFIG_ROOT_PATH + "." + k, v.unwrapped().toString()));
133150
}
134151

152+
// Set default constraint validator factory.
153+
var delegateFactory =
154+
new CompositeConstraintValidatorFactory(
155+
app, hbvConfig.getDefaultConstraintValidatorFactory());
156+
if (this.factories != null) {
157+
this.factories.forEach(delegateFactory::add);
158+
this.factories.clear();
159+
}
160+
hbvConfig.constraintValidatorFactory(delegateFactory);
135161
if (configurer != null) {
136162
configurer.accept(hbvConfig);
137163
}
138-
164+
var services = app.getServices();
139165
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));
166+
var validator = factory.getValidator();
167+
services.put(Validator.class, validator);
168+
services.put(BeanValidator.class, new BeanValidatorImpl(validator));
169+
// Allow to access validator factory so hibernate can access later
170+
var constraintValidatorFactory = factory.getConstraintValidatorFactory();
171+
services.put(ConstraintValidatorFactory.class, constraintValidatorFactory);
143172

144173
if (!disableDefaultViolationHandler) {
145174
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+
}

modules/jooby-hibernate-validator/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
/** Hibernate Validator Module. */
77
module io.jooby.hibernate.validator {
88
exports io.jooby.hibernate.validator;
9+
exports io.jooby.internal.hibernate.validator;
910

1011
requires transitive io.jooby;
1112
requires static com.github.spotbugs.annotations;

modules/jooby-hibernate/src/main/java/io/jooby/hibernate/HibernateModule.java

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,7 @@
55
*/
66
package io.jooby.hibernate;
77

8-
import java.util.Collections;
9-
import java.util.HashMap;
10-
import java.util.List;
11-
import java.util.Objects;
12-
import java.util.Optional;
8+
import java.util.*;
139
import java.util.stream.Collectors;
1410
import java.util.stream.Stream;
1511

@@ -127,7 +123,8 @@
127123
*
128124
* Transaction and lifecycle of session/entityManager is managed by {@link TransactionalRequest}.
129125
*
130-
* <p>Complete documentation is available at: https://jooby.io/modules/hibernate.
126+
* <p>Complete documentation is available at: <a
127+
* href="https://jooby.io/modules/hibernate">hibernate</a>.
131128
*
132129
* @author edgar
133130
* @since 2.0.0
@@ -159,7 +156,7 @@ public HibernateModule(@NonNull String name, Class<?>... classes) {
159156
*
160157
* @param classes Persistent classes.
161158
*/
162-
public HibernateModule(Class... classes) {
159+
public HibernateModule(Class<?>... classes) {
163160
this("db", classes);
164161
}
165162

@@ -207,6 +204,17 @@ public HibernateModule(@NonNull String name, List<Class<?>> classes) {
207204
return this;
208205
}
209206

207+
/**
208+
* Allow to customize a {@link StatelessSession} before opening it.
209+
*
210+
* @param sessionProvider Session customizer.
211+
* @return This module.
212+
*/
213+
public @NonNull HibernateModule with(@NonNull StatelessSessionProvider sessionProvider) {
214+
this.statelessSessionProvider = sessionProvider;
215+
return this;
216+
}
217+
210218
/**
211219
* Hook into Hibernate bootstrap components and allow to customize them.
212220
*
@@ -294,6 +302,19 @@ public void install(@NonNull Jooby application) {
294302
var sfb = metadata.getSessionFactoryBuilder();
295303
sfb.applyName(name);
296304
sfb.applyNameAsJndiName(false);
305+
/*
306+
Bind Validator instance, so hibernate doesn't create a new factory.
307+
Need to scan due hibernate doesn't depend on validation classes
308+
*/
309+
registry.entrySet().stream()
310+
.filter(
311+
it ->
312+
it.getKey()
313+
.getType()
314+
.getName()
315+
.equals("jakarta.validation.ConstraintValidatorFactory"))
316+
.findFirst()
317+
.ifPresent(it -> sfb.applyValidatorFactory(it.getValue().get()));
297318

298319
configurer.configure(sfb, config);
299320

0 commit comments

Comments
 (0)