From f0d08eb764454d172321b087758ae453be40e960 Mon Sep 17 00:00:00 2001 From: Hari Nair <50370970+HariNairJHUAPL@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:41:37 -0500 Subject: [PATCH] 3 add include annotation (#4) * add Included annotation * update version number * Add Include description in README * update version to 1.1.0 --- README.md | 44 +++++- demo/pom.xml | 4 +- .../main/java/jackfruit/demo/DemoClass.java | 10 +- .../main/java/jackfruit/demo/Included.java | 16 ++ jackfruit/pom.xml | 2 +- .../main/java/jackfruit/JackfruitVersion.java | 4 +- .../java/jackfruit/annotations/Include.java | 21 +++ .../jackfruit/processor/ConfigProcessor.java | 139 +++++++++++------- pom.xml | 2 +- 9 files changed, 178 insertions(+), 64 deletions(-) create mode 100644 demo/src/main/java/jackfruit/demo/Included.java create mode 100644 jackfruit/src/main/java/jackfruit/annotations/Include.java diff --git a/README.md b/README.md index d6b967e..fd3927f 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Include Jackfruit in your project with the following POM: ``` -Find the latest version at [Maven Central](https://central.sonatype.com/). +Find the latest version at [Maven Central](https://central.sonatype.com/artifact/edu.jhuapl.ses/jackfruit). The annotation processor runs on any interface or abstract class annotated with `@Jackfruit` ``` @@ -65,7 +65,7 @@ public interface DemoInterface { } ``` -This corresponds to this Apache Configuration file: +This corresponds to the following Apache Configuration file: ``` # One line comment prefix.key = 1 @@ -138,7 +138,7 @@ Jackfruit annotations can be inherited by derived classes. The `@Jackfruit` ann ## Supported Annotations -The `@Jackfruit` annotation goes on the abstract type. The remaining annotations are for use on methods. +The `@Jackfruit` annotation goes on the abstract type. The remaining annotations are for use on methods. Jackfruit will only process methods annotated either with `@DefaultValue` or `@Include`. ### Jackfruit This annotation goes on the abstract type to signify that it should be run through the annotation processor. There is an optional "prefix" argument that can be used to add a prefix to all of the configuration keys created by the processor. The Jackfruit annotation is not inherited by derived classes. @@ -156,7 +156,43 @@ 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. 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. +The `@DefaultValue` annotation is a String used to initialize the parameter. 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. This annotation must be present if `@Include` is not used. + +### Include + +The `@Include` annotation allows the user to include another configuration class within this one. For example, if ThisBlock.java contains + +``` +@Jackfruit(prefix = "thisBlock") +public interface ThisBlock { + @Comment("thisBlock") + @DefaultValue("1") + int intMethod(); + +@Include +OtherBlock otherBlock(); +} +``` +and OtherBlock.java contains +``` +@Jackfruit(prefix = "otherBlock") +public interface OtherBlock { + @Comment("OtherBlock") + @DefaultValue("2") + int intMethod(); +} + +``` +running new ThisBlockFactory().getTemplate() will create +``` +# thisBlock +thisBlock.intMethod = 1 + +# OtherBlock +otherBlock.intMethod = 2 +``` + +If `@Include` is present, no other annotations will be honored. ### Key diff --git a/demo/pom.xml b/demo/pom.xml index c0bbae0..55b80a2 100644 --- a/demo/pom.xml +++ b/demo/pom.xml @@ -7,7 +7,7 @@ jackfruit-parent edu.jhuapl.ses - 1.0-SNAPSHOT + 1.0.1-SNAPSHOT jackfruit-demo @@ -37,6 +37,8 @@ + + \ No newline at end of file diff --git a/demo/src/main/java/jackfruit/demo/DemoClass.java b/demo/src/main/java/jackfruit/demo/DemoClass.java index 1ed2bd2..e5ccf04 100644 --- a/demo/src/main/java/jackfruit/demo/DemoClass.java +++ b/demo/src/main/java/jackfruit/demo/DemoClass.java @@ -20,10 +20,8 @@ package jackfruit.demo; * #L% */ -import jackfruit.annotations.Comment; -import jackfruit.annotations.DefaultValue; -import jackfruit.annotations.Jackfruit; -import jackfruit.annotations.ParserClass; +import jackfruit.annotations.*; + import java.util.List; /** @@ -98,6 +96,10 @@ public abstract class DemoClass extends DemoSuperClass { @ParserClass(SomeRandomClassParser.class) public abstract List randoms(); + @Comment("Access another configuration block") + @Include + public abstract Included included(); + public void noAnnotationsOnThisMethod() { System.out.println("This method was not processed since it has no DefaultValue annotation"); } diff --git a/demo/src/main/java/jackfruit/demo/Included.java b/demo/src/main/java/jackfruit/demo/Included.java new file mode 100644 index 0000000..0fd1a6d --- /dev/null +++ b/demo/src/main/java/jackfruit/demo/Included.java @@ -0,0 +1,16 @@ +package jackfruit.demo; + +import jackfruit.annotations.Comment; +import jackfruit.annotations.DefaultValue; +import jackfruit.annotations.Jackfruit; + +@Jackfruit(prefix = "included") +public interface Included { + + @DefaultValue("1") + int includedIntMethod(); + + @DefaultValue("1.5") + double includedDoubleMethod(); + +} diff --git a/jackfruit/pom.xml b/jackfruit/pom.xml index b7dc674..88e8ef6 100644 --- a/jackfruit/pom.xml +++ b/jackfruit/pom.xml @@ -7,7 +7,7 @@ jackfruit-parent edu.jhuapl.ses - 1.0-SNAPSHOT + 1.0.1-SNAPSHOT jackfruit diff --git a/jackfruit/src/main/java/jackfruit/JackfruitVersion.java b/jackfruit/src/main/java/jackfruit/JackfruitVersion.java index d5dc6d8..f414ad5 100644 --- a/jackfruit/src/main/java/jackfruit/JackfruitVersion.java +++ b/jackfruit/src/main/java/jackfruit/JackfruitVersion.java @@ -2,8 +2,8 @@ package jackfruit; public class JackfruitVersion { - public final static String version = "1.0-SNAPSHOT"; + public final static String version = "1.0.1-SNAPSHOT"; public final static String packageName = "jackfruit"; - public final static String dateString = "23.09.02"; + public final static String dateString = "23.12.21"; } diff --git a/jackfruit/src/main/java/jackfruit/annotations/Include.java b/jackfruit/src/main/java/jackfruit/annotations/Include.java new file mode 100644 index 0000000..3989971 --- /dev/null +++ b/jackfruit/src/main/java/jackfruit/annotations/Include.java @@ -0,0 +1,21 @@ +package jackfruit.annotations; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * The Include annotation includes another class annotated with @Jackfruit. Example: + *

+ * + * @Include + * AnotherBlockType anotherBlockType(); + * + *

+ * This allows for a configuration type to access other configuration types + */ +@Retention(RetentionPolicy.SOURCE) +@Target(ElementType.METHOD) +public @interface Include { +} diff --git a/jackfruit/src/main/java/jackfruit/processor/ConfigProcessor.java b/jackfruit/src/main/java/jackfruit/processor/ConfigProcessor.java index 0f1512d..d1d854a 100644 --- a/jackfruit/src/main/java/jackfruit/processor/ConfigProcessor.java +++ b/jackfruit/src/main/java/jackfruit/processor/ConfigProcessor.java @@ -21,49 +21,20 @@ package jackfruit.processor; */ import com.google.auto.service.AutoService; -import com.squareup.javapoet.AnnotationSpec; -import com.squareup.javapoet.ClassName; -import com.squareup.javapoet.JavaFile; -import com.squareup.javapoet.MethodSpec; -import com.squareup.javapoet.ParameterSpec; -import com.squareup.javapoet.ParameterizedTypeName; -import com.squareup.javapoet.TypeName; -import com.squareup.javapoet.TypeSpec; -import com.squareup.javapoet.TypeVariableName; +import com.squareup.javapoet.*; import jackfruit.JackfruitVersion; -import jackfruit.annotations.Comment; -import jackfruit.annotations.DefaultValue; -import jackfruit.annotations.Jackfruit; -import jackfruit.annotations.Key; -import jackfruit.annotations.ParserClass; +import jackfruit.annotations.*; import java.io.IOException; import java.io.PrintWriter; import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.time.OffsetDateTime; import java.time.format.DateTimeFormatter; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; import java.util.stream.Collectors; -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.Generated; -import javax.annotation.processing.Messager; -import javax.annotation.processing.Processor; -import javax.annotation.processing.RoundEnvironment; -import javax.annotation.processing.SupportedAnnotationTypes; -import javax.annotation.processing.SupportedSourceVersion; +import javax.annotation.processing.*; import javax.lang.model.SourceVersion; -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.element.*; import javax.lang.model.type.DeclaredType; import javax.lang.model.type.MirroredTypeException; import javax.lang.model.type.TypeKind; @@ -101,6 +72,7 @@ public class ConfigProcessor extends AbstractProcessor { supportedMethodAnnotations = new ArrayList<>(); supportedMethodAnnotations.add(Comment.class); supportedMethodAnnotations.add(DefaultValue.class); + supportedMethodAnnotations.add(Include.class); supportedMethodAnnotations.add(Key.class); supportedMethodAnnotations.add(ParserClass.class); @@ -147,7 +119,8 @@ public class ConfigProcessor extends AbstractProcessor { .addMember( "comments", String.format( - "\"version %s built %s\"", JackfruitVersion.version, JackfruitVersion.dateString)) + "\"version %s built %s\"", + JackfruitVersion.version, JackfruitVersion.dateString)) .build(); TypeSpec.Builder classBuilder = @@ -184,18 +157,26 @@ public class ConfigProcessor extends AbstractProcessor { Collections.reverse(classHierarchy); classHierarchy.add((DeclaredType) annotatedType.asType()); - // create a list of methods annotated with DefaultValue - ignore everything else + // create a map of methods annotated with DefaultValue Map enclosedMethods = new LinkedHashMap<>(); + // the default values for each method Map defaultAnnotationsMap = new LinkedHashMap<>(); + // Map of included config types + Map includedMap = 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 ex) { - enclosedMethods.put(ex.getSimpleName(), ex); - AnnotationBundle defaultValues = defaultAnnotationsMap.get(ex.getSimpleName()); - defaultAnnotationsMap.put( - ex.getSimpleName(), buildAnnotationBundle(ex, defaultValues)); + if (e.getKind() == ElementKind.METHOD && e instanceof ExecutableElement ex) { + + if (ex.getAnnotation(Include.class) != null) { + AnnotationBundle defaultValues = defaultAnnotationsMap.get(ex.getSimpleName()); + AnnotationBundle annotationBundle = buildAnnotationBundle(ex, defaultValues); + includedMap.put(ex.getSimpleName(), annotationBundle); + } else if (ex.getAnnotation(DefaultValue.class) != null) { + enclosedMethods.put(ex.getSimpleName(), ex); + AnnotationBundle defaultValues = defaultAnnotationsMap.get(ex.getSimpleName()); + AnnotationBundle annotationBundle = buildAnnotationBundle(ex, defaultValues); + defaultAnnotationsMap.put(ex.getSimpleName(), annotationBundle); + } } } } @@ -239,17 +220,19 @@ public class ConfigProcessor extends AbstractProcessor { if (m.isDefault()) continue; if (m.getName().equals("toConfig")) { - MethodSpec toConfig = buildToConfig(tvn, m, annotationsMap, prefixMemberName); + MethodSpec toConfig = + buildToConfig(tvn, m, annotationsMap, includedMap, prefixMemberName); methods.add(toConfig); } if (m.getName().equals("getTemplate")) { - MethodSpec getTemplate = buildGetTemplate(tvn, m, annotationsMap); + MethodSpec getTemplate = buildGetTemplate(tvn, m, annotationsMap, includedMap); methods.add(getTemplate); } if (m.getName().equals("fromConfig")) { - MethodSpec fromConfig = buildFromConfig(tvn, m, annotationsMap, prefixMemberName); + MethodSpec fromConfig = + buildFromConfig(tvn, m, annotationsMap, includedMap, prefixMemberName); methods.add(fromConfig); } } @@ -316,8 +299,8 @@ public class ConfigProcessor extends AbstractProcessor { .printMessage( Diagnostic.Kind.ERROR, String.format( - "Unsupported kind %s for type %s!", - erasure.getKind().toString(), erasure)); + "Element %s: Unsupported kind %s for type %s!", + e, erasure.getKind().toString(), erasure)); } builder.addAllTypeArgs(typeArgs); @@ -334,6 +317,8 @@ public class ConfigProcessor extends AbstractProcessor { builder.comment(((Comment) annotation).value()); } else if (annotation instanceof DefaultValue) { builder.defaultValue(((DefaultValue) annotation).value()); + } else if (annotation instanceof Include) { + // do nothing } else if (annotation instanceof ParserClass pc) { // this works, but there has to be a better way? @@ -353,8 +338,7 @@ public class ConfigProcessor extends AbstractProcessor { } AnnotationBundle bundle = builder.build(); - if (ConfigProcessorUtils.isList(bundle.erasure(), processingEnv) - && bundle.typeArgs().isEmpty()) + if (ConfigProcessorUtils.isList(bundle.erasure(), processingEnv) && bundle.typeArgs().isEmpty()) messager.printMessage( Diagnostic.Kind.ERROR, String.format("No parameter type for List on method %s!", e.getSimpleName())); @@ -374,6 +358,7 @@ public class ConfigProcessor extends AbstractProcessor { TypeVariableName tvn, Method m, Map annotationsMap, + Map includedMap, String prefixMemberName) { ParameterSpec ps = ParameterSpec.builder(tvn, "t").build(); ParameterSpec layout = @@ -383,6 +368,7 @@ public class ConfigProcessor extends AbstractProcessor { .getCanonicalName()), "layout") .build(); + MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(m.getName()) .addAnnotation(Override.class) @@ -481,6 +467,15 @@ public class ConfigProcessor extends AbstractProcessor { } } + // add included classes + Types types = processingEnv.getTypeUtils(); + for (Name name : includedMap.keySet()) { + AnnotationBundle bundle = includedMap.get(name); + String className = types.asElement(bundle.erasure()).getSimpleName().toString(); + methodBuilder.addStatement( + "config.append(new $LFactory().toConfig(t.$L(), layout))", className, name); + } + methodBuilder.addCode("return config;"); return methodBuilder.build(); @@ -495,7 +490,12 @@ public class ConfigProcessor extends AbstractProcessor { * @return */ private MethodSpec buildGetTemplate( - TypeVariableName tvn, Method m, Map annotationsMap) { + TypeVariableName tvn, + Method m, + Map annotationsMap, + Map includedMap) { + + Types types = processingEnv.getTypeUtils(); // this builds the getTemplate() method MethodSpec.Builder methodBuilder = @@ -505,6 +505,23 @@ public class ConfigProcessor extends AbstractProcessor { .returns(tvn); TypeSpec.Builder typeBuilder = TypeSpec.anonymousClassBuilder("").addSuperinterface(tvn); + for (Name name : includedMap.keySet()) { + AnnotationBundle bundle = includedMap.get(name); + + MethodSpec.Builder builder = + MethodSpec.methodBuilder(name.toString()) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(TypeName.get(bundle.erasure())) + .addJavadoc(bundle.comment()); + + builder.addStatement( + "return new $LFactory().getTemplate()", + types.asElement(bundle.erasure()).getSimpleName()); + + typeBuilder.addMethod(builder.build()); + } + for (ExecutableElement method : annotationsMap.keySet()) { AnnotationBundle bundle = annotationsMap.get(method); @@ -602,6 +619,7 @@ public class ConfigProcessor extends AbstractProcessor { TypeVariableName tvn, Method m, Map annotationsMap, + Map includedMap, String prefix) { MethodSpec.Builder methodBuilder = @@ -612,6 +630,25 @@ public class ConfigProcessor extends AbstractProcessor { .addParameter(org.apache.commons.configuration2.Configuration.class, "config"); TypeSpec.Builder typeBuilder = TypeSpec.anonymousClassBuilder("").addSuperinterface(tvn); + + Types types = processingEnv.getTypeUtils(); + for (Name name : includedMap.keySet()) { + AnnotationBundle bundle = includedMap.get(name); + + MethodSpec.Builder builder = + MethodSpec.methodBuilder(name.toString()) + .addModifiers(Modifier.PUBLIC) + .addAnnotation(Override.class) + .returns(TypeName.get(bundle.erasure())) + .addJavadoc(bundle.comment()); + + builder.addStatement( + "return new $LFactory().fromConfig(config)", + types.asElement(bundle.erasure()).getSimpleName()); + + typeBuilder.addMethod(builder.build()); + } + for (ExecutableElement method : annotationsMap.keySet()) { AnnotationBundle bundle = annotationsMap.get(method); MethodSpec.Builder builder = diff --git a/pom.xml b/pom.xml index 4610438..40d1343 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ edu.jhuapl.ses jackfruit-parent - 1.0-SNAPSHOT + 1.1.0-SNAPSHOT pom jackfruit-parent