> violations = validator.validate(bean);
+ ...
+ }
+}
+----
+
+=== Configuration
+Any property defined at `validation` will be added automatically:
+
+.application.conf
+[source, properties]
+----
+validation.fail_fast = true
+----
+
+Or programmatically:
+
+[source, java]
+----
+import io.jooby.avaje.validator.AvajeValidatorModule;
+
+{
+ install(new AvajeValidatorModule().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 98537fc089..5a957840bf 100644
--- a/docs/asciidoc/modules/modules.adoc
+++ b/docs/asciidoc/modules/modules.adoc
@@ -27,6 +27,7 @@ Available modules are listed next.
* link:/modules/redis[Redis]: Redis module.
=== Validation
+ * link:/modules/avaje-validator[Avaje Validator]: Avaje Validator module.
* link:/modules/hibernate-validator[Hibernate Validator]: Hibernate Validator module.
=== Development Tools
diff --git a/modules/jooby-avaje-inject/pom.xml b/modules/jooby-avaje-inject/pom.xml
index 5da897dfcf..30f4e6e877 100644
--- a/modules/jooby-avaje-inject/pom.xml
+++ b/modules/jooby-avaje-inject/pom.xml
@@ -11,7 +11,11 @@
4.0.0
jooby-avaje-inject
-
+
+
+ full
+
+
com.github.spotbugs
@@ -21,7 +25,6 @@
io.jooby
jooby
- ${jooby.version}
@@ -33,7 +36,7 @@
io.avaje
avaje-inject-generator
- provided
+ test
@@ -74,30 +77,4 @@
test
-
-
-
-
- org.apache.maven.plugins
- maven-compiler-plugin
-
-
- test
- test-compile
-
-
-
-
- -parameters
-
-
-
- io.avaje
- avaje-inject-generator
-
-
-
-
-
-
diff --git a/modules/jooby-avaje-validator/pom.xml b/modules/jooby-avaje-validator/pom.xml
new file mode 100644
index 0000000000..ae0ee8095e
--- /dev/null
+++ b/modules/jooby-avaje-validator/pom.xml
@@ -0,0 +1,117 @@
+
+
+
+
+ io.jooby
+ modules
+ 3.3.1-SNAPSHOT
+
+
+ 4.0.0
+ jooby-avaje-validator
+
+
+ full
+
+
+
+
+ io.jooby
+ jooby
+
+
+
+ io.jooby
+ jooby-validation
+ ${project.version}
+
+
+
+
+ io.avaje
+ avaje-validator
+
+
+
+ jakarta.validation
+ jakarta.validation-api
+
+
+
+
+ io.jooby
+ jooby-netty
+ test
+
+
+
+ io.jooby
+ jooby-apt
+ test
+
+
+ io.avaje
+ avaje-validator-generator
+ test
+
+
+
+ io.jooby
+ jooby-jackson
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ test
+
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+ io.jooby
+ jooby-test
+ 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
+
+
+
+
+
+
diff --git a/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java
new file mode 100644
index 0000000000..bdc50beae5
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/AvajeValidatorModule.java
@@ -0,0 +1,177 @@
+/*
+ * Jooby https://jooby.io
+ * Apache License Version 2.0 https://jooby.io/LICENSE.txt
+ * Copyright 2014 Edgar Espina
+ */
+package io.jooby.avaje.validator;
+
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+import java.util.Optional;
+import java.util.function.Consumer;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import io.avaje.validation.ConstraintViolationException;
+import io.avaje.validation.Validator;
+import io.jooby.Context;
+import io.jooby.Extension;
+import io.jooby.Jooby;
+import io.jooby.StatusCode;
+import io.jooby.validation.MvcValidator;
+
+/**
+ * Avaje Validator Module: https://jooby.io/modules/avaje-validator.
+ *
+ * {@code
+ * {
+ * install(new AvajeValidatorModule());
+ *
+ * }
+ *
+ * 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}
+ *
+ * @authors kliushnichenko, SentryMan
+ * @since 3.3.1
+ */
+public class AvajeValidatorModule implements Extension {
+
+ private Consumer configurer;
+ private StatusCode statusCode = StatusCode.UNPROCESSABLE_ENTITY;
+ private String title = "Validation failed";
+ private boolean disableDefaultViolationHandler = false;
+
+ /**
+ * Setups a configurer callback.
+ *
+ * @param configurer Configurer callback.
+ * @return This module.
+ */
+ public AvajeValidatorModule 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 AvajeValidatorModule 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 AvajeValidatorModule validationTitle(@NonNull String title) {
+ this.title = title;
+ return this;
+ }
+
+ /**
+ * Disables default constraint violation handler. By default {@link AvajeValidatorModule} 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 AvajeValidatorModule disableViolationHandler() {
+ this.disableDefaultViolationHandler = true;
+ return this;
+ }
+
+ @Override
+ public void install(@NonNull Jooby app) throws Exception {
+
+ var props = app.getEnvironment();
+
+ final var locales = new ArrayList();
+ final var builder = Validator.builder();
+ Optional.ofNullable(props.getProperty("validation.failFast", "false"))
+ .map(Boolean::valueOf)
+ .ifPresent(builder::failFast);
+
+ Optional.ofNullable(props.getProperty("validation.resourcebundle.names"))
+ .map(s -> s.split(","))
+ .ifPresent(builder::addResourceBundles);
+
+ Optional.ofNullable(props.getProperty("validation.locale.default"))
+ .map(Locale::forLanguageTag)
+ .ifPresent(
+ l -> {
+ builder.setDefaultLocale(l);
+ locales.add(l);
+ });
+
+ Optional.ofNullable(props.getProperty("validation.locale.addedLocales")).stream()
+ .flatMap(s -> Arrays.stream(s.split(",")))
+ .map(Locale::forLanguageTag)
+ .forEach(
+ l -> {
+ builder.addLocales(l);
+ locales.add(l);
+ });
+
+ Optional.ofNullable(props.getProperty("validation.temporal.tolerance.value"))
+ .map(Long::valueOf)
+ .ifPresent(
+ duration -> {
+ final var unit =
+ Optional.ofNullable(props.getProperty("validation.temporal.tolerance.chronoUnit"))
+ .map(ChronoUnit::valueOf)
+ .orElse(ChronoUnit.MILLIS);
+ builder.temporalTolerance(Duration.of(duration, unit));
+ });
+
+ if (configurer != null) {
+ configurer.accept(builder);
+ }
+
+ Validator validator = builder.build();
+ 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(Context ctx, Object bean) throws ConstraintViolationException {
+ validator.validate(bean, ctx.locale());
+ }
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/ConstraintViolationHandler.java b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/ConstraintViolationHandler.java
new file mode 100644
index 0000000000..76181c7343
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/ConstraintViolationHandler.java
@@ -0,0 +1,95 @@
+package io.jooby.avaje.validator;
+
+import static io.jooby.validation.ValidationResult.ErrorType.FIELD;
+import static io.jooby.validation.ValidationResult.ErrorType.GLOBAL;
+import static java.util.stream.Collectors.groupingBy;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import edu.umd.cs.findbugs.annotations.NonNull;
+import io.avaje.validation.ConstraintViolation;
+import io.avaje.validation.ConstraintViolationException;
+import io.jooby.Context;
+import io.jooby.ErrorHandler;
+import io.jooby.StatusCode;
+import io.jooby.validation.ValidationResult;
+
+/**
+ * Catches and transform {@link ConstraintViolationException} into {@link ValidationResult}
+ *
+ * Payload example:
+ *
+ *
{@code
+ * {
+ * "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 {
+
+ 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) {
+ var ex = (ConstraintViolationException) cause;
+
+ var violations = ex.violations();
+
+ Map> groupedByPath =
+ violations.stream().collect(groupingBy(violation -> violation.path().toString()));
+
+ List errors = collectErrors(groupedByPath);
+
+ ValidationResult result = new ValidationResult(title, statusCode.value(), errors);
+ ctx.setResponseCode(statusCode).render(result);
+ }
+
+ private List collectErrors(
+ Map> groupedViolations) {
+ List errors = new ArrayList<>();
+ for (Map.Entry> entry : groupedViolations.entrySet()) {
+ 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 errors;
+ }
+
+ private List extractMessages(List violations) {
+ return violations.stream().map(ConstraintViolation::message).toList();
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/package-info.java b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/package-info.java
new file mode 100644
index 0000000000..aaf68dc3bb
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/main/java/io/jooby/avaje/validator/package-info.java
@@ -0,0 +1 @@
+package io.jooby.avaje.validator;
diff --git a/modules/jooby-avaje-validator/src/main/java/module-info.java b/modules/jooby-avaje-validator/src/main/java/module-info.java
new file mode 100644
index 0000000000..d6ecdf13b8
--- /dev/null
+++ b/modules/jooby-avaje-validator/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
+ */
+/**
+ * Avaje Validator Module.
+ */
+module io.jooby.avaje.validator {
+ exports io.jooby.avaje.validator;
+
+ requires transitive io.jooby;
+ requires static com.github.spotbugs.annotations;
+ requires typesafe.config;
+ requires transitive io.avaje.validation;
+ requires transitive io.jooby.validation;
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/AvajeValidatorModuleTest.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/AvajeValidatorModuleTest.java
new file mode 100644
index 0000000000..f1c9e74e6c
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/AvajeValidatorModuleTest.java
@@ -0,0 +1,197 @@
+package io.jooby.avaje.validator;
+
+import io.jooby.avaje.validator.app.App;
+import io.jooby.avaje.validator.app.NewAccountRequest;
+import io.jooby.avaje.validator.app.Person;
+import io.jooby.test.JoobyTest;
+import io.jooby.validation.ValidationResult;
+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.avaje.validator.app.App.DEFAULT_TITLE;
+import static io.restassured.RestAssured.given;
+
+@JoobyTest(value = App.class, port = 8099)
+public class AvajeValidatorModuleTest {
+
+ 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 ValidationResult.Error(
+ "firstName", List.of("must not be empty"), ValidationResult.ErrorType.FIELD);
+ ValidationResult expectedResult = buildResult(List.of(fieldError));
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.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 ValidationResult.Error(
+ "firstName", List.of("must not be empty"), ValidationResult.ErrorType.FIELD);
+ ValidationResult expectedResult = buildResult(List.of(fieldError));
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.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 ValidationResult.Error(
+ "firstName", List.of("must not be empty"), ValidationResult.ErrorType.FIELD);
+ ValidationResult expectedResult = buildResult(List.of(fieldError));
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.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 ValidationResult.Error(
+ "firstName", List.of("must not be empty"), ValidationResult.ErrorType.FIELD);
+ ValidationResult expectedResult = buildResult(List.of(fieldError));
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.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 errors =
+ List.of(
+ new ValidationResult.Error(
+ "password",
+ List.of("length must be between 8 and 24"),
+ ValidationResult.ErrorType.FIELD),
+ new ValidationResult.Error(
+ "person.firstName", List.of("must not be empty"), ValidationResult.ErrorType.FIELD),
+ new ValidationResult.Error(
+ "confirmPassword",
+ List.of("length must be between 8 and 24"),
+ ValidationResult.ErrorType.FIELD),
+ new ValidationResult.Error(
+ "login",
+ List.of("length must be between 3 and 16"),
+ ValidationResult.ErrorType.FIELD));
+
+ ValidationResult expectedResult = buildResult(errors);
+
+ Assertions.assertThat(expectedResult)
+ .usingRecursiveComparison()
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors")
+ .ignoringCollectionOrderInFieldsMatchingRegexes("errors\\.messages")
+ .isEqualTo(actualResult);
+ }
+
+ private ValidationResult buildResult(List errors) {
+ return new ValidationResult(DEFAULT_TITLE, UNPROCESSABLE_ENTITY_CODE, errors);
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/App.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/App.java
new file mode 100644
index 0000000000..885feaabfc
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/App.java
@@ -0,0 +1,25 @@
+package io.jooby.avaje.validator.app;
+
+import io.jooby.Jooby;
+import io.jooby.StatusCode;
+import io.jooby.avaje.validator.AvajeValidatorModule;
+import io.jooby.avaje.validator.ConstraintViolationHandler;
+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 AvajeValidatorModule());
+
+ mvc(new Controller());
+
+ error(
+ ConstraintViolationException.class,
+ new ConstraintViolationHandler(STATUS_CODE, DEFAULT_TITLE));
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Controller.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Controller.java
new file mode 100644
index 0000000000..1adfb5ae11
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Controller.java
@@ -0,0 +1,27 @@
+package io.jooby.avaje.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-avaje-validator/src/test/java/io/jooby/avaje/validator/app/NewAccountRequest.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/NewAccountRequest.java
new file mode 100644
index 0000000000..129618e7ce
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/NewAccountRequest.java
@@ -0,0 +1,59 @@
+package io.jooby.avaje.validator.app;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+
+@Valid
+@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-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatch.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatch.java
new file mode 100644
index 0000000000..ae11aea568
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatch.java
@@ -0,0 +1,20 @@
+package io.jooby.avaje.validator.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 = {})
+@Target({TYPE, ANNOTATION_TYPE})
+@Retention(RUNTIME)
+public @interface PasswordsShouldMatch {
+ String message() default "Passwords should match";
+
+ Class>[] groups() default {};
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatchValidator.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatchValidator.java
new file mode 100644
index 0000000000..073c025057
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/PasswordsShouldMatchValidator.java
@@ -0,0 +1,21 @@
+package io.jooby.avaje.validator.app;
+
+import io.avaje.validation.adapter.AbstractConstraintAdapter;
+import io.avaje.validation.adapter.ConstraintAdapter;
+import io.avaje.validation.adapter.ValidationContext.AdapterCreateRequest;
+
+@ConstraintAdapter(PasswordsShouldMatch.class)
+public class PasswordsShouldMatchValidator extends AbstractConstraintAdapter {
+
+ public PasswordsShouldMatchValidator(AdapterCreateRequest request) {
+ super(request);
+ }
+
+ @Override
+ public boolean isValid(NewAccountRequest request) {
+ if (request.getPassword() == null || request.getConfirmPassword() == null) {
+ return false;
+ }
+ return request.getPassword().equals(request.getConfirmPassword());
+ }
+}
diff --git a/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Person.java b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Person.java
new file mode 100644
index 0000000000..0687bc0ac1
--- /dev/null
+++ b/modules/jooby-avaje-validator/src/test/java/io/jooby/avaje/validator/app/Person.java
@@ -0,0 +1,33 @@
+package io.jooby.avaje.validator.app;
+
+import jakarta.validation.Valid;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+
+@Valid
+public class Person {
+
+ @NotEmpty 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-hibernate-validator/src/main/java/io/jooby/hibernate/validator/ConstraintViolationHandler.java b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/ConstraintViolationHandler.java
index ad35352567..dcc5f16bbc 100644
--- a/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/ConstraintViolationHandler.java
+++ b/modules/jooby-hibernate-validator/src/main/java/io/jooby/hibernate/validator/ConstraintViolationHandler.java
@@ -46,7 +46,7 @@
* }
*
* @author kliushnichenko
- * @since 3.2.10
+ * @since 3.3.1
*/
public class ConstraintViolationHandler implements ErrorHandler {
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 970191425b..5f7a3afe95 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
@@ -7,6 +7,7 @@
import com.typesafe.config.Config;
import edu.umd.cs.findbugs.annotations.NonNull;
+import io.jooby.Context;
import io.jooby.Extension;
import io.jooby.Jooby;
import io.jooby.StatusCode;
@@ -47,7 +48,7 @@
* and transforms it into a {@link io.jooby.validation.ValidationResult}
*
* @author kliushnichenko
- * @since 3.2.10
+ * @since 3.3.1
*/
public class HibernateValidatorModule implements Extension {
@@ -142,7 +143,7 @@ static class MvcValidatorImpl implements MvcValidator {
}
@Override
- public void validate(Object bean) throws ConstraintViolationException {
+ public void validate(Context ctx, Object bean) throws ConstraintViolationException {
Set> 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 d445ff20d0..4d81969f9d 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
@@ -20,33 +20,27 @@
*/
public final class BeanValidator {
+ private BeanValidator() {}
+
public static T validate(Context ctx, T bean) {
MvcValidator validator = ctx.require(MvcValidator.class);
if (bean instanceof Collection>) {
- validateCollection(validator, (Collection>) bean);
+ validateCollection(validator, ctx, (Collection>) bean);
} else if (bean.getClass().isArray()) {
- validateCollection(validator, Arrays.asList((Object[]) bean));
+ validateCollection(validator, ctx, Arrays.asList((Object[]) bean));
} else if (bean instanceof Map, ?>) {
- validateCollection(validator, ((Map, ?>) bean).values());
+ validateCollection(validator, ctx, ((Map, ?>) bean).values());
} else {
- validateObject(validator, bean);
+ validator.validate(ctx, bean);
}
return bean;
}
- private static void validateCollection(MvcValidator validator, Collection> beans) {
- for (Object item : beans) {
- validateObject(validator, item);
- }
- }
-
- private static void validateObject(MvcValidator validator, Object bean) {
- try {
- validator.validate(bean);
- } catch (Throwable e) {
- SneakyThrows.propagate(e);
+ private static void validateCollection(MvcValidator validator, Context ctx, Collection> beans) {
+ for (var item : beans) {
+ validator.validate(ctx, item);
}
}
}
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 d17b7b6eb7..7fcca35fbb 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,5 +1,7 @@
package io.jooby.validation;
+import io.jooby.Context;
+
/**
* 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.
@@ -11,7 +13,8 @@ public interface MvcValidator {
/**
* Method should validate the bean and throw an exception if any constraint violations are detected
* @param bean bean to be validated
+ * @param ctx request context
* @throws RuntimeException an exception with violations to be thrown (e.g. ConstraintViolationException)
*/
- void validate(Object bean) throws RuntimeException;
+ void validate(Context ctx, Object bean) throws RuntimeException;
}
diff --git a/modules/pom.xml b/modules/pom.xml
index 3369be6e33..d18d100347 100644
--- a/modules/pom.xml
+++ b/modules/pom.xml
@@ -60,6 +60,7 @@
jooby-validation
+ jooby-avaje-validator
jooby-hibernate-validator
jooby-pac4j
diff --git a/pom.xml b/pom.xml
index 870dd9dd6a..18ffc63809 100644
--- a/pom.xml
+++ b/pom.xml
@@ -25,7 +25,6 @@
3.2.2
2.17.2
2.11.0
- 2.1
3.0.1
3.0.4
1.4.0
@@ -47,7 +46,6 @@
1.4.3
- 10.3
7.0.0
@@ -80,6 +78,11 @@
2.5.2
+
+ 10.3
+ 2.1
+ 2.1
+
2.0.1
3.1.0
@@ -346,6 +349,12 @@
${jooby.version}
+
+ io.jooby
+ jooby-avaje-validator
+ ${jooby.version}
+
+
io.jooby
jooby-hibernate
@@ -673,26 +682,39 @@
io.avaje
avaje-inject
- ${avaje-inject.version}
+ ${avaje.inject.version}
io.avaje
avaje-inject-generator
- ${avaje-inject.version}
+ ${avaje.inject.version}
io.avaje
avaje-jsonb
- ${avaje-jsonb.version}
+ ${avaje.jsonb.version}
io.avaje
avaje-jsonb-generator
- ${avaje-jsonb.version}
+ ${avaje.jsonb.version}
+
+
+
+
+ io.avaje
+ avaje-validator
+ ${avaje.validator.version}
+
+
+
+ io.avaje
+ avaje-validator-generator
+ ${avaje.validator.version}