Skip to content

Commit 824c7fc

Browse files
committed
further work on Support for generic types (e.g. List<@notblank String>) #40
1 parent f19bf61 commit 824c7fc

File tree

9 files changed

+447
-59
lines changed

9 files changed

+447
-59
lines changed

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

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -203,12 +203,16 @@ public Object annotationValue(Type annotation, String name) {
203203
@Override
204204
public List<List<AnnotationDescription>> genericAnnotations() {
205205
TypeDescription.Generic typeDescription = definedShape().getType();
206-
return typeDescription.getSort().isParameterized() //
207-
? typeDescription.getTypeArguments().stream() //
208-
.filter(Objects::nonNull) //
209-
.map(TypeDescription.Generic::getDeclaredAnnotations) //
210-
.collect(toList()) //
211-
: emptyList();
206+
if (typeDescription.getSort().isParameterized()) {
207+
return typeDescription.getTypeArguments().stream() //
208+
.filter(Objects::nonNull) //
209+
.map(TypeDescription.Generic::getDeclaredAnnotations) //
210+
.collect(toList());
211+
}
212+
if (typeDescription.isArray()) {
213+
return List.of(typeDescription.getComponentType().getDeclaredAnnotations());
214+
}
215+
return emptyList();
212216
}
213217

214218
@Override

vaadoo-bytebuddy/src/main/java/com/github/pfichtner/vaadoo/org/jmolecules/bytebuddy/VaadooImplementor.java

Lines changed: 135 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -279,36 +279,53 @@ private Stream<InjectionTask> handleGenericAnnotations(Parameter parameter,
279279
List<List<AnnotationDescription>> genericAnnotations) {
280280
TypeDescription.Generic genericType = parameter.genericType();
281281

282-
// Only process if this is a parameterized type
283-
if (!genericType.getSort().isParameterized()) {
284-
return empty();
285-
}
282+
if (genericType.getSort().isParameterized()) {
283+
List<TypeDescription.Generic> typeArguments = genericType.getTypeArguments();
284+
return range(0, genericAnnotations.size()).boxed().flatMap(i -> {
285+
List<AnnotationDescription> typeArgAnnotations = genericAnnotations.get(i);
286+
if (typeArgAnnotations.isEmpty() || i >= typeArguments.size()) {
287+
return empty();
288+
}
286289

287-
List<TypeDescription.Generic> typeArguments = genericType.getTypeArguments();
288-
return range(0, genericAnnotations.size()).boxed().flatMap(i -> {
289-
List<AnnotationDescription> typeArgAnnotations = genericAnnotations.get(i);
290-
if (typeArgAnnotations.isEmpty() || i >= typeArguments.size()) {
291-
return empty();
292-
}
290+
TypeDescription.Generic typeArgument = typeArguments.get(i);
291+
if (typeArgument == null) {
292+
return empty();
293+
}
293294

294-
TypeDescription.Generic typeArgument = typeArguments.get(i);
295-
if (typeArgument == null) {
295+
return typeArgAnnotations.stream().flatMap(annotation -> {
296+
TypeDescription annotationType = annotation.getAnnotationType();
297+
if (isStandardJr380Anno(annotationType)) {
298+
Optional<Method> fragmentMethod = codeFragmentMethod(annotationType,
299+
typeArgument.asErasure());
300+
if (fragmentMethod.isPresent()) {
301+
return Stream.of(new GenericTypeInjectionTask(parameter, typeArgument.asErasure(),
302+
fragmentMethod.get(), annotation, i));
303+
}
304+
}
305+
return empty();
306+
});
307+
});
308+
} else if (genericType.isArray()) {
309+
List<AnnotationDescription> typeArgAnnotations = genericAnnotations.get(0);
310+
if (typeArgAnnotations.isEmpty()) {
296311
return empty();
297312
}
298313

299-
// Create injection tasks for each annotation on the type argument
314+
TypeDescription.Generic typeArgument = genericType.getComponentType();
300315
return typeArgAnnotations.stream().flatMap(annotation -> {
301316
TypeDescription annotationType = annotation.getAnnotationType();
302317
if (isStandardJr380Anno(annotationType)) {
303318
Optional<Method> fragmentMethod = codeFragmentMethod(annotationType, typeArgument.asErasure());
304319
if (fragmentMethod.isPresent()) {
305320
return Stream.of(new GenericTypeInjectionTask(parameter, typeArgument.asErasure(),
306-
fragmentMethod.get(), annotation));
321+
fragmentMethod.get(), annotation, 0));
307322
}
308323
}
309324
return empty();
310325
});
311-
});
326+
}
327+
328+
return empty();
312329
}
313330

314331
private Stream<InjectionTask> jsr380(Parameter parameter, TypeDescription annotation,
@@ -401,13 +418,15 @@ private static class GenericTypeInjectionTask implements InjectionTask {
401418
TypeDescription elementType;
402419
Method fragmentMethod;
403420
AnnotationDescription annotation;
421+
int index;
404422

405423
GenericTypeInjectionTask(Parameter parameter, TypeDescription elementType, Method fragmentMethod,
406-
AnnotationDescription annotation) {
424+
AnnotationDescription annotation, int index) {
407425
this.parameter = parameter;
408426
this.elementType = elementType;
409427
this.fragmentMethod = fragmentMethod;
410428
this.annotation = annotation;
429+
this.index = index;
411430
}
412431

413432
@Override
@@ -428,19 +447,97 @@ private void generateIterationWithValidation(ValidationCodeInjector injector, Me
428447
mv.visitVarInsn(ALOAD, containerParam.offset());
429448
mv.visitJumpInsn(IFNULL, ifNullLabel);
430449

431-
// Generate: for (Object e : parameter)
432-
generateForEachLoopWithValidation(injector, mv, containerParam, annotation);
450+
TypeDescription containerType = containerParam.type();
451+
if (containerType.isArray()) {
452+
generateArrayLoopWithValidation(injector, mv, containerParam, annotation);
453+
} else if (containerType.isAssignableTo(Map.class)) {
454+
generateMapLoopWithValidation(injector, mv, containerParam, annotation);
455+
} else {
456+
// Assume Iterable (Collection, List, Set)
457+
generateForEachLoopWithValidation(injector, mv, containerParam, annotation);
458+
}
433459

434460
mv.visitLabel(ifNullLabel);
435461
}
436462

437-
private void generateForEachLoopWithValidation(ValidationCodeInjector injector, MethodVisitor mv,
463+
private void generateArrayLoopWithValidation(ValidationCodeInjector injector, MethodVisitor mv,
438464
Parameter containerParam, AnnotationDescription annotation) {
439-
// Get the element type from the container's generic type
440-
TypeDescription elementType = getGenericElementType(containerParam);
465+
TypeDescription containerType = containerParam.type();
466+
TypeDescription elementType = containerType.getComponentType();
467+
468+
// Load the array
469+
mv.visitVarInsn(ALOAD, containerParam.offset());
470+
mv.visitInsn(net.bytebuddy.jar.asm.Opcodes.ARRAYLENGTH);
471+
472+
// Store length in a local variable
473+
int lengthVar = containerParam.offset() + 1;
474+
mv.visitVarInsn(net.bytebuddy.jar.asm.Opcodes.ISTORE, lengthVar);
441475

476+
// Initialize index in a local variable
477+
int indexVar = lengthVar + 1;
478+
mv.visitInsn(net.bytebuddy.jar.asm.Opcodes.ICONST_0);
479+
mv.visitVarInsn(net.bytebuddy.jar.asm.Opcodes.ISTORE, indexVar);
480+
481+
Label loopStart = new Label();
482+
Label loopEnd = new Label();
483+
484+
mv.visitLabel(loopStart);
485+
486+
// Loop condition: if index >= length then goto end
487+
mv.visitVarInsn(net.bytebuddy.jar.asm.Opcodes.ILOAD, indexVar);
488+
mv.visitVarInsn(net.bytebuddy.jar.asm.Opcodes.ILOAD, lengthVar);
489+
mv.visitJumpInsn(net.bytebuddy.jar.asm.Opcodes.IF_ICMPGE, loopEnd);
490+
491+
// Load element from array: array[index]
492+
mv.visitVarInsn(ALOAD, containerParam.offset());
493+
mv.visitVarInsn(net.bytebuddy.jar.asm.Opcodes.ILOAD, indexVar);
494+
495+
int elementVar = indexVar + 1;
496+
if (elementType.isPrimitive()) {
497+
Type primitiveType = Type.getType(elementType.getDescriptor());
498+
mv.visitInsn(primitiveType.getOpcode(net.bytebuddy.jar.asm.Opcodes.IALOAD));
499+
mv.visitVarInsn(primitiveType.getOpcode(net.bytebuddy.jar.asm.Opcodes.ISTORE), elementVar);
500+
} else {
501+
mv.visitInsn(net.bytebuddy.jar.asm.Opcodes.AALOAD);
502+
mv.visitVarInsn(ASTORE, elementVar);
503+
}
504+
505+
// Call the fragment method using the injector
506+
injectValidation(injector, mv, containerParam, annotation, elementType, elementVar);
507+
508+
// increment index and goto start
509+
mv.visitIincInsn(indexVar, 1);
510+
mv.visitJumpInsn(GOTO, loopStart);
511+
512+
mv.visitLabel(loopEnd);
513+
}
514+
515+
private void generateMapLoopWithValidation(ValidationCodeInjector injector, MethodVisitor mv,
516+
Parameter containerParam, AnnotationDescription annotation) {
517+
// Load the map
518+
mv.visitVarInsn(ALOAD, containerParam.offset());
519+
520+
if (index == 0) {
521+
// Iterate over keys
522+
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "keySet", "()Ljava/util/Set;", true);
523+
} else {
524+
// Iterate over values
525+
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/Map", "values", "()Ljava/util/Collection;", true);
526+
}
527+
528+
// Now we have an Iterable on the stack, we can use the foreach loop logic
529+
generateIteratorLoopWithValidation(injector, mv, containerParam, annotation, elementType);
530+
}
531+
532+
private void generateForEachLoopWithValidation(ValidationCodeInjector injector, MethodVisitor mv,
533+
Parameter containerParam, AnnotationDescription annotation) {
442534
// Load the container and get its iterator
443535
mv.visitVarInsn(ALOAD, containerParam.offset());
536+
generateIteratorLoopWithValidation(injector, mv, containerParam, annotation, elementType);
537+
}
538+
539+
private void generateIteratorLoopWithValidation(ValidationCodeInjector injector, MethodVisitor mv,
540+
Parameter containerParam, AnnotationDescription annotation, TypeDescription elementType) {
444541
mv.visitMethodInsn(INVOKEINTERFACE, "java/lang/Iterable", "iterator", "()Ljava/util/Iterator;", true);
445542

446543
// Store iterator in a local variable
@@ -471,15 +568,8 @@ private void generateForEachLoopWithValidation(ValidationCodeInjector injector,
471568
// Store element in local variable
472569
mv.visitVarInsn(ASTORE, elementVar);
473570

474-
// Use the injector to call the fragment method with the element
475-
// Create a synthetic parameter representing the element
476-
SyntheticElementParameter elementParam = new SyntheticElementParameter(elementVar, elementType);
477-
478571
// Call the fragment method using the injector
479-
@SuppressWarnings("unchecked")
480-
Class<? extends Jsr380CodeFragment> clazz = (Class<? extends Jsr380CodeFragment>) fragmentMethod
481-
.getDeclaringClass();
482-
injector.useFragmentClass(clazz).inject(mv, elementParam, fragmentMethod, annotation);
572+
injectValidation(injector, mv, containerParam, annotation, elementType, elementVar);
483573

484574
// Loop condition: check if hasNext()
485575
mv.visitLabel(loopTest);
@@ -488,21 +578,28 @@ private void generateForEachLoopWithValidation(ValidationCodeInjector injector,
488578
mv.visitJumpInsn(IFNE, loopStart);
489579
}
490580

491-
private TypeDescription getGenericElementType(Parameter containerParam) {
492-
// Get the generic type from the parameter itself, which has the type
493-
// information
494-
TypeDescription.Generic generic = containerParam.genericType();
495-
return generic == null || generic.getTypeArguments().isEmpty()
496-
? TypeDescription.ForLoadedType.of(Object.class)
497-
: generic.getTypeArguments().get(0).asErasure();
581+
private void injectValidation(ValidationCodeInjector injector, MethodVisitor mv, Parameter containerParam,
582+
AnnotationDescription annotation, TypeDescription elementType, int elementVar) {
583+
// Create a synthetic parameter representing the element
584+
SyntheticElementParameter elementParam = new SyntheticElementParameter(containerParam, elementVar,
585+
elementType);
586+
587+
// Call the fragment method using the injector
588+
@SuppressWarnings("unchecked")
589+
Class<? extends Jsr380CodeFragment> clazz = (Class<? extends Jsr380CodeFragment>) fragmentMethod
590+
.getDeclaringClass();
591+
injector.useFragmentClass(clazz).inject(mv, elementParam, fragmentMethod, annotation);
498592
}
593+
499594
}
500595

501596
private static class SyntheticElementParameter implements Parameters.Parameter {
597+
private final Parameter containerParam;
502598
private final int offset;
503599
private final TypeDescription type;
504600

505-
SyntheticElementParameter(int offset, TypeDescription type) {
601+
SyntheticElementParameter(Parameter containerParam, int offset, TypeDescription type) {
602+
this.containerParam = containerParam;
506603
this.offset = offset;
507604
this.type = type;
508605
}
@@ -524,7 +621,7 @@ public TypeDescription type() {
524621

525622
@Override
526623
public String name() {
527-
return "element";
624+
return containerParam.name() + "[]";
528625
}
529626

530627
@Override
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.github.pfichtner.vaadoo;
2+
3+
import static com.github.pfichtner.vaadoo.Buildable.a;
4+
import static com.github.pfichtner.vaadoo.TestClassBuilder.testClass;
5+
6+
import java.util.List;
7+
import java.util.Map;
8+
9+
import org.junit.jupiter.api.Test;
10+
11+
import com.github.pfichtner.vaadoo.TestClassBuilder.AnnotationDefinition;
12+
import com.github.pfichtner.vaadoo.TestClassBuilder.ConstructorDefinition;
13+
import com.github.pfichtner.vaadoo.TestClassBuilder.DefaultParameterDefinition;
14+
import com.github.pfichtner.vaadoo.TestClassBuilder.TypeDefinition;
15+
16+
import jakarta.validation.constraints.NotBlank;
17+
import jakarta.validation.constraints.NotNull;
18+
19+
class GenericTypesTest {
20+
21+
TestClassBuilder baseTestClass = testClass("com.example.GenericGenerated");
22+
Transformer transformer = new Transformer();
23+
24+
@Test
25+
void arrayWithAnnotatedElements() throws Exception {
26+
var arrayOfNotBlankStrings = TypeDefinition.of(String[].class, String.class,
27+
AnnotationDefinition.of(NotBlank.class));
28+
var constructor = ConstructorDefinition.of(
29+
DefaultParameterDefinition.of(arrayOfNotBlankStrings, AnnotationDefinition.of(NotNull.class))
30+
);
31+
var unloaded = a(baseTestClass.thatImplementsValueObject().withConstructor(constructor));
32+
new Approver(new Transformer()).approveTransformed("arrayWithAnnotatedElements", constructor.params(), unloaded);
33+
}
34+
35+
@Test
36+
void primitiveArrayWithAnnotatedElements() throws Exception {
37+
var arrayOfInts = TypeDefinition.of(int[].class, int.class,
38+
AnnotationDefinition.of(jakarta.validation.constraints.Min.class, Map.of("value", 1L)));
39+
var constructor = ConstructorDefinition.of(
40+
DefaultParameterDefinition.of(arrayOfInts, AnnotationDefinition.of(NotNull.class))
41+
);
42+
var unloaded = a(baseTestClass.thatImplementsValueObject().withConstructor(constructor));
43+
new Approver(new Transformer()).approveTransformed("primitiveArrayWithAnnotatedElements", constructor.params(), unloaded);
44+
}
45+
46+
@Test
47+
void mapWithAnnotatedKeysAndValues() throws Exception {
48+
var mapOfNotBlankStringsToNotNullIntegers = TypeDefinition.of(Map.class,
49+
List.of(String.class, Integer.class),
50+
List.of(
51+
List.of(AnnotationDefinition.of(NotBlank.class)),
52+
List.of(AnnotationDefinition.of(NotNull.class))
53+
));
54+
var constructor = ConstructorDefinition.of(
55+
DefaultParameterDefinition.of(mapOfNotBlankStringsToNotNullIntegers, AnnotationDefinition.of(NotNull.class))
56+
);
57+
var unloaded = a(baseTestClass.thatImplementsValueObject().withConstructor(constructor));
58+
new Approver(new Transformer()).approveTransformed("mapWithAnnotatedKeysAndValues", constructor.params(), unloaded);
59+
}
60+
61+
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,8 +102,12 @@ void alreadyHasValidateMethod() throws Exception {
102102
void genericAnnotatedType() throws Exception {
103103
var listOfNotBlankStrings = TypeDefinition.of(List.class, String.class,
104104
AnnotationDefinition.of(NotBlank.class));
105-
var constructor = ConstructorDefinition
106-
.of(DefaultParameterDefinition.of(listOfNotBlankStrings, AnnotationDefinition.of(NotNull.class)));
105+
// var arrayOfNotBlankStrings = TypeDefinition.of(String[].class, null, AnnotationDefinition.of(NotBlank.class));
106+
var constructor = ConstructorDefinition.of(
107+
DefaultParameterDefinition.of(listOfNotBlankStrings, AnnotationDefinition.of(NotNull.class))
108+
// ,
109+
// DefaultParameterDefinition.of(arrayOfNotBlankStrings, AnnotationDefinition.of(NotNull.class))
110+
);
107111
var unloaded = a(baseTestClass.thatImplementsValueObject().withConstructor(constructor));
108112
new Approver(new Transformer()).approveTransformed("genericAnnotatedType", constructor.params(), unloaded);
109113
}

0 commit comments

Comments
 (0)