From d106d80d900acc87214866146ebcab6e59450d0b Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Fri, 29 Jul 2022 22:38:41 +0200 Subject: [PATCH 1/7] chore: create annotation module --- annotation/build.gradle.kts | 19 +++++++++++++++++++ .../io/rpg/annotation/BuilderProcessor.java | 7 +++++++ .../io/rpg/annotation/BuilderProperty.java | 11 +++++++++++ 3 files changed, 37 insertions(+) create mode 100644 annotation/build.gradle.kts create mode 100644 annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java create mode 100644 annotation/src/main/java/io/rpg/annotation/BuilderProperty.java diff --git a/annotation/build.gradle.kts b/annotation/build.gradle.kts new file mode 100644 index 0000000..319c01d --- /dev/null +++ b/annotation/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + `java-library` +} + +group = "io.rpg" +version = "1.0.0" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") +} + +tasks.getByName("test") { + useJUnitPlatform() +} \ No newline at end of file diff --git a/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java b/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java new file mode 100644 index 0000000..443ad5c --- /dev/null +++ b/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java @@ -0,0 +1,7 @@ +package io.rpg.annotation; + +import javax.annotation.processing.AbstractProcessor; + + +public class BuilderProcessor extends AbstractProcessor { +} diff --git a/annotation/src/main/java/io/rpg/annotation/BuilderProperty.java b/annotation/src/main/java/io/rpg/annotation/BuilderProperty.java new file mode 100644 index 0000000..956b721 --- /dev/null +++ b/annotation/src/main/java/io/rpg/annotation/BuilderProperty.java @@ -0,0 +1,11 @@ +package io.rpg.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.SOURCE) +public @interface BuilderProperty { +} From 3e90f5b41745d099e213109758477e6bde8800bc Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Fri, 29 Jul 2022 22:38:55 +0200 Subject: [PATCH 2/7] chore: add dependency on annotation module --- settings.gradle | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/settings.gradle b/settings.gradle index caaa243..586c494 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1,3 @@ -rootProject.name = "rpg" \ No newline at end of file +rootProject.name = "rpg" +include 'annotation' + From 60cd81769b3f6ae97cc295fa6d46b33f0dbc0582 Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Fri, 29 Jul 2022 23:26:19 +0200 Subject: [PATCH 3/7] chore: implement annotation processor --- .../io/rpg/annotation/BuilderProcessor.java | 104 +++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java b/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java index 443ad5c..9743562 100644 --- a/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java +++ b/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java @@ -1,7 +1,109 @@ package io.rpg.annotation; import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedAnnotationTypes; +import javax.annotation.processing.SupportedSourceVersion; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.ExecutableType; +import javax.tools.Diagnostic; +import javax.tools.JavaFileObject; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; - +/** + * Requires parameterless constructor. + */ +@SupportedAnnotationTypes("io.rpg.annotation.BuilderProcessor") +@SupportedSourceVersion(SourceVersion.RELEASE_17) public class BuilderProcessor extends AbstractProcessor { + private final int INDENTATION = 2; + private final String INDENT = " ".repeat(INDENTATION); + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + for (TypeElement annotation : annotations) { + Set annotatedElements = roundEnv.getElementsAnnotatedWith(annotation); + + // Annotated method with one parameter && name wit "set" prefix is recognized as setter + Map> annotatedMethods = annotatedElements.stream().collect(Collectors.partitioningBy(element -> + ((ExecutableType) element.asType()).getParameterTypes().size() == 1 + && element.getSimpleName().toString().startsWith("set")) + ); + + + + List annotatedSetters = annotatedMethods.get(true); + List otherMethods = annotatedMethods.get(false); + + otherMethods.forEach(element -> { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "@BuilderProperty annotation must be" + + " applied to a method with name starting with \"set\" prefix with exactly one parameter", element); + }); + + if (annotatedElements.isEmpty()) { + continue; + } + + String className = ((TypeElement) annotatedSetters.get(0).getEnclosingElement()).getQualifiedName().toString(); + + Map setterMap = annotatedSetters.stream().collect(Collectors.toMap( + setter -> setter.getSimpleName().toString(), + setter -> ((ExecutableType) setter.asType()) + .getParameterTypes().get(0).toString() + )); + + String packageName = null; + int lastDotIndex = className.lastIndexOf('.'); + + if (lastDotIndex > 0) { + packageName = className.substring(0, lastDotIndex); + } + + String simpleClassName = className.substring(lastDotIndex + 1); + String builderClassName = className + "Builder"; + String builderSimpleClassName = builderClassName.substring(lastDotIndex + 1); + + try { + JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(builderClassName); + try (PrintWriter writer = new PrintWriter(builderFile.openWriter())) { + if (packageName != null) { + writer.println("package " + packageName + ';'); + } + + // begin class definition + writer.println("public class " + builderSimpleClassName + '{'); + + writer.println(INDENT + "private " + simpleClassName + " object = new " + simpleClassName + "();"); + + writer.println(INDENT + "public " + simpleClassName + " build() {"); + writer.println(INDENT.repeat(2) + "return object;"); + writer.println(INDENT + "}"); + + // generate setters for builder + setterMap.forEach((methodName, argumentType) -> { + writer.println(INDENT + " public " + builderSimpleClassName + " " + methodName + "(" + argumentType + "value) {"); + writer.println(INDENT.repeat(2) + "object." + methodName + "(value);"); + writer.println(INDENT.repeat(2) + "return this;"); + writer.println('}'); + }); + + // end class definition + writer.println('}'); + + } + } catch (IOException e) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "IOException while creating builder file"); + e.printStackTrace(); + } + } + + return true; + } } From ac20b3e50ce73c78731387bd2493a1d774c1c608 Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Fri, 29 Jul 2022 23:54:27 +0200 Subject: [PATCH 4/7] chore: add module-info to annotation module --- annotation/src/main/java/module-info.java | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 annotation/src/main/java/module-info.java diff --git a/annotation/src/main/java/module-info.java b/annotation/src/main/java/module-info.java new file mode 100644 index 0000000..897104d --- /dev/null +++ b/annotation/src/main/java/module-info.java @@ -0,0 +1,4 @@ +module io.rpg.annotation { + requires java.compiler; + exports io.rpg.annotation; +} \ No newline at end of file From 461985d00ef2affba1f896f0c72278f426259f45 Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Fri, 29 Jul 2022 23:55:10 +0200 Subject: [PATCH 5/7] chore: add dependencty on annotation module --- build.gradle | 3 +++ src/main/java/module-info.java | 1 + 2 files changed, 4 insertions(+) diff --git a/build.gradle b/build.gradle index 4331aaf..6037adf 100644 --- a/build.gradle +++ b/build.gradle @@ -60,10 +60,13 @@ dependencies { implementation("com.kkafara.rt:result-type:1.0.3") + implementation(project(":annotation")) + testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}") testImplementation("org.junit.jupiter:junit-jupiter-params:${junitVersion}") testImplementation("org.mockito:mockito-core:4.5.1") + } test { diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java index 0d4d7f3..0fdc9c4 100644 --- a/src/main/java/module-info.java +++ b/src/main/java/module-info.java @@ -13,6 +13,7 @@ requires org.apache.logging.log4j.core; requires org.apache.commons.io; requires result.type; + requires io.rpg.annotation; opens io.rpg.model.location to com.google.gson; opens io.rpg.model.object to com.google.gson; From da42d765dd347dae6170078f2f9c33d16fac4404 Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Sat, 30 Jul 2022 00:37:06 +0200 Subject: [PATCH 6/7] chore: add all required setup to make it work :D --- annotation/build.gradle.kts | 2 ++ .../java/io/rpg/annotation/BuilderProcessor.java | 15 +++++++-------- annotation/src/main/java/module-info.java | 4 +++- build.gradle | 8 ++++++++ .../java/io/rpg/model/location/LocationModel.java | 8 +++++++- 5 files changed, 27 insertions(+), 10 deletions(-) diff --git a/annotation/build.gradle.kts b/annotation/build.gradle.kts index 319c01d..b6d1091 100644 --- a/annotation/build.gradle.kts +++ b/annotation/build.gradle.kts @@ -10,6 +10,8 @@ repositories { } dependencies { + implementation("com.google.auto.service:auto-service:1.0.1") + annotationProcessor("com.google.auto.service:auto-service:1.0.1") testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.2") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") } diff --git a/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java b/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java index 9743562..b29e45b 100644 --- a/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java +++ b/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java @@ -1,9 +1,8 @@ package io.rpg.annotation; -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.RoundEnvironment; -import javax.annotation.processing.SupportedAnnotationTypes; -import javax.annotation.processing.SupportedSourceVersion; +import com.google.auto.service.AutoService; + +import javax.annotation.processing.*; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; @@ -20,8 +19,9 @@ /** * Requires parameterless constructor. */ -@SupportedAnnotationTypes("io.rpg.annotation.BuilderProcessor") +@SupportedAnnotationTypes("io.rpg.annotation.BuilderProperty") @SupportedSourceVersion(SourceVersion.RELEASE_17) +@AutoService(Processor.class) public class BuilderProcessor extends AbstractProcessor { private final int INDENTATION = 2; private final String INDENT = " ".repeat(INDENTATION); @@ -29,6 +29,7 @@ public class BuilderProcessor extends AbstractProcessor { @Override public boolean process(Set annotations, RoundEnvironment roundEnv) { for (TypeElement annotation : annotations) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "PROCESSING ANNOTATION"); Set annotatedElements = roundEnv.getElementsAnnotatedWith(annotation); // Annotated method with one parameter && name wit "set" prefix is recognized as setter @@ -37,8 +38,6 @@ public boolean process(Set annotations, RoundEnvironment && element.getSimpleName().toString().startsWith("set")) ); - - List annotatedSetters = annotatedMethods.get(true); List otherMethods = annotatedMethods.get(false); @@ -88,7 +87,7 @@ public boolean process(Set annotations, RoundEnvironment // generate setters for builder setterMap.forEach((methodName, argumentType) -> { - writer.println(INDENT + " public " + builderSimpleClassName + " " + methodName + "(" + argumentType + "value) {"); + writer.println(INDENT + " public " + builderSimpleClassName + " " + methodName + "(" + argumentType + " value) {"); writer.println(INDENT.repeat(2) + "object." + methodName + "(value);"); writer.println(INDENT.repeat(2) + "return this;"); writer.println('}'); diff --git a/annotation/src/main/java/module-info.java b/annotation/src/main/java/module-info.java index 897104d..793bdd2 100644 --- a/annotation/src/main/java/module-info.java +++ b/annotation/src/main/java/module-info.java @@ -1,4 +1,6 @@ module io.rpg.annotation { requires java.compiler; + requires com.google.auto.service; + exports io.rpg.annotation; -} \ No newline at end of file +} diff --git a/build.gradle b/build.gradle index 6037adf..bbf44cd 100644 --- a/build.gradle +++ b/build.gradle @@ -62,6 +62,8 @@ dependencies { implementation(project(":annotation")) + annotationProcessor(project(":annotation")) + testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}") testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}") testImplementation("org.junit.jupiter:junit-jupiter-params:${junitVersion}") @@ -73,6 +75,12 @@ test { useJUnitPlatform() } +tasks.withType(JavaCompile) { + doFirst { + println "AnnotationProcessorPath for $name is ${options.getAnnotationProcessorPath().getFiles()}" + } +} + compileJava { options.compilerArgs += ["--add-exports=javafx.graphics/com.sun.javafx.scene=io.rpg"] } diff --git a/src/main/java/io/rpg/model/location/LocationModel.java b/src/main/java/io/rpg/model/location/LocationModel.java index 5b34f26..5097ec6 100644 --- a/src/main/java/io/rpg/model/location/LocationModel.java +++ b/src/main/java/io/rpg/model/location/LocationModel.java @@ -1,6 +1,7 @@ package io.rpg.model.location; import com.kkafara.rt.Result; +import io.rpg.annotation.BuilderProperty; import io.rpg.model.actions.ActionConsumer; import io.rpg.model.actions.BaseActionEmitter; import io.rpg.model.actions.LocationChangeAction; @@ -36,12 +37,17 @@ public LocationModel(@NotNull String tag, @NotNull List gameObjects) this.gameObjects = gameObjects; } - private LocationModel() { + public LocationModel() { this.positionListeners = new HashMap<>(); this.positionGameObjectMap = new HashMap<>(); this.directionToLocationMap = new HashMap<>(); } + @BuilderProperty + public void setTag(String tag) { + this.tag = tag; + } + public String getTag() { return tag; } From 97a0f91d6266a897595cea5e9a024c2e2bba504c Mon Sep 17 00:00:00 2001 From: Kacper Kafara Date: Sat, 30 Jul 2022 00:48:19 +0200 Subject: [PATCH 7/7] fix: indentationn & new lines in processor --- .../src/main/java/io/rpg/annotation/BuilderProcessor.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java b/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java index b29e45b..9407b67 100644 --- a/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java +++ b/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java @@ -73,11 +73,11 @@ public boolean process(Set annotations, RoundEnvironment JavaFileObject builderFile = processingEnv.getFiler().createSourceFile(builderClassName); try (PrintWriter writer = new PrintWriter(builderFile.openWriter())) { if (packageName != null) { - writer.println("package " + packageName + ';'); + writer.println("package " + packageName + ';' + '\n'); } // begin class definition - writer.println("public class " + builderSimpleClassName + '{'); + writer.println("public class " + builderSimpleClassName + " {"); writer.println(INDENT + "private " + simpleClassName + " object = new " + simpleClassName + "();"); @@ -90,7 +90,7 @@ public boolean process(Set annotations, RoundEnvironment writer.println(INDENT + " public " + builderSimpleClassName + " " + methodName + "(" + argumentType + " value) {"); writer.println(INDENT.repeat(2) + "object." + methodName + "(value);"); writer.println(INDENT.repeat(2) + "return this;"); - writer.println('}'); + writer.println(INDENT + '}'); }); // end class definition