Resolve "inherit annotations values"

This commit is contained in:
Hari Nair
2022-10-02 19:09:59 +00:00
parent 3cd4e9b9a7
commit 28f03eb287
5 changed files with 116 additions and 96 deletions

View File

@@ -149,7 +149,7 @@ The `@Comment` annotation specifies the comment that appears in the configuratio
### DefaultValue
The `@DefaultValue` annotation is a String used to initialize the parameter. This is a required annotation. Strings and primitives (and their corresponding wrapper types) are read natively. Other objects will need to use the `@ParserClass` annotation to specify a class which implements the `jackfruit.annotations.Parser` interface to convert the object to and from a String.
The `@DefaultValue` annotation is a String used to initialize the parameter. This is a required annotation. If it is absent no other annotations on this method will be processed. Strings and primitives (and their corresponding wrapper types) are read natively. Other objects will need to use the `@ParserClass` annotation to specify a class which implements the `jackfruit.annotations.Parser` interface to convert the object to and from a String.
### Key

View File

@@ -4,14 +4,12 @@ import java.util.List;
import jackfruit.annotations.Comment;
import jackfruit.annotations.DefaultValue;
import jackfruit.annotations.Jackfruit;
import jackfruit.annotations.Key;
import jackfruit.annotations.ParserClass;
@Jackfruit(prefix = "prefix")
public abstract class DemoClass extends DemoSuperClass {
@Key("key")
@Comment("This method overrides one defined in DemoSuperClass")
@Comment("This method's key name is inherited from DemoSuperSuperClass")
@DefaultValue("1")
@Override
public abstract int intMethod();
@@ -29,15 +27,14 @@ public abstract class DemoClass extends DemoSuperClass {
@DefaultValue("Default String")
public abstract String StringMethod();
@Comment("This string is serialized into an object\n\tThis comment contains a newline character, and this line starts with a tab.")
@DefaultValue("serialized string")
@ParserClass(SomeRandomClassParser.class)
public abstract SomeRandomClass randomClass();
@Comment("List of Doubles")
@DefaultValue("0. 5.34 17")
public abstract List<Double> doubles();
@DefaultValue("set in DemoClass, parser inherited from DemoSuperClass")
@Override
public abstract SomeRandomClass randomClass();
@Comment("List of RandomClass")
@DefaultValue("""
obj1

View File

@@ -3,6 +3,7 @@ package jackfruit.demo;
import jackfruit.annotations.Comment;
import jackfruit.annotations.DefaultValue;
import jackfruit.annotations.Jackfruit;
import jackfruit.annotations.ParserClass;
// prefix can be superseded by inherited classes
@Jackfruit
@@ -17,4 +18,9 @@ public abstract class DemoSuperClass extends DemoSuperSuperClass {
@DefaultValue("2")
@Override
public abstract int intMethod();
@Comment("This string is serialized into an object\n\tThis comment contains a newline character, and this line starts with a tab.")
@DefaultValue("serialized string")
@ParserClass(SomeRandomClassParser.class)
public abstract SomeRandomClass randomClass();
}

View File

@@ -3,6 +3,7 @@ package jackfruit.demo;
import jackfruit.annotations.Comment;
import jackfruit.annotations.DefaultValue;
import jackfruit.annotations.Jackfruit;
import jackfruit.annotations.Key;
@Jackfruit
public abstract class DemoSuperSuperClass {
@@ -17,5 +18,6 @@ public abstract class DemoSuperSuperClass {
@Comment("from DemoSuperSuperClass")
@DefaultValue("3")
@Key("key")
public abstract int intMethod();
}

View File

@@ -22,6 +22,7 @@ import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.Name;
import javax.lang.model.element.PackageElement;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
@@ -66,16 +67,19 @@ import jackfruit.annotations.ParserClass;
@AutoService(Processor.class)
public class ConfigProcessor extends AbstractProcessor {
private List<Class<? extends Annotation>> supportedMethodAnnotations;
private Messager messager;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
List<Class<? extends Annotation>> supportedMethodAnnotations = new ArrayList<>();
supportedMethodAnnotations = new ArrayList<>();
supportedMethodAnnotations.add(Comment.class);
supportedMethodAnnotations.add(DefaultValue.class);
supportedMethodAnnotations.add(Key.class);
supportedMethodAnnotations.add(ParserClass.class);
Messager messager = processingEnv.getMessager();
messager = processingEnv.getMessager();
// find interfaces or abstract classes with the Jackfruit annotation
List<Element> annotatedElements = roundEnv.getElementsAnnotatedWith(Jackfruit.class).stream()
@@ -116,7 +120,7 @@ public class ConfigProcessor extends AbstractProcessor {
*/
// this contains a hierarchy of parent classes
List<DeclaredType> superClasses = new ArrayList<>();
List<DeclaredType> classHierarchy = new ArrayList<>();
{
TypeElement thisElement = annotatedType;
TypeMirror superClass = thisElement.getSuperclass();
@@ -127,22 +131,27 @@ public class ConfigProcessor extends AbstractProcessor {
break;
}
superClasses.add(superType);
classHierarchy.add(superType);
thisElement = (TypeElement) superType.asElement();
superClass = thisElement.getSuperclass();
}
}
Collections.reverse(superClasses);
superClasses.add((DeclaredType) annotatedType.asType());
Collections.reverse(classHierarchy);
classHierarchy.add((DeclaredType) annotatedType.asType());
// create a list of methods annotated with DefaultValue - ignore everything else
Map<String, ExecutableElement> enclosedMethods = new LinkedHashMap<>();
for (DeclaredType superClass : superClasses) {
for (Element e : superClass.asElement().getEnclosedElements()) {
Map<Name, ExecutableElement> enclosedMethods = new LinkedHashMap<>();
Map<Name, AnnotationBundle> defaultAnnotationsMap = new LinkedHashMap<>();
for (DeclaredType thisType : classHierarchy) {
for (Element e : thisType.asElement().getEnclosedElements()) {
if (e.getKind() == ElementKind.METHOD && e.getAnnotation(DefaultValue.class) != null
&& e instanceof ExecutableElement) {
enclosedMethods.put(e.getSimpleName().toString(), (ExecutableElement) e);
ExecutableElement ex = (ExecutableElement) e;
enclosedMethods.put(ex.getSimpleName(), ex);
AnnotationBundle defaultValues = defaultAnnotationsMap.get(ex.getSimpleName());
defaultAnnotationsMap.put(ex.getSimpleName(),
buildAnnotationBundle(ex, defaultValues));
}
}
}
@@ -150,83 +159,8 @@ public class ConfigProcessor extends AbstractProcessor {
// holds the annotation information on each method
Map<ExecutableElement, AnnotationBundle> annotationsMap = new LinkedHashMap<>();
for (ExecutableElement e : enclosedMethods.values()) {
ImmutableAnnotationBundle.Builder builder = ImmutableAnnotationBundle.builder();
builder.key(e.getSimpleName().toString());
builder.comment("");
Types types = processingEnv.getTypeUtils();
TypeMirror returnType = e.getReturnType();
TypeMirror erasure = types.erasure(returnType);
builder.erasure(erasure);
List<TypeMirror> typeArgs = new ArrayList<>();
if (erasure.getKind() == TypeKind.DECLARED) {
// these are the parameter types for a generic class
List<? extends TypeMirror> args = ((DeclaredType) returnType).getTypeArguments();
typeArgs.addAll(args);
} else if (erasure.getKind().isPrimitive()) {
// no type arguments here
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
String.format("Unsupported kind %s for type %s!", erasure.getKind().toString(),
erasure.toString()));
}
builder.addAllTypeArgs(typeArgs);
String defaultValue = null;
List<Annotation> methodAnnotations = new ArrayList<>();
for (var a : supportedMethodAnnotations)
methodAnnotations.add(e.getAnnotation(a));
for (Annotation annotation : methodAnnotations) {
if (annotation == null)
continue;
if (annotation instanceof Key) {
builder.key(((Key) annotation).value());
} else if (annotation instanceof Comment) {
builder.comment(((Comment) annotation).value());
} else if (annotation instanceof DefaultValue) {
DefaultValue d = (DefaultValue) annotation;
defaultValue = d.value();
} else if (annotation instanceof ParserClass) {
ParserClass pc = (ParserClass) annotation;
// this works, but there has to be a better way?
TypeMirror tm;
try {
// see
// https://stackoverflow.com/questions/7687829/java-6-annotation-processing-getting-a-class-from-an-annotation
tm = processingEnv.getElementUtils().getTypeElement(pc.value().toString())
.asType();
} catch (MirroredTypeException mte) {
tm = mte.getTypeMirror();
}
builder.parserClass(tm);
} else {
throw new IllegalArgumentException(
"Unknown annotation type " + annotation.getClass().getSimpleName());
}
}
// if a method does not have a default value, it will not be included in the generated
// code
if (defaultValue == null) {
messager.printMessage(Diagnostic.Kind.WARNING,
String.format("No default value on method %s!", e.getSimpleName()));
continue;
}
builder.defaultValue(defaultValue);
AnnotationBundle bundle = builder.build();
if (ConfigProcessorUtils.isList(bundle.erasure(), processingEnv)
&& bundle.typeArgs().size() == 0)
messager.printMessage(Diagnostic.Kind.ERROR,
String.format("No parameter type for List on method %s!", e.getSimpleName()));
annotationsMap.put(e, bundle);
AnnotationBundle defaultValues = defaultAnnotationsMap.get(e.getSimpleName());
annotationsMap.put(e, buildAnnotationBundle(e, defaultValues));
}
// generate the methods from the interface
@@ -275,6 +209,87 @@ public class ConfigProcessor extends AbstractProcessor {
}
/**
*
* @param e annotated method
* @param defaultValues default values for annotations - could be from a parent class
* @return annotation values
*/
private AnnotationBundle buildAnnotationBundle(ExecutableElement e,
AnnotationBundle defaultValues) {
ImmutableAnnotationBundle.Builder builder = ImmutableAnnotationBundle.builder();
builder.key(e.getSimpleName().toString());
builder.comment("");
builder.defaultValue("");
if (defaultValues != null) {
builder.key(defaultValues.key());
builder.comment(defaultValues.comment());
builder.defaultValue(defaultValues.defaultValue());
if (defaultValues.parserClass().isPresent())
builder.parserClass(defaultValues.parserClass().get());
}
Types types = processingEnv.getTypeUtils();
TypeMirror returnType = e.getReturnType();
TypeMirror erasure = types.erasure(returnType);
builder.erasure(erasure);
List<TypeMirror> typeArgs = new ArrayList<>();
if (erasure.getKind() == TypeKind.DECLARED) {
// these are the parameter types for a generic class
List<? extends TypeMirror> args = ((DeclaredType) returnType).getTypeArguments();
typeArgs.addAll(args);
} else if (erasure.getKind().isPrimitive()) {
// no type arguments here
} else {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, String.format(
"Unsupported kind %s for type %s!", erasure.getKind().toString(), erasure.toString()));
}
builder.addAllTypeArgs(typeArgs);
List<Annotation> methodAnnotations = new ArrayList<>();
for (var a : supportedMethodAnnotations)
methodAnnotations.add(e.getAnnotation(a));
for (Annotation annotation : methodAnnotations) {
if (annotation == null)
continue;
if (annotation instanceof Key) {
builder.key(((Key) annotation).value());
} else if (annotation instanceof Comment) {
builder.comment(((Comment) annotation).value());
} else if (annotation instanceof DefaultValue) {
builder.defaultValue(((DefaultValue) annotation).value());
} else if (annotation instanceof ParserClass) {
ParserClass pc = (ParserClass) annotation;
// this works, but there has to be a better way?
TypeMirror tm;
try {
// see
// https://stackoverflow.com/questions/7687829/java-6-annotation-processing-getting-a-class-from-an-annotation
tm = processingEnv.getElementUtils().getTypeElement(pc.value().toString()).asType();
} catch (MirroredTypeException mte) {
tm = mte.getTypeMirror();
}
builder.parserClass(tm);
} else {
throw new IllegalArgumentException(
"Unknown annotation type " + annotation.getClass().getSimpleName());
}
}
AnnotationBundle bundle = builder.build();
if (ConfigProcessorUtils.isList(bundle.erasure(), processingEnv)
&& bundle.typeArgs().size() == 0)
messager.printMessage(Diagnostic.Kind.ERROR,
String.format("No parameter type for List on method %s!", e.getSimpleName()));
return bundle;
}
/**
* Build the method to generate an Apache Commons {@link Configuration}
*