Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

hibernate-validator: better support for custom ConstraintValidatorFactory #3596

Merged
merged 2 commits into from
Dec 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 32 additions & 63 deletions docs/asciidoc/modules/hibernate-validator.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -224,8 +224,8 @@ import jakarta.validation.Validator;

{
post("/validate", ctx -> {
Validator validator = require(Validator.class);
Set<ConstraintViolation<Bean>> violations = validator.validate(ctx.body(Bean.class));
var validator = require(Validator.class);
var violations = validator.validate(ctx.body(Bean.class));
if (!violations.isEmpty()) {
...
}
Expand Down Expand Up @@ -281,72 +281,23 @@ 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
instantiating your custom `ConstraintValidator`

1) Implement custom `ConstraintValidatorFactory`:
In this case you need to implement a custom `ConstraintValidator` that will rely on your DI framework.

.Custom Annotation and Validator
[source, java]
----
public class MyConstraintValidatorFactory implements ConstraintValidatorFactory {

private final Function<Class<?>, ?> require;
private final ConstraintValidatorFactory defaultFactory;

public MyConstraintValidatorFactory(Function<Class<?>, ?> require) {
this.require = require;
try (ValidatorFactory factory = Validation.byDefaultProvider()
.configure().buildValidatorFactory()) {
this.defaultFactory = factory.getConstraintValidatorFactory();
}
}

@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");
}
}
----
@Constraint(validatedBy = MyCustomValidator.class)
@Target({TYPE, ANNOTATION_TYPE})
@Retention(RUNTIME)
public @interface MyCustomAnnotation {
String message() default "My custom message";

2) Register your custom `ConstraintValidatorFactory`:
Class<?>[] groups() default {};

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

<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> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

for avaje it's important to add @Singleton annotation, otherwise, the instantiation will fail, and the error message is not clear - it hides the real reason related to DI.


// This is the service you want to inject
Expand Down Expand Up @@ -381,8 +332,26 @@ Or programmatically:
import io.jooby.hibernate.validator.HibernateValidatorModule;

{
install(new HibernateValidatorModule().doWith(cfg -> {
cfg.failFast(true);
}));
var cfg = byProvider(HibernateValidator.class).configure();
cfg.failFast(true);
install(new HibernateValidatorModule(cfg));
}
----

=== Hibernate integration

Just install `HibernateValidatorModule` before `HibernateModule`, like:

[source, java]
----
import io.jooby.hibernate.validator.HibernateValidatorModule;

{
install(new HibernateValidatorModule());

install(new HibernateModule());
}
----

The `HibernateModule` will detect the constraint validator factory and setup. This avoid creating
a new instance of constraint validator factory.
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 @@ -53,18 +54,32 @@
*/
public class HibernateValidatorModule implements Extension {
private static final String CONFIG_ROOT_PATH = "hibernate.validator";
// TODO: remove it on next major
private Consumer<HibernateValidatorConfiguration> configurer;
private StatusCode statusCode = StatusCode.UNPROCESSABLE_ENTITY;
private String title = "Validation failed";
private boolean disableDefaultViolationHandler = false;
private boolean logException = false;
private List<ConstraintValidatorFactory> factories;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just curious, do you have real use cases in mind where you need multiple factories?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't need it if we rely on DI. So this is for people who doesn't like DI (reflective or not), so they can just do:

 getInstance(Class type) {
    if (type == Validator1) {..}
    else if (type== Validator2) {...}
    else return null
  }

private final HibernateValidatorConfiguration configuration;

public HibernateValidatorModule(@NonNull HibernateValidatorConfiguration configuration) {
this.configuration = configuration;
}

public HibernateValidatorModule() {
this(byProvider(HibernateValidator.class).configure());
}

/**
* Setups a configurer callback.
*
* @param configurer Configurer callback.
* @return This module.
* @deprecated Use {@link
* HibernateValidatorModule#HibernateValidatorModule(HibernateValidatorConfiguration)}
*/
@Deprecated
public HibernateValidatorModule doWith(
@NonNull final Consumer<HibernateValidatorConfiguration> configurer) {
this.configurer = configurer;
Expand Down Expand Up @@ -118,28 +133,53 @@ 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();
var hbvConfig = byProvider(HibernateValidator.class).configure();

if (config.hasPath(CONFIG_ROOT_PATH)) {
config
.getConfig(CONFIG_ROOT_PATH)
.root()
.forEach(
(k, v) ->
hbvConfig.addProperty(CONFIG_ROOT_PATH + "." + k, v.unwrapped().toString()));
configuration.addProperty(CONFIG_ROOT_PATH + "." + k, v.unwrapped().toString()));
}

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

try (var factory = hbvConfig.buildValidatorFactory()) {
Validator validator = factory.getValidator();
app.getServices().put(Validator.class, validator);
app.getServices().put(BeanValidator.class, new BeanValidatorImpl(validator));
var services = app.getServices();
try (var factory = configuration.buildValidatorFactory()) {
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should work for Avaje/Guice, but not for Dagger. A note in docs that, for the Dagger, the factory needs to be implemented manually, would be helpful.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if possible to implement a registry for Dagger, will add a note.

} 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);
}
}
}
}
Loading
Loading