From daca7271818dc03e7b2d47d740256129bc5873f5 Mon Sep 17 00:00:00 2001 From: kliushnichenko Date: Tue, 6 Aug 2024 22:32:42 +0300 Subject: [PATCH 01/13] jooby-hbv module draft --- jooby/pom.xml | 6 ++ .../main/java/io/jooby/DefaultContext.java | 24 +++++-- jooby/src/main/java/io/jooby/Jooby.java | 15 ++++ .../main/java/io/jooby/MessageValidator.java | 9 +++ jooby/src/main/java/io/jooby/Router.java | 4 ++ .../java/io/jooby/internal/RouterImpl.java | 16 +++++ jooby/src/main/java/module-info.java | 1 + modules/jooby-hbv/pom.xml | 62 ++++++++++++++++ .../src/main/java/io/jooby/hbv/HbvModule.java | 70 +++++++++++++++++++ .../main/java/io/jooby/hbv/package-info.java | 1 + .../jooby-hbv/src/main/java/module-info.java | 17 +++++ modules/pom.xml | 1 + pom.xml | 12 +++- 13 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 jooby/src/main/java/io/jooby/MessageValidator.java create mode 100644 modules/jooby-hbv/pom.xml create mode 100644 modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java create mode 100644 modules/jooby-hbv/src/main/java/io/jooby/hbv/package-info.java create mode 100644 modules/jooby-hbv/src/main/java/module-info.java diff --git a/jooby/pom.xml b/jooby/pom.xml index c8c464b4d7..ecf7ce0f72 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -49,6 +49,12 @@ jakarta.inject-api + + + jakarta.validation + jakarta.validation-api + + com.typesafe diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 21302366ff..20c35289ad 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -20,13 +20,12 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Instant; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; import org.slf4j.Logger; import edu.umd.cs.findbugs.annotations.NonNull; @@ -417,7 +416,20 @@ default boolean isSecure() { T result = ValueConverters.convert(body(), type, getRouter()); return result; } - return (T) decoder(contentType).decode(this, type); + T object = (T) decoder(contentType).decode(this, type); + + MessageValidator messageValidator = getRouter().getMessageValidator(); + if (messageValidator != null) { + if (messageValidator.predicate().test(type)) { + Validator validator = messageValidator.validator(); + Set> violations = validator.validate(object); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } + } + return object; + } catch (Exception x) { throw SneakyThrows.propagate(x); } diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 12d3d0d985..43aa2f0285 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.lang.reflect.Constructor; +import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -41,6 +42,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import jakarta.validation.Validator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -627,6 +629,19 @@ public Jooby decoder(@NonNull MediaType contentType, @NonNull MessageDecoder dec return this; } + @NonNull + @Override + public Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate) { + router.messageValidator(validator, predicate); + return this; + } + + @Nullable + @Override + public MessageValidator getMessageValidator() { + return router.getMessageValidator(); + } + @NonNull @Override public Jooby encoder(@NonNull MediaType contentType, @NonNull MessageEncoder encoder) { router.encoder(contentType, encoder); diff --git a/jooby/src/main/java/io/jooby/MessageValidator.java b/jooby/src/main/java/io/jooby/MessageValidator.java new file mode 100644 index 0000000000..f4dcb0f420 --- /dev/null +++ b/jooby/src/main/java/io/jooby/MessageValidator.java @@ -0,0 +1,9 @@ +package io.jooby; + +import jakarta.validation.Validator; + +import java.lang.reflect.Type; +import java.util.function.Predicate; + +public record MessageValidator(Validator validator, Predicate predicate) { +} diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index 1ed9754edf..7b10360e92 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -8,6 +8,7 @@ import static java.util.Collections.unmodifiableList; import static java.util.Objects.requireNonNull; +import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -28,6 +29,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import jakarta.validation.Validator; import org.slf4j.Logger; import com.typesafe.config.Config; @@ -508,6 +510,8 @@ default Object execute(@NonNull Context context) { */ @NonNull Router decoder(@NonNull MediaType contentType, @NonNull MessageDecoder decoder); + @NonNull Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate); + @Nullable MessageValidator getMessageValidator(); /** * Returns the worker thread pool. This thread pool is used to run application blocking code. * diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 4c7819e191..8e21138065 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -8,6 +8,7 @@ import static java.util.Objects.requireNonNull; import java.io.FileNotFoundException; +import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.NoSuchFileException; import java.nio.file.Path; @@ -35,6 +36,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import jakarta.validation.Validator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -147,6 +149,8 @@ public Stack executor(Executor executor) { private Map decoders = new HashMap<>(); + private MessageValidator messageValidator = null; + private Map attributes = new ConcurrentHashMap<>(); private ServiceRegistry services = new ServiceRegistryImpl(); @@ -368,6 +372,18 @@ public Router decoder(@NonNull MediaType contentType, @NonNull MessageDecoder de return this; } + @NonNull + @Override + public Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate) { + this.messageValidator = new MessageValidator(validator, predicate); + return this; + } + + @Override + public MessageValidator getMessageValidator() { + return messageValidator; + } + @NonNull @Override public Executor getWorker() { return worker; diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index cedb71a4ac..3d3521cb4f 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -22,6 +22,7 @@ * True core deps */ requires jakarta.inject; + requires jakarta.validation; requires org.slf4j; requires static com.github.spotbugs.annotations; requires typesafe.config; diff --git a/modules/jooby-hbv/pom.xml b/modules/jooby-hbv/pom.xml new file mode 100644 index 0000000000..f2a1be439e --- /dev/null +++ b/modules/jooby-hbv/pom.xml @@ -0,0 +1,62 @@ + + + + + io.jooby + modules + 3.3.0-SNAPSHOT + + + 4.0.0 + jooby-hbv + + + + io.jooby + jooby + ${jooby.version} + + + + + org.hibernate.validator + hibernate-validator + 8.0.1.Final + + + + jakarta.el + jakarta.el-api + 5.0.1 + + + + org.glassfish.expressly + expressly + 5.0.0 + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.jacoco + org.jacoco.agent + runtime + test + + + + org.mockito + mockito-core + test + + + + diff --git a/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java b/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java new file mode 100644 index 0000000000..07c3f76e8e --- /dev/null +++ b/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.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.hbv; + + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Extension; +import io.jooby.Jooby; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorConfiguration; + +import java.lang.reflect.Type; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import static jakarta.validation.Validation.byProvider; +import static java.util.Objects.requireNonNull; + +public class HbvModule implements Extension { + + private final Predicate predicate; + private Consumer configurer; + + public HbvModule() { + this(none()); + } + + public HbvModule(Predicate predicate) { + this.predicate = requireNonNull(predicate, "Predicate is required."); + } + + public HbvModule(final Class... classes) { + this.predicate = typeIs(Set.of(classes)); + } + + public HbvModule doWith(final Consumer configurer) { + this.configurer = requireNonNull(configurer, "Configurer callback is required."); + return this; + } + + @Override + public void install(@NonNull Jooby application) { + HibernateValidatorConfiguration cfg = byProvider(HibernateValidator.class).configure(); + + if (configurer != null) { + configurer.accept(cfg); + } + + try (ValidatorFactory factory = cfg.buildValidatorFactory()) { + Validator validator = factory.getValidator(); + application.messageValidator(validator, predicate); + application.getServices().put(Validator.class, validator); + } + + } + + static Predicate typeIs(final Set> classes) { + return type -> classes.contains((Class) type); + } + + static Predicate none() { + return type -> false; + } +} diff --git a/modules/jooby-hbv/src/main/java/io/jooby/hbv/package-info.java b/modules/jooby-hbv/src/main/java/io/jooby/hbv/package-info.java new file mode 100644 index 0000000000..da7909aad1 --- /dev/null +++ b/modules/jooby-hbv/src/main/java/io/jooby/hbv/package-info.java @@ -0,0 +1 @@ +package io.jooby.hbv; diff --git a/modules/jooby-hbv/src/main/java/module-info.java b/modules/jooby-hbv/src/main/java/module-info.java new file mode 100644 index 0000000000..e9a1799ee9 --- /dev/null +++ b/modules/jooby-hbv/src/main/java/module-info.java @@ -0,0 +1,17 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +/** + * Hbv module. + */ +module io.jooby.hbv { + exports io.jooby.hbv; + + requires transitive io.jooby; + requires static com.github.spotbugs.annotations; + requires typesafe.config; + requires org.hibernate.validator; + requires jakarta.validation; +} diff --git a/modules/pom.xml b/modules/pom.xml index c9ed80a218..fa063b5c37 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -58,6 +58,7 @@ jooby-thymeleaf jooby-node jooby-camel + jooby-hbv jooby-pac4j diff --git a/pom.xml b/pom.xml index b4f510b489..49d9cb32a9 100644 --- a/pom.xml +++ b/pom.xml @@ -80,10 +80,11 @@ 2.5.2 - + 2.0.1 - 4.8.6 + 3.1.0 4.0.0 + 4.8.6 4.12.0 @@ -893,6 +894,13 @@ ${jakarta.inject.version} + + + jakarta.validation + jakarta.validation-api + ${jakarta.validation.version} + + org.flywaydb From 1bf0e0032cd614e22d02e3adb73ba6febb81dc27 Mon Sep 17 00:00:00 2001 From: kliushnichenko Date: Tue, 6 Aug 2024 22:32:42 +0300 Subject: [PATCH 02/13] jooby-hbv module draft --- jooby/pom.xml | 6 ++ .../main/java/io/jooby/DefaultContext.java | 24 +++++-- jooby/src/main/java/io/jooby/Jooby.java | 15 ++++ .../main/java/io/jooby/MessageValidator.java | 9 +++ jooby/src/main/java/io/jooby/Router.java | 4 ++ .../java/io/jooby/internal/RouterImpl.java | 16 +++++ jooby/src/main/java/module-info.java | 1 + modules/jooby-hbv/pom.xml | 62 ++++++++++++++++ .../src/main/java/io/jooby/hbv/HbvModule.java | 70 +++++++++++++++++++ .../main/java/io/jooby/hbv/package-info.java | 1 + .../jooby-hbv/src/main/java/module-info.java | 17 +++++ modules/pom.xml | 1 + pom.xml | 12 +++- 13 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 jooby/src/main/java/io/jooby/MessageValidator.java create mode 100644 modules/jooby-hbv/pom.xml create mode 100644 modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java create mode 100644 modules/jooby-hbv/src/main/java/io/jooby/hbv/package-info.java create mode 100644 modules/jooby-hbv/src/main/java/module-info.java diff --git a/jooby/pom.xml b/jooby/pom.xml index 1445e1cdbf..682fe2816a 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -49,6 +49,12 @@ jakarta.inject-api + + + jakarta.validation + jakarta.validation-api + + com.typesafe diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 21302366ff..20c35289ad 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -20,13 +20,12 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Instant; -import java.util.Date; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; import org.slf4j.Logger; import edu.umd.cs.findbugs.annotations.NonNull; @@ -417,7 +416,20 @@ default boolean isSecure() { T result = ValueConverters.convert(body(), type, getRouter()); return result; } - return (T) decoder(contentType).decode(this, type); + T object = (T) decoder(contentType).decode(this, type); + + MessageValidator messageValidator = getRouter().getMessageValidator(); + if (messageValidator != null) { + if (messageValidator.predicate().test(type)) { + Validator validator = messageValidator.validator(); + Set> violations = validator.validate(object); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } + } + return object; + } catch (Exception x) { throw SneakyThrows.propagate(x); } diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 12d3d0d985..43aa2f0285 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -12,6 +12,7 @@ import java.io.IOException; import java.lang.reflect.Constructor; +import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -41,6 +42,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import jakarta.validation.Validator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -627,6 +629,19 @@ public Jooby decoder(@NonNull MediaType contentType, @NonNull MessageDecoder dec return this; } + @NonNull + @Override + public Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate) { + router.messageValidator(validator, predicate); + return this; + } + + @Nullable + @Override + public MessageValidator getMessageValidator() { + return router.getMessageValidator(); + } + @NonNull @Override public Jooby encoder(@NonNull MediaType contentType, @NonNull MessageEncoder encoder) { router.encoder(contentType, encoder); diff --git a/jooby/src/main/java/io/jooby/MessageValidator.java b/jooby/src/main/java/io/jooby/MessageValidator.java new file mode 100644 index 0000000000..f4dcb0f420 --- /dev/null +++ b/jooby/src/main/java/io/jooby/MessageValidator.java @@ -0,0 +1,9 @@ +package io.jooby; + +import jakarta.validation.Validator; + +import java.lang.reflect.Type; +import java.util.function.Predicate; + +public record MessageValidator(Validator validator, Predicate predicate) { +} diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index 1f47ec2734..7073d82de8 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -8,6 +8,7 @@ import static java.util.Collections.unmodifiableList; import static java.util.Objects.requireNonNull; +import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -28,6 +29,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import jakarta.validation.Validator; import org.slf4j.Logger; import com.typesafe.config.Config; @@ -508,6 +510,8 @@ default Object execute(@NonNull Context context) { */ @NonNull Router decoder(@NonNull MediaType contentType, @NonNull MessageDecoder decoder); + @NonNull Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate); + @Nullable MessageValidator getMessageValidator(); /** * Returns the worker thread pool. This thread pool is used to run application blocking code. * diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index a93b96fa38..33f7f6689b 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -8,6 +8,7 @@ import static java.util.Objects.requireNonNull; import java.io.FileNotFoundException; +import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.NoSuchFileException; import java.nio.file.Path; @@ -35,6 +36,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import jakarta.validation.Validator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -147,6 +149,8 @@ public Stack executor(Executor executor) { private Map decoders = new HashMap<>(); + private MessageValidator messageValidator = null; + private Map attributes = new ConcurrentHashMap<>(); private ServiceRegistry services = new ServiceRegistryImpl(); @@ -368,6 +372,18 @@ public Router decoder(@NonNull MediaType contentType, @NonNull MessageDecoder de return this; } + @NonNull + @Override + public Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate) { + this.messageValidator = new MessageValidator(validator, predicate); + return this; + } + + @Override + public MessageValidator getMessageValidator() { + return messageValidator; + } + @NonNull @Override public Executor getWorker() { return worker; diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index cedb71a4ac..3d3521cb4f 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -22,6 +22,7 @@ * True core deps */ requires jakarta.inject; + requires jakarta.validation; requires org.slf4j; requires static com.github.spotbugs.annotations; requires typesafe.config; diff --git a/modules/jooby-hbv/pom.xml b/modules/jooby-hbv/pom.xml new file mode 100644 index 0000000000..f2a1be439e --- /dev/null +++ b/modules/jooby-hbv/pom.xml @@ -0,0 +1,62 @@ + + + + + io.jooby + modules + 3.3.0-SNAPSHOT + + + 4.0.0 + jooby-hbv + + + + io.jooby + jooby + ${jooby.version} + + + + + org.hibernate.validator + hibernate-validator + 8.0.1.Final + + + + jakarta.el + jakarta.el-api + 5.0.1 + + + + org.glassfish.expressly + expressly + 5.0.0 + + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + org.jacoco + org.jacoco.agent + runtime + test + + + + org.mockito + mockito-core + test + + + + diff --git a/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java b/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java new file mode 100644 index 0000000000..07c3f76e8e --- /dev/null +++ b/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.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.hbv; + + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Extension; +import io.jooby.Jooby; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorConfiguration; + +import java.lang.reflect.Type; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import static jakarta.validation.Validation.byProvider; +import static java.util.Objects.requireNonNull; + +public class HbvModule implements Extension { + + private final Predicate predicate; + private Consumer configurer; + + public HbvModule() { + this(none()); + } + + public HbvModule(Predicate predicate) { + this.predicate = requireNonNull(predicate, "Predicate is required."); + } + + public HbvModule(final Class... classes) { + this.predicate = typeIs(Set.of(classes)); + } + + public HbvModule doWith(final Consumer configurer) { + this.configurer = requireNonNull(configurer, "Configurer callback is required."); + return this; + } + + @Override + public void install(@NonNull Jooby application) { + HibernateValidatorConfiguration cfg = byProvider(HibernateValidator.class).configure(); + + if (configurer != null) { + configurer.accept(cfg); + } + + try (ValidatorFactory factory = cfg.buildValidatorFactory()) { + Validator validator = factory.getValidator(); + application.messageValidator(validator, predicate); + application.getServices().put(Validator.class, validator); + } + + } + + static Predicate typeIs(final Set> classes) { + return type -> classes.contains((Class) type); + } + + static Predicate none() { + return type -> false; + } +} diff --git a/modules/jooby-hbv/src/main/java/io/jooby/hbv/package-info.java b/modules/jooby-hbv/src/main/java/io/jooby/hbv/package-info.java new file mode 100644 index 0000000000..da7909aad1 --- /dev/null +++ b/modules/jooby-hbv/src/main/java/io/jooby/hbv/package-info.java @@ -0,0 +1 @@ +package io.jooby.hbv; diff --git a/modules/jooby-hbv/src/main/java/module-info.java b/modules/jooby-hbv/src/main/java/module-info.java new file mode 100644 index 0000000000..e9a1799ee9 --- /dev/null +++ b/modules/jooby-hbv/src/main/java/module-info.java @@ -0,0 +1,17 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +/** + * Hbv module. + */ +module io.jooby.hbv { + exports io.jooby.hbv; + + requires transitive io.jooby; + requires static com.github.spotbugs.annotations; + requires typesafe.config; + requires org.hibernate.validator; + requires jakarta.validation; +} diff --git a/modules/pom.xml b/modules/pom.xml index 50fbb428a9..f19c6bfbc0 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -58,6 +58,7 @@ jooby-thymeleaf jooby-node jooby-camel + jooby-hbv jooby-pac4j diff --git a/pom.xml b/pom.xml index 7d7d961340..ad0646c2d4 100644 --- a/pom.xml +++ b/pom.xml @@ -80,10 +80,11 @@ 2.5.2 - + 2.0.1 - 4.8.6 + 3.1.0 4.0.0 + 4.8.6 4.12.0 @@ -893,6 +894,13 @@ ${jakarta.inject.version} + + + jakarta.validation + jakarta.validation-api + ${jakarta.validation.version} + + org.flywaydb From 887dc9dd22c9375c562afdd5a7cff79c48eb687c Mon Sep 17 00:00:00 2001 From: kliushnichenko Date: Sat, 17 Aug 2024 15:06:23 +0300 Subject: [PATCH 03/13] rollback core changes --- .../main/java/io/jooby/DefaultContext.java | 24 +++++-------------- .../java/io/jooby/internal/RouterImpl.java | 16 ------------- 2 files changed, 6 insertions(+), 34 deletions(-) diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 20c35289ad..21302366ff 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -20,12 +20,13 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Instant; -import java.util.*; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; -import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; -import jakarta.validation.Validator; import org.slf4j.Logger; import edu.umd.cs.findbugs.annotations.NonNull; @@ -416,20 +417,7 @@ default boolean isSecure() { T result = ValueConverters.convert(body(), type, getRouter()); return result; } - T object = (T) decoder(contentType).decode(this, type); - - MessageValidator messageValidator = getRouter().getMessageValidator(); - if (messageValidator != null) { - if (messageValidator.predicate().test(type)) { - Validator validator = messageValidator.validator(); - Set> violations = validator.validate(object); - if (!violations.isEmpty()) { - throw new ConstraintViolationException(violations); - } - } - } - return object; - + return (T) decoder(contentType).decode(this, type); } catch (Exception x) { throw SneakyThrows.propagate(x); } diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 33f7f6689b..a93b96fa38 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -8,7 +8,6 @@ import static java.util.Objects.requireNonNull; import java.io.FileNotFoundException; -import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.NoSuchFileException; import java.nio.file.Path; @@ -36,7 +35,6 @@ import java.util.stream.IntStream; import java.util.stream.Stream; -import jakarta.validation.Validator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -149,8 +147,6 @@ public Stack executor(Executor executor) { private Map decoders = new HashMap<>(); - private MessageValidator messageValidator = null; - private Map attributes = new ConcurrentHashMap<>(); private ServiceRegistry services = new ServiceRegistryImpl(); @@ -372,18 +368,6 @@ public Router decoder(@NonNull MediaType contentType, @NonNull MessageDecoder de return this; } - @NonNull - @Override - public Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate) { - this.messageValidator = new MessageValidator(validator, predicate); - return this; - } - - @Override - public MessageValidator getMessageValidator() { - return messageValidator; - } - @NonNull @Override public Executor getWorker() { return worker; From 8d71495aa86f43a72a2c31f37b9e1b170eddcd0d Mon Sep 17 00:00:00 2001 From: kliushnichenko Date: Sat, 17 Aug 2024 15:08:50 +0300 Subject: [PATCH 04/13] cleanup hbv module --- .../src/main/java/io/jooby/hbv/HbvModule.java | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java b/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java index 07c3f76e8e..657539a4b4 100644 --- a/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java +++ b/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java @@ -14,31 +14,15 @@ import org.hibernate.validator.HibernateValidator; import org.hibernate.validator.HibernateValidatorConfiguration; -import java.lang.reflect.Type; -import java.util.Set; import java.util.function.Consumer; -import java.util.function.Predicate; import static jakarta.validation.Validation.byProvider; import static java.util.Objects.requireNonNull; public class HbvModule implements Extension { - private final Predicate predicate; private Consumer configurer; - public HbvModule() { - this(none()); - } - - public HbvModule(Predicate predicate) { - this.predicate = requireNonNull(predicate, "Predicate is required."); - } - - public HbvModule(final Class... classes) { - this.predicate = typeIs(Set.of(classes)); - } - public HbvModule doWith(final Consumer configurer) { this.configurer = requireNonNull(configurer, "Configurer callback is required."); return this; @@ -54,17 +38,8 @@ public void install(@NonNull Jooby application) { try (ValidatorFactory factory = cfg.buildValidatorFactory()) { Validator validator = factory.getValidator(); - application.messageValidator(validator, predicate); application.getServices().put(Validator.class, validator); } } - - static Predicate typeIs(final Set> classes) { - return type -> classes.contains((Class) type); - } - - static Predicate none() { - return type -> false; - } } From 06eb67e4a88ce58eaa8038a294ec6d303b30e4b0 Mon Sep 17 00:00:00 2001 From: kliushnichenko Date: Sat, 24 Aug 2024 14:36:57 +0300 Subject: [PATCH 05/13] basic implementation for MVC --- jooby/pom.xml | 6 --- modules/jooby-apt/pom.xml | 6 +++ .../io/jooby/internal/apt/MvcParameter.java | 6 +++ .../java/io/jooby/internal/apt/MvcRoute.java | 11 ++++- .../src/test/java/tests/validation/Bean.java | 13 ++++++ .../BeanValidationGeneratorTest.java | 29 ++++++++++++ .../validation/BeanValidationsController.java | 25 +++++++++++ modules/jooby-hbv/pom.xml | 6 +++ .../src/main/java/io/jooby/hbv/HbvModule.java | 1 - modules/jooby-validation/pom.xml | 28 ++++++++++++ .../io/jooby/validation/ValidationHelper.java | 45 +++++++++++++++++++ .../io/jooby/validation/package-info.java | 1 + .../src/main/java/module-info.java | 14 ++++++ modules/pom.xml | 3 ++ 14 files changed, 186 insertions(+), 8 deletions(-) create mode 100644 modules/jooby-apt/src/test/java/tests/validation/Bean.java create mode 100644 modules/jooby-apt/src/test/java/tests/validation/BeanValidationGeneratorTest.java create mode 100644 modules/jooby-apt/src/test/java/tests/validation/BeanValidationsController.java create mode 100644 modules/jooby-validation/pom.xml create mode 100644 modules/jooby-validation/src/main/java/io/jooby/validation/ValidationHelper.java create mode 100644 modules/jooby-validation/src/main/java/io/jooby/validation/package-info.java create mode 100644 modules/jooby-validation/src/main/java/module-info.java diff --git a/jooby/pom.xml b/jooby/pom.xml index 682fe2816a..1445e1cdbf 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -49,12 +49,6 @@ jakarta.inject-api - - - jakarta.validation - jakarta.validation-api - - com.typesafe diff --git a/modules/jooby-apt/pom.xml b/modules/jooby-apt/pom.xml index 6869351a77..20f4d4c121 100644 --- a/modules/jooby-apt/pom.xml +++ b/modules/jooby-apt/pom.xml @@ -19,6 +19,12 @@ + + io.jooby + jooby-validation + ${jooby.version} + + io.jooby jooby diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcParameter.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcParameter.java index 925404c9fd..738cb3ea71 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcParameter.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcParameter.java @@ -22,6 +22,7 @@ public class MvcParameter { private final VariableElement parameter; private final Map annotations; private final TypeDefinition type; + private final boolean requireBeanValidation; public MvcParameter(MvcContext context, MvcRoute route, VariableElement parameter) { this.route = route; @@ -29,6 +30,7 @@ public MvcParameter(MvcContext context, MvcRoute route, VariableElement paramete this.annotations = annotationMap(parameter); this.type = new TypeDefinition(context.getProcessingEnvironment().getTypeUtils(), parameter.asType()); + this.requireBeanValidation = annotations.get("jakarta.validation.Valid") != null; } public TypeDefinition getType() { @@ -144,4 +146,8 @@ private Map annotationMap(VariableElement parameter) { private List annotationFromAnnotationType(Element element) { return Optional.ofNullable(element.getAnnotationMirrors()).orElse(Collections.emptyList()); } + + public boolean isRequireBeanValidation() { + return requireBeanValidation; + } } diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java index 9d806f6099..94bdfc1c40 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java @@ -203,7 +203,16 @@ public List generateHandlerCall(boolean kt) { /* Parameters */ var paramList = new StringJoiner(", ", "(", ")"); for (var parameter : getParameters(true)) { - paramList.add(parameter.generateMapping(kt)); + String generatedParameter = parameter.generateMapping(kt); + if (parameter.isRequireBeanValidation()) { + generatedParameter = CodeBlock.of( + "io.jooby.validation.ValidationHelper.validate(", + "ctx, ", + generatedParameter, + ")"); + } + + paramList.add(generatedParameter); } var throwsException = !method.getThrownTypes().isEmpty(); var returnTypeGenerics = diff --git a/modules/jooby-apt/src/test/java/tests/validation/Bean.java b/modules/jooby-apt/src/test/java/tests/validation/Bean.java new file mode 100644 index 0000000000..2309ed5dc2 --- /dev/null +++ b/modules/jooby-apt/src/test/java/tests/validation/Bean.java @@ -0,0 +1,13 @@ +package tests.validation; + +class Bean { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/modules/jooby-apt/src/test/java/tests/validation/BeanValidationGeneratorTest.java b/modules/jooby-apt/src/test/java/tests/validation/BeanValidationGeneratorTest.java new file mode 100644 index 0000000000..308813ff6c --- /dev/null +++ b/modules/jooby-apt/src/test/java/tests/validation/BeanValidationGeneratorTest.java @@ -0,0 +1,29 @@ +package tests.validation; + +import io.jooby.apt.ProcessorRunner; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class BeanValidationGeneratorTest { + + @Test + public void generate_validation_forBean() throws Exception { + new ProcessorRunner(new BeanValidationsController()).withSource( + false, + source -> { + assertTrue(source.contains( + "c.validateQueryBean(io.jooby.validation.ValidationHelper.validate(ctx, ctx.query(\"bean\").isMissing() ? ctx.query().toNullable(tests.validation.Bean.class) : ctx.query(\"bean\").toNullable(tests.validation.Bean.class)))") + ); + + assertTrue(source.contains( + "c.validateFormBean(io.jooby.validation.ValidationHelper.validate(ctx, ctx.form(\"bean\").isMissing() ? ctx.form().toNullable(tests.validation.Bean.class) : ctx.form(\"bean\").toNullable(tests.validation.Bean.class)))") + ); + + assertTrue(source.contains( + "c.validateBodyBean(io.jooby.validation.ValidationHelper.validate(ctx, ctx.body(tests.validation.Bean.class)))") + ); + + }); + } +} diff --git a/modules/jooby-apt/src/test/java/tests/validation/BeanValidationsController.java b/modules/jooby-apt/src/test/java/tests/validation/BeanValidationsController.java new file mode 100644 index 0000000000..1c8b2f3c75 --- /dev/null +++ b/modules/jooby-apt/src/test/java/tests/validation/BeanValidationsController.java @@ -0,0 +1,25 @@ +package tests.validation; + +import io.jooby.annotation.FormParam; +import io.jooby.annotation.POST; +import io.jooby.annotation.QueryParam; +import jakarta.validation.Valid; + +public class BeanValidationsController { + + @POST("/validate/query-bean") + public Bean validateQueryBean(@Valid @QueryParam Bean bean) { + return bean; + } + + @POST("/validate/form-bean") + public Bean validateFormBean(@Valid @FormParam Bean bean) { + return bean; + } + + @POST("/validate/body-bean") + public Bean validateBodyBean(@Valid Bean bean) { + return bean; + } + +} diff --git a/modules/jooby-hbv/pom.xml b/modules/jooby-hbv/pom.xml index f2a1be439e..f17bc73441 100644 --- a/modules/jooby-hbv/pom.xml +++ b/modules/jooby-hbv/pom.xml @@ -19,6 +19,12 @@ ${jooby.version} + + io.jooby + jooby-validation + ${jooby.version} + + org.hibernate.validator diff --git a/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java b/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java index 657539a4b4..af49f0019f 100644 --- a/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java +++ b/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java @@ -40,6 +40,5 @@ public void install(@NonNull Jooby application) { Validator validator = factory.getValidator(); application.getServices().put(Validator.class, validator); } - } } diff --git a/modules/jooby-validation/pom.xml b/modules/jooby-validation/pom.xml new file mode 100644 index 0000000000..41758f50cd --- /dev/null +++ b/modules/jooby-validation/pom.xml @@ -0,0 +1,28 @@ + + + + + io.jooby + modules + 3.3.0-SNAPSHOT + + + 4.0.0 + jooby-validation + + + + io.jooby + jooby + ${jooby.version} + + + + jakarta.validation + jakarta.validation-api + + + + diff --git a/modules/jooby-validation/src/main/java/io/jooby/validation/ValidationHelper.java b/modules/jooby-validation/src/main/java/io/jooby/validation/ValidationHelper.java new file mode 100644 index 0000000000..652eb17588 --- /dev/null +++ b/modules/jooby-validation/src/main/java/io/jooby/validation/ValidationHelper.java @@ -0,0 +1,45 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +package io.jooby.validation; + +import io.jooby.Context; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; + +import java.util.*; + +public final class ValidationHelper { + + public static T validate(Context ctx, T bean) { + Validator validator = ctx.require(Validator.class); + + if (bean instanceof Collection) { + validateCollection(validator, (Collection) bean); + } else if (bean.getClass().isArray()) { + validateCollection(validator, Arrays.asList((Object[])bean)); + } else if (bean instanceof Map) { + validateCollection(validator, ((Map)bean).values()); + } else { + validateObject(validator, bean); + } + + return bean; + } + + private static void validateCollection(Validator validator, Collection beans) { + for (Object item : beans) { + validateObject(validator, item); + } + } + + private static void validateObject(Validator validator, Object bean) { + Set> violations = validator.validate(bean); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } +} diff --git a/modules/jooby-validation/src/main/java/io/jooby/validation/package-info.java b/modules/jooby-validation/src/main/java/io/jooby/validation/package-info.java new file mode 100644 index 0000000000..1cfba80ce3 --- /dev/null +++ b/modules/jooby-validation/src/main/java/io/jooby/validation/package-info.java @@ -0,0 +1 @@ +package io.jooby.validation; diff --git a/modules/jooby-validation/src/main/java/module-info.java b/modules/jooby-validation/src/main/java/module-info.java new file mode 100644 index 0000000000..a221cbf4c3 --- /dev/null +++ b/modules/jooby-validation/src/main/java/module-info.java @@ -0,0 +1,14 @@ +/* + * Jooby https://jooby.io + * Apache License Version 2.0 https://jooby.io/LICENSE.txt + * Copyright 2014 Edgar Espina + */ +/** + * Validation Module. + */ +module io.jooby.validation { + exports io.jooby.validation; + + requires jakarta.validation; + requires io.jooby; +} diff --git a/modules/pom.xml b/modules/pom.xml index f19c6bfbc0..742407f0d1 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -58,6 +58,9 @@ jooby-thymeleaf jooby-node jooby-camel + + + jooby-validation jooby-hbv jooby-pac4j From 7adc2a2d5c9ed2d9e74a84f3fc32542aef19844d Mon Sep 17 00:00:00 2001 From: kliushnichenko Date: Sat, 24 Aug 2024 15:16:29 +0300 Subject: [PATCH 06/13] rebase --- modules/jooby-hbv/pom.xml | 2 +- modules/jooby-validation/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/jooby-hbv/pom.xml b/modules/jooby-hbv/pom.xml index f17bc73441..f293345950 100644 --- a/modules/jooby-hbv/pom.xml +++ b/modules/jooby-hbv/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 3.3.0-SNAPSHOT + 3.2.10-SNAPSHOT 4.0.0 diff --git a/modules/jooby-validation/pom.xml b/modules/jooby-validation/pom.xml index 41758f50cd..17626a6a41 100644 --- a/modules/jooby-validation/pom.xml +++ b/modules/jooby-validation/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 3.3.0-SNAPSHOT + 3.2.10-SNAPSHOT 4.0.0 From 646f3554d0c8c74e12822defb5b4b547fadf158c Mon Sep 17 00:00:00 2001 From: kliushnichenko Date: Sat, 24 Aug 2024 15:42:02 +0300 Subject: [PATCH 07/13] rollback core changes cleanup cleanup cleanup --- .../main/java/io/jooby/DefaultContext.java | 24 +++++-------------- jooby/src/main/java/io/jooby/Jooby.java | 15 ------------ .../main/java/io/jooby/MessageValidator.java | 9 ------- jooby/src/main/java/io/jooby/Router.java | 4 ---- .../java/io/jooby/internal/RouterImpl.java | 16 ------------- jooby/src/main/java/module-info.java | 1 - 6 files changed, 6 insertions(+), 63 deletions(-) delete mode 100644 jooby/src/main/java/io/jooby/MessageValidator.java diff --git a/jooby/src/main/java/io/jooby/DefaultContext.java b/jooby/src/main/java/io/jooby/DefaultContext.java index 20c35289ad..21302366ff 100644 --- a/jooby/src/main/java/io/jooby/DefaultContext.java +++ b/jooby/src/main/java/io/jooby/DefaultContext.java @@ -20,12 +20,13 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Instant; -import java.util.*; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; -import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; -import jakarta.validation.Validator; import org.slf4j.Logger; import edu.umd.cs.findbugs.annotations.NonNull; @@ -416,20 +417,7 @@ default boolean isSecure() { T result = ValueConverters.convert(body(), type, getRouter()); return result; } - T object = (T) decoder(contentType).decode(this, type); - - MessageValidator messageValidator = getRouter().getMessageValidator(); - if (messageValidator != null) { - if (messageValidator.predicate().test(type)) { - Validator validator = messageValidator.validator(); - Set> violations = validator.validate(object); - if (!violations.isEmpty()) { - throw new ConstraintViolationException(violations); - } - } - } - return object; - + return (T) decoder(contentType).decode(this, type); } catch (Exception x) { throw SneakyThrows.propagate(x); } diff --git a/jooby/src/main/java/io/jooby/Jooby.java b/jooby/src/main/java/io/jooby/Jooby.java index 43aa2f0285..12d3d0d985 100644 --- a/jooby/src/main/java/io/jooby/Jooby.java +++ b/jooby/src/main/java/io/jooby/Jooby.java @@ -12,7 +12,6 @@ import java.io.IOException; import java.lang.reflect.Constructor; -import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -42,7 +41,6 @@ import java.util.function.Supplier; import java.util.stream.Collectors; -import jakarta.validation.Validator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -629,19 +627,6 @@ public Jooby decoder(@NonNull MediaType contentType, @NonNull MessageDecoder dec return this; } - @NonNull - @Override - public Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate) { - router.messageValidator(validator, predicate); - return this; - } - - @Nullable - @Override - public MessageValidator getMessageValidator() { - return router.getMessageValidator(); - } - @NonNull @Override public Jooby encoder(@NonNull MediaType contentType, @NonNull MessageEncoder encoder) { router.encoder(contentType, encoder); diff --git a/jooby/src/main/java/io/jooby/MessageValidator.java b/jooby/src/main/java/io/jooby/MessageValidator.java deleted file mode 100644 index f4dcb0f420..0000000000 --- a/jooby/src/main/java/io/jooby/MessageValidator.java +++ /dev/null @@ -1,9 +0,0 @@ -package io.jooby; - -import jakarta.validation.Validator; - -import java.lang.reflect.Type; -import java.util.function.Predicate; - -public record MessageValidator(Validator validator, Predicate predicate) { -} diff --git a/jooby/src/main/java/io/jooby/Router.java b/jooby/src/main/java/io/jooby/Router.java index 7073d82de8..1f47ec2734 100644 --- a/jooby/src/main/java/io/jooby/Router.java +++ b/jooby/src/main/java/io/jooby/Router.java @@ -8,7 +8,6 @@ import static java.util.Collections.unmodifiableList; import static java.util.Objects.requireNonNull; -import java.lang.reflect.Type; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -29,7 +28,6 @@ import java.util.stream.IntStream; import java.util.stream.Stream; -import jakarta.validation.Validator; import org.slf4j.Logger; import com.typesafe.config.Config; @@ -510,8 +508,6 @@ default Object execute(@NonNull Context context) { */ @NonNull Router decoder(@NonNull MediaType contentType, @NonNull MessageDecoder decoder); - @NonNull Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate); - @Nullable MessageValidator getMessageValidator(); /** * Returns the worker thread pool. This thread pool is used to run application blocking code. * diff --git a/jooby/src/main/java/io/jooby/internal/RouterImpl.java b/jooby/src/main/java/io/jooby/internal/RouterImpl.java index 33f7f6689b..a93b96fa38 100644 --- a/jooby/src/main/java/io/jooby/internal/RouterImpl.java +++ b/jooby/src/main/java/io/jooby/internal/RouterImpl.java @@ -8,7 +8,6 @@ import static java.util.Objects.requireNonNull; import java.io.FileNotFoundException; -import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; import java.nio.file.NoSuchFileException; import java.nio.file.Path; @@ -36,7 +35,6 @@ import java.util.stream.IntStream; import java.util.stream.Stream; -import jakarta.validation.Validator; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -149,8 +147,6 @@ public Stack executor(Executor executor) { private Map decoders = new HashMap<>(); - private MessageValidator messageValidator = null; - private Map attributes = new ConcurrentHashMap<>(); private ServiceRegistry services = new ServiceRegistryImpl(); @@ -372,18 +368,6 @@ public Router decoder(@NonNull MediaType contentType, @NonNull MessageDecoder de return this; } - @NonNull - @Override - public Router messageValidator(@NonNull Validator validator, @NonNull Predicate predicate) { - this.messageValidator = new MessageValidator(validator, predicate); - return this; - } - - @Override - public MessageValidator getMessageValidator() { - return messageValidator; - } - @NonNull @Override public Executor getWorker() { return worker; diff --git a/jooby/src/main/java/module-info.java b/jooby/src/main/java/module-info.java index 3d3521cb4f..cedb71a4ac 100644 --- a/jooby/src/main/java/module-info.java +++ b/jooby/src/main/java/module-info.java @@ -22,7 +22,6 @@ * True core deps */ requires jakarta.inject; - requires jakarta.validation; requires org.slf4j; requires static com.github.spotbugs.annotations; requires typesafe.config; From 229b5842c13e853bc30f87648dfcba03eb6da721 Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Mon, 26 Aug 2024 21:56:54 +0300 Subject: [PATCH 08/13] error handler, tests, refactoring --- jooby/pom.xml | 6 - modules/jooby-apt/pom.xml | 5 +- .../java/io/jooby/apt/JoobyProcessor.java | 18 ++ .../java/io/jooby/internal/apt/MvcRoute.java | 9 +- .../java/io/jooby/internal/apt/MvcRouter.java | 4 + .../src/test/java/tests/validation/Bean.java | 6 + .../BeanValidationGeneratorTest.java | 13 +- .../validation/BeanValidationsController.java | 21 +- modules/jooby-hbv/pom.xml | 25 +-- .../src/main/java/io/jooby/hbv/HbvModule.java | 62 +++++- .../jooby-hbv/src/main/java/module-info.java | 1 + modules/jooby-jakarta-validation/pom.xml | 96 +++++++++ .../io/jooby/validation/BeanValidator.java} | 2 +- .../ConstraintViolationHandler.java | 93 +++++++++ .../main/java/io/jooby/validation/Errors.java | 6 + .../java/io/jooby/validation/FieldError.java | 6 + .../io/jooby/validation/ValidationResult.java | 4 + .../io/jooby/validation/package-info.java | 0 .../src/main/java/module-info.java | 1 + .../jooby/validation/BeanValidatorTest.java | 188 ++++++++++++++++++ .../java/io/jooby/validation/app/App.java | 52 +++++ .../validation/app/NewAccountRequest.java | 59 ++++++ .../validation/app/PasswordsShouldMatch.java | 22 ++ .../app/PasswordsShouldMatchValidator.java | 15 ++ .../java/io/jooby/validation/app/Person.java | 33 +++ modules/jooby-validation/pom.xml | 28 --- modules/pom.xml | 3 +- 27 files changed, 704 insertions(+), 74 deletions(-) create mode 100644 modules/jooby-jakarta-validation/pom.xml rename modules/{jooby-validation/src/main/java/io/jooby/validation/ValidationHelper.java => jooby-jakarta-validation/src/main/java/io/jooby/validation/BeanValidator.java} (97%) create mode 100644 modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ConstraintViolationHandler.java create mode 100644 modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/Errors.java create mode 100644 modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/FieldError.java create mode 100644 modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ValidationResult.java rename modules/{jooby-validation => jooby-jakarta-validation}/src/main/java/io/jooby/validation/package-info.java (100%) rename modules/{jooby-validation => jooby-jakarta-validation}/src/main/java/module-info.java (85%) create mode 100644 modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/BeanValidatorTest.java create mode 100644 modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/App.java create mode 100644 modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/NewAccountRequest.java create mode 100644 modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatch.java create mode 100644 modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatchValidator.java create mode 100644 modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/Person.java delete mode 100644 modules/jooby-validation/pom.xml diff --git a/jooby/pom.xml b/jooby/pom.xml index 682fe2816a..1445e1cdbf 100644 --- a/jooby/pom.xml +++ b/jooby/pom.xml @@ -49,12 +49,6 @@ jakarta.inject-api - - - jakarta.validation - jakarta.validation-api - - com.typesafe diff --git a/modules/jooby-apt/pom.xml b/modules/jooby-apt/pom.xml index 20f4d4c121..7592056dbe 100644 --- a/modules/jooby-apt/pom.xml +++ b/modules/jooby-apt/pom.xml @@ -21,13 +21,14 @@ io.jooby - jooby-validation + jooby ${jooby.version} + test io.jooby - jooby + jooby-jakarta-validation ${jooby.version} test diff --git a/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java b/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java index b3396b3ca6..1d32245042 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java +++ b/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java @@ -105,6 +105,7 @@ public boolean process(Set annotations, RoundEnvironment return false; } else { var routeMap = buildRouteRegistry(annotations, roundEnv); + verifyBeanValidationDependency(routeMap.values()); for (var router : routeMap.values()) { try { var sourceCode = router.toSourceCode(null); @@ -371,4 +372,21 @@ public static RuntimeException propagate(final Throwable x) { private static E sneakyThrow0(final Throwable x) throws E { throw (E) x; } + + private void verifyBeanValidationDependency(Collection routers) { + var hasBeanValidation = routers.stream().anyMatch(MvcRouter::hasBeanValidation); + if (hasBeanValidation) { + TypeElement validatorElement = processingEnv.getElementUtils() + .getTypeElement("io.jooby.validation.BeanValidator"); + + if (validatorElement == null) { + processingEnv.getMessager().printMessage( + Diagnostic.Kind.ERROR, + "Unable to load 'BeanValidator' class. " + + "Bean validation usage (@Valid) was detected, but the appropriate dependency is missing. " + + "Please ensure that you have added the corresponding validation dependency " + + "(e.g., jooby-hbv)."); + } + } + } } diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java index 94bdfc1c40..af7deeb23b 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRoute.java @@ -30,6 +30,7 @@ public class MvcRoute { private String generatedName; private final boolean suspendFun; private boolean uncheckedCast; + private final boolean hasBeanValidation; public MvcRoute(MvcContext context, MvcRouter router, ExecutableElement method) { this.context = context; @@ -37,6 +38,7 @@ public MvcRoute(MvcContext context, MvcRouter router, ExecutableElement method) this.method = method; this.parameters = method.getParameters().stream().map(it -> new MvcParameter(context, this, it)).toList(); + this.hasBeanValidation = parameters.stream().anyMatch(MvcParameter::isRequireBeanValidation); this.suspendFun = !parameters.isEmpty() && parameters.get(parameters.size() - 1).getType().is("kotlin.coroutines.Continuation"); @@ -51,6 +53,7 @@ public MvcRoute(MvcContext context, MvcRouter router, MvcRoute route) { this.method = route.method; this.parameters = method.getParameters().stream().map(it -> new MvcParameter(context, this, it)).toList(); + this.hasBeanValidation = parameters.stream().anyMatch(MvcParameter::isRequireBeanValidation); this.returnType = new TypeDefinition( context.getProcessingEnvironment().getTypeUtils(), method.getReturnType()); @@ -206,7 +209,7 @@ public List generateHandlerCall(boolean kt) { String generatedParameter = parameter.generateMapping(kt); if (parameter.isRequireBeanValidation()) { generatedParameter = CodeBlock.of( - "io.jooby.validation.ValidationHelper.validate(", + "io.jooby.validation.BeanValidator.validate(", "ctx, ", generatedParameter, ")"); @@ -491,4 +494,8 @@ private String javadocComment(boolean kt) { public void setUncheckedCast(boolean value) { this.uncheckedCast = value; } + + public boolean hasBeanValidation() { + return hasBeanValidation; + } } diff --git a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRouter.java b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRouter.java index 373c1c9f7d..ede2282439 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRouter.java +++ b/modules/jooby-apt/src/main/java/io/jooby/internal/apt/MvcRouter.java @@ -362,4 +362,8 @@ public String toString() { buffer.append("}"); return buffer.toString(); } + + public boolean hasBeanValidation() { + return getRoutes().stream().anyMatch(MvcRoute::hasBeanValidation); + } } diff --git a/modules/jooby-apt/src/test/java/tests/validation/Bean.java b/modules/jooby-apt/src/test/java/tests/validation/Bean.java index 2309ed5dc2..04e51d49bb 100644 --- a/modules/jooby-apt/src/test/java/tests/validation/Bean.java +++ b/modules/jooby-apt/src/test/java/tests/validation/Bean.java @@ -1,5 +1,7 @@ package tests.validation; +import io.jooby.Context; + class Bean { private String name; @@ -10,4 +12,8 @@ public String getName() { public void setName(String name) { this.name = name; } + + public static Bean map(Context ctx) { + return ctx.body(Bean.class); + } } diff --git a/modules/jooby-apt/src/test/java/tests/validation/BeanValidationGeneratorTest.java b/modules/jooby-apt/src/test/java/tests/validation/BeanValidationGeneratorTest.java index 308813ff6c..c5a61572af 100644 --- a/modules/jooby-apt/src/test/java/tests/validation/BeanValidationGeneratorTest.java +++ b/modules/jooby-apt/src/test/java/tests/validation/BeanValidationGeneratorTest.java @@ -13,17 +13,24 @@ public void generate_validation_forBean() throws Exception { false, source -> { assertTrue(source.contains( - "c.validateQueryBean(io.jooby.validation.ValidationHelper.validate(ctx, ctx.query(\"bean\").isMissing() ? ctx.query().toNullable(tests.validation.Bean.class) : ctx.query(\"bean\").toNullable(tests.validation.Bean.class)))") + "c.validateQueryBean(io.jooby.validation.BeanValidator.validate(ctx, ctx.query(\"bean\").isMissing() ? ctx.query().toNullable(tests.validation.Bean.class) : ctx.query(\"bean\").toNullable(tests.validation.Bean.class)))") ); assertTrue(source.contains( - "c.validateFormBean(io.jooby.validation.ValidationHelper.validate(ctx, ctx.form(\"bean\").isMissing() ? ctx.form().toNullable(tests.validation.Bean.class) : ctx.form(\"bean\").toNullable(tests.validation.Bean.class)))") + "c.validateFormBean(io.jooby.validation.BeanValidator.validate(ctx, ctx.form(\"bean\").isMissing() ? ctx.form().toNullable(tests.validation.Bean.class) : ctx.form(\"bean\").toNullable(tests.validation.Bean.class)))") ); assertTrue(source.contains( - "c.validateBodyBean(io.jooby.validation.ValidationHelper.validate(ctx, ctx.body(tests.validation.Bean.class)))") + "c.validateBindParamBean(io.jooby.validation.BeanValidator.validate(ctx, tests.validation.Bean.map(ctx)))") ); + assertTrue(source.contains( + "c.validateBodyBean(io.jooby.validation.BeanValidator.validate(ctx, ctx.body(tests.validation.Bean.class)))") + ); + + assertTrue(source.contains( + "c.validateListOfBodyBeans(io.jooby.validation.BeanValidator.validate(ctx, ctx.body(io.jooby.Reified.list(tests.validation.Bean.class).getType())))") + ); }); } } diff --git a/modules/jooby-apt/src/test/java/tests/validation/BeanValidationsController.java b/modules/jooby-apt/src/test/java/tests/validation/BeanValidationsController.java index 1c8b2f3c75..b23167e388 100644 --- a/modules/jooby-apt/src/test/java/tests/validation/BeanValidationsController.java +++ b/modules/jooby-apt/src/test/java/tests/validation/BeanValidationsController.java @@ -1,10 +1,10 @@ package tests.validation; -import io.jooby.annotation.FormParam; -import io.jooby.annotation.POST; -import io.jooby.annotation.QueryParam; +import io.jooby.annotation.*; import jakarta.validation.Valid; +import java.util.List; + public class BeanValidationsController { @POST("/validate/query-bean") @@ -17,9 +17,24 @@ public Bean validateFormBean(@Valid @FormParam Bean bean) { return bean; } + //todo: revive when flash `toNullable` will be fixed +// @POST("/validate/flash-bean") +// public Bean validateFlashBean(@Valid @FlashParam Bean bean) { +// return bean; +// } + + @POST("/validate/bind-param-bean") + public Bean validateBindParamBean(@Valid @BindParam Bean bean) { + return bean; + } + @POST("/validate/body-bean") public Bean validateBodyBean(@Valid Bean bean) { return bean; } + @POST("/validate/list-of-body-beans") + public List validateListOfBodyBeans(@Valid List bean) { + return bean; + } } diff --git a/modules/jooby-hbv/pom.xml b/modules/jooby-hbv/pom.xml index f293345950..2f6b92492f 100644 --- a/modules/jooby-hbv/pom.xml +++ b/modules/jooby-hbv/pom.xml @@ -21,11 +21,11 @@ io.jooby - jooby-validation + jooby-jakarta-validation ${jooby.version} - + org.hibernate.validator hibernate-validator @@ -43,26 +43,5 @@ expressly 5.0.0 - - - - org.junit.jupiter - junit-jupiter-engine - test - - - - org.jacoco - org.jacoco.agent - runtime - test - - - - org.mockito - mockito-core - test - - diff --git a/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java b/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java index af49f0019f..c94ab43219 100644 --- a/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java +++ b/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java @@ -9,6 +9,9 @@ import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Extension; import io.jooby.Jooby; +import io.jooby.StatusCode; +import io.jooby.validation.ConstraintViolationHandler; +import jakarta.validation.ConstraintViolationException; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; import org.hibernate.validator.HibernateValidator; @@ -17,19 +20,64 @@ import java.util.function.Consumer; import static jakarta.validation.Validation.byProvider; -import static java.util.Objects.requireNonNull; public class HbvModule implements Extension { private Consumer configurer; + private StatusCode statusCode = StatusCode.UNPROCESSABLE_ENTITY; + private String title = "Validation failed"; + private boolean disableDefaultViolationHandler = false; - public HbvModule doWith(final Consumer configurer) { - this.configurer = requireNonNull(configurer, "Configurer callback is required."); + /** + * Setups a configurer callback. + * + * @param configurer Configurer callback. + * @return This module. + */ + public HbvModule doWith(@NonNull final Consumer configurer) { + this.configurer = configurer; + return this; + } + + /** + * Overrides the default status code for the errors produced by validation. + * Default code is UNPROCESSABLE_ENTITY(422) + * + * @param statusCode new status code + * @return This module. + */ + public HbvModule statusCode(@NonNull StatusCode statusCode) { + this.statusCode = statusCode; + return this; + } + + /** + * Overrides the default title for the errors produced by validation. + * Default title is "Validation failed" + * + * @param title new title + * @return This module. + */ + public HbvModule validationTitle(@NonNull String title) { + this.title = title; + return this; + } + + /** + * Disables default constraint violation handler. + * By default {@link HbvModule} provide built-in error handler for the {@link ConstraintViolationException} + * Such exceptions are transformed into response of {@link io.jooby.validation.ValidationResult} + * Use this flag to disable default error handler and provide your custom. + * + * @return This module. + */ + public HbvModule disableViolationHandler() { + this.disableDefaultViolationHandler = true; return this; } @Override - public void install(@NonNull Jooby application) { + public void install(@NonNull Jooby app) { HibernateValidatorConfiguration cfg = byProvider(HibernateValidator.class).configure(); if (configurer != null) { @@ -38,7 +86,11 @@ public void install(@NonNull Jooby application) { try (ValidatorFactory factory = cfg.buildValidatorFactory()) { Validator validator = factory.getValidator(); - application.getServices().put(Validator.class, validator); + app.getServices().put(Validator.class, validator); + + if (!disableDefaultViolationHandler) { + app.error(ConstraintViolationException.class, new ConstraintViolationHandler(statusCode, title)); + } } } } diff --git a/modules/jooby-hbv/src/main/java/module-info.java b/modules/jooby-hbv/src/main/java/module-info.java index e9a1799ee9..6d050e93e5 100644 --- a/modules/jooby-hbv/src/main/java/module-info.java +++ b/modules/jooby-hbv/src/main/java/module-info.java @@ -14,4 +14,5 @@ requires typesafe.config; requires org.hibernate.validator; requires jakarta.validation; + requires io.jooby.validation; } diff --git a/modules/jooby-jakarta-validation/pom.xml b/modules/jooby-jakarta-validation/pom.xml new file mode 100644 index 0000000000..a03024f7aa --- /dev/null +++ b/modules/jooby-jakarta-validation/pom.xml @@ -0,0 +1,96 @@ + + + + + io.jooby + modules + 3.2.10-SNAPSHOT + + + 4.0.0 + jooby-jakarta-validation + + + + io.jooby + jooby + ${jooby.version} + + + + jakarta.validation + jakarta.validation-api + + + + + io.jooby + jooby-netty + ${jooby.version} + test + + + + org.hibernate.validator + hibernate-validator + 8.0.1.Final + test + + + + jakarta.el + jakarta.el-api + 5.0.1 + test + + + + org.glassfish.expressly + expressly + 5.0.0 + test + + + + io.jooby + jooby-jackson + ${jooby.version} + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + io.jooby + jooby-test + ${jooby.version} + test + + + + io.rest-assured + rest-assured + test + + + + org.assertj + assertj-core + 3.26.3 + test + + + + diff --git a/modules/jooby-validation/src/main/java/io/jooby/validation/ValidationHelper.java b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/BeanValidator.java similarity index 97% rename from modules/jooby-validation/src/main/java/io/jooby/validation/ValidationHelper.java rename to modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/BeanValidator.java index 652eb17588..d08208d927 100644 --- a/modules/jooby-validation/src/main/java/io/jooby/validation/ValidationHelper.java +++ b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/BeanValidator.java @@ -12,7 +12,7 @@ import java.util.*; -public final class ValidationHelper { +public final class BeanValidator { public static T validate(Context ctx, T bean) { Validator validator = ctx.require(Validator.class); diff --git a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ConstraintViolationHandler.java b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ConstraintViolationHandler.java new file mode 100644 index 0000000000..9eb8ad3b97 --- /dev/null +++ b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ConstraintViolationHandler.java @@ -0,0 +1,93 @@ +package io.jooby.validation; + +import edu.umd.cs.findbugs.annotations.NonNull; +import io.jooby.Context; +import io.jooby.ErrorHandler; +import io.jooby.StatusCode; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.util.stream.Collectors.groupingBy; + +/** + * Catches and transform {@link ConstraintViolationException} into {@link ValidationResult} + * + * Payload example: + * + * { + * "title": "Validation failed", + * "status": 422, + * "errors": { + * "objectErrors": [ + * "Passwords should match" + * ], + * "fieldErrors": [ + * { + * "field": "firstName", + * "messages": [ + * "must not be empty", + * "must not be null" + * ] + * } + * ] + * } + * } + * + */ +public class ConstraintViolationHandler implements ErrorHandler { + + private static final String ROOT_VIOLATIONS_PATH = ""; + + private final StatusCode statusCode; + private final String title; + + public ConstraintViolationHandler(StatusCode statusCode, String title) { + this.statusCode = statusCode; + this.title = title; + } + + @Override + public void apply(@NonNull Context ctx, @NonNull Throwable cause, @NonNull StatusCode code) { + ConstraintViolationException ex = (ConstraintViolationException) cause; + + Set> violations = ex.getConstraintViolations(); + + Map>> groupedByPath = violations.stream() + .collect(groupingBy(violation -> violation.getPropertyPath().toString())); + + List fieldErrors = collectFieldErrors(groupedByPath); + List objectErrors = collectObjectErrors(groupedByPath); + + Errors errors = new Errors(objectErrors, fieldErrors); + ValidationResult result = new ValidationResult(title, statusCode.value(), errors); + ctx.setResponseCode(statusCode).render(result); + } + + private List collectFieldErrors(Map>> groupedViolations) { + List fieldErrors = new ArrayList<>(); + for (Map.Entry>> entry : groupedViolations.entrySet()) { + var field = entry.getKey(); + if (!ROOT_VIOLATIONS_PATH.equals(field)) { + fieldErrors.add(new FieldError(field, extractMessages(entry.getValue()))); + } + } + return fieldErrors; + } + + private List collectObjectErrors(Map>> groupedViolations) { + List> violations = groupedViolations.get(ROOT_VIOLATIONS_PATH); + if (violations != null) { + return extractMessages(violations); + } + return List.of(); + } + + private List extractMessages(List> violations) { + return violations.stream().map(ConstraintViolation::getMessage).toList(); + } +} diff --git a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/Errors.java b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/Errors.java new file mode 100644 index 0000000000..b303ed0b8c --- /dev/null +++ b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/Errors.java @@ -0,0 +1,6 @@ +package io.jooby.validation; + +import java.util.List; + +public record Errors(List objectErrors, List fieldErrors) { +} diff --git a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/FieldError.java b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/FieldError.java new file mode 100644 index 0000000000..62ce64be2f --- /dev/null +++ b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/FieldError.java @@ -0,0 +1,6 @@ +package io.jooby.validation; + +import java.util.List; + +public record FieldError(String field, List messages) { +} diff --git a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ValidationResult.java b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ValidationResult.java new file mode 100644 index 0000000000..9aa721bbad --- /dev/null +++ b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ValidationResult.java @@ -0,0 +1,4 @@ +package io.jooby.validation; + +public record ValidationResult(String title, int status, Errors errors) { +} diff --git a/modules/jooby-validation/src/main/java/io/jooby/validation/package-info.java b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/package-info.java similarity index 100% rename from modules/jooby-validation/src/main/java/io/jooby/validation/package-info.java rename to modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/package-info.java diff --git a/modules/jooby-validation/src/main/java/module-info.java b/modules/jooby-jakarta-validation/src/main/java/module-info.java similarity index 85% rename from modules/jooby-validation/src/main/java/module-info.java rename to modules/jooby-jakarta-validation/src/main/java/module-info.java index a221cbf4c3..150ace1e8e 100644 --- a/modules/jooby-validation/src/main/java/module-info.java +++ b/modules/jooby-jakarta-validation/src/main/java/module-info.java @@ -11,4 +11,5 @@ requires jakarta.validation; requires io.jooby; + requires com.github.spotbugs.annotations; } diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/BeanValidatorTest.java b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/BeanValidatorTest.java new file mode 100644 index 0000000000..ea56b4ae56 --- /dev/null +++ b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/BeanValidatorTest.java @@ -0,0 +1,188 @@ +package io.jooby.validation; + +import io.jooby.test.JoobyTest; +import io.jooby.validation.app.App; +import io.jooby.validation.app.NewAccountRequest; +import io.jooby.validation.app.Person; +import io.restassured.RestAssured; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import static io.jooby.StatusCode.UNPROCESSABLE_ENTITY_CODE; +import static io.jooby.validation.app.App.DEFAULT_TITLE; +import static io.restassured.RestAssured.given; + +@JoobyTest(value = App.class, port = 8099) +public class BeanValidatorTest { + + protected static RequestSpecification SPEC = new RequestSpecBuilder() + .setPort(8099) + .setContentType(ContentType.JSON) + .setAccept(ContentType.JSON) + .build(); + + static { + RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); + } + + @Test + public void validate_personBean_shouldDetect2Violations() { + Person person = new Person(null, "Last Name"); + + ValidationResult actualResult = given().spec(SPEC). + with() + .body(person) + .post("/create-person") + .then() + .assertThat() + .statusCode(UNPROCESSABLE_ENTITY_CODE) + .extract().as(ValidationResult.class); + + var fieldError = new FieldError( + "firstName", + List.of("must not be empty", "must not be null") + ); + ValidationResult expectedResult = buildResult(new Errors(List.of(), List.of(fieldError))); + + Assertions.assertThat(expectedResult) + .usingRecursiveComparison() + .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.fieldErrors\\.messages") + .isEqualTo(actualResult); + } + + @Test + public void validate_arrayOfPerson_shouldDetect2Violations() { + Person person1 = new Person("First Name", "Last Name"); + Person person2 = new Person(null, "Last Name 2"); + + ValidationResult actualResult = given().spec(SPEC). + with() + .body(new Person[]{person1, person2}) + .post("/create-array-of-persons") + .then() + .assertThat() + .statusCode(UNPROCESSABLE_ENTITY_CODE) + .extract().as(ValidationResult.class); + + var fieldError = new FieldError( + "firstName", + List.of("must not be empty", "must not be null") + ); + ValidationResult expectedResult = buildResult(new Errors(List.of(), List.of(fieldError))); + + Assertions.assertThat(expectedResult) + .usingRecursiveComparison() + .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.fieldErrors\\.messages") + .isEqualTo(actualResult); + } + + @Test + public void validate_listOfPerson_shouldDetect2Violations() { + Person person1 = new Person("First Name", "Last Name"); + Person person2 = new Person(null, "Last Name 2"); + + ValidationResult actualResult = given().spec(SPEC). + with() + .body(List.of(person1, person2)) + .post("/create-list-of-persons") + .then() + .assertThat() + .statusCode(UNPROCESSABLE_ENTITY_CODE) + .extract().as(ValidationResult.class); + + var fieldError = new FieldError( + "firstName", + List.of("must not be empty", "must not be null") + ); + ValidationResult expectedResult = buildResult(new Errors(List.of(), List.of(fieldError))); + + Assertions.assertThat(expectedResult) + .usingRecursiveComparison() + .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.fieldErrors\\.messages") + .isEqualTo(actualResult); + } + + @Test + public void validate_mapOfPerson_shouldDetect2Violations() { + Person person1 = new Person("First Name", "Last Name"); + Person person2 = new Person(null, "Last Name 2"); + + ValidationResult actualResult = given().spec(SPEC). + with() + .body(Map.of("1", person1, "2", person2)) + .post("/create-map-of-persons") + .then() + .assertThat() + .statusCode(UNPROCESSABLE_ENTITY_CODE) + .extract().as(ValidationResult.class); + + var fieldError = new FieldError( + "firstName", + List.of("must not be empty", "must not be null") + ); + ValidationResult expectedResult = buildResult(new Errors(List.of(), List.of(fieldError))); + + Assertions.assertThat(expectedResult) + .usingRecursiveComparison() + .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.fieldErrors\\.messages") + .isEqualTo(actualResult); + } + + @Test + public void validate_newAccountBean_shouldDetect6Violations() { + NewAccountRequest request = new NewAccountRequest(); + request.setLogin("jk"); + request.setPassword("123"); + request.setConfirmPassword("1234"); + request.setPerson(new Person(null, "Last Name")); + + ValidationResult actualResult = given().spec(SPEC). + with() + .body(request) + .post("/create-new-account") + .then() + .assertThat() + .statusCode(UNPROCESSABLE_ENTITY_CODE) + .extract().as(ValidationResult.class); + + List fieldErrors = new ArrayList<>() {{ + add(new FieldError( + "person.firstName", + List.of("must not be empty", "must not be null")) + ); + add(new FieldError( + "login", + List.of("size must be between 3 and 16")) + ); + add(new FieldError( + "password", + List.of("size must be between 8 and 24")) + ); + add(new FieldError( + "confirmPassword", + List.of("size must be between 8 and 24")) + ); + }}; + + String objectErrors = "Passwords should match"; + Errors errors = new Errors(List.of(objectErrors), fieldErrors); + ValidationResult expectedResult = buildResult(errors); + + Assertions.assertThat(expectedResult) + .usingRecursiveComparison() + .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.fieldErrors") + .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.fieldErrors\\.messages") + .isEqualTo(actualResult); + } + + private ValidationResult buildResult(Errors errors) { + return new ValidationResult(DEFAULT_TITLE, UNPROCESSABLE_ENTITY_CODE, errors); + } +} diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/App.java b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/App.java new file mode 100644 index 0000000000..2f64661deb --- /dev/null +++ b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/App.java @@ -0,0 +1,52 @@ +package io.jooby.validation.app; + +import io.jooby.Jooby; +import io.jooby.Reified; +import io.jooby.StatusCode; +import io.jooby.jackson.JacksonModule; +import io.jooby.validation.BeanValidator; +import io.jooby.validation.ConstraintViolationHandler; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.hibernate.validator.HibernateValidator; +import org.hibernate.validator.HibernateValidatorConfiguration; + +import static jakarta.validation.Validation.byProvider; + +public class App extends Jooby { + + private static final StatusCode STATUS_CODE = StatusCode.UNPROCESSABLE_ENTITY; + public static final String DEFAULT_TITLE = "Validation failed"; + + { + provideValidator(this); + install(new JacksonModule()); + + post("/create-person", ctx -> BeanValidator.validate(ctx, ctx.body(Person.class))); + + post("/create-array-of-persons", ctx -> BeanValidator.validate(ctx, ctx.body(Person[].class))); + + post("/create-list-of-persons", ctx -> { + return BeanValidator.validate(ctx, ctx.body(Reified.list(Person.class).getType())); + }); + + post("/create-map-of-persons", ctx -> { + return BeanValidator.validate(ctx, ctx.body(Reified.map(String.class, Person.class).getType())); + }); + + post("/create-new-account", ctx -> { + return BeanValidator.validate(ctx, ctx.body(NewAccountRequest.class)); + }); + + error(ConstraintViolationException.class, new ConstraintViolationHandler(STATUS_CODE, DEFAULT_TITLE)); + } + + private void provideValidator(Jooby app) { + HibernateValidatorConfiguration cfg = byProvider(HibernateValidator.class).configure(); + try (ValidatorFactory factory = cfg.buildValidatorFactory()) { + Validator validator = factory.getValidator(); + app.getServices().put(Validator.class, validator); + } + } +} \ No newline at end of file diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/NewAccountRequest.java b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/NewAccountRequest.java new file mode 100644 index 0000000000..268b10acd2 --- /dev/null +++ b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/NewAccountRequest.java @@ -0,0 +1,59 @@ +package io.jooby.validation.app; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +@PasswordsShouldMatch +public class NewAccountRequest { + @NotNull + @NotEmpty + @Size(min = 3, max = 16) + private String login; + + @NotNull + @NotEmpty + @Size(min = 8, max = 24) + private String password; + + @NotNull + @NotEmpty + @Size(min = 8, max = 24) + private String confirmPassword; + + @Valid + private Person person; + + public String getLogin() { + return login; + } + + public void setLogin(String login) { + this.login = login; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getConfirmPassword() { + return confirmPassword; + } + + public void setConfirmPassword(String confirmPassword) { + this.confirmPassword = confirmPassword; + } + + public Person getPerson() { + return person; + } + + public void setPerson(Person person) { + this.person = person; + } +} diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatch.java b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatch.java new file mode 100644 index 0000000000..1c63d31ed3 --- /dev/null +++ b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatch.java @@ -0,0 +1,22 @@ +package io.jooby.validation.app; + +import jakarta.validation.Constraint; +import jakarta.validation.Payload; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.ANNOTATION_TYPE; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Constraint(validatedBy = PasswordsShouldMatchValidator.class) +@Target({TYPE, ANNOTATION_TYPE}) +@Retention(RUNTIME) +public @interface PasswordsShouldMatch { + String message() default "Passwords should match"; + + Class[] groups() default {}; + + Class[] payload() default {}; +} diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatchValidator.java b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatchValidator.java new file mode 100644 index 0000000000..dddcd3325e --- /dev/null +++ b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatchValidator.java @@ -0,0 +1,15 @@ +package io.jooby.validation.app; + +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +public class PasswordsShouldMatchValidator implements ConstraintValidator { + + @Override + public boolean isValid(NewAccountRequest request, ConstraintValidatorContext constraintContext) { + if (request.getPassword() == null || request.getConfirmPassword() == null) { + return false; + } + return request.getPassword().equals(request.getConfirmPassword()); + } +} diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/Person.java b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/Person.java new file mode 100644 index 0000000000..b84ae36da4 --- /dev/null +++ b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/Person.java @@ -0,0 +1,33 @@ +package io.jooby.validation.app; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; + +public class Person { + + @NotEmpty + @NotNull + private String firstName; + private String lastName; + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } +} diff --git a/modules/jooby-validation/pom.xml b/modules/jooby-validation/pom.xml deleted file mode 100644 index 17626a6a41..0000000000 --- a/modules/jooby-validation/pom.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - io.jooby - modules - 3.2.10-SNAPSHOT - - - 4.0.0 - jooby-validation - - - - io.jooby - jooby - ${jooby.version} - - - - jakarta.validation - jakarta.validation-api - - - - diff --git a/modules/pom.xml b/modules/pom.xml index fbad803452..e7e0acbb3a 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -58,10 +58,9 @@ jooby-thymeleaf jooby-node jooby-camel - jooby-hbv - jooby-validation + jooby-jakarta-validation jooby-hbv jooby-pac4j From a470d9bc55927f42433c69a0287207db2893c283 Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Sat, 31 Aug 2024 18:11:48 +0300 Subject: [PATCH 09/13] resolve discussions --- .../main/java/io/jooby/hbv/package-info.java | 1 - .../pom.xml | 2 +- .../validator/HibernateValidatorModule.java} | 16 ++--- .../hibernate/validator/package-info.java | 1 + .../src/main/java/module-info.java | 6 +- .../ConstraintViolationHandler.java | 72 +++++++++---------- .../main/java/io/jooby/validation/Errors.java | 6 -- .../java/io/jooby/validation/FieldError.java | 6 -- .../io/jooby/validation/ValidationResult.java | 10 ++- .../jooby/validation/BeanValidatorTest.java | 71 ++++++++++-------- modules/pom.xml | 2 +- 11 files changed, 100 insertions(+), 93 deletions(-) delete mode 100644 modules/jooby-hbv/src/main/java/io/jooby/hbv/package-info.java rename modules/{jooby-hbv => jooby-hibernate-validator}/pom.xml (96%) rename modules/{jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java => jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java} (82%) create mode 100644 modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/package-info.java rename modules/{jooby-hbv => jooby-hibernate-validator}/src/main/java/module-info.java (76%) delete mode 100644 modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/Errors.java delete mode 100644 modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/FieldError.java diff --git a/modules/jooby-hbv/src/main/java/io/jooby/hbv/package-info.java b/modules/jooby-hbv/src/main/java/io/jooby/hbv/package-info.java deleted file mode 100644 index da7909aad1..0000000000 --- a/modules/jooby-hbv/src/main/java/io/jooby/hbv/package-info.java +++ /dev/null @@ -1 +0,0 @@ -package io.jooby.hbv; diff --git a/modules/jooby-hbv/pom.xml b/modules/jooby-hibernate-validator/pom.xml similarity index 96% rename from modules/jooby-hbv/pom.xml rename to modules/jooby-hibernate-validator/pom.xml index 2f6b92492f..26ba7257ce 100644 --- a/modules/jooby-hbv/pom.xml +++ b/modules/jooby-hibernate-validator/pom.xml @@ -10,7 +10,7 @@ 4.0.0 - jooby-hbv + jooby-hibernate-validator diff --git a/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java similarity index 82% rename from modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java rename to modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java index c94ab43219..cc5cc57b7f 100644 --- a/modules/jooby-hbv/src/main/java/io/jooby/hbv/HbvModule.java +++ b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java @@ -3,8 +3,7 @@ * Apache License Version 2.0 https://jooby.io/LICENSE.txt * Copyright 2014 Edgar Espina */ -package io.jooby.hbv; - +package io.jooby.hibernate.validator; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Extension; @@ -21,7 +20,7 @@ import static jakarta.validation.Validation.byProvider; -public class HbvModule implements Extension { +public class HibernateValidatorModule implements Extension { private Consumer configurer; private StatusCode statusCode = StatusCode.UNPROCESSABLE_ENTITY; @@ -34,7 +33,7 @@ public class HbvModule implements Extension { * @param configurer Configurer callback. * @return This module. */ - public HbvModule doWith(@NonNull final Consumer configurer) { + public HibernateValidatorModule doWith(@NonNull final Consumer configurer) { this.configurer = configurer; return this; } @@ -46,7 +45,7 @@ public HbvModule doWith(@NonNull final Consumer * @param statusCode new status code * @return This module. */ - public HbvModule statusCode(@NonNull StatusCode statusCode) { + public HibernateValidatorModule statusCode(@NonNull StatusCode statusCode) { this.statusCode = statusCode; return this; } @@ -58,20 +57,21 @@ public HbvModule statusCode(@NonNull StatusCode statusCode) { * @param title new title * @return This module. */ - public HbvModule validationTitle(@NonNull String title) { + public HibernateValidatorModule validationTitle(@NonNull String title) { this.title = title; return this; } /** * Disables default constraint violation handler. - * By default {@link HbvModule} provide built-in error handler for the {@link ConstraintViolationException} + * By default {@link HibernateValidatorModule} provides + * built-in error handler for the {@link ConstraintViolationException} * Such exceptions are transformed into response of {@link io.jooby.validation.ValidationResult} * Use this flag to disable default error handler and provide your custom. * * @return This module. */ - public HbvModule disableViolationHandler() { + public HibernateValidatorModule disableViolationHandler() { this.disableDefaultViolationHandler = true; return this; } diff --git a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/package-info.java b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/package-info.java new file mode 100644 index 0000000000..81b5073c5b --- /dev/null +++ b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/package-info.java @@ -0,0 +1 @@ +package io.jooby.hibernate.validator; diff --git a/modules/jooby-hbv/src/main/java/module-info.java b/modules/jooby-hibernate-validator/src/main/java/module-info.java similarity index 76% rename from modules/jooby-hbv/src/main/java/module-info.java rename to modules/jooby-hibernate-validator/src/main/java/module-info.java index 6d050e93e5..1bb15c0fbe 100644 --- a/modules/jooby-hbv/src/main/java/module-info.java +++ b/modules/jooby-hibernate-validator/src/main/java/module-info.java @@ -4,10 +4,10 @@ * Copyright 2014 Edgar Espina */ /** - * Hbv module. + * Hibernate Validator Module. */ -module io.jooby.hbv { - exports io.jooby.hbv; +module io.jooby.hibernate.validator { + exports io.jooby.hibernate.validator; requires transitive io.jooby; requires static com.github.spotbugs.annotations; diff --git a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ConstraintViolationHandler.java b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ConstraintViolationHandler.java index 9eb8ad3b97..ffc38c06fa 100644 --- a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ConstraintViolationHandler.java +++ b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ConstraintViolationHandler.java @@ -12,32 +12,40 @@ import java.util.Map; import java.util.Set; +import static io.jooby.validation.ValidationResult.ErrorType.FIELD; +import static io.jooby.validation.ValidationResult.ErrorType.GLOBAL; import static java.util.stream.Collectors.groupingBy; /** * Catches and transform {@link ConstraintViolationException} into {@link ValidationResult} - * + *

* Payload example: - * + *

{@code
  * {
- *     "title": "Validation failed",
- *     "status": 422,
- *     "errors": {
- *         "objectErrors": [
- *              "Passwords should match"
- *         ],
- *         "fieldErrors": [
- *             {
- *                 "field": "firstName",
- *                 "messages": [
- *                     "must not be empty",
- *                     "must not be null"
- *                 ]
- *             }
- *         ]
- *     }
+ *    "title": "Validation failed",
+ *    "status": 422,
+ *    "errors": [
+ *       {
+ *          "field": null,
+ *          "messages": [
+ *             "Passwords should match"
+ *          ],
+ *          "type": "GLOBAL"
+ *       },
+ *       {
+ *          "field": "firstName",
+ *          "messages": [
+ *             "must not be empty",
+ *             "must not be null"
+ *          ],
+ *          "type": "FIELD"
+ *       }
+ *    ]
  * }
- * 
+ * }
+ * + * @author kliushnichenko + * @since 3.2.10 */ public class ConstraintViolationHandler implements ErrorHandler { @@ -60,31 +68,23 @@ public void apply(@NonNull Context ctx, @NonNull Throwable cause, @NonNull Statu Map>> groupedByPath = violations.stream() .collect(groupingBy(violation -> violation.getPropertyPath().toString())); - List fieldErrors = collectFieldErrors(groupedByPath); - List objectErrors = collectObjectErrors(groupedByPath); + List errors = collectErrors(groupedByPath); - Errors errors = new Errors(objectErrors, fieldErrors); ValidationResult result = new ValidationResult(title, statusCode.value(), errors); ctx.setResponseCode(statusCode).render(result); } - private List collectFieldErrors(Map>> groupedViolations) { - List fieldErrors = new ArrayList<>(); + private List collectErrors(Map>> groupedViolations) { + List errors = new ArrayList<>(); for (Map.Entry>> entry : groupedViolations.entrySet()) { - var field = entry.getKey(); - if (!ROOT_VIOLATIONS_PATH.equals(field)) { - fieldErrors.add(new FieldError(field, extractMessages(entry.getValue()))); + var path = entry.getKey(); + if (ROOT_VIOLATIONS_PATH.equals(path)) { + errors.add(new ValidationResult.Error(null, extractMessages(entry.getValue()), GLOBAL)); + } else { + errors.add(new ValidationResult.Error(path, extractMessages(entry.getValue()), FIELD)); } } - return fieldErrors; - } - - private List collectObjectErrors(Map>> groupedViolations) { - List> violations = groupedViolations.get(ROOT_VIOLATIONS_PATH); - if (violations != null) { - return extractMessages(violations); - } - return List.of(); + return errors; } private List extractMessages(List> violations) { diff --git a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/Errors.java b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/Errors.java deleted file mode 100644 index b303ed0b8c..0000000000 --- a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/Errors.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.jooby.validation; - -import java.util.List; - -public record Errors(List objectErrors, List fieldErrors) { -} diff --git a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/FieldError.java b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/FieldError.java deleted file mode 100644 index 62ce64be2f..0000000000 --- a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/FieldError.java +++ /dev/null @@ -1,6 +0,0 @@ -package io.jooby.validation; - -import java.util.List; - -public record FieldError(String field, List messages) { -} diff --git a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ValidationResult.java b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ValidationResult.java index 9aa721bbad..f88791c993 100644 --- a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ValidationResult.java +++ b/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ValidationResult.java @@ -1,4 +1,12 @@ package io.jooby.validation; -public record ValidationResult(String title, int status, Errors errors) { +import java.util.List; + +public record ValidationResult(String title, int status, List errors) { + public record Error(String field, List messages, ErrorType type) { + } + + public enum ErrorType { + FIELD, GLOBAL + } } diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/BeanValidatorTest.java b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/BeanValidatorTest.java index ea56b4ae56..e506a591a9 100644 --- a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/BeanValidatorTest.java +++ b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/BeanValidatorTest.java @@ -45,15 +45,16 @@ public void validate_personBean_shouldDetect2Violations() { .statusCode(UNPROCESSABLE_ENTITY_CODE) .extract().as(ValidationResult.class); - var fieldError = new FieldError( + var fieldError = new ValidationResult.Error( "firstName", - List.of("must not be empty", "must not be null") + List.of("must not be empty", "must not be null"), + ValidationResult.ErrorType.FIELD ); - ValidationResult expectedResult = buildResult(new Errors(List.of(), List.of(fieldError))); + ValidationResult expectedResult = buildResult(List.of(fieldError)); Assertions.assertThat(expectedResult) .usingRecursiveComparison() - .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.fieldErrors\\.messages") + .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages") .isEqualTo(actualResult); } @@ -71,15 +72,16 @@ public void validate_arrayOfPerson_shouldDetect2Violations() { .statusCode(UNPROCESSABLE_ENTITY_CODE) .extract().as(ValidationResult.class); - var fieldError = new FieldError( + var fieldError = new ValidationResult.Error( "firstName", - List.of("must not be empty", "must not be null") + List.of("must not be empty", "must not be null"), + ValidationResult.ErrorType.FIELD ); - ValidationResult expectedResult = buildResult(new Errors(List.of(), List.of(fieldError))); + ValidationResult expectedResult = buildResult(List.of(fieldError)); Assertions.assertThat(expectedResult) .usingRecursiveComparison() - .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.fieldErrors\\.messages") + .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages") .isEqualTo(actualResult); } @@ -97,15 +99,16 @@ public void validate_listOfPerson_shouldDetect2Violations() { .statusCode(UNPROCESSABLE_ENTITY_CODE) .extract().as(ValidationResult.class); - var fieldError = new FieldError( + var fieldError = new ValidationResult.Error( "firstName", - List.of("must not be empty", "must not be null") + List.of("must not be empty", "must not be null"), + ValidationResult.ErrorType.FIELD ); - ValidationResult expectedResult = buildResult(new Errors(List.of(), List.of(fieldError))); + ValidationResult expectedResult = buildResult( List.of(fieldError)); Assertions.assertThat(expectedResult) .usingRecursiveComparison() - .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.fieldErrors\\.messages") + .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages") .isEqualTo(actualResult); } @@ -123,15 +126,16 @@ public void validate_mapOfPerson_shouldDetect2Violations() { .statusCode(UNPROCESSABLE_ENTITY_CODE) .extract().as(ValidationResult.class); - var fieldError = new FieldError( + var fieldError = new ValidationResult.Error( "firstName", - List.of("must not be empty", "must not be null") + List.of("must not be empty", "must not be null"), + ValidationResult.ErrorType.FIELD ); - ValidationResult expectedResult = buildResult(new Errors(List.of(), List.of(fieldError))); + ValidationResult expectedResult = buildResult(List.of(fieldError)); Assertions.assertThat(expectedResult) .usingRecursiveComparison() - .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.fieldErrors\\.messages") + .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages") .isEqualTo(actualResult); } @@ -152,37 +156,44 @@ public void validate_newAccountBean_shouldDetect6Violations() { .statusCode(UNPROCESSABLE_ENTITY_CODE) .extract().as(ValidationResult.class); - List fieldErrors = new ArrayList<>() {{ - add(new FieldError( + List errors = new ArrayList<>() {{ + add(new ValidationResult.Error( + null, + List.of("Passwords should match"), + ValidationResult.ErrorType.GLOBAL) + ); + add(new ValidationResult.Error( "person.firstName", - List.of("must not be empty", "must not be null")) + List.of("must not be empty", "must not be null"), + ValidationResult.ErrorType.FIELD) ); - add(new FieldError( + add(new ValidationResult.Error( "login", - List.of("size must be between 3 and 16")) + List.of("size must be between 3 and 16"), + ValidationResult.ErrorType.FIELD) ); - add(new FieldError( + add(new ValidationResult.Error( "password", - List.of("size must be between 8 and 24")) + List.of("size must be between 8 and 24"), + ValidationResult.ErrorType.FIELD) ); - add(new FieldError( + add(new ValidationResult.Error( "confirmPassword", - List.of("size must be between 8 and 24")) + List.of("size must be between 8 and 24"), + ValidationResult.ErrorType.FIELD) ); }}; - String objectErrors = "Passwords should match"; - Errors errors = new Errors(List.of(objectErrors), fieldErrors); ValidationResult expectedResult = buildResult(errors); Assertions.assertThat(expectedResult) .usingRecursiveComparison() - .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.fieldErrors") - .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.fieldErrors\\.messages") + .ignoringCollectionOrderInFieldsMatchingRegexes("errors") + .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages") .isEqualTo(actualResult); } - private ValidationResult buildResult(Errors errors) { + private ValidationResult buildResult(List errors) { return new ValidationResult(DEFAULT_TITLE, UNPROCESSABLE_ENTITY_CODE, errors); } } diff --git a/modules/pom.xml b/modules/pom.xml index e7e0acbb3a..feb3cfac3f 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -61,7 +61,7 @@ jooby-jakarta-validation - jooby-hbv + jooby-hibernate-validator jooby-pac4j From baf422ebe43222679cac1d292d927acf76c80fe5 Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Sun, 1 Sep 2024 22:29:48 +0300 Subject: [PATCH 10/13] mvc validation revisited --- modules/jooby-apt/pom.xml | 8 +- modules/jooby-hibernate-validator/pom.xml | 74 +++++++++++++- .../ConstraintViolationHandler.java | 3 +- .../validator/HibernateValidatorModule.java | 22 ++++- .../HibernateValidatorModuleTest.java} | 13 +-- .../io/jooby/hibernate/validator/app/App.java | 24 +++++ .../hibernate/validator/app/Controller.java | 32 +++++++ .../validator}/app/NewAccountRequest.java | 2 +- .../validator}/app/PasswordsShouldMatch.java | 2 +- .../app/PasswordsShouldMatchValidator.java | 2 +- .../hibernate/validator}/app/Person.java | 2 +- modules/jooby-jakarta-validation/pom.xml | 96 ------------------- .../java/io/jooby/validation/app/App.java | 52 ---------- modules/jooby-validation/pom.xml | 22 +++++ .../io/jooby/validation/BeanValidator.java | 27 +++--- .../io/jooby/validation/MvcValidator.java | 6 ++ .../io/jooby/validation/ValidationResult.java | 0 .../io/jooby/validation/package-info.java | 0 .../src/main/java/module-info.java | 1 - modules/pom.xml | 2 +- 20 files changed, 213 insertions(+), 177 deletions(-) rename modules/{jooby-jakarta-validation/src/main/java/io/jooby/validation => jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator}/ConstraintViolationHandler.java (97%) rename modules/{jooby-jakarta-validation/src/test/java/io/jooby/validation/BeanValidatorTest.java => jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/HibernateValidatorModuleTest.java} (95%) create mode 100644 modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/App.java create mode 100644 modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/Controller.java rename modules/{jooby-jakarta-validation/src/test/java/io/jooby/validation => jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator}/app/NewAccountRequest.java (96%) rename modules/{jooby-jakarta-validation/src/test/java/io/jooby/validation => jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator}/app/PasswordsShouldMatch.java (93%) rename modules/{jooby-jakarta-validation/src/test/java/io/jooby/validation => jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator}/app/PasswordsShouldMatchValidator.java (92%) rename modules/{jooby-jakarta-validation/src/test/java/io/jooby/validation => jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator}/app/Person.java (93%) delete mode 100644 modules/jooby-jakarta-validation/pom.xml delete mode 100644 modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/App.java create mode 100644 modules/jooby-validation/pom.xml rename modules/{jooby-jakarta-validation => jooby-validation}/src/main/java/io/jooby/validation/BeanValidator.java (50%) create mode 100644 modules/jooby-validation/src/main/java/io/jooby/validation/MvcValidator.java rename modules/{jooby-jakarta-validation => jooby-validation}/src/main/java/io/jooby/validation/ValidationResult.java (100%) rename modules/{jooby-jakarta-validation => jooby-validation}/src/main/java/io/jooby/validation/package-info.java (100%) rename modules/{jooby-jakarta-validation => jooby-validation}/src/main/java/module-info.java (89%) diff --git a/modules/jooby-apt/pom.xml b/modules/jooby-apt/pom.xml index 7592056dbe..41b61d61e2 100644 --- a/modules/jooby-apt/pom.xml +++ b/modules/jooby-apt/pom.xml @@ -28,11 +28,17 @@ io.jooby - jooby-jakarta-validation + jooby-validation ${jooby.version} test + + jakarta.validation + jakarta.validation-api + test + + com.google.testing.compile diff --git a/modules/jooby-hibernate-validator/pom.xml b/modules/jooby-hibernate-validator/pom.xml index 26ba7257ce..61b9bcc358 100644 --- a/modules/jooby-hibernate-validator/pom.xml +++ b/modules/jooby-hibernate-validator/pom.xml @@ -21,7 +21,7 @@ io.jooby - jooby-jakarta-validation + jooby-validation ${jooby.version} @@ -43,5 +43,77 @@ expressly 5.0.0 + + + + io.jooby + jooby-netty + ${jooby.version} + test + + + io.jooby + jooby-jackson + ${jooby.version} + test + + + + org.junit.jupiter + junit-jupiter-api + test + + + + org.junit.jupiter + junit-jupiter-engine + test + + + + io.jooby + jooby-test + ${jooby.version} + test + + + + io.rest-assured + rest-assured + test + + + + org.assertj + assertj-core + 3.26.3 + test +
+ + + + + org.apache.maven.plugins + maven-compiler-plugin + + + test + test-compile + + + + + -parameters + + + + io.jooby + jooby-apt + + + + + + diff --git a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ConstraintViolationHandler.java b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/ConstraintViolationHandler.java similarity index 97% rename from modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ConstraintViolationHandler.java rename to modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/ConstraintViolationHandler.java index ffc38c06fa..ad35352567 100644 --- a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ConstraintViolationHandler.java +++ b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/ConstraintViolationHandler.java @@ -1,9 +1,10 @@ -package io.jooby.validation; +package io.jooby.hibernate.validator; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Context; import io.jooby.ErrorHandler; import io.jooby.StatusCode; +import io.jooby.validation.ValidationResult; import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; diff --git a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java index cc5cc57b7f..32631e1fd7 100644 --- a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java +++ b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java @@ -9,13 +9,15 @@ import io.jooby.Extension; import io.jooby.Jooby; import io.jooby.StatusCode; -import io.jooby.validation.ConstraintViolationHandler; +import io.jooby.validation.MvcValidator; +import jakarta.validation.ConstraintViolation; import jakarta.validation.ConstraintViolationException; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; import org.hibernate.validator.HibernateValidator; import org.hibernate.validator.HibernateValidatorConfiguration; +import java.util.Set; import java.util.function.Consumer; import static jakarta.validation.Validation.byProvider; @@ -87,10 +89,28 @@ public void install(@NonNull Jooby app) { try (ValidatorFactory factory = cfg.buildValidatorFactory()) { Validator validator = factory.getValidator(); app.getServices().put(Validator.class, validator); + app.getServices().put(MvcValidator.class, new MvcValidatorImpl(validator)); if (!disableDefaultViolationHandler) { app.error(ConstraintViolationException.class, new ConstraintViolationHandler(statusCode, title)); } } } + + static class MvcValidatorImpl implements MvcValidator { + + private final Validator validator; + + MvcValidatorImpl(Validator validator) { + this.validator = validator; + } + + @Override + public void validate(Object bean) { + Set> violations = validator.validate(bean); + if (!violations.isEmpty()) { + throw new ConstraintViolationException(violations); + } + } + } } diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/BeanValidatorTest.java b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/HibernateValidatorModuleTest.java similarity index 95% rename from modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/BeanValidatorTest.java rename to modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/HibernateValidatorModuleTest.java index e506a591a9..c9edafe2eb 100644 --- a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/BeanValidatorTest.java +++ b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/HibernateValidatorModuleTest.java @@ -1,9 +1,10 @@ -package io.jooby.validation; +package io.jooby.hibernate.validator; +import io.jooby.hibernate.validator.app.App; +import io.jooby.hibernate.validator.app.NewAccountRequest; +import io.jooby.hibernate.validator.app.Person; import io.jooby.test.JoobyTest; -import io.jooby.validation.app.App; -import io.jooby.validation.app.NewAccountRequest; -import io.jooby.validation.app.Person; +import io.jooby.validation.ValidationResult; import io.restassured.RestAssured; import io.restassured.builder.RequestSpecBuilder; import io.restassured.http.ContentType; @@ -16,11 +17,11 @@ import java.util.Map; import static io.jooby.StatusCode.UNPROCESSABLE_ENTITY_CODE; -import static io.jooby.validation.app.App.DEFAULT_TITLE; +import static io.jooby.hibernate.validator.app.App.DEFAULT_TITLE; import static io.restassured.RestAssured.given; @JoobyTest(value = App.class, port = 8099) -public class BeanValidatorTest { +public class HibernateValidatorModuleTest { protected static RequestSpecification SPEC = new RequestSpecBuilder() .setPort(8099) diff --git a/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/App.java b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/App.java new file mode 100644 index 0000000000..1e17d14972 --- /dev/null +++ b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/App.java @@ -0,0 +1,24 @@ +package io.jooby.hibernate.validator.app; + +import io.jooby.Jooby; +import io.jooby.StatusCode; +import io.jooby.hibernate.validator.ConstraintViolationHandler; +import io.jooby.hibernate.validator.HibernateValidatorModule; +import io.jooby.jackson.JacksonModule; +import jakarta.validation.ConstraintViolationException; + +public class App extends Jooby { + + private static final StatusCode STATUS_CODE = StatusCode.UNPROCESSABLE_ENTITY; + public static final String DEFAULT_TITLE = "Validation failed"; + + { + install(new JacksonModule()); + install(new HibernateValidatorModule()); + + mvc(new Controller()); + + error(ConstraintViolationException.class, new ConstraintViolationHandler(STATUS_CODE, DEFAULT_TITLE)); + } + +} \ No newline at end of file diff --git a/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/Controller.java b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/Controller.java new file mode 100644 index 0000000000..bf6ac84465 --- /dev/null +++ b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/Controller.java @@ -0,0 +1,32 @@ +package io.jooby.hibernate.validator.app; + +import io.jooby.annotation.POST; +import io.jooby.annotation.Path; +import jakarta.validation.Valid; + +import java.util.List; +import java.util.Map; + +@Path("") +public class Controller { + + @POST("/create-person") + public void createPerson(@Valid Person person) { + } + + @POST("/create-array-of-persons") + public void createArrayOfPersons(@Valid Person[] persons) { + } + + @POST("/create-list-of-persons") + public void createListOfPersons(@Valid List persons) { + } + + @POST("/create-map-of-persons") + public void createMapOfPersons(@Valid Map persons) { + } + + @POST("/create-new-account") + public void createNewAccount(@Valid NewAccountRequest request) { + } +} diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/NewAccountRequest.java b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/NewAccountRequest.java similarity index 96% rename from modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/NewAccountRequest.java rename to modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/NewAccountRequest.java index 268b10acd2..e67bcae8e4 100644 --- a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/NewAccountRequest.java +++ b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/NewAccountRequest.java @@ -1,4 +1,4 @@ -package io.jooby.validation.app; +package io.jooby.hibernate.validator.app; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatch.java b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatch.java similarity index 93% rename from modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatch.java rename to modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatch.java index 1c63d31ed3..dcd01c1852 100644 --- a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatch.java +++ b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatch.java @@ -1,4 +1,4 @@ -package io.jooby.validation.app; +package io.jooby.hibernate.validator.app; import jakarta.validation.Constraint; import jakarta.validation.Payload; diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatchValidator.java b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatchValidator.java similarity index 92% rename from modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatchValidator.java rename to modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatchValidator.java index dddcd3325e..e1fbbf0eb4 100644 --- a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/PasswordsShouldMatchValidator.java +++ b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/PasswordsShouldMatchValidator.java @@ -1,4 +1,4 @@ -package io.jooby.validation.app; +package io.jooby.hibernate.validator.app; import jakarta.validation.ConstraintValidator; import jakarta.validation.ConstraintValidatorContext; diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/Person.java b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/Person.java similarity index 93% rename from modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/Person.java rename to modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/Person.java index b84ae36da4..ca849bdbea 100644 --- a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/Person.java +++ b/modules/jooby-hibernate-validator/src/test/java/io/jooby/hibernate/validator/app/Person.java @@ -1,4 +1,4 @@ -package io.jooby.validation.app; +package io.jooby.hibernate.validator.app; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; diff --git a/modules/jooby-jakarta-validation/pom.xml b/modules/jooby-jakarta-validation/pom.xml deleted file mode 100644 index a03024f7aa..0000000000 --- a/modules/jooby-jakarta-validation/pom.xml +++ /dev/null @@ -1,96 +0,0 @@ - - - - - io.jooby - modules - 3.2.10-SNAPSHOT - - - 4.0.0 - jooby-jakarta-validation - - - - io.jooby - jooby - ${jooby.version} - - - - jakarta.validation - jakarta.validation-api - - - - - io.jooby - jooby-netty - ${jooby.version} - test - - - - org.hibernate.validator - hibernate-validator - 8.0.1.Final - test - - - - jakarta.el - jakarta.el-api - 5.0.1 - test - - - - org.glassfish.expressly - expressly - 5.0.0 - test - - - - io.jooby - jooby-jackson - ${jooby.version} - test - - - - org.junit.jupiter - junit-jupiter-api - test - - - - org.junit.jupiter - junit-jupiter-engine - test - - - - io.jooby - jooby-test - ${jooby.version} - test - - - - io.rest-assured - rest-assured - test - - - - org.assertj - assertj-core - 3.26.3 - test - - - - diff --git a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/App.java b/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/App.java deleted file mode 100644 index 2f64661deb..0000000000 --- a/modules/jooby-jakarta-validation/src/test/java/io/jooby/validation/app/App.java +++ /dev/null @@ -1,52 +0,0 @@ -package io.jooby.validation.app; - -import io.jooby.Jooby; -import io.jooby.Reified; -import io.jooby.StatusCode; -import io.jooby.jackson.JacksonModule; -import io.jooby.validation.BeanValidator; -import io.jooby.validation.ConstraintViolationHandler; -import jakarta.validation.ConstraintViolationException; -import jakarta.validation.Validator; -import jakarta.validation.ValidatorFactory; -import org.hibernate.validator.HibernateValidator; -import org.hibernate.validator.HibernateValidatorConfiguration; - -import static jakarta.validation.Validation.byProvider; - -public class App extends Jooby { - - private static final StatusCode STATUS_CODE = StatusCode.UNPROCESSABLE_ENTITY; - public static final String DEFAULT_TITLE = "Validation failed"; - - { - provideValidator(this); - install(new JacksonModule()); - - post("/create-person", ctx -> BeanValidator.validate(ctx, ctx.body(Person.class))); - - post("/create-array-of-persons", ctx -> BeanValidator.validate(ctx, ctx.body(Person[].class))); - - post("/create-list-of-persons", ctx -> { - return BeanValidator.validate(ctx, ctx.body(Reified.list(Person.class).getType())); - }); - - post("/create-map-of-persons", ctx -> { - return BeanValidator.validate(ctx, ctx.body(Reified.map(String.class, Person.class).getType())); - }); - - post("/create-new-account", ctx -> { - return BeanValidator.validate(ctx, ctx.body(NewAccountRequest.class)); - }); - - error(ConstraintViolationException.class, new ConstraintViolationHandler(STATUS_CODE, DEFAULT_TITLE)); - } - - private void provideValidator(Jooby app) { - HibernateValidatorConfiguration cfg = byProvider(HibernateValidator.class).configure(); - try (ValidatorFactory factory = cfg.buildValidatorFactory()) { - Validator validator = factory.getValidator(); - app.getServices().put(Validator.class, validator); - } - } -} \ No newline at end of file diff --git a/modules/jooby-validation/pom.xml b/modules/jooby-validation/pom.xml new file mode 100644 index 0000000000..07e8b280c8 --- /dev/null +++ b/modules/jooby-validation/pom.xml @@ -0,0 +1,22 @@ + + + + + io.jooby + modules + 3.2.10-SNAPSHOT + + + 4.0.0 + jooby-validation + + + + io.jooby + jooby + ${jooby.version} + + + diff --git a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/BeanValidator.java b/modules/jooby-validation/src/main/java/io/jooby/validation/BeanValidator.java similarity index 50% rename from modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/BeanValidator.java rename to modules/jooby-validation/src/main/java/io/jooby/validation/BeanValidator.java index d08208d927..e1a57b7a7d 100644 --- a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/BeanValidator.java +++ b/modules/jooby-validation/src/main/java/io/jooby/validation/BeanValidator.java @@ -6,23 +6,23 @@ package io.jooby.validation; import io.jooby.Context; -import jakarta.validation.ConstraintViolation; -import jakarta.validation.ConstraintViolationException; -import jakarta.validation.Validator; +import io.jooby.SneakyThrows; -import java.util.*; +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; public final class BeanValidator { public static T validate(Context ctx, T bean) { - Validator validator = ctx.require(Validator.class); + MvcValidator validator = ctx.require(MvcValidator.class); if (bean instanceof Collection) { validateCollection(validator, (Collection) bean); } else if (bean.getClass().isArray()) { - validateCollection(validator, Arrays.asList((Object[])bean)); - } else if (bean instanceof Map) { - validateCollection(validator, ((Map)bean).values()); + validateCollection(validator, Arrays.asList((Object[]) bean)); + } else if (bean instanceof Map) { + validateCollection(validator, ((Map) bean).values()); } else { validateObject(validator, bean); } @@ -30,16 +30,17 @@ public static T validate(Context ctx, T bean) { return bean; } - private static void validateCollection(Validator validator, Collection beans) { + private static void validateCollection(MvcValidator validator, Collection beans) { for (Object item : beans) { validateObject(validator, item); } } - private static void validateObject(Validator validator, Object bean) { - Set> violations = validator.validate(bean); - if (!violations.isEmpty()) { - throw new ConstraintViolationException(violations); + private static void validateObject(MvcValidator validator, Object bean) { + try { + validator.validate(bean); + } catch (Throwable e) { + SneakyThrows.propagate(e); } } } diff --git a/modules/jooby-validation/src/main/java/io/jooby/validation/MvcValidator.java b/modules/jooby-validation/src/main/java/io/jooby/validation/MvcValidator.java new file mode 100644 index 0000000000..20c1c633c4 --- /dev/null +++ b/modules/jooby-validation/src/main/java/io/jooby/validation/MvcValidator.java @@ -0,0 +1,6 @@ +package io.jooby.validation; + +public interface MvcValidator { + + void validate(Object bean) throws E; +} diff --git a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ValidationResult.java b/modules/jooby-validation/src/main/java/io/jooby/validation/ValidationResult.java similarity index 100% rename from modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/ValidationResult.java rename to modules/jooby-validation/src/main/java/io/jooby/validation/ValidationResult.java diff --git a/modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/package-info.java b/modules/jooby-validation/src/main/java/io/jooby/validation/package-info.java similarity index 100% rename from modules/jooby-jakarta-validation/src/main/java/io/jooby/validation/package-info.java rename to modules/jooby-validation/src/main/java/io/jooby/validation/package-info.java diff --git a/modules/jooby-jakarta-validation/src/main/java/module-info.java b/modules/jooby-validation/src/main/java/module-info.java similarity index 89% rename from modules/jooby-jakarta-validation/src/main/java/module-info.java rename to modules/jooby-validation/src/main/java/module-info.java index 150ace1e8e..5b51bf4965 100644 --- a/modules/jooby-jakarta-validation/src/main/java/module-info.java +++ b/modules/jooby-validation/src/main/java/module-info.java @@ -9,7 +9,6 @@ module io.jooby.validation { exports io.jooby.validation; - requires jakarta.validation; requires io.jooby; requires com.github.spotbugs.annotations; } diff --git a/modules/pom.xml b/modules/pom.xml index feb3cfac3f..2cebb44c90 100644 --- a/modules/pom.xml +++ b/modules/pom.xml @@ -60,7 +60,7 @@ jooby-camel - jooby-jakarta-validation + jooby-validation jooby-hibernate-validator jooby-pac4j From c23d4b0cb0c92206d6a4eb73f97323cccc85d099 Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Mon, 2 Sep 2024 21:03:07 +0300 Subject: [PATCH 11/13] javadoc, support configuration over hocon config --- .../java/io/jooby/apt/JoobyProcessor.java | 2 +- .../validator/HibernateValidatorModule.java | 48 ++++++++++++++++--- .../io/jooby/validation/BeanValidator.java | 12 +++-- .../io/jooby/validation/MvcValidator.java | 15 +++++- 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java b/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java index 1d32245042..2d535bd8e0 100644 --- a/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java +++ b/modules/jooby-apt/src/main/java/io/jooby/apt/JoobyProcessor.java @@ -385,7 +385,7 @@ private void verifyBeanValidationDependency(Collection routers) { "Unable to load 'BeanValidator' class. " + "Bean validation usage (@Valid) was detected, but the appropriate dependency is missing. " + "Please ensure that you have added the corresponding validation dependency " + - "(e.g., jooby-hbv)."); + "(e.g., jooby-hibernate-validator)."); } } } diff --git a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java index 32631e1fd7..970191425b 100644 --- a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java +++ b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/HibernateValidatorModule.java @@ -5,6 +5,7 @@ */ package io.jooby.hibernate.validator; +import com.typesafe.config.Config; import edu.umd.cs.findbugs.annotations.NonNull; import io.jooby.Extension; import io.jooby.Jooby; @@ -22,8 +23,36 @@ import static jakarta.validation.Validation.byProvider; +/** + * Hibernate Validator Module: https://jooby.io/modules/hibernate-validator. + * + *
{@code
+ * {
+ *   install(new HibernateValidatorModule());
+ *
+ * }
+ *
+ * public class Controller {
+ *
+ *   @POST("/create")
+ *   public void create(@Valid Bean bean) {
+ *   }
+ *
+ * }
+ * }
+ * + *

Supports validation of a single bean, list, array, or map.

+ * + *

The module also provides a built-in error handler that catches {@link ConstraintViolationException} + * and transforms it into a {@link io.jooby.validation.ValidationResult}

+ * + * @author kliushnichenko + * @since 3.2.10 + */ public class HibernateValidatorModule implements Extension { + private static final String CONFIG_ROOT_PATH = "hibernate.validator"; + private Consumer configurer; private StatusCode statusCode = StatusCode.UNPROCESSABLE_ENTITY; private String title = "Validation failed"; @@ -79,14 +108,21 @@ public HibernateValidatorModule disableViolationHandler() { } @Override - public void install(@NonNull Jooby app) { - HibernateValidatorConfiguration cfg = byProvider(HibernateValidator.class).configure(); + public void install(@NonNull Jooby app) throws Exception { + Config config = app.getConfig(); + HibernateValidatorConfiguration 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())); + } if (configurer != null) { - configurer.accept(cfg); + configurer.accept(hbvConfig); } - try (ValidatorFactory factory = cfg.buildValidatorFactory()) { + try (ValidatorFactory factory = hbvConfig.buildValidatorFactory()) { Validator validator = factory.getValidator(); app.getServices().put(Validator.class, validator); app.getServices().put(MvcValidator.class, new MvcValidatorImpl(validator)); @@ -97,7 +133,7 @@ public void install(@NonNull Jooby app) { } } - static class MvcValidatorImpl implements MvcValidator { + static class MvcValidatorImpl implements MvcValidator { private final Validator validator; @@ -106,7 +142,7 @@ static class MvcValidatorImpl implements MvcValidator> violations = validator.validate(bean); if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); diff --git a/modules/jooby-validation/src/main/java/io/jooby/validation/BeanValidator.java b/modules/jooby-validation/src/main/java/io/jooby/validation/BeanValidator.java index e1a57b7a7d..d445ff20d0 100644 --- a/modules/jooby-validation/src/main/java/io/jooby/validation/BeanValidator.java +++ b/modules/jooby-validation/src/main/java/io/jooby/validation/BeanValidator.java @@ -12,10 +12,16 @@ import java.util.Collection; import java.util.Map; +/** + * This class is a helper that provides a single entry point for bean validation without being strictly tied + * to a specific bean validation implementation. Initially, it is utilized in `jooby-apt` for MVC bean validation, + * but it can also be used independently in the scripting API. + * It relies on an implementation of {@link MvcValidator} that should be available in the service registry + */ public final class BeanValidator { public static T validate(Context ctx, T bean) { - MvcValidator validator = ctx.require(MvcValidator.class); + MvcValidator validator = ctx.require(MvcValidator.class); if (bean instanceof Collection) { validateCollection(validator, (Collection) bean); @@ -30,13 +36,13 @@ public static T validate(Context ctx, T bean) { return bean; } - private static void validateCollection(MvcValidator validator, Collection beans) { + private static void validateCollection(MvcValidator validator, Collection beans) { for (Object item : beans) { validateObject(validator, item); } } - private static void validateObject(MvcValidator validator, Object bean) { + private static void validateObject(MvcValidator validator, Object bean) { try { validator.validate(bean); } catch (Throwable e) { diff --git a/modules/jooby-validation/src/main/java/io/jooby/validation/MvcValidator.java b/modules/jooby-validation/src/main/java/io/jooby/validation/MvcValidator.java index 20c1c633c4..d17b7b6eb7 100644 --- a/modules/jooby-validation/src/main/java/io/jooby/validation/MvcValidator.java +++ b/modules/jooby-validation/src/main/java/io/jooby/validation/MvcValidator.java @@ -1,6 +1,17 @@ package io.jooby.validation; -public interface MvcValidator { +/** + * This interface should be implemented by modules that provide bean validation functionality. + * An instance of this interface must be registered in the Jooby service registry. + * Doing so will enable bean validation for MVC routes. + * For an example implementation, refer to the HibernateValidatorModule. + */ +public interface MvcValidator { - void validate(Object bean) throws E; + /** + * Method should validate the bean and throw an exception if any constraint violations are detected + * @param bean bean to be validated + * @throws RuntimeException an exception with violations to be thrown (e.g. ConstraintViolationException) + */ + void validate(Object bean) throws RuntimeException; } From 8b7930851239c390b085b357637c11a8ae139bcb Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Thu, 5 Sep 2024 15:39:16 +0300 Subject: [PATCH 12/13] hibernate-validator module ascii doc --- .../asciidoc/modules/hibernate-validator.adoc | 367 ++++++++++++++++++ docs/asciidoc/modules/modules.adoc | 3 + modules/jooby-bom/pom.xml | 5 + 3 files changed, 375 insertions(+) create mode 100644 docs/asciidoc/modules/hibernate-validator.adoc diff --git a/docs/asciidoc/modules/hibernate-validator.adoc b/docs/asciidoc/modules/hibernate-validator.adoc new file mode 100644 index 0000000000..aaf6000bd6 --- /dev/null +++ b/docs/asciidoc/modules/hibernate-validator.adoc @@ -0,0 +1,367 @@ +== Hibernate Validator + +Bean validation via https://hibernate.org/validator/[Hibernate Validator]. + +=== Usage + +1) Add the dependency: + +[dependency, artifactId="jooby-hibernate-validator"] +. + +2) Install + +.Java +[source, java, role="primary"] +---- +import io.jooby.hibernate.validator.HibernateValidatorModule; + +{ + install(new HibernateValidatorModule()); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.hibernate.validator.HibernateValidatorModule + +{ + install(new HibernateValidatorModule()) +} +---- + +3) Usage in MVC routes + +.Java +[source,java,role="primary"] +---- +import io.jooby.annotation.*; +import jakarta.validation.Valid; + +@Path("/mvc") +public class Controller { + + @POST("/validate-body") + public void validateBody(@Valid Bean bean) { // <1> + ... + } + + @POST("/validate-query") + public void validateQuery(@Valid @QueryParam Bean bean) { // <2> + ... + } + + @POST("/validate-list") + public void validateList(@Valid List beans) { // <3> + ... + } + + @POST("/validate-map") + public void validateMap(@Valid Map beans) { // <4> + ... + } +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.annotation.*; +import jakarta.validation.Valid + +@Path("/mvc") +class Controller { + + @POST("/validate-body") + fun validateBody(@Valid bean: Bean) : Unit { // <1> + ... + } + + @POST("/validate-query") + fun validateQuery(@Valid @QueryParam bean: Bean) : Unit { // <2> + ... + } + + @POST("/validate-list") + fun validateList(@Valid beans: List) : Unit { // <3> + ... + } + + @POST("/validate-map") + fun validateMap(@Valid beans: Map) : Unit { // <4> + ... + } +} +---- + +<1> Validate a bean decoded from the request body +<2> Validate a bean parsed from query parameters. This works the same for `@FormParam` or `@BindParam` +<3> Validate a list of beans. This also applies to arrays `@Valid Bean[] beans` +<4> Validate a map of beans + +4) Usage in in script/lambda routes + +Jooby doesn't provide fully native bean validation in script/lambda at the moment, +but you can use a helper that we utilize under the hood in MVC routes: + +.Java +[source, java, role="primary"] +---- +import io.jooby.validation.BeanValidator; + +{ + post("/validate", ctx -> { + Bean bean = BeanValidator.validate(ctx, ctx.body(Bean.class)); + ... + }); +} +---- + +.Kotlin +[source, kt, role="secondary"] +---- +import io.jooby.validation.BeanValidator + +{ + post("/validate") { + val bean = BeanValidator.validate(ctx, ctx.body(Bean.class)) + ... + } +} +---- + +`BeanValidator.validate()` behaves identically to validation in MVC routes. +It also supports validating list, array, and map of beans + +=== Constraint Violations Rendering + +`HibernateValidatorModule` provides default built-in error handler that +catches `ConstraintViolationException` and transforms it into the following response: + +.JSON: +---- +{ + "title": "Validation failed", + "status": 422, + "errors": [ + { + "field": "firstName", + "messages": [ + "must not be empty", + "must not be null" + ], + "type": "FIELD" + }, + { + "field": null, + "messages": [ + "passwords are not the same" + ], + "type": "GLOBAL" + } + ] +} +---- + +It is possible to override the `title` and `status` code of the response above: + +[source, java] +---- + +{ + install(new JacksonModule()); + install(new HibernateValidatorModule() + .statusCode(StatusCode.BAD_REQUEST) + .validationTitle("Incorrect input data") + ); +} +---- + +If the default error handler doesn't fully meet your needs, you can always disable it and provide your own: + +[source, java] +---- + +{ + install(new JacksonModule()); + install(new HibernateValidatorModule().disableViolationHandler()); + + error(ConstraintViolationException.class, new MyConstraintViolationHandler()); +} +---- + +=== Manual Validation + +The module exposes `Validator` as a service, allowing you to run validation manually at any time. + +==== Script/lambda: + +[source, java] +---- +import jakarta.validation.Validator; + +{ + post("/validate", ctx -> { + Validator validator = require(Validator.class); + Set> violations = validator.validate(ctx.body(Bean.class)); + if (!violations.isEmpty()) { + ... + } + ... + }); +} +---- + +==== MVC routes with dependency injection: + +1) Install DI framework at first. + +[source, java] +---- +import io.jooby.hibernate.validator.HibernateValidatorModule; + +{ + install(new GuiceModule()); // <1> + install(new HibernateValidatorModule()); +} +---- + +<1> `Guice` is just an example, you can achieve the same with `Avaje` or `Dagger` + +2) Inject `Validator` in controller, service etc. + +[source, java] +---- +import jakarta.validation.Validator; +import jakarta.inject.Inject; + +@Path("/mvc") +public class Controller { + + private final Validator validator; + + @Inject + public Controller(Validator validator) { + this.validator = validator; + } + + @POST("/validate") + public void validate(Bean bean) { + Set> violations = validator.validate(bean); + ... + } +} +---- + +=== Business rules validation + +As you know, `Hibernate Validator` allows you to build fully custom `ConstraintValidator`. +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`: + +[source, java] +---- +public class MyConstraintValidatorFactory implements ConstraintValidatorFactory { + + private final Function, ?> require; + private final ConstraintValidatorFactory defaultFactory; + + public MyConstraintValidatorFactory(Function, ?> require) { + this.require = require; + try (ValidatorFactory factory = Validation.byDefaultProvider() + .configure().buildValidatorFactory()) { + this.defaultFactory = factory.getConstraintValidatorFactory(); + } + } + + @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"); + } +} +---- + +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 + private final MyService myService; + + @Inject + public MyCustomValidator(MyService myService) { + this.myService = myService; + } + + @Override + public boolean isValid(Bean bean, ConstraintValidatorContext context) { + // Use the injected service for validation logic + return myService.isValid(bean); + } +} +---- + +=== Configuration +Any property defined at `hibernate.validator` will be added automatically: + +.application.conf +[source, properties] +---- +hibernate.validator.fail_fast = true +---- + +Or programmatically: + +[source, java] +---- +import io.jooby.hibernate.validator.HibernateValidatorModule; + +{ + install(new HibernateValidatorModule().doWith(cfg -> { + cfg.failFast(true); + })); +} +---- \ No newline at end of file diff --git a/docs/asciidoc/modules/modules.adoc b/docs/asciidoc/modules/modules.adoc index 24f220e2bf..7b5df486b7 100644 --- a/docs/asciidoc/modules/modules.adoc +++ b/docs/asciidoc/modules/modules.adoc @@ -26,6 +26,9 @@ Available modules are listed next. * link:/modules/kafka[Kafka]: Kafka module. * link:/modules/redis[Redis]: Redis module. +=== Validation + * link:/modules/hibernate-validator[Hibernate Validator]: Hibernate Validator module. + === Development Tools * link:#hot-reload[Jooby Run]: Run and hot reload your application. * link:/modules/node[Node]: Download, Install and Run node locally. diff --git a/modules/jooby-bom/pom.xml b/modules/jooby-bom/pom.xml index 484646dcfe..24d28c1934 100644 --- a/modules/jooby-bom/pom.xml +++ b/modules/jooby-bom/pom.xml @@ -124,6 +124,11 @@ jooby-hibernate ${project.version}
+ + io.jooby + jooby-hibernate-validator + ${project.version} + io.jooby jooby-hikari From 4d73e3168ccd7143db167e200694e38c7d777a1c Mon Sep 17 00:00:00 2001 From: Volodymyr Kliushnichenko Date: Thu, 5 Sep 2024 18:20:53 +0300 Subject: [PATCH 13/13] update version to 3.3.1-SNAPSHOT --- modules/jooby-hibernate-validator/pom.xml | 2 +- modules/jooby-validation/pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/jooby-hibernate-validator/pom.xml b/modules/jooby-hibernate-validator/pom.xml index 61b9bcc358..70f4d2ff4e 100644 --- a/modules/jooby-hibernate-validator/pom.xml +++ b/modules/jooby-hibernate-validator/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 3.2.10-SNAPSHOT + 3.3.1-SNAPSHOT 4.0.0 diff --git a/modules/jooby-validation/pom.xml b/modules/jooby-validation/pom.xml index 07e8b280c8..0f1617badb 100644 --- a/modules/jooby-validation/pom.xml +++ b/modules/jooby-validation/pom.xml @@ -6,7 +6,7 @@ io.jooby modules - 3.2.10-SNAPSHOT + 3.3.1-SNAPSHOT 4.0.0