Skip to content

Commit b23c433

Browse files
committed
fix: ensure @pattern precaching works for generic/container elements (#40)
- Compute pattern flags mask in ValidationCodeInjector when missing for synthetic parameters - Add support for 'flags' in TestClassBuilder's @pattern creation - Ensure stable test results by sorting AnnotationDefinition values and scrubbing array hashcodes - Add targeted tests for @pattern on List, Map, and arrays
1 parent 6fcd1af commit b23c433

10 files changed

+393
-4
lines changed

vaadoo-bytebuddy/src/main/java/com/github/pfichtner/vaadoo/ValidationCodeInjector.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,16 @@ public void inject(MethodVisitor mv, Parameter parameter, Method sourceMethod,
443443
annotationDescription);
444444
// Add the wrapper to the precomputed masks map so it can be looked up later
445445
Map<Parameter, Integer> masks = new HashMap<>(precomputedMasks);
446-
masks.put(wrapper, precomputedMasks.get(parameter));
446+
Integer mask = precomputedMasks.get(parameter);
447+
if (mask == null && annotationDescription.getAnnotationType()
448+
.represents(jakarta.validation.constraints.Pattern.class)) {
449+
EnumerationDescription[] flags = annotationDescription.getValue("flags")
450+
.resolve(EnumerationDescription[].class);
451+
mask = com.github.pfichtner.vaadoo.fragments.impl.Template.bitwiseOr(java.util.stream.Stream.of(flags)
452+
.map(EnumerationDescription::getValue).map(jakarta.validation.constraints.Pattern.Flag::valueOf)
453+
.toArray(jakarta.validation.constraints.Pattern.Flag[]::new));
454+
}
455+
masks.put(wrapper, mask);
447456

448457
// Create a new injector with the updated masks and use it to inject
449458
ClassVisitor classVisitor = new ValidationCallCodeInjectorClassVisitor(sourceMethod, mv,

vaadoo-bytebuddy/src/test/java/com/github/pfichtner/vaadoo/Approver.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ private static Options configure(Options options) {
7070
}
7171

7272
public static Scrubber scrubber() {
73-
return new RegExScrubber("auxiliary\\.\\S+\\s+\\S+[),]", i -> format("auxiliary.[AUX1_%d AUX1_%d]", i, i));
73+
Scrubber s1 = new RegExScrubber("auxiliary\\.\\S+\\s+\\S+[),]",
74+
i -> format("auxiliary.[AUX1_%d AUX1_%d]", i, i));
75+
Scrubber s2 = new RegExScrubber("\\[Ljakarta\\.validation\\.constraints\\.Pattern\\$Flag;@[0-9a-f]+",
76+
i -> "[Ljakarta.validation.constraints.Pattern$Flag;@HASHCODE");
77+
return (s) -> s2.scrub(s1.scrub(s));
7478
}
7579

7680
public static String decompile(Unloaded<?> clazz) throws IOException {

vaadoo-bytebuddy/src/test/java/com/github/pfichtner/vaadoo/GenericTypesTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,4 +105,28 @@ void arrayWithAnnotatedElementsExceptionMessage() throws Exception {
105105
assertThat(e2).hasMessageContaining("myArray[1] must not be blank");
106106
}
107107

108+
@Test
109+
void arrayWithPatternAnnotatedElements() throws Exception {
110+
var arrayOfPatternStrings = TypeDefinition.of(String[].class, String.class,
111+
AnnotationDefinition.of(jakarta.validation.constraints.Pattern.class, Map.of("regexp", "\\d*")));
112+
var constructor = ConstructorDefinition.of(
113+
DefaultParameterDefinition.of(arrayOfPatternStrings, AnnotationDefinition.of(NotNull.class))
114+
);
115+
var unloaded = a(baseTestClass.thatImplementsValueObject().withConstructor(constructor));
116+
new Approver(new Transformer()).approveTransformed("arrayWithPatternAnnotatedElements", constructor.params(),
117+
unloaded);
118+
}
119+
120+
@Test
121+
void mapWithPatternAnnotatedKeysAndValues() throws Exception {
122+
var mapOfPatternStringsToPatternStrings = TypeDefinition.of(Map.class, List.of(String.class, String.class),
123+
List.of(List.of(AnnotationDefinition.of(jakarta.validation.constraints.Pattern.class, Map.of("regexp", "K\\d*"))),
124+
List.of(AnnotationDefinition.of(jakarta.validation.constraints.Pattern.class, Map.of("regexp", "V\\d*")))));
125+
var constructor = ConstructorDefinition.of(DefaultParameterDefinition
126+
.of(mapOfPatternStringsToPatternStrings, AnnotationDefinition.of(NotNull.class)));
127+
var unloaded = a(baseTestClass.thatImplementsValueObject().withConstructor(constructor));
128+
new Approver(new Transformer()).approveTransformed("mapWithPatternAnnotatedKeysAndValues", constructor.params(),
129+
unloaded);
130+
}
131+
108132
}

vaadoo-bytebuddy/src/test/java/com/github/pfichtner/vaadoo/Jsr380DynamicClassTest.java

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,30 @@ void patternArg() throws Exception {
6969
var constructor = ConstructorDefinition.of(DefaultParameterDefinition.of(String.class,
7070
AnnotationDefinition.of(Pattern.class, Map.of("regexp", "\\d*"))));
7171
var unloaded = a(baseTestClass.thatImplementsValueObject().withConstructor(constructor));
72-
new Approver(new Transformer()).approveTransformed("regexp", constructor.params(), unloaded);
72+
new Approver(new Transformer()).approveTransformed("patternArg", constructor.params(), unloaded);
73+
}
74+
75+
@Test
76+
void containerPatternArg() throws Exception {
77+
var listOfPatternStrings = TypeDefinition.of(List.class, String.class,
78+
AnnotationDefinition.of(Pattern.class, Map.of("regexp", "\\d*")));
79+
var constructor = ConstructorDefinition.of(
80+
DefaultParameterDefinition.of(listOfPatternStrings, AnnotationDefinition.of(NotNull.class))
81+
);
82+
var unloaded = a(baseTestClass.thatImplementsValueObject().withConstructor(constructor));
83+
new Approver(new Transformer()).approveTransformed("containerPatternArg", constructor.params(), unloaded);
84+
}
85+
86+
@Test
87+
void containerPatternWithFlagsArg() throws Exception {
88+
var listOfPatternStrings = TypeDefinition.of(List.class, String.class,
89+
AnnotationDefinition.of(Pattern.class,
90+
Map.of("regexp", "\\d*", "flags", new Pattern.Flag[] { Pattern.Flag.CASE_INSENSITIVE })));
91+
var constructor = ConstructorDefinition.of(
92+
DefaultParameterDefinition.of(listOfPatternStrings, AnnotationDefinition.of(NotNull.class)));
93+
var unloaded = a(baseTestClass.thatImplementsValueObject().withConstructor(constructor));
94+
new Approver(new Transformer()).approveTransformed("containerPatternWithFlagsArg", constructor.params(),
95+
unloaded);
7396
}
7497

7598
@Test

vaadoo-bytebuddy/src/test/java/com/github/pfichtner/vaadoo/TestClassBuilder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,13 @@ public static class AnnotationDefinition {
130130
Class<? extends Annotation> annotation;
131131
LinkedHashMap<String, Object> values;
132132

133+
@Override
134+
public String toString() {
135+
return "TestClassBuilder.AnnotationDefinition(annotation=" + annotation + ", values={"
136+
+ values.entrySet().stream().map(e -> e.getKey() + "=" + e.getValue()).sorted().collect(joining(", "))
137+
+ "})";
138+
}
139+
133140
public static AnnotationDefinition of(Class<? extends Annotation> annotation) {
134141
return of(annotation, emptyMap());
135142
}
@@ -387,6 +394,8 @@ private static AnnotationDescription createAnnotation(AnnotationDefinition annot
387394
.define("fraction", getAnnotationValue(annotationDefinition, "fraction", 0));
388395
} else if (anno.equals(Pattern.class)) {
389396
builder = builder.define("regexp", getAnnotationValue(annotationDefinition, "regexp", ""));
397+
Pattern.Flag[] flags = getAnnotationValue(annotationDefinition, "flags", new Pattern.Flag[0]);
398+
builder = builder.defineEnumerationArray("flags", Pattern.Flag.class, flags);
390399
}
391400
return builder.build();
392401
} catch (Exception e) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
Story:
2+
arrayWithPatternAnnotatedElements
3+
4+
params annotations
5+
-: TestClassBuilder.DefaultParameterDefinition(typeDefinition=TestClassBuilder.TypeDefinition(type=class [Ljava.lang.String;, genericType=class java.lang.String, genericTypeAnnotations=[TestClassBuilder.AnnotationDefinition(annotation=interface jakarta.validation.constraints.Pattern, values={regexp=\d*})]), annotations=[TestClassBuilder.AnnotationDefinition(annotation=interface jakarta.validation.constraints.NotNull, values={})])
6+
7+
8+
Source:
9+
Analysing type com.example.GenericGenerated
10+
/*
11+
* Decompiled with CFR.
12+
*/
13+
package com.example;
14+
15+
import jakarta.validation.constraints.NotNull;
16+
import jakarta.validation.constraints.Pattern;
17+
import org.jmolecules.ddd.types.ValueObject;
18+
19+
public class GenericGenerated
20+
implements ValueObject {
21+
public GenericGenerated(@NotNull(groups={}, message="{jakarta.validation.constraints.NotNull.message}", payload={}) @Pattern(flags={}, groups={}, message="{jakarta.validation.constraints.Pattern.message}", payload={}, regexp="\d*") String[] stringArray) {
22+
}
23+
}
24+
25+
26+
27+
Transformed:
28+
Analysing type com.example.GenericGenerated
29+
/*
30+
* Decompiled with CFR.
31+
*/
32+
package com.example;
33+
34+
import com.example.GenericGenerated;
35+
import jakarta.validation.constraints.Pattern;
36+
import java.util.Map;
37+
import java.util.concurrent.ConcurrentHashMap;
38+
import org.jmolecules.ddd.types.ValueObject;
39+
40+
public class GenericGenerated
41+
implements ValueObject {
42+
private static final /* synthetic */ Map regexpCache;
43+
44+
public GenericGenerated(@Pattern(flags={}, groups={}, message="{jakarta.validation.constraints.Pattern.message}", payload={}, regexp="\d*") String[] stringArray) {
45+
GenericGenerated.validate(stringArray);
46+
this(stringArray, null);
47+
}
48+
49+
private /* synthetic */ GenericGenerated(String[] stringArray, auxiliary.[AUX1_1 AUX1_1] {
50+
}
51+
52+
private static void validate(String[] stringArray) {
53+
if (stringArray == null) {
54+
throw new IllegalArgumentException("stringArray must not be null");
55+
}
56+
if (stringArray != null) {
57+
int n = stringArray.length;
58+
for (int i = 0; i < n; ++i) {
59+
String string = stringArray[i];
60+
if (string == null || GenericGenerated.getCachedPattern("\\d*", 0).matcher(string).matches()) continue;
61+
throw new IllegalArgumentException("stringArray[" + i + "] must match \"\\d*\"");
62+
}
63+
}
64+
}
65+
66+
static {
67+
regexpCache = new ConcurrentHashMap();
68+
}
69+
70+
private static /* synthetic */ java.util.regex.Pattern getCachedPattern(String string, int n) {
71+
String string2 = string + "\u0000" + n;
72+
return regexpCache.computeIfAbsent(string2, object -> java.util.regex.Pattern.compile(string, n));
73+
}
74+
}
75+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
Story:
2+
mapWithPatternAnnotatedKeysAndValues
3+
4+
params annotations
5+
-: TestClassBuilder.DefaultParameterDefinition(typeDefinition=TestClassBuilder.TypeDefinition(type=interface java.util.Map, genericType=class java.lang.String, genericTypeAnnotations=[TestClassBuilder.AnnotationDefinition(annotation=interface jakarta.validation.constraints.Pattern, values={regexp=K\d*})]), annotations=[TestClassBuilder.AnnotationDefinition(annotation=interface jakarta.validation.constraints.NotNull, values={})])
6+
7+
8+
Source:
9+
Analysing type com.example.GenericGenerated
10+
/*
11+
* Decompiled with CFR.
12+
*/
13+
package com.example;
14+
15+
import jakarta.validation.constraints.NotNull;
16+
import jakarta.validation.constraints.Pattern;
17+
import java.util.Map;
18+
import org.jmolecules.ddd.types.ValueObject;
19+
20+
public class GenericGenerated
21+
implements ValueObject {
22+
public GenericGenerated(@NotNull(groups={}, message="{jakarta.validation.constraints.NotNull.message}", payload={}) Map<@Pattern(flags={}, groups={}, message="{jakarta.validation.constraints.Pattern.message}", payload={}, regexp="K\d*") String, @Pattern(flags={}, groups={}, message="{jakarta.validation.constraints.Pattern.message}", payload={}, regexp="V\d*") String> map) {
23+
}
24+
}
25+
26+
27+
28+
Transformed:
29+
Analysing type com.example.GenericGenerated
30+
/*
31+
* Decompiled with CFR.
32+
*/
33+
package com.example;
34+
35+
import com.example.GenericGenerated;
36+
import jakarta.validation.constraints.Pattern;
37+
import java.util.Map;
38+
import java.util.concurrent.ConcurrentHashMap;
39+
import org.jmolecules.ddd.types.ValueObject;
40+
41+
public class GenericGenerated
42+
implements ValueObject {
43+
private static final /* synthetic */ Map regexpCache;
44+
45+
public GenericGenerated(Map<@Pattern(flags={}, groups={}, message="{jakarta.validation.constraints.Pattern.message}", payload={}, regexp="K\d*") String, @Pattern(flags={}, groups={}, message="{jakarta.validation.constraints.Pattern.message}", payload={}, regexp="V\d*") String> map) {
46+
GenericGenerated.validate(map);
47+
this(map, null);
48+
}
49+
50+
private /* synthetic */ GenericGenerated(Map map, auxiliary.[AUX1_1 AUX1_1] {
51+
}
52+
53+
private static void validate(Map map) {
54+
String string;
55+
if (map == null) {
56+
throw new IllegalArgumentException("map must not be null");
57+
}
58+
if (map != null) {
59+
for (Map.Entry entry : map.entrySet()) {
60+
string = (String)entry.getKey();
61+
if (string == null || GenericGenerated.getCachedPattern("K\\d*", 0).matcher(string).matches()) continue;
62+
throw new IllegalArgumentException("map[key=" + entry.getKey() + "] must match \"K\\d*\"");
63+
}
64+
}
65+
if (map != null) {
66+
for (Map.Entry entry : map.entrySet()) {
67+
string = (String)entry.getValue();
68+
if (string == null || GenericGenerated.getCachedPattern("V\\d*", 0).matcher(string).matches()) continue;
69+
throw new IllegalArgumentException("map[value for key=" + entry.getKey() + "] must match \"V\\d*\"");
70+
}
71+
}
72+
}
73+
74+
static {
75+
regexpCache = new ConcurrentHashMap();
76+
}
77+
78+
private static /* synthetic */ java.util.regex.Pattern getCachedPattern(String string, int n) {
79+
String string2 = string + "\u0000" + n;
80+
return regexpCache.computeIfAbsent(string2, object -> java.util.regex.Pattern.compile(string, n));
81+
}
82+
}
83+
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
Story:
2+
containerPatternArg
3+
4+
params annotations
5+
-: TestClassBuilder.DefaultParameterDefinition(typeDefinition=TestClassBuilder.TypeDefinition(type=interface java.util.List, genericType=class java.lang.String, genericTypeAnnotations=[TestClassBuilder.AnnotationDefinition(annotation=interface jakarta.validation.constraints.Pattern, values={regexp=\d*})]), annotations=[TestClassBuilder.AnnotationDefinition(annotation=interface jakarta.validation.constraints.NotNull, values={})])
6+
7+
8+
Source:
9+
Analysing type com.example.Generated
10+
/*
11+
* Decompiled with CFR.
12+
*/
13+
package com.example;
14+
15+
import jakarta.validation.constraints.NotNull;
16+
import jakarta.validation.constraints.Pattern;
17+
import java.util.List;
18+
import org.jmolecules.ddd.types.ValueObject;
19+
20+
public class Generated
21+
implements ValueObject {
22+
public Generated(@NotNull(groups={}, message="{jakarta.validation.constraints.NotNull.message}", payload={}) List<@Pattern(flags={}, groups={}, message="{jakarta.validation.constraints.Pattern.message}", payload={}, regexp="\d*") String> list) {
23+
}
24+
}
25+
26+
27+
28+
Transformed:
29+
Analysing type com.example.Generated
30+
/*
31+
* Decompiled with CFR.
32+
*/
33+
package com.example;
34+
35+
import com.example.Generated;
36+
import jakarta.validation.constraints.Pattern;
37+
import java.util.Iterator;
38+
import java.util.List;
39+
import java.util.Map;
40+
import java.util.concurrent.ConcurrentHashMap;
41+
import org.jmolecules.ddd.types.ValueObject;
42+
43+
public class Generated
44+
implements ValueObject {
45+
private static final /* synthetic */ Map regexpCache;
46+
47+
public Generated(List<@Pattern(flags={}, groups={}, message="{jakarta.validation.constraints.Pattern.message}", payload={}, regexp="\d*") String> list) {
48+
Generated.validate(list);
49+
this(list, null);
50+
}
51+
52+
private /* synthetic */ Generated(List list, auxiliary.[AUX1_1 AUX1_1] {
53+
}
54+
55+
private static void validate(List list) {
56+
if (list == null) {
57+
throw new IllegalArgumentException("list must not be null");
58+
}
59+
if (list != null) {
60+
Iterator iterator = list.iterator();
61+
int n = 0;
62+
while (iterator.hasNext()) {
63+
String string = (String)iterator.next();
64+
if (string != null && !Generated.getCachedPattern("\\d*", 0).matcher(string).matches()) {
65+
throw new IllegalArgumentException("list[" + n + "] must match \"\\d*\"");
66+
}
67+
++n;
68+
}
69+
}
70+
}
71+
72+
static {
73+
regexpCache = new ConcurrentHashMap();
74+
}
75+
76+
private static /* synthetic */ java.util.regex.Pattern getCachedPattern(String string, int n) {
77+
String string2 = string + "\u0000" + n;
78+
return regexpCache.computeIfAbsent(string2, object -> java.util.regex.Pattern.compile(string, n));
79+
}
80+
}
81+

0 commit comments

Comments
 (0)