Skip to content

Commit

Permalink
Add helper class to map annotations with implementation classes by pr…
Browse files Browse the repository at this point in the history
…oviding initialization commands (via new)
  • Loading branch information
tobiasstamann committed Jan 21, 2025
1 parent d4c9546 commit ca1630d
Show file tree
Hide file tree
Showing 6 changed files with 617 additions and 0 deletions.
19 changes: 19 additions & 0 deletions aptk-api/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.toolisticon.aptk</groupId>
<artifactId>aptk-parent</artifactId>
<version>0.29.1-SNAPSHOT</version>
</parent>
<artifactId>aptk-api</artifactId>
<name>aptk-api</name>

<build>
<plugins>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.toolisticon.aptk.api;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;


/**
* This annotation supports mapping of annotation attributes to constructor or static method invocations.
* It also allows mapping of annotated method parameters if annotated annotation type can be placed on method parameters.
*
*/

@Documented
@Retention(RUNTIME)
@Target(ANNOTATION_TYPE)
public @interface AnnotationToClassMapper {

Class<?> mappedClass();

/**
* the attribute names to map against method or constructor parameters.
* In case if an annotated annotation can be placed on Method Parameters, an empty String will trigger the usage of the corresponding parameter name.
* 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.
* @return
*/
String[] mappedAttributeNames();

}
8 changes: 8 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
<modules>

<!-- modules -->
<module>aptk-api</module>
<module>common</module>
<module>tools</module>
<module>example</module>
Expand All @@ -29,6 +30,7 @@
<!-- support for toolisticon cute -->
<module>cute</module>


</modules>


Expand Down Expand Up @@ -812,6 +814,12 @@
<dependencies>

<!-- internal -->
<dependency>
<groupId>io.toolisticon.aptk</groupId>
<artifactId>aptk-api</artifactId>
<version>${project.version}</version>
</dependency>

<dependency>
<groupId>io.toolisticon.aptk</groupId>
<artifactId>aptk-tools</artifactId>
Expand Down
9 changes: 9 additions & 0 deletions tools/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@

<dependencies>

<dependency>
<groupId>io.toolisticon.aptk</groupId>
<artifactId>aptk-api</artifactId>
</dependency>

<dependency>
<groupId>io.toolisticon.aptk</groupId>
<artifactId>aptk-common</artifactId>
Expand Down Expand Up @@ -57,6 +62,10 @@
<source>${java.compile.source.version}</source>
<target>${java.compile.target.version}</target>
<proc>none</proc>

<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,291 @@
package io.toolisticon.aptk.tools;

import java.util.Arrays;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import javax.lang.model.element.Modifier;

import io.toolisticon.aptk.api.AnnotationToClassMapper;
import io.toolisticon.aptk.tools.corematcher.AptkCoreMatchers;
import io.toolisticon.aptk.tools.corematcher.ValidationMessage;
import io.toolisticon.aptk.tools.wrapper.AnnotationMirrorWrapper;
import io.toolisticon.aptk.tools.wrapper.AnnotationValueWrapper;
import io.toolisticon.aptk.tools.wrapper.ElementWrapper;
import io.toolisticon.aptk.tools.wrapper.ExecutableElementWrapper;
import io.toolisticon.aptk.tools.wrapper.TypeElementWrapper;

public class AnnotationToClassMapperHelper {


static class AnnotationToClassMapperWrapper {

AnnotationMirrorWrapper annotation;

private AnnotationToClassMapperWrapper(AnnotationMirrorWrapper annotation) {
this.annotation = annotation;
}

TypeMirrorWrapper mappedClass() {
return annotation.getAttribute("mappedClass").get().getClassValue();
}


/**
* the attribute names to map against method or constructor parameters.
* @return
*/
String[] mappedAttributeNames() {
return annotation.getAttributeWithDefault("mappedAttributeNames").getArrayValue().stream().map(e -> e.getStringValue()).collect(Collectors.toList()).toArray(new String[0]);
}

static Optional<AnnotationToClassMapperWrapper> wrap(AnnotationMirrorWrapper annotation) {

if (annotation != null && annotation.asElement().hasAnnotation(AnnotationToClassMapper.class)) {
return Optional.of(new AnnotationToClassMapperWrapper(annotation.asElement().getAnnotationMirror(AnnotationToClassMapper.class).get()));
} else {
return Optional.empty();
}

}

static boolean hasAnnotation(AnnotationMirrorWrapper annotation) {
return wrap(annotation).isPresent();
}


}


private final ElementWrapper<?> elementWrapper;
private final AnnotationMirrorWrapper annotation;

private AnnotationToClassMapperHelper(ElementWrapper<?> elementWrapper, AnnotationMirrorWrapper annotation) {
this.elementWrapper = elementWrapper;
this.annotation = annotation;
}

/**
* Gets an instance of the helper class.
* @param elementWrapper the element wrapper of the annotated element
* @param annotation the annotation mirror of the annotation annotated with AnnotationToClassMapper annotation
* @return
*/
public static AnnotationToClassMapperHelper getInstance(ElementWrapper<?> elementWrapper, AnnotationMirrorWrapper annotation) {
return new AnnotationToClassMapperHelper(elementWrapper, annotation);
}

AnnotationToClassMapperWrapper getValidatorAnnotation() {
return AnnotationToClassMapperWrapper.wrap(this.annotation).get();
}

// visible for testing
AnnotationMirrorWrapper getAnnotationMirrorWrapper() {
return annotation;
}


enum InternalValidationMessages implements ValidationMessage {

ERROR_BROKEN_VALIDATOR_ATTRIBUTE_NAME_MISMATCH ("INVALID_ATTRIBUTE_NAME", "Passed attribute names for annotation '{}' aren't valid: {}"),
ERROR_BROKEN_VALIDATOR_CONSTRUCTOR_PARAMETER_MAPPING ("NO_MATCHING_CONSTRUCTOR", "No matching constructor could be found for class : {}"),
ERROR_BROKEN_VALIDATOR_MISSING_NOARG_CONSTRUCTOR("MISSING_NOARG_CONSTRUCTOR", "Haven't found a noarg constructor for class: {}"),
ERROR_BROKEN_VALIDATOR_INCORRECT_METHOD_PARAMETER_MAPPING("INCORRECT_METHOD_PARAMETER_MAPPING", "Empty attributeNames can only be used if annotated element represents a method parameter"),
;

private final String code;

private final String message;

InternalValidationMessages(String code, String message) {
this.code = code;
this.message = message;
}


@Override
public String getCode() {
return code;
}

@Override
public String getMessage() {
return message;
}



}

private boolean isLocaleVariableName(String name) {
return name.matches("[{].*[}]");
}

private String getLocalVariableName(String name) {
Pattern pattern = Pattern.compile("[{](.*)[}]");
Matcher matcher = pattern.matcher(name);
return matcher.matches()? matcher.group(1): null;
}


/**
* Validates if the annotation has been properly configured and if constructor is available.
* @return true if annotion configuration is correct and constructor is available, otherwise false
*/
public boolean validate() {
// must check if parameter types are assignable
AnnotationToClassMapperWrapper mapperAnnotation = getValidatorAnnotation();
TypeMirrorWrapper mappedTypeMirror = mapperAnnotation.mappedClass();
String[] attributeNamesToConstructorParameterMapping = mapperAnnotation.mappedAttributeNames();




if (attributeNamesToConstructorParameterMapping.length > 0) {

// First check if annotation attribute Names are correct
String[] invalidNames = Arrays.stream(attributeNamesToConstructorParameterMapping).filter(e -> !e.isEmpty() && !isLocaleVariableName(e) && !this.annotation.hasAttribute(e)).toArray(String[]::new);
if (invalidNames.length > 0) {
this.elementWrapper.compilerMessage(this.annotation.unwrap()).asError().write(InternalValidationMessages.ERROR_BROKEN_VALIDATOR_ATTRIBUTE_NAME_MISMATCH, this.annotation.asElement().getSimpleName(), invalidNames);
return false;
}



// loop over constructors and find if one is matching
outer:
for (ExecutableElementWrapper constructor : mappedTypeMirror.getTypeElement().get().getConstructors(Modifier.PUBLIC)) {

if (constructor.getParameters().size() != attributeNamesToConstructorParameterMapping.length) {
continue;
}

int i = 0;
for (String attributeName : attributeNamesToConstructorParameterMapping) {

if(!isLocaleVariableName(attributeName)) {
TypeMirrorWrapper attribute;
if (attributeName.isEmpty()) {

// This will only work if annotated element is a method parameter
if (!this.elementWrapper.isMethodParameter()) {
this.elementWrapper.compilerMessage(annotation.unwrap()).asError().write(InternalValidationMessages.ERROR_BROKEN_VALIDATOR_INCORRECT_METHOD_PARAMETER_MAPPING);
return false;
}

attribute = this.elementWrapper.asType();
} else {
attribute = this.annotation.getAttributeTypeMirror(attributeName).get();
}

if (!attribute.isAssignableTo(constructor.getParameters().get(i).asType())) {
continue outer;
}
}
// next
i = i + 1;
}

// if this is reached, the we have found a matching constructor
return true;
}

this.elementWrapper.compilerMessage(annotation.unwrap()).asError().write(InternalValidationMessages.ERROR_BROKEN_VALIDATOR_CONSTRUCTOR_PARAMETER_MAPPING, mappedTypeMirror.getSimpleName());
return false;
} else {
// must have a noarg constructor or just the default
TypeElementWrapper validatorImplTypeElement = mappedTypeMirror.getTypeElement().get();
boolean hasNoargConstructor = validatorImplTypeElement.filterEnclosedElements().applyFilter(AptkCoreMatchers.IS_CONSTRUCTOR).applyFilter(AptkCoreMatchers.HAS_NO_PARAMETERS).getResult().size() == 1;
boolean hasJustDefaultConstructor = validatorImplTypeElement.filterEnclosedElements().applyFilter(AptkCoreMatchers.IS_CONSTRUCTOR).hasSize(0);

if (!(hasNoargConstructor || hasJustDefaultConstructor)) {
this.elementWrapper.compilerMessage(annotation.unwrap()).asError().write(InternalValidationMessages.ERROR_BROKEN_VALIDATOR_MISSING_NOARG_CONSTRUCTOR, validatorImplTypeElement.getSimpleName());
return false;
}
}



return true;
}


/**
* Creates the command needed to initialize an instance based on annotation configuration.
* @return
*/
public String createInstanceInitializationCommand() {
StringBuilder stringBuilder = new StringBuilder();

String genericTypeString = "";
// Need to handle generic validator separately
if (getValidatorAnnotation().mappedClass().getTypeElement().get().hasTypeParameters()) {
TypeMirrorWrapper annotatedElementsTypeMirror = this.elementWrapper.asType();
if (annotatedElementsTypeMirror.isCollection() || annotatedElementsTypeMirror.isIterable() || annotatedElementsTypeMirror.isArray()) {
genericTypeString = "<" +annotatedElementsTypeMirror.getWrappedComponentType().getTypeDeclaration() + ">";
} else {
genericTypeString = "<" + annotatedElementsTypeMirror.getTypeDeclaration() + ">";
}

}

stringBuilder.append("new ").append(getValidatorAnnotation().mappedClass().getQualifiedName()).append(genericTypeString).append("(");

boolean isFirst = true;
for (String attributeName : getValidatorAnnotation().mappedAttributeNames()) {

// add separator
if (!isFirst) {
stringBuilder.append(", ");
} else {
isFirst = false;
}

if (attributeName.isEmpty()) {
stringBuilder.append(this.elementWrapper.getSimpleName());
} else if (isLocaleVariableName(attributeName)) {
stringBuilder.append(getLocalVariableName(attributeName));
} else {
stringBuilder.append(getValidatorExpressionAttributeValueStringRepresentation(annotation.getAttributeWithDefault(attributeName), annotation.getAttributeTypeMirror(attributeName).get()));
}

}

stringBuilder.append(")");
return stringBuilder.toString();
}


String getValidatorExpressionAttributeValueStringRepresentation(AnnotationValueWrapper annotationValueWrapper, TypeMirrorWrapper annotationAttributeTypeMirror) {

if (annotationValueWrapper.isArray()) {
return annotationValueWrapper.getArrayValue().stream().map(e -> getValidatorExpressionAttributeValueStringRepresentation(e, annotationAttributeTypeMirror.getWrappedComponentType())).collect(Collectors.joining(", ", "new " + annotationAttributeTypeMirror.getWrappedComponentType().getQualifiedName() + "[]{", "}"));
} else if (annotationValueWrapper.isString()) {
return "\"" + annotationValueWrapper.getStringValue() + "\"";
} else if (annotationValueWrapper.isClass()) {
return annotationValueWrapper.getClassValue().getQualifiedName() + ".class";
} else if (annotationValueWrapper.isInteger()) {
return annotationValueWrapper.getIntegerValue().toString();
} else if (annotationValueWrapper.isLong()) {
return annotationValueWrapper.getLongValue() + "L";
} else if (annotationValueWrapper.isBoolean()) {
return annotationValueWrapper.getBooleanValue().toString();
} else if (annotationValueWrapper.isFloat()) {
return annotationValueWrapper.getFloatValue() + "f";
} else if (annotationValueWrapper.isDouble()) {
return annotationValueWrapper.getDoubleValue().toString();
} else if (annotationValueWrapper.isEnum()) {
return TypeElementWrapper.toTypeElement(annotationValueWrapper.getEnumValue().getEnclosingElement().get()).getQualifiedName() + "." + annotationValueWrapper.getEnumValue().getSimpleName();
} else {
throw new IllegalStateException("Got unsupported annotation attribute type : USUALLY THIS CANNOT HAPPEN.");
}

}

public String getStringRepresentationOfAnnotation() {
return annotation.getStringRepresentation();
}

}
Loading

0 comments on commit ca1630d

Please sign in to comment.