diff --git a/blackbox-test/src/test/java/example/avaje/notblank/ANotBlank.java b/blackbox-test/src/test/java/example/avaje/notblank/ANotBlank.java new file mode 100644 index 00000000..41b330c7 --- /dev/null +++ b/blackbox-test/src/test/java/example/avaje/notblank/ANotBlank.java @@ -0,0 +1,16 @@ +package example.avaje.notblank; + +import io.avaje.validation.constraints.NotBlank; +import jakarta.validation.Valid; + +@Valid +public record ANotBlank( + @NotBlank + String basic, + @NotBlank(max = 4) + String withMax, + + @NotBlank(max = 4, message = "NotBlank n max 4") + String withCustom +) { +} diff --git a/blackbox-test/src/test/java/example/avaje/notblank/ANotBlankTest.java b/blackbox-test/src/test/java/example/avaje/notblank/ANotBlankTest.java new file mode 100644 index 00000000..3bb2b087 --- /dev/null +++ b/blackbox-test/src/test/java/example/avaje/notblank/ANotBlankTest.java @@ -0,0 +1,89 @@ +package example.avaje.notblank; + +import io.avaje.validation.ConstraintViolation; +import io.avaje.validation.ConstraintViolationException; +import io.avaje.validation.Validator; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Locale; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +class ANotBlankTest { + + final Validator validator = Validator.builder().addLocales(Locale.GERMAN).build(); + + @Test + void valid() { + var value = new ANotBlank("ok", "ok", "ok"); + validator.validate(value); + } + + @Test + void inValidNull() { + var value = new ANotBlank(null, null, null); + try { + validator.validate(value); + fail("not get here"); + } catch (ConstraintViolationException e) { + assertThat(e.violations()).hasSize(3); + } + } + + @Test + void invalidBlank() { + var violation = one(new ANotBlank("", "ok", "ok")); + assertThat(violation.message()).isEqualTo("must not be blank"); + + var violation1 = one(new ANotBlank("ok", " ", "ok")); + assertThat(violation1.message()).isEqualTo("must not be blank"); + + var violation2 = one(new ANotBlank("ok", "ok", "\t")); + assertThat(violation2.message()).isEqualTo("NotBlank n max 4"); + } + + @Test + void invalidMax() { + var violation = one(new ANotBlank("ok", "NotValid", "ok")); + assertThat(violation.message()).isEqualTo("maximum length 4 exceeded"); + + var violation1 = one(new ANotBlank("ok", "ok", "NotValid")); + assertThat(violation1.message()).isEqualTo("NotBlank n max 4"); + } + + @Test + void invalidAsDE() { + var violation = one(new ANotBlank(" ", "ok", "ok"), Locale.GERMAN); + assertThat(violation.message()).isEqualTo("darf nicht leer sein"); + } + + @Test + void invalidWithMaxDE() { + var violation = one(new ANotBlank("ok", "NotValid", "ok"), Locale.GERMAN); + assertThat(violation.message()).isEqualTo("Länge muss zwischen 1 und 4 sein"); + } + + @Test + void invalidCustomMessageDE() { + var violation = one(new ANotBlank("ok", "ok", "NotValid"), Locale.GERMAN); + assertThat(violation.message()).isEqualTo("NotBlank n max 4"); + } + + ConstraintViolation one(Object any) { + return one(any, Locale.ENGLISH); + } + + ConstraintViolation one(Object any, Locale locale) { + try { + validator.validate(any, locale); + fail("not expected"); + return null; + } catch (ConstraintViolationException e) { + var violations = new ArrayList<>(e.violations()); + assertThat(violations).hasSize(1); + return violations.get(0); + } + } +} diff --git a/validator-constraints/src/main/java/io/avaje/validation/constraints/NotBlank.java b/validator-constraints/src/main/java/io/avaje/validation/constraints/NotBlank.java index f09cd654..42c1e5b2 100644 --- a/validator-constraints/src/main/java/io/avaje/validation/constraints/NotBlank.java +++ b/validator-constraints/src/main/java/io/avaje/validation/constraints/NotBlank.java @@ -28,6 +28,9 @@ @Repeatable(List.class) public @interface NotBlank { + /** Set the maximum length. By default this is 0 meaning unlimited. */ + int max() default 0; + String message() default "{avaje.NotBlank.message}"; Class[] groups() default {}; diff --git a/validator/src/main/java/io/avaje/validation/adapter/ValidationContext.java b/validator/src/main/java/io/avaje/validation/adapter/ValidationContext.java index 00445fdb..5980e432 100644 --- a/validator/src/main/java/io/avaje/validation/adapter/ValidationContext.java +++ b/validator/src/main/java/io/avaje/validation/adapter/ValidationContext.java @@ -164,7 +164,7 @@ interface AdapterCreateRequest { Message message(); - Message message(String key); + Message message(String key, Object... extraKeyValues); String targetType(); diff --git a/validator/src/main/java/io/avaje/validation/core/CoreAdapterBuilder.java b/validator/src/main/java/io/avaje/validation/core/CoreAdapterBuilder.java index b3c1d6cb..d78ceeae 100644 --- a/validator/src/main/java/io/avaje/validation/core/CoreAdapterBuilder.java +++ b/validator/src/main/java/io/avaje/validation/core/CoreAdapterBuilder.java @@ -134,9 +134,14 @@ public ValidationContext.Message message() { } @Override - public ValidationContext.Message message(String messageKey) { + public ValidationContext.Message message(String messageKey, Object... extraKeyValues) { Map newAttributes = new HashMap<>(attributes); newAttributes.put("message", messageKey); + if (extraKeyValues != null) { + for (int i = 0; i < extraKeyValues.length; i += 2) { + newAttributes.put(String.valueOf(extraKeyValues[i]), extraKeyValues[i + 1]); + } + } return ctx.message(newAttributes); } } diff --git a/validator/src/main/java/io/avaje/validation/core/adapters/BasicAdapters.java b/validator/src/main/java/io/avaje/validation/core/adapters/BasicAdapters.java index 1a82ee7d..abac981d 100644 --- a/validator/src/main/java/io/avaje/validation/core/adapters/BasicAdapters.java +++ b/validator/src/main/java/io/avaje/validation/core/adapters/BasicAdapters.java @@ -1,9 +1,6 @@ package io.avaje.validation.core.adapters; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -99,7 +96,7 @@ private static boolean useLength(AdapterCreateRequest request) { @Override public boolean validate(Object value, ValidationRequest req, String propertyName) { - if (!checkGroups(groups, req) || value == null) { + if (value == null || !checkGroups(groups, req)) { return true; } @@ -132,15 +129,47 @@ public boolean validate(Object value, ValidationRequest req, String propertyName } } - private static final class NotBlankAdapter extends AbstractConstraintAdapter { + private static final class NotBlankAdapter implements ValidationAdapter { + + private final ValidationContext.Message message; + private final ValidationContext.Message maxLengthMessage; + private final Set> groups; + private final int maxLength; NotBlankAdapter(AdapterCreateRequest request) { - super(request); + this.groups = request.groups(); + this.message = request.message(); + this.maxLength = maxLength(request); + if (maxLength > 0 && standardMessage(request)) { + maxLengthMessage = request.message("{avaje.Length.max.message}", "min", 1); + } else { + maxLengthMessage = null; + } + } + + private static int maxLength(AdapterCreateRequest request) { + final Integer max = (Integer) request.attribute("max"); + return Objects.requireNonNullElse(max, 0); + } + + private static boolean standardMessage(AdapterCreateRequest request) { + return "{avaje.NotBlank.message}".equals(request.attribute("message")); } @Override - public boolean isValid(CharSequence cs) { - return (cs != null) && !isBlank(cs); + public boolean validate(CharSequence value, ValidationRequest req, String propertyName) { + if (!checkGroups(groups, req)) { + return true; + } + if (value == null || isBlank(value)) { + req.addViolation(message, propertyName); + return false; + } + if (maxLength > 0 && value.length() > maxLength) { + req.addViolation(maxLengthMessage != null ? maxLengthMessage : message, propertyName); + return false; + } + return true; } static boolean isBlank(final CharSequence cs) { diff --git a/validator/src/main/resources/io/avaje/validation/Messages_en.properties b/validator/src/main/resources/io/avaje/validation/Messages_en.properties index 3c66c064..3268d773 100644 --- a/validator/src/main/resources/io/avaje/validation/Messages_en.properties +++ b/validator/src/main/resources/io/avaje/validation/Messages_en.properties @@ -1,3 +1,2 @@ -## Intentionally blank avaje.Length.max.message = maximum length {max} exceeded avaje.Size.max.message = maximum size {max} exceeded