Skip to content

Commit e97cb39

Browse files
Message interpolation support
1 parent 93ffc6b commit e97cb39

File tree

8 files changed

+240
-25
lines changed

8 files changed

+240
-25
lines changed

pom.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333

3434
<properties>
3535
<lombok.version>1.18.24</lombok.version>
36+
<commons-lang3.version>3.12.0</commons-lang3.version>
3637
<slf4j.version>1.7.26</slf4j.version>
3738
<cdi.version>2.0</cdi.version>
3839
<bean-validation.version>2.0.1.Final</bean-validation.version>

validator-core/src/main/java/com/github/microtweak/validator/conditional/core/DelegatedConditionalConstraintValidator.java

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.github.microtweak.validator.conditional.core;
22

33
import com.github.microtweak.validator.conditional.core.internal.CvConstraintDescriptorImpl;
4+
import com.github.microtweak.validator.conditional.core.internal.CvMessageInterpolatorContext;
45
import com.github.microtweak.validator.conditional.core.internal.annotated.ValidationPoint;
56
import com.github.microtweak.validator.conditional.core.internal.helper.BeanValidationHelper;
67
import com.github.microtweak.validator.conditional.core.internal.helper.ConstraintValidatorRegistrar;
@@ -10,17 +11,20 @@
1011

1112
import javax.validation.ConstraintValidator;
1213
import javax.validation.ConstraintValidatorContext;
14+
import javax.validation.MessageInterpolator;
15+
import javax.validation.metadata.ConstraintDescriptor;
1316
import java.lang.annotation.Annotation;
1417

1518
@Slf4j
1619
public class DelegatedConditionalConstraintValidator implements ConstraintValidator<ConditionalValidate, Object> {
1720

1821
private static final PlatformProvider platform = PlatformProvider.getInstance();
22+
private static final BeanValidationImplementationProvider bvProvider = BeanValidationImplementationProvider.getInstance();
1923
private static final ConstraintValidatorRegistrar registrar = new ConstraintValidatorRegistrar();
24+
2025
private ExpressionEvaluator evaluator;
2126

2227
static {
23-
final BeanValidationImplementationProvider bvProvider = BeanValidationImplementationProvider.getInstance();
2428
registrar.addValidators(bvProvider.getAvailableConstraintValidators());
2529
}
2630

@@ -44,12 +48,14 @@ public boolean isValid(Object validatedBean, ConstraintValidatorContext context)
4448
continue;
4549
}
4650

47-
final ConstraintValidator<Annotation, Object> validator = platform.getInitializedConstraintValidator(descriptor);
51+
final ConstraintValidator<Annotation, Object> validator = BeanValidationHelper.getInitializedConstraintValidator(platform.getConstraintValidatorFactory(), descriptor);
4852

4953
if (!validator.isValid(value, context)) {
5054
isAllConstraintsValid = false;
5155

52-
context.buildConstraintViolationWithTemplate( descriptor.getMessageTemplate() )
56+
final String interpolatedMessage = getInterpolatedMessage(descriptor, value);
57+
58+
context.buildConstraintViolationWithTemplate(interpolatedMessage)
5359
.addPropertyNode(validationPoint.getName())
5460
.addConstraintViolation();
5561
}
@@ -59,4 +65,9 @@ public boolean isValid(Object validatedBean, ConstraintValidatorContext context)
5965
return isAllConstraintsValid;
6066
}
6167

68+
private String getInterpolatedMessage(ConstraintDescriptor<?> constraintDescriptor, Object validatedValue) {
69+
final MessageInterpolator.Context msgInterpolatorCtx = new CvMessageInterpolatorContext(constraintDescriptor, validatedValue);
70+
return platform.getMessageInterpolator().interpolate(constraintDescriptor.getMessageTemplate(), msgInterpolatorCtx);
71+
}
72+
6273
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.github.microtweak.validator.conditional.core.internal;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
6+
import javax.validation.MessageInterpolator;
7+
import javax.validation.ValidationException;
8+
import javax.validation.metadata.ConstraintDescriptor;
9+
10+
import static java.lang.String.format;
11+
12+
@Getter
13+
@AllArgsConstructor
14+
public class CvMessageInterpolatorContext implements MessageInterpolator.Context {
15+
16+
private final ConstraintDescriptor<?> constraintDescriptor;
17+
private final Object validatedValue;
18+
19+
@Override
20+
public <T> T unwrap(Class<T> type) {
21+
if (type.isAssignableFrom(CvMessageInterpolatorContext.class)) {
22+
return type.cast(this);
23+
}
24+
throw new ValidationException( format("Type %s not supported for unwrapping.", type) );
25+
}
26+
27+
}

validator-core/src/main/java/com/github/microtweak/validator/conditional/core/internal/helper/BeanValidationHelper.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
import org.apache.commons.lang3.reflect.FieldUtils;
1010

1111
import javax.validation.ConstraintValidator;
12+
import javax.validation.ConstraintValidatorContext;
13+
import javax.validation.ConstraintValidatorFactory;
14+
import javax.validation.MessageInterpolator;
15+
import javax.validation.metadata.ConstraintDescriptor;
1216
import java.lang.annotation.Annotation;
1317
import java.util.Collections;
1418
import java.util.LinkedHashSet;
@@ -78,4 +82,13 @@ public static <A extends Annotation> Set<CvConstraintDescriptorImpl<A>> getAllCo
7882
);
7983
}
8084

85+
@SuppressWarnings("unchecked")
86+
public static ConstraintValidator<Annotation, Object> getInitializedConstraintValidator(ConstraintValidatorFactory constraintValidatorFactory, ConstraintDescriptor<Annotation> descriptor) {
87+
return (ConstraintValidator<Annotation, Object>) descriptor.getConstraintValidatorClasses().stream()
88+
.map(constraintValidatorFactory::getInstance)
89+
.peek(validator -> validator.initialize(descriptor.getAnnotation()))
90+
.findFirst()
91+
.orElseThrow(() -> null);
92+
}
93+
8194
}

validator-core/src/main/java/com/github/microtweak/validator/conditional/core/spi/PlatformProvider.java

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,9 @@
22

33
import javax.enterprise.inject.Instance;
44
import javax.enterprise.inject.spi.CDI;
5-
import javax.validation.ConstraintValidator;
5+
import javax.validation.ConstraintValidatorFactory;
6+
import javax.validation.MessageInterpolator;
67
import javax.validation.ValidatorFactory;
7-
import javax.validation.metadata.ConstraintDescriptor;
8-
import java.lang.annotation.Annotation;
98
import java.util.Comparator;
109
import java.util.HashSet;
1110
import java.util.ServiceLoader;
@@ -34,17 +33,12 @@ default boolean isAvailable() {
3433

3534
ValidatorFactory getValidatorFactory();
3635

37-
default <CV extends ConstraintValidator<?, ?>> CV getConstraintValidatorInstance(Class<CV> constraintValidatorClass) {
38-
return getValidatorFactory().getConstraintValidatorFactory().getInstance(constraintValidatorClass);
36+
default ConstraintValidatorFactory getConstraintValidatorFactory() {
37+
return getValidatorFactory().getConstraintValidatorFactory();
3938
}
4039

41-
@SuppressWarnings("unchecked")
42-
default ConstraintValidator<Annotation, Object> getInitializedConstraintValidator(ConstraintDescriptor<Annotation> descriptor) {
43-
return (ConstraintValidator<Annotation, Object>) descriptor.getConstraintValidatorClasses().stream()
44-
.map(this::getConstraintValidatorInstance)
45-
.peek(validator -> validator.initialize(descriptor.getAnnotation()))
46-
.findFirst()
47-
.orElseThrow(() -> null);
40+
default MessageInterpolator getMessageInterpolator() {
41+
return getValidatorFactory().getMessageInterpolator();
4842
}
4943

5044
class DefaultPlatformHelper implements PlatformProvider {

validator-core/src/test/java/com/github/microtweak/validator/conditional/internal/BeanValidationHelperTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,9 +138,9 @@ public void invokeConstraintValidator() {
138138

139139
for (final ValidationPoint validationPoint : BeanValidationHelper.getAllValidationPointsAt(address.getClass())) {
140140
final ConstraintValidator<Annotation, Object> validator = BeanValidationHelper.getAllConstraintDescriptorOf(validationPoint, registrar).stream()
141-
.map(platform::getInitializedConstraintValidator)
142-
.findFirst()
143-
.orElseThrow(() -> new NullPointerException("No validator found"));
141+
.map(descriptor -> BeanValidationHelper.getInitializedConstraintValidator(platform.getConstraintValidatorFactory(), descriptor))
142+
.findFirst()
143+
.orElseThrow(() -> new NullPointerException("No validator found"));
144144

145145
final BooleanSupplier constraintValidatorExecutor = () -> validator.isValid(validationPoint.getValidatedValue(address), null);
146146

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
package com.github.microtweak.validator.conditional.internal;
2+
3+
import com.github.microtweak.validator.conditional.core.constraint.AssertTrueWhen;
4+
import com.github.microtweak.validator.conditional.core.constraint.DecimalMaxWhen;
5+
import com.github.microtweak.validator.conditional.core.constraint.SizeWhen;
6+
import com.github.microtweak.validator.conditional.core.internal.CvConstraintAnnotationDescriptor;
7+
import com.github.microtweak.validator.conditional.core.internal.CvConstraintDescriptorImpl;
8+
import com.github.microtweak.validator.conditional.core.internal.CvMessageInterpolatorContext;
9+
import com.github.microtweak.validator.conditional.core.spi.PlatformProvider;
10+
import com.github.microtweak.validator.conditional.internal.literal.ConstraintLiteral;
11+
import com.github.microtweak.validator.conditional.junit5.ProviderTest;
12+
import org.apache.commons.lang3.function.TriFunction;
13+
import org.junit.jupiter.api.Test;
14+
15+
import javax.validation.MessageInterpolator;
16+
import javax.validation.metadata.ConstraintDescriptor;
17+
import java.lang.annotation.Annotation;
18+
import java.util.Locale;
19+
import java.util.function.BiFunction;
20+
21+
import static org.junit.jupiter.api.Assertions.assertAll;
22+
import static org.junit.jupiter.api.Assertions.assertEquals;
23+
24+
@ProviderTest
25+
public class CvMessageInterpolatorContextTests {
26+
27+
private static final PlatformProvider platform = PlatformProvider.getInstance();
28+
29+
private static final TriFunction<Annotation, Object, Locale, String> interpolateMessageCustomLocaleFn = (conditionalConstraint, validatedValue, customLocale) -> {
30+
final CvConstraintAnnotationDescriptor annotationDescriptor = new CvConstraintAnnotationDescriptor(conditionalConstraint);
31+
32+
final ConstraintDescriptor<?> constraintDescriptor = new CvConstraintDescriptorImpl<>(annotationDescriptor, null);
33+
34+
final MessageInterpolator.Context interpolatorContext = new CvMessageInterpolatorContext(constraintDescriptor, validatedValue);
35+
36+
return platform.getMessageInterpolator().interpolate(constraintDescriptor.getMessageTemplate(), interpolatorContext, customLocale);
37+
};
38+
39+
private static final BiFunction<Annotation, Object, String> interpolateMessageFn = (conditionalConstraint, validatedValue) ->
40+
interpolateMessageCustomLocaleFn.apply(conditionalConstraint, validatedValue, Locale.ENGLISH);
41+
42+
@Test
43+
public void interpolateSimpleMessageInline() {
44+
final AssertTrueWhen assertTrueWhen = new ConstraintLiteral<>(AssertTrueWhen.class)
45+
.message("must be true")
46+
.build();
47+
48+
final String interpolated = interpolateMessageFn.apply(assertTrueWhen, null);
49+
50+
assertEquals("must be true", interpolated);
51+
}
52+
53+
54+
@Test
55+
public void interpolateSimpleMessageResourceBundle() {
56+
final AssertTrueWhen assertTrueWhen = new ConstraintLiteral<>(AssertTrueWhen.class).build();
57+
58+
final String interpolated = interpolateMessageFn.apply(assertTrueWhen, null);
59+
60+
assertEquals("must be true", interpolated);
61+
}
62+
63+
@Test
64+
public void interpolateWithCustomLocaleSimpleMessageResourceBundle() {
65+
final AssertTrueWhen assertTrueWhen = new ConstraintLiteral<>(AssertTrueWhen.class).build();
66+
67+
final String interpolated = interpolateMessageCustomLocaleFn.apply(assertTrueWhen, false, Locale.ITALIAN);
68+
69+
assertEquals("deve essere true", interpolated);
70+
}
71+
72+
@Test
73+
public void interpolateMessageWithAttributesInline() {
74+
final SizeWhen sizeWhen = new ConstraintLiteral<>(SizeWhen.class)
75+
.message("size must be between {min} and {max}")
76+
.attribute("min", 5)
77+
.attribute("max", 10)
78+
.build();
79+
80+
final String interpolated = interpolateMessageFn.apply(sizeWhen, "");
81+
82+
assertEquals("size must be between 5 and 10", interpolated);
83+
}
84+
85+
@Test
86+
public void interpolateMessageWithAttributesResourceBundle() {
87+
final SizeWhen sizeWhen = new ConstraintLiteral<>(SizeWhen.class)
88+
.attribute("min", 5)
89+
.attribute("max", 10)
90+
.build();
91+
92+
final String interpolated = interpolateMessageFn.apply(sizeWhen, "");
93+
94+
assertEquals("size must be between 5 and 10", interpolated);
95+
}
96+
97+
@Test
98+
public void interpolateMessageWithExpressionLanguageInline() {
99+
final String defaultMessage = "must be less than ${inclusive == true ? 'or equal to ' : ''}{value}";
100+
final String expectedValue = "10.0";
101+
final String validatedValue = "1.0";
102+
103+
assertAll(
104+
() -> {
105+
final DecimalMaxWhen decimalMaxWhen = new ConstraintLiteral<>(DecimalMaxWhen.class)
106+
.message(defaultMessage)
107+
.attribute("value", "10.0")
108+
.attribute("inclusive", false)
109+
.build();
110+
111+
final String interpolated = interpolateMessageFn.apply(decimalMaxWhen, validatedValue);
112+
assertEquals("must be less than " + expectedValue, interpolated);
113+
},
114+
() -> {
115+
final DecimalMaxWhen decimalMaxWhen = new ConstraintLiteral<>(DecimalMaxWhen.class)
116+
.message(defaultMessage)
117+
.attribute("value", "10.0")
118+
.attribute("inclusive", true)
119+
.build();
120+
121+
final String interpolated = interpolateMessageFn.apply(decimalMaxWhen, validatedValue);
122+
assertEquals("must be less than or equal to " + expectedValue, interpolated);
123+
}
124+
);
125+
}
126+
127+
@Test
128+
public void interpolateMessageWithExpressionLanguageResourceBundle() {
129+
final String expectedValue = "10.0";
130+
final String validatedValue = "1.0";
131+
132+
assertAll(
133+
() -> {
134+
final DecimalMaxWhen decimalMaxWhen = new ConstraintLiteral<>(DecimalMaxWhen.class)
135+
.attribute("value", "10.0")
136+
.attribute("inclusive", false)
137+
.build();
138+
139+
final String interpolated = interpolateMessageFn.apply(decimalMaxWhen, validatedValue);
140+
assertEquals("must be less than " + expectedValue, interpolated);
141+
},
142+
() -> {
143+
final DecimalMaxWhen decimalMaxWhen = new ConstraintLiteral<>(DecimalMaxWhen.class)
144+
.attribute("value", "10.0")
145+
.attribute("inclusive", true)
146+
.build();
147+
148+
final String interpolated = interpolateMessageFn.apply(decimalMaxWhen, validatedValue);
149+
assertEquals("must be less than or equal to " + expectedValue, interpolated);
150+
}
151+
);
152+
}
153+
154+
}

validator-core/src/test/java/com/github/microtweak/validator/conditional/internal/literal/ConstraintLiteral.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,34 @@
22

33
import com.github.microtweak.validator.conditional.core.WhenActivatedValidateAs;
44
import com.github.microtweak.validator.conditional.core.internal.helper.AnnotationHelper;
5-
import lombok.RequiredArgsConstructor;
65

76
import javax.validation.Payload;
87
import java.lang.annotation.Annotation;
98
import java.util.HashMap;
109
import java.util.Map;
1110

12-
@RequiredArgsConstructor
1311
public class ConstraintLiteral<A extends Annotation> {
1412

1513
private final Class<? extends A> annotationType;
16-
private final Map<String, Object> attributes = new HashMap<>();
14+
private final Class<? extends Annotation> actualAnnotationType;
15+
private final Map<String, Object> attributes = new HashMap<>();;
16+
17+
public ConstraintLiteral(Class<? extends A> annotationType) {
18+
this.annotationType = annotationType;
19+
20+
final WhenActivatedValidateAs validateAs = annotationType.getAnnotation(WhenActivatedValidateAs.class);
21+
this.actualAnnotationType = validateAs != null ? validateAs.value() : null;
22+
}
23+
24+
private boolean isCvConstraint() {
25+
return actualAnnotationType != null;
26+
}
27+
28+
private String getDefaultMessage() {
29+
final Class<? extends Annotation> annotationType = actualAnnotationType != null ? actualAnnotationType : this.annotationType;
30+
return "{" + annotationType.getName() + ".message}";
31+
}
32+
1733

1834
public ConstraintLiteral<A> attribute(String name, Object value) {
1935
attributes.put(name, value);
@@ -34,13 +50,12 @@ public ConstraintLiteral<A> payload(Class<? extends Payload>[] payload) {
3450

3551
public A build() {
3652
final Map<String, Object> attrs = new HashMap<>(this.attributes);
37-
attrs.putIfAbsent("message", "It is not valid");
53+
// attrs.putIfAbsent("message", "It is not valid");
54+
attrs.putIfAbsent("message", getDefaultMessage());
3855
attrs.putIfAbsent("groups", new Class[0]);
3956
attrs.putIfAbsent("payload", new Class[0]);
4057

41-
final boolean isCvConstraint = annotationType.isAnnotationPresent(WhenActivatedValidateAs.class);
42-
43-
if (isCvConstraint && !attributes.containsKey("expression")) {
58+
if (isCvConstraint() && !attributes.containsKey("expression")) {
4459
attrs.putIfAbsent("expression", "true");
4560
}
4661

0 commit comments

Comments
 (0)