Skip to content

Commit 45a89af

Browse files
authored
Feature/parameter types validator/processor (#150)
* Annotation processor to validate converter and validators types * added unit test for annotation validation processor * code cleanup * add position hints for errors * add position hints for errors * code cleanup
1 parent 82c51b2 commit 45a89af

5 files changed

Lines changed: 227 additions & 0 deletions

File tree

pom.xml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,12 @@
149149
<version>3.0.2</version>
150150
<scope>test</scope>
151151
</dependency>
152+
<dependency>
153+
<groupId>io.toolisticon.cute</groupId>
154+
<artifactId>cute</artifactId>
155+
<version>1.9.0</version>
156+
<scope>test</scope>
157+
</dependency>
152158
</dependencies>
153159

154160
<build>
@@ -158,6 +164,11 @@
158164
<groupId>org.apache.maven.plugins</groupId>
159165
<artifactId>maven-compiler-plugin</artifactId>
160166
<version>3.14.1</version>
167+
<configuration>
168+
<!-- this is needed to skip our annotation processor before it's compiled here, because it's not
169+
compiled yet and would break the build-->
170+
<proc>none</proc>
171+
</configuration>
161172
</plugin>
162173
<plugin>
163174
<groupId>org.apache.maven.plugins</groupId>
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
package com.github.joschi.jadconfig;
2+
3+
import javax.annotation.processing.AbstractProcessor;
4+
import javax.annotation.processing.ProcessingEnvironment;
5+
import javax.annotation.processing.RoundEnvironment;
6+
import javax.lang.model.SourceVersion;
7+
import javax.lang.model.element.AnnotationMirror;
8+
import javax.lang.model.element.AnnotationValue;
9+
import javax.lang.model.element.Element;
10+
import javax.lang.model.element.ElementKind;
11+
import javax.lang.model.element.ExecutableElement;
12+
import javax.lang.model.element.TypeElement;
13+
import javax.lang.model.element.VariableElement;
14+
import javax.lang.model.type.PrimitiveType;
15+
import javax.lang.model.type.TypeMirror;
16+
import javax.lang.model.util.Types;
17+
import java.util.List;
18+
import java.util.Optional;
19+
import java.util.Set;
20+
21+
public class ParameterTypesValidator extends AbstractProcessor {
22+
23+
@Override
24+
public Set<String> getSupportedAnnotationTypes() {
25+
return Set.of(Parameter.class.getCanonicalName());
26+
}
27+
28+
@Override
29+
public SourceVersion getSupportedSourceVersion() {
30+
return SourceVersion.RELEASE_8;
31+
}
32+
33+
@Override
34+
public synchronized void init(ProcessingEnvironment processingEnv) {
35+
super.init(processingEnv);
36+
}
37+
38+
@Override
39+
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
40+
Types typeUtils = processingEnv.getTypeUtils();
41+
42+
annotations.stream()
43+
.flatMap(annotation -> roundEnv.getElementsAnnotatedWith(annotation).stream())
44+
.map(element -> (VariableElement) element)
45+
.forEach(element -> {
46+
processField(element, typeUtils);
47+
});
48+
return false; // do not claim this annotation, let other processors handle it as well
49+
}
50+
51+
private void processField(VariableElement field, Types typeUtils) {
52+
final String fieldName = getFieldName(field);
53+
TypeMirror fieldType = getBoxedType(typeUtils, field.asType());
54+
55+
final AnnotationMirror annotationMirror = getParameterAnnotation(field);
56+
final String parameterName = getParameterValue(annotationMirror);
57+
58+
verifyConverterType(annotationMirror, typeUtils, field, fieldType, parameterName, fieldName);
59+
verifyValidators(annotationMirror, typeUtils, field, fieldType, parameterName, fieldName);
60+
}
61+
62+
private static String getParameterValue(AnnotationMirror annotationMirror) {
63+
return annotationMirror.getElementValues().entrySet().stream()
64+
.filter(entry -> entry.getKey().getSimpleName().toString().equals("value"))
65+
.map(entry -> (String) entry.getValue().getValue())
66+
.findFirst()
67+
.orElseThrow(() -> new IllegalStateException("Value is mandatory!"));
68+
}
69+
70+
private static AnnotationMirror getParameterAnnotation(Element element) {
71+
return element.getAnnotationMirrors()
72+
.stream()
73+
.filter(mirror -> mirror.getAnnotationType().toString().equals(
74+
Parameter.class.getCanonicalName())).findFirst()
75+
.orElseThrow(() -> new IllegalStateException("This should not happen, the element should be always annotated with Parameter annotation"));
76+
}
77+
78+
private static String getFieldName(VariableElement field) {
79+
final String className = getParentClassName(field);
80+
return className + "#" + field.getSimpleName().toString();
81+
}
82+
83+
private static String getParentClassName(VariableElement field) {
84+
final Element enclosingElement = field.getEnclosingElement();
85+
if (enclosingElement.getKind() == ElementKind.CLASS) {
86+
final TypeElement classElement = (TypeElement) enclosingElement;
87+
return classElement.getQualifiedName().toString();
88+
} else {
89+
return enclosingElement.getSimpleName().toString();
90+
}
91+
}
92+
93+
private void verifyConverterType(AnnotationMirror annotationMirror, Types types, VariableElement field, TypeMirror fieldType, String parameterName, String fieldName) {
94+
getConverter(annotationMirror).ifPresent(converterValue -> {
95+
TypeElement converterType = (TypeElement) types.asElement(converterValue);
96+
List<? extends Element> members = processingEnv.getElementUtils().getAllMembers(converterType);
97+
ExecutableElement convertFromMethod = getConvertFromMethod(members);
98+
99+
final TypeMirror converterReturnType = convertFromMethod.getReturnType();
100+
if (!types.isSameType(converterReturnType, fieldType)) {
101+
processingEnv.getMessager().printError("Property " + parameterName + " assigned to field " + fieldName + " has type " + fieldType + " but converter expects " + converterReturnType, field);
102+
}
103+
});
104+
}
105+
106+
private static ExecutableElement getConvertFromMethod(List<? extends Element> members) {
107+
return members.stream()
108+
.filter(e -> e.getKind() == ElementKind.METHOD)
109+
.map(e -> (ExecutableElement) e)
110+
.filter(m -> m.getSimpleName().contentEquals("convertFrom"))
111+
.findFirst()
112+
.orElseThrow(() -> new IllegalStateException("This should not happen, converter should contain a convertFrom method"));
113+
}
114+
115+
private static Optional<TypeMirror> getConverter(AnnotationMirror annotationMirror) {
116+
return annotationMirror.getElementValues().entrySet().stream()
117+
.filter(entry -> entry.getKey().getSimpleName().toString().equals("converter"))
118+
.map(entry -> (TypeMirror) entry.getValue().getValue())
119+
.findFirst();
120+
}
121+
122+
private void verifyValidators(AnnotationMirror annotationMirror, Types types, VariableElement field, TypeMirror fieldType, String parameterName, String fieldName) {
123+
annotationMirror.getElementValues().entrySet().stream()
124+
.filter(entry -> entry.getKey().getSimpleName().toString().equals("validators"))
125+
.flatMap(entry -> {
126+
List<? extends AnnotationValue> values = (List<? extends AnnotationValue>) entry.getValue().getValue();
127+
return values.stream().map(v -> (TypeMirror) v.getValue());
128+
})
129+
.map(type -> (TypeElement) types.asElement(type))
130+
.forEach(validatorType -> verifyValidator(types, field, fieldType, parameterName, fieldName, validatorType));
131+
}
132+
133+
private void verifyValidator(Types types, VariableElement field, TypeMirror fieldType, String parameterName, String fieldName, TypeElement validatorType) {
134+
final ExecutableElement validatorMethod = getValidateMethod(validatorType);
135+
final TypeMirror acceptedValidatorType = getValidatorType(types, validatorMethod);
136+
137+
if (!types.isSameType(acceptedValidatorType, fieldType)) {
138+
processingEnv.getMessager().printError("Property " + parameterName + " assigned to field " + fieldName + " has type " + fieldType + " but validator expects " + acceptedValidatorType, field);
139+
}
140+
}
141+
142+
private ExecutableElement getValidateMethod(TypeElement validatorType) {
143+
List<? extends Element> members =
144+
processingEnv.getElementUtils().getAllMembers(validatorType);
145+
146+
return members.stream()
147+
.filter(e -> e.getKind() == ElementKind.METHOD)
148+
.map(e -> (ExecutableElement) e)
149+
.filter(m -> m.getSimpleName().contentEquals("validate"))
150+
.findFirst()
151+
.orElseThrow(() -> new IllegalStateException("This should not happen"));
152+
}
153+
154+
private static TypeMirror getValidatorType(Types types, ExecutableElement convertFromMethod) {
155+
final List<? extends VariableElement> converterReturnType = convertFromMethod.getParameters();
156+
final TypeMirror acceptedValidatorType = converterReturnType.get(1).asType();
157+
return getBoxedType(types, acceptedValidatorType);
158+
}
159+
160+
private static TypeMirror getBoxedType(Types types, TypeMirror type) {
161+
if (type.getKind().isPrimitive()) {
162+
TypeElement boxed = types.boxedClass((PrimitiveType) type);
163+
return boxed.asType();
164+
} else {
165+
return type;
166+
}
167+
}
168+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
com.github.joschi.jadconfig.ParameterTypesValidator
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.github.joschi.jadconfig;
2+
3+
import io.toolisticon.cute.Cute;
4+
import io.toolisticon.cute.CuteApi;
5+
import org.junit.jupiter.api.BeforeEach;
6+
import org.junit.jupiter.api.Test;
7+
8+
class ParameterTypesValidatorTest {
9+
10+
CuteApi.BlackBoxTestSourceFilesInterface compileTestBuilder;
11+
12+
@BeforeEach
13+
public void init() {
14+
compileTestBuilder = Cute
15+
.blackBoxTest()
16+
.given()
17+
.processor(ParameterTypesValidator.class);
18+
}
19+
20+
@Test
21+
public void testPropertyValidatorProcessor() {
22+
compileTestBuilder
23+
.andSourceFiles("/com/github/joschi/jadconfig/MyAnnotationValidatorTestClass.java")
24+
.whenCompiled().thenExpectThat()
25+
.compilationFails()
26+
.andThat()
27+
.compilerMessage()
28+
.ofKindError().atLine(9).atColumn(17).equals("Property my_int assigned to field MyAnnotationValidatorTestClass#myIntField has type java.lang.Integer but converter expects java.lang.Long")
29+
.andThat()
30+
.compilerMessage()
31+
.ofKindError().atLine(12).atColumn(22).equals("Property my_duration assigned to field MyAnnotationValidatorTestClass#myDurationField has type java.time.Duration but validator expects com.github.joschi.jadconfig.util.Duration")
32+
.executeTest();
33+
}
34+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import com.github.joschi.jadconfig.converters.LongConverter;
2+
import com.github.joschi.jadconfig.Parameter;
3+
import com.github.joschi.jadconfig.validators.PositiveDurationValidator;
4+
5+
import java.time.Duration;
6+
7+
public class MyAnnotationValidatorTestClass {
8+
@Parameter(value = "my_int", converter = LongConverter.class)
9+
private int myIntField = 10;
10+
11+
@Parameter(value = "my_duration", validators = {PositiveDurationValidator.class})
12+
private Duration myDurationField = Duration.ofDays(1);
13+
}

0 commit comments

Comments
 (0)