diff --git a/annotation/build.gradle.kts b/annotation/build.gradle.kts new file mode 100644 index 0000000..b6d1091 --- /dev/null +++ b/annotation/build.gradle.kts @@ -0,0 +1,21 @@ +plugins { + `java-library` +} + +group = "io.rpg" +version = "1.0.0" + +repositories { + mavenCentral() +} + +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") +} + +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..9407b67 --- /dev/null +++ b/annotation/src/main/java/io/rpg/annotation/BuilderProcessor.java @@ -0,0 +1,108 @@ +package io.rpg.annotation; + +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; +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.BuilderProperty") +@SupportedSourceVersion(SourceVersion.RELEASE_17) +@AutoService(Processor.class) +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) { + 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 + 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 + ';' + '\n'); + } + + // 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(INDENT + '}'); + }); + + // end class definition + writer.println('}'); + + } + } catch (IOException e) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, "IOException while creating builder file"); + e.printStackTrace(); + } + } + + return true; + } +} 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 { +} diff --git a/annotation/src/main/java/module-info.java b/annotation/src/main/java/module-info.java new file mode 100644 index 0000000..793bdd2 --- /dev/null +++ b/annotation/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module io.rpg.annotation { + requires java.compiler; + requires com.google.auto.service; + + exports io.rpg.annotation; +} diff --git a/build.gradle b/build.gradle index 4331aaf..bbf44cd 100644 --- a/build.gradle +++ b/build.gradle @@ -60,16 +60,27 @@ dependencies { implementation("com.kkafara.rt:result-type:1.0.3") + 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}") testImplementation("org.mockito:mockito-core:4.5.1") + } 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/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' + 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; } 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;