diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/BeanValidationModule.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/BeanValidationModule.java index 566381c..34c906f 100644 --- a/src/main/java/org/unbrokendome/jackson/beanvalidation/BeanValidationModule.java +++ b/src/main/java/org/unbrokendome/jackson/beanvalidation/BeanValidationModule.java @@ -7,6 +7,7 @@ import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler; +import javax.annotation.Nullable; import javax.validation.ConstraintViolationException; import javax.validation.ValidatorFactory; import java.io.IOException; @@ -17,6 +18,7 @@ public final class BeanValidationModule extends Module { private final ValidatorFactory validatorFactory; private final EnumSet features; + @Nullable private ConstructorValidatorFactory constructorValidatorFactory; public BeanValidationModule(ValidatorFactory validatorFactory) { @@ -58,13 +60,19 @@ public BeanValidationModule disable(BeanValidationFeature feature) { return this; } + public BeanValidationModule setConstructorValidatorFactory(@Nullable ConstructorValidatorFactory factory) { + this.constructorValidatorFactory = factory; + return this; + } + @Override public void setupModule(SetupContext context) { BeanValidationFeatureSet featureSet = new BeanValidationFeatureSet(features); - context.addBeanDeserializerModifier(new ValidationBeanDeserializerModifier(validatorFactory, featureSet)); + context.addBeanDeserializerModifier(new ValidationBeanDeserializerModifier( + validatorFactory, featureSet, constructorValidatorFactory)); context.addDeserializationProblemHandler(new DeserializationProblemHandler() { @Override diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/ConstructorValidatorFactory.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/ConstructorValidatorFactory.java new file mode 100644 index 0000000..e2e126e --- /dev/null +++ b/src/main/java/org/unbrokendome/jackson/beanvalidation/ConstructorValidatorFactory.java @@ -0,0 +1,9 @@ +package org.unbrokendome.jackson.beanvalidation; + +import javax.validation.Validator; +import javax.validation.ValidatorFactory; +import java.lang.reflect.Constructor; + +public interface ConstructorValidatorFactory { + Validator getValidator(ValidatorFactory validatorFactory, Constructor constructor, Object[] parameterValues); +} diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidatingValueInstantiator.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidatingValueInstantiator.java index ffe5c84..43ba71d 100644 --- a/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidatingValueInstantiator.java +++ b/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidatingValueInstantiator.java @@ -16,15 +16,14 @@ import javax.validation.ConstraintViolationException; import javax.validation.ElementKind; import javax.validation.Path; +import javax.validation.Validator; import javax.validation.ValidatorFactory; import javax.validation.executable.ExecutableValidator; import java.io.IOException; import java.lang.reflect.Constructor; -import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; import java.util.Set; @@ -34,16 +33,21 @@ class ValidatingValueInstantiator extends AbstractDelegatingValueInstantiator { private final ValidatorFactory validatorFactory; private final BeanValidationFeatureSet features; + private final ConstructorValidatorFactory constructorValidatorFactory; private boolean validationEnabled = false; ValidatingValueInstantiator( StdValueInstantiator delegate, ValidatorFactory validatorFactory, - BeanValidationFeatureSet features + BeanValidationFeatureSet features, + @Nullable ConstructorValidatorFactory constructorValidatorFactory ) { super(delegate); this.validatorFactory = validatorFactory; this.features = features; + this.constructorValidatorFactory = constructorValidatorFactory != null + ? constructorValidatorFactory + : (factory, constructor, parameterValues) -> factory.getValidator(); } @@ -184,21 +188,11 @@ private Set> validateCreatorArgs( } Member creatorMember = getWithArgsCreator().getMember(); - ExecutableValidator executableValidator = validatorFactory.getValidator().forExecutables(); if (creatorMember instanceof Constructor) { - Map hints = new HashMap<>(args.length); Constructor constructor = (Constructor) creatorMember; - Field[] declaredFields = constructor.getDeclaringClass().getDeclaredFields(); - if (declaredFields.length >= args.length) { - for (int i = 0; i < args.length; i++) { - hints.put(declaredFields[i].getName(), args[i]); - } - } - ValidationContextHolder.hints.set(hints); - Set violations = executableValidator.validateConstructorParameters(constructor, args); - ValidationContextHolder.hints.remove(); - return violations; + Validator validator = constructorValidatorFactory.getValidator(validatorFactory, constructor, args); + return validator.forExecutables().validateConstructorParameters(constructor, args); } else if (creatorMember instanceof Method) { // Bean validation doesn't support parameter validation for static methods :-( diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidationBeanDeserializerModifier.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidationBeanDeserializerModifier.java index 98def8a..08ba275 100644 --- a/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidationBeanDeserializerModifier.java +++ b/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidationBeanDeserializerModifier.java @@ -8,6 +8,7 @@ import com.fasterxml.jackson.databind.deser.impl.MethodProperty; import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator; +import javax.annotation.Nullable; import javax.validation.Validator; import javax.validation.ValidatorFactory; import java.util.ArrayList; @@ -18,11 +19,14 @@ final class ValidationBeanDeserializerModifier extends BeanDeserializerModifier private final ValidatorFactory validatorFactory; private final BeanValidationFeatureSet features; + private final ConstructorValidatorFactory constructorValidatorFactory; - ValidationBeanDeserializerModifier(ValidatorFactory validatorFactory, BeanValidationFeatureSet features) { + ValidationBeanDeserializerModifier(ValidatorFactory validatorFactory, BeanValidationFeatureSet features, + @Nullable ConstructorValidatorFactory constructorValidatorFactory) { this.validatorFactory = validatorFactory; this.features = features; + this.constructorValidatorFactory = constructorValidatorFactory; } @@ -49,7 +53,7 @@ public BeanDeserializerBuilder updateBuilder( ValueInstantiator valueInstantiator = builder.getValueInstantiator(); if (valueInstantiator instanceof StdValueInstantiator) { builder.setValueInstantiator(new ValidatingValueInstantiator( - (StdValueInstantiator) valueInstantiator, validatorFactory, features)); + (StdValueInstantiator) valueInstantiator, validatorFactory, features, constructorValidatorFactory)); } return builder; diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidationContextHolder.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidationContextHolder.java deleted file mode 100644 index 10553f2..0000000 --- a/src/main/java/org/unbrokendome/jackson/beanvalidation/ValidationContextHolder.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.unbrokendome.jackson.beanvalidation; - -import java.util.Map; - -public class ValidationContextHolder { - - static ThreadLocal> hints = new ThreadLocal<>(); - - public static Map getHints() { - return hints.get(); - } - -} diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/constraints/NotBlankWhen.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/constraints/NotBlankWhen.java deleted file mode 100644 index cf3a14b..0000000 --- a/src/main/java/org/unbrokendome/jackson/beanvalidation/constraints/NotBlankWhen.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.unbrokendome.jackson.beanvalidation.constraints; - -import javax.validation.Constraint; -import javax.validation.Payload; -import java.lang.annotation.Documented; -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.ElementType.TYPE_USE; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Documented -@Constraint(validatedBy = NotBlankWhenValidator.class) -@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) -@Retention(RUNTIME) -@Repeatable(NotBlankWhen.List.class) -public @interface NotBlankWhen { - - String field(); - - String value(); - - String message() default "{javax.validation.constraints.NotBlank.message}"; - - Class[] groups() default {}; - - Class[] payload() default {}; - - /** - * Defines several {@code @NotBlankWhen} constraints on the same element. - * - * @see org.unbrokendome.jackson.beanvalidation.constraints.NotBlankWhen - */ - @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) - @Retention(RUNTIME) - @Documented - public @interface List { - NotBlankWhen[] value(); - } -} diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/constraints/NotBlankWhenValidator.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/constraints/NotBlankWhenValidator.java deleted file mode 100644 index 1b80a23..0000000 --- a/src/main/java/org/unbrokendome/jackson/beanvalidation/constraints/NotBlankWhenValidator.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.unbrokendome.jackson.beanvalidation.constraints; - -import org.unbrokendome.jackson.beanvalidation.ValidationContextHolder; - -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import java.util.Objects; - -public class NotBlankWhenValidator implements ConstraintValidator { - - private String field; - private String value; - - @Override - public void initialize(NotBlankWhen notBlankWhen) { - field = notBlankWhen.field(); - value = notBlankWhen.value(); - } - - @Override - public boolean isValid(String data, ConstraintValidatorContext context) { - Object actualValue = ValidationContextHolder.getHints().get(field); - return actualValue == null || - !Objects.equals(actualValue.toString(), value) || - !(data == null || data.trim().isEmpty()); - } -} diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhen.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhen.java deleted file mode 100644 index d3a6c32..0000000 --- a/src/main/java/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhen.java +++ /dev/null @@ -1,41 +0,0 @@ -package org.unbrokendome.jackson.beanvalidation.constraints; - -import javax.validation.Constraint; -import javax.validation.Payload; -import java.lang.annotation.Documented; -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.ElementType.*; -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -@Documented -@Constraint(validatedBy = NotNullWhenValidator.class) -@Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) -@Retention(RUNTIME) -@Repeatable(NotNullWhen.List.class) -public @interface NotNullWhen { - - String field(); - - String value(); - - String message() default "{javax.validation.constraints.NotNull.message}"; - - Class[] groups() default {}; - - Class[] payload() default {}; - - /** - * Defines several {@code @NotNullWhen} constraints on the same element. - * - * @see NotNullWhen - */ - @Target({METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE}) - @Retention(RUNTIME) - @Documented - public @interface List { - NotNullWhen[] value(); - } -} diff --git a/src/main/java/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidator.java b/src/main/java/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidator.java deleted file mode 100644 index d1cead9..0000000 --- a/src/main/java/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidator.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.unbrokendome.jackson.beanvalidation.constraints; - -import org.unbrokendome.jackson.beanvalidation.ValidationContextHolder; -import javax.validation.ConstraintValidator; -import javax.validation.ConstraintValidatorContext; -import java.util.Objects; - -public class NotNullWhenValidator implements ConstraintValidator { - - private String field; - private String value; - - @Override - public void initialize(NotNullWhen notBlankWhen) { - field = notBlankWhen.field(); - value = notBlankWhen.value(); - } - - @Override - public boolean isValid(Object data, ConstraintValidatorContext context) { - Object actualValue = ValidationContextHolder.getHints().get(field); - return actualValue == null || - !Objects.equals(actualValue.toString(), value) || - data != null; - } -} diff --git a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/AbstractValidationTest.kt b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/AbstractValidationTest.kt index e5908ae..51af843 100644 --- a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/AbstractValidationTest.kt +++ b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/AbstractValidationTest.kt @@ -9,7 +9,7 @@ import javax.validation.ConstraintViolationException import javax.validation.Validation -abstract class AbstractValidationTest { +abstract class AbstractValidationTest(constructorValidatorFactory: ConstructorValidatorFactory? = null) { private val validatorFactory = Validation.byDefaultProvider() .configure() @@ -17,6 +17,7 @@ abstract class AbstractValidationTest { .buildValidatorFactory() protected val beanValidationModule: BeanValidationModule = BeanValidationModule(validatorFactory) + .setConstructorValidatorFactory(constructorValidatorFactory) protected val objectMapper: ObjectMapper = ObjectMapper() .registerModule(beanValidationModule) diff --git a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/HibernateConstructorValidatorFactory.kt b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/HibernateConstructorValidatorFactory.kt new file mode 100644 index 0000000..e0f155a --- /dev/null +++ b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/HibernateConstructorValidatorFactory.kt @@ -0,0 +1,19 @@ +package org.unbrokendome.jackson.beanvalidation.constraints + +import org.hibernate.validator.HibernateValidatorFactory +import org.unbrokendome.jackson.beanvalidation.ConstructorValidatorFactory +import java.lang.reflect.Constructor +import javax.validation.Validator +import javax.validation.ValidatorFactory + +class HibernateConstructorValidatorFactory : ConstructorValidatorFactory { + override fun getValidator(validatorFactory: ValidatorFactory, constructor: Constructor, parameterValues: Array): Validator { + val declaredFields = constructor.declaringClass.declaredFields + val payload: Map = parameterValues + .mapIndexed { index, value -> declaredFields[index].name to value} + .toMap() + + return validatorFactory.unwrap(HibernateValidatorFactory::class.java) + .usingContext().constraintValidatorPayload(payload).validator + } +} diff --git a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotBlankWhenValidationTest.kt b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotBlankWhenValidationTest.kt deleted file mode 100644 index 91c1e8c..0000000 --- a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotBlankWhenValidationTest.kt +++ /dev/null @@ -1,74 +0,0 @@ -package org.unbrokendome.jackson.beanvalidation.constraints - -import assertk.assertThat -import assertk.assertions.hasSize -import com.fasterxml.jackson.annotation.JsonCreator -import com.fasterxml.jackson.annotation.JsonProperty -import org.junit.jupiter.api.Test -import org.unbrokendome.jackson.beanvalidation.AbstractValidationTest -import org.unbrokendome.jackson.beanvalidation.JsonValidated -import org.unbrokendome.jackson.beanvalidation.assertions.hasViolation - - -class NotBlankWhenValidationTest : AbstractValidationTest() { - - @JsonValidated - class BeanWithCrossValidation - @JsonCreator constructor( - @param:JsonProperty("hasDetails") - val hasDetails: Boolean, - @param:JsonProperty("details") - @NotBlankWhen(field = "hasDetails", value = "true") - val details: String - ) - - @Test - fun `should not report violation when precondition is not matched`() { - - val json = """{ "hasDetails": false, "details": "" }""" - - assertNoViolationsOnDeserialization(json) - } - - - @Test - fun `should not report violation when precondition is matched and value is valid`() { - - val json = """{ "hasDetails": false, "details": "secure" }""" - - assertNoViolationsOnDeserialization(json) - } - - @Test - fun `should report violation when precondition is matched and value is null`() { - - val json = """{ "hasDetails": true }""" - - val violations = assertViolationsOnDeserialization(json) - - assertThat(violations).hasSize(1) - assertThat(violations).hasViolation("details") - } - - @Test - fun `should report violation when precondition is matched and value is empty`() { - - val json = """{ "hasDetails": true, "details": "" }""" - - val violations = assertViolationsOnDeserialization(json) - - assertThat(violations).hasSize(1) - assertThat(violations).hasViolation("details") - } - - @Test - fun `should report violation when precondition is matched and value is blank`() { - - val json = """{ "hasDetails": true, "details": " " }""" - - val violations = assertViolationsOnDeserialization(json) - - assertThat(violations).hasSize(1) - assertThat(violations).hasViolation("details") - } -} \ No newline at end of file diff --git a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhen.kt b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhen.kt new file mode 100644 index 0000000..b9795f9 --- /dev/null +++ b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhen.kt @@ -0,0 +1,19 @@ +package org.unbrokendome.jackson.beanvalidation.constraints + +import javax.validation.Constraint +import javax.validation.Payload +import kotlin.annotation.AnnotationTarget.* +import kotlin.reflect.KClass + +@Target(FUNCTION, PROPERTY_GETTER, PROPERTY_SETTER, FIELD, ANNOTATION_CLASS, CONSTRUCTOR, VALUE_PARAMETER) +@Retention +@MustBeDocumented +@Constraint(validatedBy = [NotNullWhenValidator::class]) +annotation class NotNullWhen( + val field: String, + val value: String, + val message: String = "{javax.validation.constraints.NotNull.message}", + val groups: Array> = [], + val payload: Array> = [] +) + diff --git a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidationTest.kt b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidationTest.kt index e113a15..389c940 100644 --- a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidationTest.kt +++ b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidationTest.kt @@ -9,7 +9,7 @@ import org.unbrokendome.jackson.beanvalidation.AbstractValidationTest import org.unbrokendome.jackson.beanvalidation.JsonValidated import org.unbrokendome.jackson.beanvalidation.assertions.hasViolation -class NotNullWhenValidationTest : AbstractValidationTest() { +class NotNullWhenValidationTest : AbstractValidationTest(HibernateConstructorValidatorFactory()) { @JsonValidated class BeanWithCrossValidation @@ -56,4 +56,4 @@ class NotNullWhenValidationTest : AbstractValidationTest() { assertNoViolationsOnDeserialization(json) } -} \ No newline at end of file +} diff --git a/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidator.kt b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidator.kt new file mode 100644 index 0000000..febf973 --- /dev/null +++ b/src/test/kotlin/org/unbrokendome/jackson/beanvalidation/constraints/NotNullWhenValidator.kt @@ -0,0 +1,25 @@ +package org.unbrokendome.jackson.beanvalidation.constraints + +import org.hibernate.validator.constraintvalidation.HibernateConstraintValidatorContext +import java.util.* +import javax.validation.ConstraintValidator +import javax.validation.ConstraintValidatorContext + +class NotNullWhenValidator : ConstraintValidator { + private var field: String = "" + private var value: String = "" + + override fun initialize(notNullWhen: NotNullWhen) { + field = notNullWhen.field + value = notNullWhen.value + } + + override fun isValid(data: String?, context: ConstraintValidatorContext): Boolean { + val hibernateValidatorCtx = context.unwrap(HibernateConstraintValidatorContext::class.java) + val constructorParams = hibernateValidatorCtx.getConstraintValidatorPayload(Map::class.java) + val actualValue = constructorParams[field] + return actualValue == null || + !Objects.equals(actualValue.toString(), value) || + data != null + } +}