Skip to content

Commit

Permalink
hibernate-validator: better support for custom ConstraintValidatorFac…
Browse files Browse the repository at this point in the history
…tory

- simplify custom ConstraintValidatorFactory based on DI as well as manually created factories
- integrates Validator factory into hibernate

- fix #3595
  • Loading branch information
jknack committed Dec 8, 2024
1 parent 2998385 commit 2493e5a
Show file tree
Hide file tree
Showing 5 changed files with 199 additions and 72 deletions.
78 changes: 21 additions & 57 deletions docs/asciidoc/modules/hibernate-validator.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,10 @@ Bean validation via https://hibernate.org/validator/[Hibernate Validator].
import io.jooby.hibernate.validator.HibernateValidatorModule;
{
install(new HibernateValidatorModule());
install(new HibernateValidatorModule()); <1>
// Optional
install(new HibernateModule()); <2>
}
----

Expand All @@ -27,10 +30,17 @@ import io.jooby.hibernate.validator.HibernateValidatorModule;
import io.jooby.hibernate.validator.HibernateValidatorModule
{
install(HibernateValidatorModule())
install(HibernateValidatorModule()) <1>
// Optional
install(new HibernateModule()) <2>
}
----

<1> Install Hibernate Validator
<2> HibernateModule must be installed after HibernateValidatorModule. This step is optional, but
required if you choose Hibernate as persistence provider.

3) Usage in MVC routes

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

In this case you need to implement a custom `ConstraintValidatorFactory` that will rely on your DI framework
In this case you need to implement a custom `ConstraintValidator` that will rely on your DI framework
instantiating your custom `ConstraintValidator`

1) Implement custom `ConstraintValidatorFactory`:

[source, java]
----
public class MyConstraintValidatorFactory implements ConstraintValidatorFactory {
private final Function<Class<?>, ?> require;
private final ConstraintValidatorFactory defaultFactory;
@Constraint(validatedBy = MyCustomValidator.class)
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
public @interface MyCustomAnnotation {
String message() default "My custom message";
public MyConstraintValidatorFactory(Function<Class<?>, ?> require) {
this.require = require;
try (ValidatorFactory factory = Validation.byDefaultProvider()
.configure().buildValidatorFactory()) {
this.defaultFactory = factory.getConstraintValidatorFactory();
}
}
Class<?>[] groups() default {};
@Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
if (isBuiltIn(key)) {
// use default factory for built-in constraint validators
return defaultFactory.getInstance(key);
} else {
// use DI to instantiate custom constraint validator
return (T) require.apply(key);
}
}
@Override
public void releaseInstance(ConstraintValidator<?, ?> instance) {
if(isBuiltIn(instance.getClass())) {
defaultFactory.releaseInstance(instance);
} else {
// No-op: lifecycle usually handled by DI framework
}
}
private boolean isBuiltIn(Class<?> key) {
return key.getName().startsWith("org.hibernate.validator");
}
Class<? extends Payload>[] payload() default {};
}
----
2) Register your custom `ConstraintValidatorFactory`:

[source, java]
----
{
install(new HibernateValidatorModule().doWith(cfg -> {
cfg.constraintValidatorFactory(new MyConstraintValidatorFactory(this::require)); // <1>
}));
}
----

<1> This approach using `require` will work with `Guice` or `Avaje`. For `Dagger`, a bit more effort is required,
but the concept is the same, and the same result can be achieved. Both `Avaje` and `Dagger` require additional
configuration due to their build-time nature.


3) Implement your custom `ConstraintValidator`

[source, java]
----
public class MyCustomValidator implements ConstraintValidator<MyCustomAnnotation, Bean> {
// This is the service you want to inject
Expand All @@ -365,6 +328,7 @@ public class MyCustomValidator implements ConstraintValidator<MyCustomAnnotation
}
----


=== Configuration
Any property defined at `hibernate.validator` will be added automatically:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@

import static jakarta.validation.Validation.byProvider;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import org.hibernate.validator.HibernateValidator;
import org.hibernate.validator.HibernateValidatorConfiguration;

import edu.umd.cs.findbugs.annotations.NonNull;
import io.jooby.Context;
import io.jooby.Extension;
import io.jooby.Jooby;
import io.jooby.StatusCode;
import io.jooby.*;
import io.jooby.internal.hibernate.validator.CompositeConstraintValidatorFactory;
import io.jooby.validation.BeanValidator;
import jakarta.validation.ConstraintValidatorFactory;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator;

Expand Down Expand Up @@ -58,6 +59,7 @@ public class HibernateValidatorModule implements Extension {
private String title = "Validation failed";
private boolean disableDefaultViolationHandler = false;
private boolean logException = false;
private List<ConstraintValidatorFactory> factories;

/**
* Setups a configurer callback.
Expand Down Expand Up @@ -118,6 +120,21 @@ public HibernateValidatorModule disableViolationHandler() {
return this;
}

/**
* Add a custom {@link ConstraintValidatorFactory}. This factory is allowed to returns <code>null
* </code> allowing next factory to create an instance (default or one provided by DI).
*
* @param factory Factory.
* @return This module.
*/
public HibernateValidatorModule with(ConstraintValidatorFactory factory) {
if (factories == null) {
factories = new ArrayList<>();
}
this.factories.add(factory);
return this;
}

@Override
public void install(@NonNull Jooby app) throws Exception {
var config = app.getConfig();
Expand All @@ -132,14 +149,26 @@ public void install(@NonNull Jooby app) throws Exception {
hbvConfig.addProperty(CONFIG_ROOT_PATH + "." + k, v.unwrapped().toString()));
}

// Set default constraint validator factory.
var delegateFactory =
new CompositeConstraintValidatorFactory(
app, hbvConfig.getDefaultConstraintValidatorFactory());
if (this.factories != null) {
this.factories.forEach(delegateFactory::add);
this.factories.clear();
}
hbvConfig.constraintValidatorFactory(delegateFactory);
if (configurer != null) {
configurer.accept(hbvConfig);
}

var services = app.getServices();
try (var factory = hbvConfig.buildValidatorFactory()) {
Validator validator = factory.getValidator();
app.getServices().put(Validator.class, validator);
app.getServices().put(BeanValidator.class, new BeanValidatorImpl(validator));
var validator = factory.getValidator();
services.put(Validator.class, validator);
services.put(BeanValidator.class, new BeanValidatorImpl(validator));
// Allow to access validator factory so hibernate can access later
var constraintValidatorFactory = factory.getConstraintValidatorFactory();
services.put(ConstraintValidatorFactory.class, constraintValidatorFactory);

if (!disableDefaultViolationHandler) {
app.error(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.internal.hibernate.validator;

import java.util.Deque;
import java.util.LinkedList;

import org.slf4j.Logger;

import io.jooby.Jooby;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorFactory;

public class CompositeConstraintValidatorFactory implements ConstraintValidatorFactory {
private final Logger log;
private final ConstraintValidatorFactory defaultFactory;
private final Deque<ConstraintValidatorFactory> factories = new LinkedList<>();

public CompositeConstraintValidatorFactory(
Jooby registry, ConstraintValidatorFactory defaultFactory) {
this.log = registry.getLog();
this.defaultFactory = defaultFactory;
this.factories.addLast(new RegistryConstraintValidatorFactory(registry));
}

public ConstraintValidatorFactory add(ConstraintValidatorFactory factory) {
this.factories.addFirst(factory);
return this;
}

@Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
if (isBuiltIn(key)) {
// use default factory for built-in constraint validators
return defaultFactory.getInstance(key);
} else {
for (var factory : factories) {
var instance = factory.getInstance(key);
if (instance != null) {
return instance;
}
}
// fallback or fail
return defaultFactory.getInstance(key);
}
}

@Override
public void releaseInstance(ConstraintValidator<?, ?> instance) {
if (isBuiltIn(instance.getClass())) {
defaultFactory.releaseInstance(instance);
} else {
if (instance instanceof AutoCloseable closeable) {
try {
closeable.close();
} catch (Exception e) {
log.debug("Failed to release constraint", e);
}
}
}
}

private boolean isBuiltIn(Class<?> key) {
var name = key.getName();
return name.startsWith("org.hibernate.validator") || name.startsWith("jakarta.validation");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Jooby https://jooby.io
* Apache License Version 2.0 https://jooby.io/LICENSE.txt
* Copyright 2014 Edgar Espina
*/
package io.jooby.internal.hibernate.validator;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.jooby.Registry;
import io.jooby.exception.RegistryException;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorFactory;

public class RegistryConstraintValidatorFactory implements ConstraintValidatorFactory {
private final Logger log = LoggerFactory.getLogger(getClass());
private final Registry registry;

public RegistryConstraintValidatorFactory(Registry registry) {
this.registry = registry;
}

@Override
public <T extends ConstraintValidator<?, ?>> T getInstance(Class<T> key) {
try {
return registry.require(key);
} catch (RegistryException notfound) {
return null;
}
}

@Override
public void releaseInstance(ConstraintValidator<?, ?> instance) {
if (instance instanceof AutoCloseable closeable) {
try {
closeable.close();
} catch (Exception e) {
log.debug("Failed to release constraint", e);
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,7 @@
*/
package io.jooby.hibernate;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

Expand Down Expand Up @@ -127,7 +123,8 @@
*
* Transaction and lifecycle of session/entityManager is managed by {@link TransactionalRequest}.
*
* <p>Complete documentation is available at: https://jooby.io/modules/hibernate.
* <p>Complete documentation is available at: <a
* href="https://jooby.io/modules/hibernate">hibernate</a>.
*
* @author edgar
* @since 2.0.0
Expand Down Expand Up @@ -159,7 +156,7 @@ public HibernateModule(@NonNull String name, Class<?>... classes) {
*
* @param classes Persistent classes.
*/
public HibernateModule(Class... classes) {
public HibernateModule(Class<?>... classes) {
this("db", classes);
}

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

/**
* Allow to customize a {@link StatelessSession} before opening it.
*
* @param sessionProvider Session customizer.
* @return This module.
*/
public @NonNull HibernateModule with(@NonNull StatelessSessionProvider sessionProvider) {
this.statelessSessionProvider = sessionProvider;
return this;
}

/**
* Hook into Hibernate bootstrap components and allow to customize them.
*
Expand Down Expand Up @@ -294,6 +302,19 @@ public void install(@NonNull Jooby application) {
var sfb = metadata.getSessionFactoryBuilder();
sfb.applyName(name);
sfb.applyNameAsJndiName(false);
/*
Bind Validator instance, so hibernate doesn't create a new factory.
Need to scan due hibernate doesn't depend on validation classes
*/
registry.entrySet().stream()
.filter(
it ->
it.getKey()
.getType()
.getName()
.equals("jakarta.validation.ConstraintValidatorFactory"))
.findFirst()
.ifPresent(it -> sfb.applyValidatorFactory(it.getValue().get()));

configurer.configure(sfb, config);

Expand Down

0 comments on commit 2493e5a

Please sign in to comment.