Skip to content

Commit 38c92ca

Browse files
committed
Add helper class to map annotations with implementation classes by providing initialization commands (via new)
1 parent d4c9546 commit 38c92ca

File tree

6 files changed

+617
-0
lines changed

6 files changed

+617
-0
lines changed

aptk-api/pom.xml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
2+
<modelVersion>4.0.0</modelVersion>
3+
<parent>
4+
<groupId>io.toolisticon.aptk</groupId>
5+
<artifactId>aptk-parent</artifactId>
6+
<version>0.29.1-SNAPSHOT</version>
7+
</parent>
8+
<artifactId>aptk-api</artifactId>
9+
<name>aptk-api</name>
10+
11+
<build>
12+
<plugins>
13+
<plugin>
14+
<artifactId>maven-compiler-plugin</artifactId>
15+
</plugin>
16+
</plugins>
17+
</build>
18+
19+
</project>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package io.toolisticon.aptk.api;
2+
3+
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
4+
import static java.lang.annotation.RetentionPolicy.RUNTIME;
5+
6+
import java.lang.annotation.Documented;
7+
import java.lang.annotation.Retention;
8+
import java.lang.annotation.Target;
9+
10+
11+
/**
12+
* This annotation supports mapping of annotation attributes to constructor or static method invocations.
13+
* It also allows mapping of annotated method parameters if annotated annotation type can be placed on method parameters.
14+
*
15+
*/
16+
17+
@Documented
18+
@Retention(RUNTIME)
19+
@Target(ANNOTATION_TYPE)
20+
public @interface AnnotationToClassMapper {
21+
22+
Class<?> mappedClass();
23+
24+
/**
25+
* the attribute names to map against method or constructor parameters.
26+
* In case if an annotated annotation can be placed on Method Parameters, an empty String will trigger the usage of the corresponding parameter name.
27+
* Names enclosed in {} will be handled as wildcards (local variable references) which cannot be validated at compile time. So be careful if you use them.
28+
* @return
29+
*/
30+
String[] mappedAttributeNames();
31+
32+
}

pom.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
<modules>
1515

1616
<!-- modules -->
17+
<module>aptk-api</module>
1718
<module>common</module>
1819
<module>tools</module>
1920
<module>example</module>
@@ -29,6 +30,7 @@
2930
<!-- support for toolisticon cute -->
3031
<module>cute</module>
3132

33+
3234
</modules>
3335

3436

@@ -812,6 +814,12 @@
812814
<dependencies>
813815

814816
<!-- internal -->
817+
<dependency>
818+
<groupId>io.toolisticon.aptk</groupId>
819+
<artifactId>aptk-api</artifactId>
820+
<version>${project.version}</version>
821+
</dependency>
822+
815823
<dependency>
816824
<groupId>io.toolisticon.aptk</groupId>
817825
<artifactId>aptk-tools</artifactId>

tools/pom.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616

1717
<dependencies>
1818

19+
<dependency>
20+
<groupId>io.toolisticon.aptk</groupId>
21+
<artifactId>aptk-api</artifactId>
22+
</dependency>
23+
1924
<dependency>
2025
<groupId>io.toolisticon.aptk</groupId>
2126
<artifactId>aptk-common</artifactId>
@@ -57,6 +62,10 @@
5762
<source>${java.compile.source.version}</source>
5863
<target>${java.compile.target.version}</target>
5964
<proc>none</proc>
65+
66+
<compilerArgs>
67+
<arg>-parameters</arg>
68+
</compilerArgs>
6069
</configuration>
6170
</plugin>
6271

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,291 @@
1+
package io.toolisticon.aptk.tools;
2+
3+
import java.util.Arrays;
4+
import java.util.Optional;
5+
import java.util.regex.Matcher;
6+
import java.util.regex.Pattern;
7+
import java.util.stream.Collectors;
8+
9+
import javax.lang.model.element.Modifier;
10+
11+
import io.toolisticon.aptk.api.AnnotationToClassMapper;
12+
import io.toolisticon.aptk.tools.corematcher.AptkCoreMatchers;
13+
import io.toolisticon.aptk.tools.corematcher.ValidationMessage;
14+
import io.toolisticon.aptk.tools.wrapper.AnnotationMirrorWrapper;
15+
import io.toolisticon.aptk.tools.wrapper.AnnotationValueWrapper;
16+
import io.toolisticon.aptk.tools.wrapper.ElementWrapper;
17+
import io.toolisticon.aptk.tools.wrapper.ExecutableElementWrapper;
18+
import io.toolisticon.aptk.tools.wrapper.TypeElementWrapper;
19+
20+
public class AnnotationToClassMapperHelper {
21+
22+
23+
static class AnnotationToClassMapperWrapper {
24+
25+
AnnotationMirrorWrapper annotation;
26+
27+
private AnnotationToClassMapperWrapper(AnnotationMirrorWrapper annotation) {
28+
this.annotation = annotation;
29+
}
30+
31+
TypeMirrorWrapper mappedClass() {
32+
return annotation.getAttribute("mappedClass").get().getClassValue();
33+
}
34+
35+
36+
/**
37+
* the attribute names to map against method or constructor parameters.
38+
* @return
39+
*/
40+
String[] mappedAttributeNames() {
41+
return annotation.getAttributeWithDefault("mappedAttributeNames").getArrayValue().stream().map(e -> e.getStringValue()).collect(Collectors.toList()).toArray(new String[0]);
42+
}
43+
44+
static Optional<AnnotationToClassMapperWrapper> wrap(AnnotationMirrorWrapper annotation) {
45+
46+
if (annotation != null && annotation.asElement().hasAnnotation(AnnotationToClassMapper.class)) {
47+
return Optional.of(new AnnotationToClassMapperWrapper(annotation.asElement().getAnnotationMirror(AnnotationToClassMapper.class).get()));
48+
} else {
49+
return Optional.empty();
50+
}
51+
52+
}
53+
54+
static boolean hasAnnotation(AnnotationMirrorWrapper annotation) {
55+
return wrap(annotation).isPresent();
56+
}
57+
58+
59+
}
60+
61+
62+
private final ElementWrapper<?> elementWrapper;
63+
private final AnnotationMirrorWrapper annotation;
64+
65+
private AnnotationToClassMapperHelper(ElementWrapper<?> elementWrapper, AnnotationMirrorWrapper annotation) {
66+
this.elementWrapper = elementWrapper;
67+
this.annotation = annotation;
68+
}
69+
70+
/**
71+
* Gets an instance of the helper class.
72+
* @param elementWrapper the element wrapper of the annotated element
73+
* @param annotation the annotation mirror of the annotation annotated with AnnotationToClassMapper annotation
74+
* @return
75+
*/
76+
public static AnnotationToClassMapperHelper getInstance(ElementWrapper<?> elementWrapper, AnnotationMirrorWrapper annotation) {
77+
return new AnnotationToClassMapperHelper(elementWrapper, annotation);
78+
}
79+
80+
AnnotationToClassMapperWrapper getValidatorAnnotation() {
81+
return AnnotationToClassMapperWrapper.wrap(this.annotation).get();
82+
}
83+
84+
// visible for testing
85+
AnnotationMirrorWrapper getAnnotationMirrorWrapper() {
86+
return annotation;
87+
}
88+
89+
90+
enum InternalValidationMessages implements ValidationMessage {
91+
92+
ERROR_BROKEN_VALIDATOR_ATTRIBUTE_NAME_MISMATCH ("INVALID_ATTRIBUTE_NAME", "Passed attribute names for annotation '{}' aren't valid: {}"),
93+
ERROR_BROKEN_VALIDATOR_CONSTRUCTOR_PARAMETER_MAPPING ("NO_MATCHING_CONSTRUCTOR", "No matching constructor could be found for class : {}"),
94+
ERROR_BROKEN_VALIDATOR_MISSING_NOARG_CONSTRUCTOR("MISSING_NOARG_CONSTRUCTOR", "Haven't found a noarg constructor for class: {}"),
95+
ERROR_BROKEN_VALIDATOR_INCORRECT_METHOD_PARAMETER_MAPPING("INCORRECT_METHOD_PARAMETER_MAPPING", "Empty attributeNames can only be used if annotated element represents a method parameter"),
96+
;
97+
98+
private final String code;
99+
100+
private final String message;
101+
102+
InternalValidationMessages(String code, String message) {
103+
this.code = code;
104+
this.message = message;
105+
}
106+
107+
108+
@Override
109+
public String getCode() {
110+
return code;
111+
}
112+
113+
@Override
114+
public String getMessage() {
115+
return message;
116+
}
117+
118+
119+
120+
}
121+
122+
private boolean isLocaleVariableName(String name) {
123+
return name.matches("[{].*[}]");
124+
}
125+
126+
private String getLocalVariableName(String name) {
127+
Pattern pattern = Pattern.compile("[{](.*)[}]");
128+
Matcher matcher = pattern.matcher(name);
129+
return matcher.matches()? matcher.group(1): null;
130+
}
131+
132+
133+
/**
134+
* Validates if the annotation has been properly configured and if constructor is available.
135+
* @return true if annotion configuration is correct and constructor is available, otherwise false
136+
*/
137+
public boolean validate() {
138+
// must check if parameter types are assignable
139+
AnnotationToClassMapperWrapper mapperAnnotation = getValidatorAnnotation();
140+
TypeMirrorWrapper mappedTypeMirror = mapperAnnotation.mappedClass();
141+
String[] attributeNamesToConstructorParameterMapping = mapperAnnotation.mappedAttributeNames();
142+
143+
144+
145+
146+
if (attributeNamesToConstructorParameterMapping.length > 0) {
147+
148+
// First check if annotation attribute Names are correct
149+
String[] invalidNames = Arrays.stream(attributeNamesToConstructorParameterMapping).filter(e -> !e.isEmpty() && !isLocaleVariableName(e) && !this.annotation.hasAttribute(e)).toArray(String[]::new);
150+
if (invalidNames.length > 0) {
151+
this.elementWrapper.compilerMessage(this.annotation.unwrap()).asError().write(InternalValidationMessages.ERROR_BROKEN_VALIDATOR_ATTRIBUTE_NAME_MISMATCH, this.annotation.asElement().getSimpleName(), invalidNames);
152+
return false;
153+
}
154+
155+
156+
157+
// loop over constructors and find if one is matching
158+
outer:
159+
for (ExecutableElementWrapper constructor : mappedTypeMirror.getTypeElement().get().getConstructors(Modifier.PUBLIC)) {
160+
161+
if (constructor.getParameters().size() != attributeNamesToConstructorParameterMapping.length) {
162+
continue;
163+
}
164+
165+
int i = 0;
166+
for (String attributeName : attributeNamesToConstructorParameterMapping) {
167+
168+
if(!isLocaleVariableName(attributeName)) {
169+
TypeMirrorWrapper attribute;
170+
if (attributeName.isEmpty()) {
171+
172+
// This will only work if annotated element is a method parameter
173+
if (!this.elementWrapper.isMethodParameter()) {
174+
this.elementWrapper.compilerMessage(annotation.unwrap()).asError().write(InternalValidationMessages.ERROR_BROKEN_VALIDATOR_INCORRECT_METHOD_PARAMETER_MAPPING);
175+
return false;
176+
}
177+
178+
attribute = this.elementWrapper.asType();
179+
} else {
180+
attribute = this.annotation.getAttributeTypeMirror(attributeName).get();
181+
}
182+
183+
if (!attribute.isAssignableTo(constructor.getParameters().get(i).asType())) {
184+
continue outer;
185+
}
186+
}
187+
// next
188+
i = i + 1;
189+
}
190+
191+
// if this is reached, the we have found a matching constructor
192+
return true;
193+
}
194+
195+
this.elementWrapper.compilerMessage(annotation.unwrap()).asError().write(InternalValidationMessages.ERROR_BROKEN_VALIDATOR_CONSTRUCTOR_PARAMETER_MAPPING, mappedTypeMirror.getSimpleName());
196+
return false;
197+
} else {
198+
// must have a noarg constructor or just the default
199+
TypeElementWrapper validatorImplTypeElement = mappedTypeMirror.getTypeElement().get();
200+
boolean hasNoargConstructor = validatorImplTypeElement.filterEnclosedElements().applyFilter(AptkCoreMatchers.IS_CONSTRUCTOR).applyFilter(AptkCoreMatchers.HAS_NO_PARAMETERS).getResult().size() == 1;
201+
boolean hasJustDefaultConstructor = validatorImplTypeElement.filterEnclosedElements().applyFilter(AptkCoreMatchers.IS_CONSTRUCTOR).hasSize(0);
202+
203+
if (!(hasNoargConstructor || hasJustDefaultConstructor)) {
204+
this.elementWrapper.compilerMessage(annotation.unwrap()).asError().write(InternalValidationMessages.ERROR_BROKEN_VALIDATOR_MISSING_NOARG_CONSTRUCTOR, validatorImplTypeElement.getSimpleName());
205+
return false;
206+
}
207+
}
208+
209+
210+
211+
return true;
212+
}
213+
214+
215+
/**
216+
* Creates the command needed to initialize an instance based on annotation configuration.
217+
* @return
218+
*/
219+
public String createInstanceInitializationCommand() {
220+
StringBuilder stringBuilder = new StringBuilder();
221+
222+
String genericTypeString = "";
223+
// Need to handle generic validator separately
224+
if (getValidatorAnnotation().mappedClass().getTypeElement().get().hasTypeParameters()) {
225+
TypeMirrorWrapper annotatedElementsTypeMirror = this.elementWrapper.asType();
226+
if (annotatedElementsTypeMirror.isCollection() || annotatedElementsTypeMirror.isIterable() || annotatedElementsTypeMirror.isArray()) {
227+
genericTypeString = "<" +annotatedElementsTypeMirror.getWrappedComponentType().getTypeDeclaration() + ">";
228+
} else {
229+
genericTypeString = "<" + annotatedElementsTypeMirror.getTypeDeclaration() + ">";
230+
}
231+
232+
}
233+
234+
stringBuilder.append("new ").append(getValidatorAnnotation().mappedClass().getQualifiedName()).append(genericTypeString).append("(");
235+
236+
boolean isFirst = true;
237+
for (String attributeName : getValidatorAnnotation().mappedAttributeNames()) {
238+
239+
// add separator
240+
if (!isFirst) {
241+
stringBuilder.append(", ");
242+
} else {
243+
isFirst = false;
244+
}
245+
246+
if (attributeName.isEmpty()) {
247+
stringBuilder.append(this.elementWrapper.getSimpleName());
248+
} else if (isLocaleVariableName(attributeName)) {
249+
stringBuilder.append(getLocalVariableName(attributeName));
250+
} else {
251+
stringBuilder.append(getValidatorExpressionAttributeValueStringRepresentation(annotation.getAttributeWithDefault(attributeName), annotation.getAttributeTypeMirror(attributeName).get()));
252+
}
253+
254+
}
255+
256+
stringBuilder.append(")");
257+
return stringBuilder.toString();
258+
}
259+
260+
261+
String getValidatorExpressionAttributeValueStringRepresentation(AnnotationValueWrapper annotationValueWrapper, TypeMirrorWrapper annotationAttributeTypeMirror) {
262+
263+
if (annotationValueWrapper.isArray()) {
264+
return annotationValueWrapper.getArrayValue().stream().map(e -> getValidatorExpressionAttributeValueStringRepresentation(e, annotationAttributeTypeMirror.getWrappedComponentType())).collect(Collectors.joining(", ", "new " + annotationAttributeTypeMirror.getWrappedComponentType().getQualifiedName() + "[]{", "}"));
265+
} else if (annotationValueWrapper.isString()) {
266+
return "\"" + annotationValueWrapper.getStringValue() + "\"";
267+
} else if (annotationValueWrapper.isClass()) {
268+
return annotationValueWrapper.getClassValue().getQualifiedName() + ".class";
269+
} else if (annotationValueWrapper.isInteger()) {
270+
return annotationValueWrapper.getIntegerValue().toString();
271+
} else if (annotationValueWrapper.isLong()) {
272+
return annotationValueWrapper.getLongValue() + "L";
273+
} else if (annotationValueWrapper.isBoolean()) {
274+
return annotationValueWrapper.getBooleanValue().toString();
275+
} else if (annotationValueWrapper.isFloat()) {
276+
return annotationValueWrapper.getFloatValue() + "f";
277+
} else if (annotationValueWrapper.isDouble()) {
278+
return annotationValueWrapper.getDoubleValue().toString();
279+
} else if (annotationValueWrapper.isEnum()) {
280+
return TypeElementWrapper.toTypeElement(annotationValueWrapper.getEnumValue().getEnclosingElement().get()).getQualifiedName() + "." + annotationValueWrapper.getEnumValue().getSimpleName();
281+
} else {
282+
throw new IllegalStateException("Got unsupported annotation attribute type : USUALLY THIS CANNOT HAPPEN.");
283+
}
284+
285+
}
286+
287+
public String getStringRepresentationOfAnnotation() {
288+
return annotation.getStringRepresentation();
289+
}
290+
291+
}

0 commit comments

Comments
 (0)