From 2493e5a12b84f1f7f8ad2875490e7ff233e71f21 Mon Sep 17 00:00:00 2001 From: Edgar Espina Date: Sun, 8 Dec 2024 20:46:09 -0300 Subject: [PATCH] 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 --- .../asciidoc/modules/hibernate-validator.adoc | 78 +++++-------------- .../validator/HibernateValidatorModule.java | 45 +++++++++-- .../CompositeConstraintValidatorFactory.java | 70 +++++++++++++++++ .../RegistryConstraintValidatorFactory.java | 43 ++++++++++ .../io/jooby/hibernate/HibernateModule.java | 35 +++++++-- 5 files changed, 199 insertions(+), 72 deletions(-) create mode 100644 modules/jooby-hibernate-validator/src/main/java/io/jooby/internal/hibernate/validator/CompositeConstraintValidatorFactory.java create mode 100644 modules/jooby-hibernate-validator/src/main/java/io/jooby/internal/hibernate/validator/RegistryConstraintValidatorFactory.java diff --git a/docs/asciidoc/modules/hibernate-validator.adoc b/docs/asciidoc/modules/hibernate-validator.adoc index 3df2d3f185..0c155fa63d 100644 --- a/docs/asciidoc/modules/hibernate-validator.adoc +++ b/docs/asciidoc/modules/hibernate-validator.adoc @@ -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> } ---- @@ -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 @@ -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, ?> 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, ?> require) { - this.require = require; - try (ValidatorFactory factory = Validation.byDefaultProvider() - .configure().buildValidatorFactory()) { - this.defaultFactory = factory.getConstraintValidatorFactory(); - } - } + Class[] groups() default {}; - @Override - public > T getInstance(Class 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[] 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 { // This is the service you want to inject @@ -365,6 +328,7 @@ public class MyCustomValidator implements ConstraintValidator factories; /** * Setups a configurer callback. @@ -118,6 +120,21 @@ public HibernateValidatorModule disableViolationHandler() { return this; } + /** + * Add a custom {@link ConstraintValidatorFactory}. This factory is allowed to returns null + * 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(); @@ -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( diff --git a/modules/jooby-hibernate-validator/src/main/java/io/jooby/internal/hibernate/validator/CompositeConstraintValidatorFactory.java b/modules/jooby-hibernate-validator/src/main/java/io/jooby/internal/hibernate/validator/CompositeConstraintValidatorFactory.java new file mode 100644 index 0000000000..18f7b01f09 --- /dev/null +++ b/modules/jooby-hibernate-validator/src/main/java/io/jooby/internal/hibernate/validator/CompositeConstraintValidatorFactory.java @@ -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 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 getInstance(Class 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"); + } +} diff --git a/modules/jooby-hibernate-validator/src/main/java/io/jooby/internal/hibernate/validator/RegistryConstraintValidatorFactory.java b/modules/jooby-hibernate-validator/src/main/java/io/jooby/internal/hibernate/validator/RegistryConstraintValidatorFactory.java new file mode 100644 index 0000000000..a3133e7f41 --- /dev/null +++ b/modules/jooby-hibernate-validator/src/main/java/io/jooby/internal/hibernate/validator/RegistryConstraintValidatorFactory.java @@ -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 getInstance(Class 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); + } + } + } +} diff --git a/modules/jooby-hibernate/src/main/java/io/jooby/hibernate/HibernateModule.java b/modules/jooby-hibernate/src/main/java/io/jooby/hibernate/HibernateModule.java index 40b661529d..21cc4f149c 100644 --- a/modules/jooby-hibernate/src/main/java/io/jooby/hibernate/HibernateModule.java +++ b/modules/jooby-hibernate/src/main/java/io/jooby/hibernate/HibernateModule.java @@ -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; @@ -127,7 +123,8 @@ * * Transaction and lifecycle of session/entityManager is managed by {@link TransactionalRequest}. * - *

Complete documentation is available at: https://jooby.io/modules/hibernate. + *

Complete documentation is available at: hibernate. * * @author edgar * @since 2.0.0 @@ -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); } @@ -207,6 +204,17 @@ public HibernateModule(@NonNull String name, List> 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. * @@ -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);