From e2f59601c0aa114f9e83da4aa5ce6becc9d7deab Mon Sep 17 00:00:00 2001 From: JFronny Date: Mon, 31 Oct 2022 20:52:48 +0100 Subject: [PATCH] Support for 1d classes of primitives --- .gitignore | 39 ++ build.gradle.kts | 2 + gson-compile-annotations/build.gradle.kts | 19 + .../gson/compile/annotations/GPrefer.java | 8 + .../compile/annotations/GSerializable.java | 10 + gson-compile-core/build.gradle.kts | 19 + .../jfronny/gson/compile/core/CCore.java | 7 + .../jfronny/gson/compile/core/Test.java | 23 + gson-compile-example/build.gradle.kts | 19 + .../java/io/gitlab/jfronny/gson/Main.java | 18 + gson-compile-processor/build.gradle.kts | 22 + .../jfronny/gson/compile/processor/Const.java | 24 + .../processor/GsonCompileProcessor.java | 588 ++++++++++++++++++ .../processor/util/AbstractProcessor2.java | 20 + .../compile/processor/util/DelegateList.java | 196 ++++++ .../util/SupportedAnnotationTypes2.java | 10 + .../valueprocessor/ConstructionSource.java | 247 ++++++++ .../util/valueprocessor/ElementException.java | 29 + .../util/valueprocessor/Properties.java | 249 ++++++++ .../util/valueprocessor/Property.java | 160 +++++ .../processor/util/valueprocessor/Value.java | 52 ++ .../util/valueprocessor/ValueCreator.java | 154 +++++ .../javax.annotation.processing.Processor | 1 + settings.gradle.kts | 5 + 24 files changed, 1921 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle.kts create mode 100644 gson-compile-annotations/build.gradle.kts create mode 100644 gson-compile-annotations/src/main/java/io/gitlab/jfronny/gson/compile/annotations/GPrefer.java create mode 100644 gson-compile-annotations/src/main/java/io/gitlab/jfronny/gson/compile/annotations/GSerializable.java create mode 100644 gson-compile-core/build.gradle.kts create mode 100644 gson-compile-core/src/main/java/io/gitlab/jfronny/gson/compile/core/CCore.java create mode 100644 gson-compile-core/src/main/java/io/gitlab/jfronny/gson/compile/core/Test.java create mode 100644 gson-compile-example/build.gradle.kts create mode 100644 gson-compile-example/src/main/java/io/gitlab/jfronny/gson/Main.java create mode 100644 gson-compile-processor/build.gradle.kts create mode 100644 gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/Const.java create mode 100644 gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/GsonCompileProcessor.java create mode 100644 gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/AbstractProcessor2.java create mode 100644 gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/DelegateList.java create mode 100644 gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/SupportedAnnotationTypes2.java create mode 100644 gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ConstructionSource.java create mode 100644 gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ElementException.java create mode 100644 gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Properties.java create mode 100644 gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Property.java create mode 100644 gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Value.java create mode 100644 gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ValueCreator.java create mode 100644 gson-compile-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor create mode 100644 settings.gradle.kts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd00d92 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..3069ae8 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,2 @@ +group = "io.gitlab.jfronny" +version = "1.0-SNAPSHOT" diff --git a/gson-compile-annotations/build.gradle.kts b/gson-compile-annotations/build.gradle.kts new file mode 100644 index 0000000..523f275 --- /dev/null +++ b/gson-compile-annotations/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("java") +} + +group = "io.gitlab.jfronny.gson" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + testImplementation("org.junit.jupiter:junit-jupiter-api:5.8.1") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.8.1") +} + +tasks.getByName("test") { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gson-compile-annotations/src/main/java/io/gitlab/jfronny/gson/compile/annotations/GPrefer.java b/gson-compile-annotations/src/main/java/io/gitlab/jfronny/gson/compile/annotations/GPrefer.java new file mode 100644 index 0000000..4dc7137 --- /dev/null +++ b/gson-compile-annotations/src/main/java/io/gitlab/jfronny/gson/compile/annotations/GPrefer.java @@ -0,0 +1,8 @@ +package io.gitlab.jfronny.gson.compile.annotations; + +import java.lang.annotation.*; + +@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface GPrefer { +} diff --git a/gson-compile-annotations/src/main/java/io/gitlab/jfronny/gson/compile/annotations/GSerializable.java b/gson-compile-annotations/src/main/java/io/gitlab/jfronny/gson/compile/annotations/GSerializable.java new file mode 100644 index 0000000..ad3d50f --- /dev/null +++ b/gson-compile-annotations/src/main/java/io/gitlab/jfronny/gson/compile/annotations/GSerializable.java @@ -0,0 +1,10 @@ +package io.gitlab.jfronny.gson.compile.annotations; + +import java.lang.annotation.*; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface GSerializable { + Class with() default void.class; + boolean generateAdapter() default false; +} diff --git a/gson-compile-core/build.gradle.kts b/gson-compile-core/build.gradle.kts new file mode 100644 index 0000000..84f9a08 --- /dev/null +++ b/gson-compile-core/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + `java-library` +} + +group = "io.gitlab.jfronny.gson" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://gitlab.com/api/v4/projects/35745143/packages/maven") // Commons +} + +dependencies { + api("io.gitlab.jfronny:commons-gson:2022.10.22+20-29-33") +} + +tasks.getByName("test") { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gson-compile-core/src/main/java/io/gitlab/jfronny/gson/compile/core/CCore.java b/gson-compile-core/src/main/java/io/gitlab/jfronny/gson/compile/core/CCore.java new file mode 100644 index 0000000..275d3ab --- /dev/null +++ b/gson-compile-core/src/main/java/io/gitlab/jfronny/gson/compile/core/CCore.java @@ -0,0 +1,7 @@ +package io.gitlab.jfronny.gson.compile.core; + +import io.gitlab.jfronny.commons.serialize.gson.api.v1.GsonHolder; + +public class CCore { + public static final GsonHolder HOLDER = new GsonHolder(); +} diff --git a/gson-compile-core/src/main/java/io/gitlab/jfronny/gson/compile/core/Test.java b/gson-compile-core/src/main/java/io/gitlab/jfronny/gson/compile/core/Test.java new file mode 100644 index 0000000..d84857a --- /dev/null +++ b/gson-compile-core/src/main/java/io/gitlab/jfronny/gson/compile/core/Test.java @@ -0,0 +1,23 @@ +package io.gitlab.jfronny.gson.compile.core; + +import io.gitlab.jfronny.gson.TypeAdapter; +import io.gitlab.jfronny.gson.stream.JsonReader; +import io.gitlab.jfronny.gson.stream.JsonWriter; + +import java.io.IOException; + +public class Test { + public static void main(String[] args) { + new TypeAdapter() { + @Override + public void write(JsonWriter jsonWriter, String s) throws IOException { + + } + + @Override + public String read(JsonReader jsonReader) throws IOException { + return null; + } + }; + } +} diff --git a/gson-compile-example/build.gradle.kts b/gson-compile-example/build.gradle.kts new file mode 100644 index 0000000..6b64260 --- /dev/null +++ b/gson-compile-example/build.gradle.kts @@ -0,0 +1,19 @@ +plugins { + id("java") +} + +group = "io.gitlab.jfronny.gson" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://gitlab.com/api/v4/projects/35030495/packages/maven") // Gson + maven("https://gitlab.com/api/v4/projects/35745143/packages/maven") // Commons +} + +dependencies { + annotationProcessor(project(":gson-compile-processor")) + compileOnly(project(":gson-compile-annotations")) + implementation(project(":gson-compile-core")) +} + diff --git a/gson-compile-example/src/main/java/io/gitlab/jfronny/gson/Main.java b/gson-compile-example/src/main/java/io/gitlab/jfronny/gson/Main.java new file mode 100644 index 0000000..45c8083 --- /dev/null +++ b/gson-compile-example/src/main/java/io/gitlab/jfronny/gson/Main.java @@ -0,0 +1,18 @@ +package io.gitlab.jfronny.gson; + +import io.gitlab.jfronny.gson.compile.annotations.GSerializable; +import io.gitlab.jfronny.gson.stream.JsonReader; +import io.gitlab.jfronny.gson.stream.JsonWriter; + +public class Main { + public static void main(String[] args) { + System.out.println("Hello world!"); + //JsonReader + } + + @GSerializable(generateAdapter = true) + public static class ExamplePojo { + public String someValue; + public Boolean someBool; + } +} \ No newline at end of file diff --git a/gson-compile-processor/build.gradle.kts b/gson-compile-processor/build.gradle.kts new file mode 100644 index 0000000..c2d257f --- /dev/null +++ b/gson-compile-processor/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + id("java") +} + +group = "io.gitlab.jfronny.gson" +version = "1.0-SNAPSHOT" + +repositories { + mavenCentral() + maven("https://gitlab.com/api/v4/projects/35745143/packages/maven") +} + +dependencies { + implementation(project(":gson-compile-annotations")) + implementation("org.jetbrains:annotations:23.0.0") + implementation("io.gitlab.jfronny:commons:2022.10.22+20-29-33") + implementation("com.squareup:javapoet:1.13.0") +} + +tasks.getByName("test") { + useJUnitPlatform() +} \ No newline at end of file diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/Const.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/Const.java new file mode 100644 index 0000000..52a61d7 --- /dev/null +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/Const.java @@ -0,0 +1,24 @@ +package io.gitlab.jfronny.gson.compile.processor; + +import com.squareup.javapoet.ClassName; + +public class Const { + public static final String PREFIX = "GC_"; + public static final String ADAPTER_PREFIX = "adapter_"; + public static final String ARG_PREFIX = "_"; + public static final String READ = "read"; + public static final String WRITE = "write"; + + public static final ClassName TYPE_ADAPTER = ClassName.get("io.gitlab.jfronny.gson", "TypeAdapter"); + public static final ClassName TYPE_ADAPTER_FACTORY = ClassName.get("io.gitlab.jfronny.gson", "TypeAdapterFactory"); + public static final ClassName GSON_ELEMENT = ClassName.get("io.gitlab.jfronny.gson", "JsonElement"); + public static final ClassName GSON_WRITER = ClassName.get("io.gitlab.jfronny.gson.stream", "JsonWriter"); + public static final ClassName GSON_READER = ClassName.get("io.gitlab.jfronny.gson.stream", "JsonReader"); + public static final ClassName GSON_TREE_READER = ClassName.get("io.gitlab.jfronny.gson.internal.bind", "JsonTreeReader"); + public static final ClassName GSON_TREE_WRITER = ClassName.get("io.gitlab.jfronny.gson.internal.bind", "JsonTreeWriter"); + static final ClassName JSON_ADAPTER = ClassName.get("io.gitlab.jfronny.gson.annotations", "JsonAdapter"); + static final ClassName TYPE_TOKEN = ClassName.get("io.gitlab.jfronny.gson.reflect", "TypeToken"); + static final ClassName GSON_TOKEN = ClassName.get("io.gitlab.jfronny.gson.stream", "JsonToken"); + + public static final ClassName CCORE = ClassName.get("io.gitlab.jfronny.gson.compile.core", "CCore"); +} diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/GsonCompileProcessor.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/GsonCompileProcessor.java new file mode 100644 index 0000000..78eb380 --- /dev/null +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/GsonCompileProcessor.java @@ -0,0 +1,588 @@ +package io.gitlab.jfronny.gson.compile.processor; + +import com.squareup.javapoet.*; +import io.gitlab.jfronny.commons.StringFormatter; +import io.gitlab.jfronny.gson.compile.annotations.GSerializable; +import io.gitlab.jfronny.gson.compile.processor.util.AbstractProcessor2; +import io.gitlab.jfronny.gson.compile.processor.util.SupportedAnnotationTypes2; +import io.gitlab.jfronny.gson.compile.processor.util.valueprocessor.*; +import io.gitlab.jfronny.gson.compile.processor.util.valueprocessor.Properties; +import org.jetbrains.annotations.Nullable; + +import javax.annotation.processing.*; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.*; +import javax.lang.model.type.*; +import javax.lang.model.util.*; +import javax.tools.Diagnostic; +import java.io.*; +import java.lang.reflect.ParameterizedType; +import java.util.*; +import java.util.stream.Collectors; + +//TODO support for records +@SupportedSourceVersion(SourceVersion.RELEASE_17) +@SupportedAnnotationTypes2({GSerializable.class}) +public class GsonCompileProcessor extends AbstractProcessor2 { + private Messager message; + private Types typeUtils; + private Filer filer; + private Set seen; + private ValueCreator valueCreator; + private Elements elements; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + message = processingEnv.getMessager(); + filer = processingEnv.getFiler(); + typeUtils = processingEnv.getTypeUtils(); + elements = processingEnv.getElementUtils(); + seen = new LinkedHashSet<>(); + valueCreator = new ValueCreator(processingEnv); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnvironment) { + Set toProcesses = new LinkedHashSet<>(); + for (TypeElement annotation : annotations) { + for (Element element : roundEnvironment.getElementsAnnotatedWith(annotation)) { + for (AnnotationMirror mirror : element.getAnnotationMirrors()) { + if (mirror.getAnnotationType().toString().equals(GSerializable.class.getCanonicalName())) { + var bld = new Object() { + TypeMirror with = null; + Boolean generateAdapter = null; + }; + elements.getElementValuesWithDefaults(mirror).forEach((executableElement, value) -> { + String name = executableElement.getSimpleName().toString(); + switch (name) { + case "with" -> { + if (bld.with != null) throw new IllegalArgumentException("Duplicate annotation parameter: with"); + bld.with = (TypeMirror) value.getValue(); + } + case "generateAdapter" -> { + if (bld.generateAdapter != null) throw new IllegalArgumentException("Duplicate annotation parameter: generateAdapter"); + bld.generateAdapter = (Boolean) value.getValue(); + } + default -> throw new IllegalArgumentException("Unexpected annotation parameter: " + name); + } + }); + if (bld.with == null) throw new IllegalArgumentException("Missing annotation parameter: with"); + if (bld.generateAdapter == null) throw new IllegalArgumentException("Missing annotation parameter: generateAdapter"); + if (bld.with.toString().equals("void")) bld.with = null; + if (bld.with != null && bld.generateAdapter) throw new IllegalArgumentException("Adapter for " + element + " already exists, not generating another!"); + toProcesses.add(new ToProcess((TypeElement) element, bld.with, bld.generateAdapter)); + } + } + } + } + for (ToProcess toProcess : toProcesses) { + try { + process(toProcess.element, toProcess.adapter, toProcess.generateAdapter); + } catch (IOException | ElementException e) { + message.printMessage(Diagnostic.Kind.ERROR, "GsonCompile threw an exception: " + StringFormatter.toString(e), toProcess.element); + } + } + return false; + } + + record ToProcess(TypeElement element, @Nullable TypeMirror adapter, boolean generateAdapter) {} + + private void process(TypeElement classElement, @Nullable TypeMirror adapter, boolean generateAdapter) throws IOException, ElementException { + ClassName className = ClassName.get(classElement); + if (!seen.add(className)) return; // Don't process the same class more than once + + ClassName generatedClassName = ClassName.get(className.packageName(), Const.PREFIX + String.join("_", className.simpleNames())); + TypeName classType = TypeName.get(classElement.asType()); + List typeVariables = new ArrayList<>(); + if (classType instanceof ParameterizedTypeName type) { + for (TypeName argument : type.typeArguments) { + typeVariables.add(TypeVariableName.get(argument.toString())); + } + } + + TypeSpec.Builder spec = TypeSpec.classBuilder(generatedClassName.simpleName()) + .addOriginatingElement(classElement) + .addTypeVariables(typeVariables) + .addModifiers(Modifier.PUBLIC); + + if (adapter != null) { + generateDelegateToAdapter(spec, classType, adapter); + } else { + if (generateAdapter) { + generateDelegatingAdapter(spec, classType, generatedClassName); + } + generateSerialisation(spec, classType, classElement, typeVariables); + } + + generateAuxiliary(spec, classType); + + //TODO register adapter as available and use in generated adapters + + JavaFile javaFile = JavaFile.builder(className.packageName(), spec.build()) + .skipJavaLangImports(true) + .indent(" ") + .build(); + javaFile.writeTo(filer); + message.printMessage(Diagnostic.Kind.NOTE, "Processed " + className); + } + + private static void generateDelegatingAdapter(TypeSpec.Builder spec, TypeName classType, ClassName generatedClassName) { + spec.addType( + TypeSpec.classBuilder("Adapter") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .superclass(ParameterizedTypeName.get(Const.TYPE_ADAPTER, classType)) + .addMethod(MethodSpec.methodBuilder("write") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(Const.GSON_WRITER, "writer") + .addParameter(classType, "value") + .addException(IOException.class) + .addCode(generatedClassName.simpleName() + ".write(writer, value);") + .build()) + .addMethod(MethodSpec.methodBuilder("read") + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addParameter(Const.GSON_READER, "reader") + .addException(IOException.class) + .returns(classType) + .addCode("return " + generatedClassName.simpleName() + ".read(reader);") + .build()) + .build() + ); + } + + private static void generateDelegateToAdapter(TypeSpec.Builder spec, TypeName classType, TypeMirror adapter) { + TypeName adapterType = TypeName.get(adapter); + spec.addField( + FieldSpec.builder(adapterType, "ADAPTER", Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .initializer("new $T()", adapterType) + .build() + ); + spec.addMethod( + MethodSpec.methodBuilder(Const.READ) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(Const.GSON_READER, "reader") + .addException(IOException.class) + .returns(classType) + .addCode("return ADAPTER.read(reader);") + .build() + ); + spec.addMethod( + MethodSpec.methodBuilder(Const.READ) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(Const.GSON_READER, "writer") + .addParameter(classType, "value") + .addException(IOException.class) + .returns(classType) + .addCode("ADAPTER.write(reader, value);") + .build() + ); + } + + private static void generateAuxiliary(TypeSpec.Builder spec, TypeName classType) { + spec.addMethod( + MethodSpec.methodBuilder(Const.READ) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(TypeName.get(Reader.class), "in") + .addException(IOException.class) + .returns(classType) + .addCode(""" + try ($1T reader = new $1T(in)) { + return read(reader); + }""", Const.GSON_READER) + .build() + ); + + spec.addMethod( + MethodSpec.methodBuilder(Const.READ) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(TypeName.get(String.class), "json") + .addException(IOException.class) + .returns(classType) + .addCode(""" + try ($1T reader = new $1T(json)) { + return read(reader); + }""", StringReader.class) + .build() + ); + + spec.addMethod( + MethodSpec.methodBuilder(Const.READ) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(Const.GSON_ELEMENT, "tree") + .addException(IOException.class) + .returns(classType) + .addCode(""" + try ($1T reader = new $1T(tree)) { + return read(reader); + }""", Const.GSON_TREE_READER) + .build() + ); + + spec.addMethod( + MethodSpec.methodBuilder(Const.WRITE) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(Writer.class, "out") + .addParameter(classType, "value") + .addException(IOException.class) + .addCode(""" + try ($1T writer = new $1T(out)) { + write(writer, value); + }""", Const.GSON_WRITER) + .build() + ); + + spec.addMethod( + MethodSpec.methodBuilder("toJson") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(classType, "value") + .addException(IOException.class) + .returns(String.class) + .addCode(""" + try ($1T writer = new $1T()) { + write(writer, value); + return writer.toString(); + }""", StringWriter.class) + .build() + ); + + spec.addMethod( + MethodSpec.methodBuilder("toJsonTree") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(classType, "value") + .addException(IOException.class) + .returns(Const.GSON_ELEMENT) + .addCode(""" + try ($1T writer = new $1T()) { + write(writer, value); + return writer.get(); + }""", Const.GSON_TREE_WRITER) + .build() + ); + } + + private void generateSerialisation(TypeSpec.Builder spec, TypeName classType, TypeElement classElement, List typeVariables) throws ElementException { + Value value = valueCreator.from(classElement); + ConstructionSource constructionSource = value.getConstructionSource(); + Properties properties = value.getProperties(); + + // public static void write(JsonWriter writer, T value) throws IOException + { + CodeBlock.Builder code = CodeBlock.builder(); + code.beginControlFlow("if (value == null)") + .addStatement("writer.nullValue()") + .addStatement("return") + .endControlFlow(); + + code.addStatement("writer.beginObject()"); + for (Property.Field field : properties.fields) { + code.addStatement("writer.name($S)", getSerializedName(field)); + generateWrite(field, spec, code, "writer", "value.$N", typeVariables); + } + for (Property.Getter getter : properties.getters) { + code.addStatement("writer.name($S)", getSerializedName(getter)); + generateWrite(getter, spec, code, "writer", "value.$N()", typeVariables); + } + code.addStatement("writer.endObject()"); + + spec.addMethod(MethodSpec.methodBuilder("write") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(Const.GSON_WRITER, "writer") + .addParameter(classType, "value") + .addException(IOException.class) + .addCode(code.build()) + .build()); + } + + // public static T read(JsonReader reader) throws IOException + { + CodeBlock.Builder code = CodeBlock.builder(); + code.beginControlFlow("if (reader.peek() == $T.NULL)", Const.GSON_TOKEN) + .addStatement("reader.nextNull()") + .addStatement("return null") + .endControlFlow(); + + boolean isEmpty = true; + for (Property param : properties.names) { + isEmpty = false; + code.addStatement("$T $L = $L", param.getType(), Const.ARG_PREFIX + param.getName(), getDefaultValue(param.getType())); + } + if (isEmpty) { + code.addStatement("reader.skipValue()"); + } else { + code.addStatement("reader.beginObject()") + .beginControlFlow("while (reader.hasNext())") + .beginControlFlow("switch (reader.nextName())"); + for (Property param : properties.names) { + String read = generateRead(param, spec, "reader", typeVariables); + if (param.getType().getKind().isPrimitive()) { + code.add("case $S -> ", getSerializedName(param)); + code.addStatement("$L = $L", Const.ARG_PREFIX + param.getName(), read); + } else { + code.beginControlFlow("case $S ->", getSerializedName(param)) + .beginControlFlow("if (reader.peek() == $T.NULL)", Const.GSON_TOKEN) + .addStatement("reader.nextNull()") + .addStatement("$L = null", Const.ARG_PREFIX + param.getName()) + .endControlFlow("else $L = $L", Const.ARG_PREFIX + param.getName(), read) + .endControlFlow(); + } + } + code.add("default -> ") + .addStatement("reader.skipValue()"); + + code.endControlFlow() + .endControlFlow() + .addStatement("reader.endObject()"); + } + + ClassName creatorName = ClassName.get((TypeElement) constructionSource.getConstructionElement().getEnclosingElement()); + if (constructionSource instanceof ConstructionSource.Builder builder) { + String args = properties.constructorParams.stream().map(s -> Const.ARG_PREFIX + s.getName()).collect(Collectors.joining(", ")); + if (constructionSource.isConstructor()) { + code.add("return new $T($L)", builder.getBuilderClass(), args); + } else { + code.add("return $T.$L($L)", creatorName, classElement.getSimpleName(), args); + } + code.add("\n").indent(); + for (Property.BuilderParam param : properties.builderParams) { + code.add(".$L($L)\n", param.getCallableName(), Const.ARG_PREFIX + param.getName()); + } + code.add(".$L();\n", builder.getBuildMethod().getSimpleName()).unindent(); + } else { + String args = properties.params.stream().map(s -> Const.ARG_PREFIX + s.getName()).collect(Collectors.joining(", ")); + if (constructionSource.isConstructor()) { + code.addStatement("return new $T($L)", classType, args); + } else { + code.addStatement("return $T.$L($L)", creatorName, constructionSource.getConstructionElement().getSimpleName(), args); + } + } + + spec.addMethod(MethodSpec.methodBuilder("read") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(classType) + .addParameter(Const.GSON_READER, "reader") + .addException(IOException.class) + .addCode(code.build()) + .build()); + } + } + + private void generateWrite(Property prop, TypeSpec.Builder klazz, CodeBlock.Builder code, String writer, String get, List typeVariables) { + TypeMirror unboxed = unbox(prop.getType()); + if (unbox(prop.getType()).getKind().isPrimitive() || prop.getType().toString().equals(String.class.getCanonicalName())) { + if (prop.getType().equals(unboxed)) { + code.addStatement("$L.value(" + get + ")", writer, prop.getCallableName()); + } else { + String pName = Const.ARG_PREFIX + prop.getName(); + code.addStatement("$T $L = " + get, prop.getType(), pName, prop.getCallableName()) + .beginControlFlow("if ($L == null)", pName) + .addStatement("$L.nullValue()", writer) + .endControlFlow("else $L.value($L)", writer, pName); + } + return; + } + //TODO handle lists, sets, arrays, other common types (-> https://github.com/bluelinelabs/LoganSquare/blob/development/docs/TypeConverters.md) + //TODO support comment annotations + code.addStatement(getAdapter(prop, klazz, typeVariables) + ".write(" + writer + ", " + get + ")", prop.getCallableName()); + } + + private String generateRead(Property prop, TypeSpec.Builder klazz, String reader, List typeVariables) { + TypeMirror unboxed = unbox(prop.getType()); + if (unboxed.getKind().isPrimitive()) { + return switch (unboxed.getKind()) { + case BOOLEAN -> reader + ".nextBoolean()"; + case BYTE -> "(byte) " + reader + ".nextInt()"; + case SHORT -> "(short) " + reader + ".nextInt()"; + case INT -> reader + ".nextInt()"; + case LONG -> reader + ".nextLong()"; + case CHAR -> "(char) " + reader + ".nextInt()"; + case FLOAT -> "(float) " + reader + ".nextDouble()"; + case DOUBLE -> reader + ".nextDouble()"; + default -> throw new IllegalArgumentException("Unsupported primitive: " + unboxed.getKind()); + }; + } + if (prop.getType().toString().equals(String.class.getCanonicalName())) { + return reader + ".nextString()"; + } + //TODO handle lists, sets, arrays, other common types (-> https://github.com/bluelinelabs/LoganSquare/blob/development/docs/TypeConverters.md) + return getAdapter(prop, klazz, typeVariables) + ".read(" + reader + ")"; + } + + private String getAdapter(Property prop, TypeSpec.Builder klazz, List typeVariables) { + String typeAdapterName = Const.ADAPTER_PREFIX + prop.getName(); + for (FieldSpec spec : klazz.fieldSpecs) { + if (spec.name.equals(typeAdapterName)) return typeAdapterName; + } + DeclaredType typeAdapterClass = findTypeAdapterClass(prop.getAnnotations()); + if (typeAdapterClass != null) { + if (isInstance(typeAdapterClass, Const.TYPE_ADAPTER.toString())) { + klazz.addField( + FieldSpec.builder(TypeName.get(typeAdapterClass), typeAdapterName) + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer("new $T()", typeAdapterClass) + .build() + ); + } else if (isInstance(typeAdapterClass, Const.TYPE_ADAPTER_FACTORY.toString())) { + TypeName typeAdapterType = ParameterizedTypeName.get(Const.TYPE_ADAPTER, TypeName.get(prop.getType()).box()); + CodeBlock.Builder block = CodeBlock.builder(); + block.add("new $T().create($T.HOLDER.getGson(), ", typeAdapterClass, Const.CCORE); + appendFieldTypeToken(block, prop, typeVariables, false); + block.add(")"); + klazz.addField( + FieldSpec.builder(typeAdapterType, typeAdapterName) + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer(block.build()) + .build() + ); + } else message.printMessage(Diagnostic.Kind.ERROR, "@JsonAdapter value must by TypeAdapter or TypeAdapterFactory reference.", prop.getElement()); + } else { + //TODO handle known custom type adapters and return proper static class + TypeName typeAdapterType = ParameterizedTypeName.get(Const.TYPE_ADAPTER, TypeName.get(prop.getType()).box()); + CodeBlock.Builder block = CodeBlock.builder(); + block.add("$T.HOLDER.getGson().getAdapter(", Const.CCORE); + appendFieldTypeToken(block, prop, typeVariables, true); + block.add(")"); + klazz.addField( + FieldSpec.builder(typeAdapterType, typeAdapterName) + .addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL) + .initializer(block.build()) + .build() + ); + } + return typeAdapterName; + } + + private void appendFieldTypeToken(CodeBlock.Builder block, Property property, List typeVariables, boolean allowClassType) { + TypeMirror type = property.getType(); + TypeName typeName = TypeName.get(type); + + if (isComplexType(type)) { + TypeName typeTokenType = ParameterizedTypeName.get(Const.TYPE_TOKEN, typeName); + List typeParams = getGenericTypes(type); + if (typeParams.isEmpty()) { + block.add("new $T() {}", typeTokenType); + } else { + block.add("($T) $T.getParameterized($T.class, ", typeTokenType, Const.TYPE_TOKEN, typeUtils.erasure(type)); + for (Iterator iterator = typeParams.iterator(); iterator.hasNext(); ) { + TypeMirror typeParam = iterator.next(); + int typeIndex = typeVariables.indexOf(TypeVariableName.get(typeParam.toString())); + block.add("(($T)typeToken.getType()).getActualTypeArguments()[$L]", ParameterizedType.class, typeIndex); + if (iterator.hasNext()) { + block.add(", "); + } + } + block.add(")"); + } + } else if (isGenericType(type)) { + TypeName typeTokenType = ParameterizedTypeName.get(Const.TYPE_TOKEN, typeName); + int typeIndex = typeVariables.indexOf(TypeVariableName.get(property.getType().toString())); + block.add("($T) $T.get((($T)typeToken.getType()).getActualTypeArguments()[$L])", + typeTokenType, Const.TYPE_TOKEN, ParameterizedType.class, typeIndex); + } else { + if (allowClassType) { + block.add("$T.class", typeName); + } else { + block.add("TypeToken.get($T.class)", typeName); + } + } + } + + private boolean isComplexType(TypeMirror type) { + Element element = typeUtils.asElement(type); + if (!(element instanceof TypeElement typeElement)) return false; + return !typeElement.getTypeParameters().isEmpty(); + } + + private boolean isGenericType(TypeMirror type) { + return type.getKind() == TypeKind.TYPEVAR; + } + + private List getGenericTypes(TypeMirror type) { + DeclaredType declaredType = asDeclaredType(type); + if (declaredType == null) { + return Collections.emptyList(); + } + ArrayList result = new ArrayList<>(); + for (TypeMirror argType : declaredType.getTypeArguments()) { + if (argType.getKind() == TypeKind.TYPEVAR) { + result.add(argType); + } + } + return result; + } + + private static String getDefaultValue(TypeMirror type) { + switch (type.getKind()) { + case BYTE: + case SHORT: + case INT: + case LONG: + case FLOAT: + case CHAR: + case DOUBLE: + return "0"; + case BOOLEAN: + return "false"; + default: + return "null"; + } + } + + private TypeMirror unbox(TypeMirror type) { + try { + return typeUtils.unboxedType(type); + } catch (IllegalArgumentException e) { + return type; + } + } + + private static String getSerializedName(Property property) { + for (AnnotationMirror annotationMirror : property.getAnnotations()) { + if (annotationMirror.getAnnotationType().asElement().toString().equals("com.google.gson.annotations.SerializedName")) { + return (String) annotationMirror.getElementValues().values().iterator().next().getValue(); + } + } + return property.getName(); + } + + private DeclaredType findTypeAdapterClass(List annotations) { + for (AnnotationMirror annotation : annotations) { + String typeName = annotation.getAnnotationType().toString(); + if (typeName.equals(Const.JSON_ADAPTER.toString())) { + Map elements = annotation.getElementValues(); + if (!elements.isEmpty()) { + AnnotationValue value = elements.values().iterator().next(); + return (DeclaredType) value.getValue(); + } + } + } + return null; + } + + private boolean isInstance(DeclaredType type, String parentClassName) { + if (type == null) return false; + TypeElement element = (TypeElement) type.asElement(); + for (TypeMirror interfaceType : element.getInterfaces()) { + if (typeUtils.erasure(interfaceType).toString().equals(parentClassName)) return true; + } + TypeMirror superclassType = element.getSuperclass(); + if (superclassType != null) { + if (typeUtils.erasure(superclassType).toString().equals(parentClassName)) { + return true; + } else { + return isInstance(asDeclaredType(superclassType), parentClassName); + } + } + return false; + } + + private static DeclaredType asDeclaredType(TypeMirror type) { + return type.accept(new SimpleTypeVisitor14<>() { + @Override + public DeclaredType visitDeclared(DeclaredType t, Object o) { + return t; + } + }, null); + } +} diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/AbstractProcessor2.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/AbstractProcessor2.java new file mode 100644 index 0000000..82d865d --- /dev/null +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/AbstractProcessor2.java @@ -0,0 +1,20 @@ +package io.gitlab.jfronny.gson.compile.processor.util; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.SupportedAnnotationTypes; +import java.util.*; +import java.util.stream.Collectors; + +public abstract class AbstractProcessor2 extends AbstractProcessor { + @Override + public Set getSupportedAnnotationTypes() { + return Optional.ofNullable(this.getClass().getAnnotation(SupportedAnnotationTypes.class)) + .map(SupportedAnnotationTypes::value) + .map(Set::of) + .or(() -> Optional.ofNullable(this.getClass().getAnnotation(SupportedAnnotationTypes2.class)) + .map(SupportedAnnotationTypes2::value) + .map(Arrays::stream) + .map(s -> s.map(Class::getCanonicalName).collect(Collectors.toSet())) + ).orElse(Set.of()); + } +} diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/DelegateList.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/DelegateList.java new file mode 100644 index 0000000..480f1ec --- /dev/null +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/DelegateList.java @@ -0,0 +1,196 @@ +package io.gitlab.jfronny.gson.compile.processor.util; + +import org.jetbrains.annotations.NotNull; + +import java.util.*; +import java.util.function.*; +import java.util.stream.Stream; + +public class DelegateList implements List { + private final List delegate; + + public DelegateList(List delegate) { + Objects.requireNonNull(delegate); + this.delegate = delegate instanceof DelegateList dl ? dl.unwrap() : delegate; + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean contains(Object o) { + return delegate.contains(o); + } + + @NotNull + @Override + public Iterator iterator() { + return delegate.iterator(); + } + + @Override + public void forEach(Consumer action) { + delegate.forEach(action); + } + + @NotNull + @Override + public Object[] toArray() { + return delegate.toArray(); + } + + @NotNull + @Override + public T1[] toArray(@NotNull T1[] t1s) { + return delegate.toArray(t1s); + } + + @Override + public T1[] toArray(IntFunction generator) { + return delegate.toArray(generator); + } + + @Override + public boolean add(T t) { + return delegate.add(t); + } + + @Override + public boolean remove(Object o) { + return delegate.remove(o); + } + + @Override + public boolean containsAll(@NotNull Collection collection) { + return delegate.containsAll(collection); + } + + @Override + public boolean addAll(@NotNull Collection collection) { + return delegate.addAll(collection); + } + + @Override + public boolean addAll(int i, @NotNull Collection collection) { + return delegate.addAll(i, collection); + } + + @Override + public boolean removeAll(@NotNull Collection collection) { + return delegate.removeAll(collection); + } + + @Override + public boolean removeIf(Predicate filter) { + return delegate.removeIf(filter); + } + + @Override + public boolean retainAll(@NotNull Collection collection) { + return delegate.retainAll(collection); + } + + @Override + public void replaceAll(UnaryOperator operator) { + delegate.replaceAll(operator); + } + + @Override + public void sort(Comparator c) { + delegate.sort(c); + } + + @Override + public void clear() { + delegate.clear(); + } + + @Override + public T get(int i) { + return delegate.get(i); + } + + @Override + public T set(int i, T t) { + return delegate.set(i, t); + } + + @Override + public void add(int i, T t) { + delegate.add(i, t); + } + + @Override + public T remove(int i) { + return delegate.remove(i); + } + + @Override + public int indexOf(Object o) { + return delegate.indexOf(o); + } + + @Override + public int lastIndexOf(Object o) { + return delegate.lastIndexOf(o); + } + + @NotNull + @Override + public ListIterator listIterator() { + return delegate.listIterator(); + } + + @NotNull + @Override + public ListIterator listIterator(int i) { + return delegate.listIterator(i); + } + + @NotNull + @Override + public List subList(int i, int i1) { + return delegate.subList(i, i1); + } + + @Override + public Spliterator spliterator() { + return delegate.spliterator(); + } + + @Override + public Stream stream() { + return delegate.stream(); + } + + @Override + public Stream parallelStream() { + return delegate.parallelStream(); + } + + @Override + public int hashCode() { + return delegate.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return obj instanceof DelegateList dl ? delegate.equals(dl.delegate) : delegate.equals(obj); + } + + @Override + public String toString() { + return delegate.toString(); + } + + private List unwrap() { + return delegate instanceof DelegateList dl ? dl.unwrap() : delegate; + } +} diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/SupportedAnnotationTypes2.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/SupportedAnnotationTypes2.java new file mode 100644 index 0000000..110b84f --- /dev/null +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/SupportedAnnotationTypes2.java @@ -0,0 +1,10 @@ +package io.gitlab.jfronny.gson.compile.processor.util; + +import java.lang.annotation.*; + +@Documented +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.RUNTIME) +public @interface SupportedAnnotationTypes2 { + Class[] value(); +} diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ConstructionSource.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ConstructionSource.java new file mode 100644 index 0000000..e265f45 --- /dev/null +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ConstructionSource.java @@ -0,0 +1,247 @@ +package io.gitlab.jfronny.gson.compile.processor.util.valueprocessor; + +import org.jetbrains.annotations.ApiStatus; + +import javax.lang.model.element.*; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.lang.model.util.Types; +import java.util.Locale; +import java.util.Objects; + +/** + * How a [Value] can be constructed. Either a constructor, factory method, or builder. + */ +public sealed interface ConstructionSource { + /** + * The target [Value] class to construct. + */ + TypeElement getTargetClass(); + /** + * The executable element to construct the [Value]. This may be a constructor, factory method, or builder. + */ + ExecutableElement getConstructionElement(); + /** + * If this source is a constructor (either of the value or the builder). + */ + boolean isConstructor(); + /** + * If this source is a builder. + */ + boolean isBuilder(); + + final class Constructor implements ConstructionSource { + private final ExecutableElement constructor; + private TypeElement targetClass; + + public Constructor(ExecutableElement constructor) { + this.constructor = Objects.requireNonNull(constructor); + } + + @Override + public TypeElement getTargetClass() { + return targetClass != null ? targetClass : (targetClass = (TypeElement) constructor.getEnclosingElement()); + } + + @Override + public ExecutableElement getConstructionElement() { + return constructor; + } + + @Override + public boolean isConstructor() { + return true; + } + + @Override + public boolean isBuilder() { + return false; + } + } + + final class Factory implements ConstructionSource { + private final Types types; + private final ExecutableElement method; + private TypeElement targetClass; + + public Factory(Types types, ExecutableElement method) { + this.types = types; + this.method = method; + } + + @Override + public TypeElement getTargetClass() { + return targetClass != null ? targetClass : (targetClass = (TypeElement) types.asElement(method.getReturnType())); + } + + @Override + public ExecutableElement getConstructionElement() { + return method; + } + + @Override + public boolean isConstructor() { + return false; + } + + @Override + public boolean isBuilder() { + return false; + } + } + + sealed abstract class Builder implements ConstructionSource { + public abstract TypeElement getBuilderClass(); + public abstract ExecutableElement getBuildMethod(); + + @Override + public boolean isBuilder() { + return true; + } + } + + final class BuilderConstructor extends Builder { + private final Types types; + private final ExecutableElement constructor; + private TypeElement targetClass; + private TypeElement builderClass; + private ExecutableElement buildMethod; + + public BuilderConstructor(Types types, ExecutableElement constructor) { + this.types = types; + this.constructor = constructor; + } + + @Override + public TypeElement getTargetClass() { + return targetClass != null ? targetClass : (targetClass = (TypeElement) types.asElement(getBuildMethod().getReturnType())); + } + + @Override + public ExecutableElement getConstructionElement() { + return constructor; + } + + @Override + public boolean isConstructor() { + return true; + } + + @Override + public TypeElement getBuilderClass() { + return builderClass != null ? builderClass : (builderClass = (TypeElement) constructor.getEnclosingElement()); + } + + @Override + public ExecutableElement getBuildMethod() { + return buildMethod != null ? buildMethod : (buildMethod = findBuildMethod((TypeElement) constructor.getEnclosingElement())); + } + } + + final class BuilderFactory extends Builder { + private final Types types; + private final ExecutableElement method; + private TypeElement targetClass; + private TypeElement builderClass; + private ExecutableElement buildMethod; + + public BuilderFactory(Types types, ExecutableElement method) { + this.types = types; + this.method = method; + } + + @Override + public TypeElement getTargetClass() { + return targetClass != null ? targetClass : (targetClass = (TypeElement) types.asElement(getBuildMethod().getReturnType())); + } + + @Override + public ExecutableElement getConstructionElement() { + return method; + } + + @Override + public boolean isConstructor() { + return false; + } + + @Override + public TypeElement getBuilderClass() { + return builderClass != null ? builderClass : (builderClass = (TypeElement) types.asElement(method.getReturnType())); + } + + @Override + public ExecutableElement getBuildMethod() { + return buildMethod != null ? buildMethod : (buildMethod = findBuildMethod((TypeElement) types.asElement(method.getReturnType()))); + } + } + + @ApiStatus.Internal + static ExecutableElement findBuildMethod(TypeElement builderClass) { + // Ok, maybe there is just one possible builder method. + { + ExecutableElement candidate = null; + boolean foundMultipleCandidates = false; + boolean isCandidateReasonableBuilderMethodName = false; + for (ExecutableElement method : ElementFilter.methodsIn(builderClass.getEnclosedElements())) { + if (isPossibleBuilderMethod(method, builderClass)) { + if (candidate == null) { + candidate = method; + } else { + // Multiple possible methods, keep the one with a reasonable builder name if possible. + foundMultipleCandidates = true; + isCandidateReasonableBuilderMethodName = isCandidateReasonableBuilderMethodName || isReasonableBuilderMethodName(candidate); + if (isCandidateReasonableBuilderMethodName) { + if (isReasonableBuilderMethodName(method)) { + // both reasonable, too ambiguous. + candidate = null; + break; + } + } else { + candidate = method; + } + } + } + } + if (candidate != null && (!foundMultipleCandidates || isCandidateReasonableBuilderMethodName)) { + return candidate; + } + } + // Last try, check to see if the immediate parent class makes sense. + { + Element candidate = builderClass.getEnclosingElement(); + if (candidate.getKind() == ElementKind.CLASS) { + for (ExecutableElement method : ElementFilter.methodsIn(builderClass.getEnclosedElements())) { + if (method.getReturnType().equals(candidate.asType()) && method.getParameters().isEmpty()) { + return method; + } + } + } + } + // Well, I give up. + return null; + } + + /** + * A possible builder method has no parameters and a return type of the class we want to + * construct. Therefore, the return type is not going to be void, primitive, or a platform + * class. + */ + @ApiStatus.Internal + static boolean isPossibleBuilderMethod(ExecutableElement method, TypeElement builderClass) { + if (!method.getParameters().isEmpty()) return false; + TypeMirror returnType = method.getReturnType(); + if (returnType.getKind() == TypeKind.VOID) return false; + if (returnType.getKind().isPrimitive()) return false; + if (returnType.equals(builderClass.asType())) return false; + String returnTypeName = returnType.toString(); + return !(returnTypeName.startsWith("java.") || returnTypeName.startsWith("javax.") || returnTypeName.startsWith("android.")); + } + + @ApiStatus.Internal + static boolean isReasonableBuilderMethodName(ExecutableElement method) { + String methodName = method.getSimpleName().toString().toLowerCase(Locale.ROOT); + return methodName.startsWith("build") || methodName.startsWith("create"); + } +} diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ElementException.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ElementException.java new file mode 100644 index 0000000..e645706 --- /dev/null +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ElementException.java @@ -0,0 +1,29 @@ +package io.gitlab.jfronny.gson.compile.processor.util.valueprocessor; + +import javax.annotation.processing.Messager; +import javax.lang.model.element.Element; +import javax.tools.Diagnostic; +import java.util.List; +import java.util.stream.Collectors; + +public class ElementException extends Exception { + private final List messages; + + public ElementException(String message, Element element) { + this(List.of(new Message(message, element))); + } + + public ElementException(List messages) { + super(messages.stream().map(Message::message).collect(Collectors.joining("\n"))); + this.messages = messages; + } + + public void printMessage(Messager messager) { + for (Message message : messages) { + if (message.element != null) messager.printMessage(Diagnostic.Kind.ERROR, message.message, message.element); + else messager.printMessage(Diagnostic.Kind.ERROR, message.message); + } + } + + public static record Message(String message, Element element) {} +} diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Properties.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Properties.java new file mode 100644 index 0000000..6ee0059 --- /dev/null +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Properties.java @@ -0,0 +1,249 @@ +package io.gitlab.jfronny.gson.compile.processor.util.valueprocessor; + +import io.gitlab.jfronny.gson.compile.processor.util.DelegateList; + +import javax.lang.model.element.*; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.ElementFilter; +import javax.lang.model.util.Types; +import java.util.*; + +public class Properties extends DelegateList> { + public final List> names; + public final List params; + public final List fields; + public final List getters; + public final List constructorParams; + public final List builderParams; + + public static Properties build(Types types, ConstructionSource constructionSource) throws ElementException { + Builder builder = new Builder(types); + // constructor params + for (VariableElement param : constructionSource.getConstructionElement().getParameters()) { + builder.addConstructorParam(param); + } + + if (constructionSource instanceof ConstructionSource.Builder csb) { + var builderClass = csb.getBuilderClass(); + for (ExecutableElement method : ElementFilter.methodsIn(builderClass.getEnclosedElements())) { + builder.addBuilderParam(builderClass.asType(), method); + } + } + + var targetClass = constructionSource.getTargetClass(); + builder.addFieldsAndGetters(targetClass); + + return builder.build(); + } + + private Properties(List> names, + List params, + List fields, + List getters, + List constructorParams, + List builderParams) { + super(names); + this.names = Objects.requireNonNull(names); + this.params = Objects.requireNonNull(params); + this.fields = Objects.requireNonNull(fields); + this.getters = Objects.requireNonNull(getters); + this.constructorParams = Objects.requireNonNull(constructorParams); + this.builderParams = Objects.requireNonNull(builderParams); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (!(obj instanceof Properties other)) return false; + return names.equals(other.names); + } + + @Override + public int hashCode() { + return names.hashCode(); + } + + @Override + public String toString() { + return names.toString(); + } + + private static class Builder { + private static final Set METHODS_TO_SKIP = Set.of("hashCode", "toString", "clone"); + + private final Types types; + public final List> names = new ArrayList<>(); + public final List params = new ArrayList<>(); + public final List fields = new ArrayList<>(); + public final List getters = new ArrayList<>(); + public final List constructorParams = new ArrayList<>(); + public final List builderParams = new ArrayList<>(); + + public Builder(Types types) { + this.types = types; + } + + public void addFieldsAndGetters(TypeElement targetClass) { + // getters + for (ExecutableElement method : ElementFilter.methodsIn(targetClass.getEnclosedElements())) { + addGetter(targetClass, method); + } + + // fields + for (VariableElement field : ElementFilter.fieldsIn(targetClass.getEnclosedElements())) { + addField(field); + } + + for (TypeMirror superInterface : targetClass.getInterfaces()) { + addFieldsAndGetters((TypeElement) types.asElement(superInterface)); + } + + TypeMirror superclass = targetClass.getSuperclass(); + if (superclass.getKind() != TypeKind.NONE && !superclass.toString().equals("java.lang.Object")) { + addFieldsAndGetters((TypeElement) types.asElement(superclass)); + } + } + + public void addGetter(TypeElement classElement, ExecutableElement method) { + Set modifiers = method.getModifiers(); + if (modifiers.contains(Modifier.PRIVATE) + || modifiers.contains(Modifier.STATIC) + || method.getReturnType().getKind() == TypeKind.VOID + || !method.getParameters().isEmpty() + || isMethodToSkip(classElement, method)) { + return; + } + getters.add(new Property.Getter(method)); + } + + public void addField(VariableElement field) { + Set modifiers = field.getModifiers(); + if (modifiers.contains(Modifier.STATIC)) return; + fields.add(new Property.Field(field)); + } + + public void addConstructorParam(VariableElement param) { + Property.ConstructorParam prop = new Property.ConstructorParam(param); + constructorParams.add(prop); + params.add(prop); + } + + public void addBuilderParam(TypeMirror builderType, ExecutableElement method) { + if (method.getReturnType().equals(builderType) && method.getParameters().size() == 1) { + Property.BuilderParam prop = new Property.BuilderParam(method); + builderParams.add(prop); + params.add(prop); + } + } + + public Properties build() throws ElementException { + stripBeans(getters); + removeExtraBuilders(); + removeGettersForTransientFields(); + mergeSerializeNames(params, fields, getters); + removeExtraFields(); + names.addAll(params); + fields.stream().filter(f -> !containsName(names, f)).forEach(names::add); + getters.stream().filter(f -> !containsName(names, f)).forEach(names::add); + return new Properties(names, params, fields, getters, constructorParams, builderParams); + } + + private void stripBeans(List getters) { + if (getters.stream().allMatch(Property.Getter::isBean)) { + for (Property.Getter getter : getters) { + getter.stripBean(); + } + } + } + + private void removeExtraBuilders() { + for (int i = builderParams.size() - 1; i >= 0; i--) { + Property.BuilderParam builderParam = builderParams.get(i); + if (containsName(constructorParams, builderParam)) { + builderParams.remove(i); + params.remove(builderParam); + } + } + } + + private void removeExtraFields() { + for (int i = fields.size() - 1; i >= 0; i--) { + Property.Field field = fields.get(i); + Set modifiers = field.element.getModifiers(); + if (modifiers.contains(Modifier.PRIVATE) + || modifiers.contains(Modifier.TRANSIENT) + || containsName(getters, field)) { + fields.remove(i); + } + } + } + + private void removeGettersForTransientFields() { + for (int i = getters.size() - 1; i >= 0; i--) { + Property.Getter getter = getters.get(i); + Property field = findName(fields, getter); + if (field != null && field.element.getModifiers().contains(Modifier.TRANSIENT)) { + getters.remove(i); + } + } + } + + private boolean isMethodToSkip(TypeElement classElement, ExecutableElement method) { + String name = method.getSimpleName().toString(); + if (METHODS_TO_SKIP.contains(name)) { + return true; + } + return isKotlinClass(classElement) && name.matches("component[0-9]+"); + } + } + + private static void merge(Property[] properties) throws ElementException { + if (properties.length == 0) return; + List annotations = null; + for (Property name : properties) { + if (name == null) continue; + if (!name.getAnnotations().isEmpty()) { + if (annotations == null) annotations = new ArrayList<>(name.getAnnotations()); + else { + for (AnnotationMirror annotation : name.getAnnotations()) { + if (annotations.contains(annotation)) { + throw new ElementException("Duplicate annotation " + annotation + " found on " + name, name.element); + } else annotations.add(annotation); + } + } + } + } + if (annotations != null) { + for (Property name : properties) { + if (name == null) continue; + name.setAnnotations(annotations); + } + } + } + + @SafeVarargs + private static void mergeSerializeNames(List>... propertyLists) throws ElementException { + if (propertyLists.length == 0) return; + for (Property name : propertyLists[0]) { + var names = new Property[propertyLists.length]; + names[0] = name; + for (int i = 1; i < propertyLists.length; i++) { + names[i] = findName(propertyLists[i], name); + } + merge(names); + } + } + + private static > N findName(List names, Property property) { + return names.stream().filter(n -> n.getName().equals(property.getName())).findFirst().orElse(null); + } + + private static boolean containsName(List> properties, Property property) { + return findName(properties, property) != null; + } + + private static boolean isKotlinClass(TypeElement element) { + return element.getAnnotationMirrors().stream().anyMatch(m -> m.getAnnotationType().toString().equals("kotlin.Metadata")); + } +} diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Property.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Property.java new file mode 100644 index 0000000..9933e7d --- /dev/null +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Property.java @@ -0,0 +1,160 @@ +package io.gitlab.jfronny.gson.compile.processor.util.valueprocessor; + +import org.jetbrains.annotations.ApiStatus; + +import javax.lang.model.element.*; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import java.util.List; + +public abstract sealed class Property { + protected final T element; + private List annotations; + + public Property(T element) { + this.element = element; + this.annotations = element.getAnnotationMirrors(); + } + + public T getElement() { + return element; + } + + /** + * The name of the property. For fields and params this is the name in code. For getters, it may have the 'get' or + * 'is' prefix stripped. + * @see #getCallableName() + */ + public String getName() { + return element.getSimpleName().toString(); + } + + /** + * The actual name of the property. This will not have any 'get' or 'is' prefix stripped. + * @see #getName() + */ + public String getCallableName() { + return getName(); + } + + /** + * The property's type. + */ + public TypeMirror getType() { + return element.asType(); + } + + /** + * Annotations relevant to the property. These may be copied from another source. For example, if this is a getter + * it may contain the annotations on the backing private field. + */ + public List getAnnotations() { + return annotations; + } + + public void setAnnotations(List annotations) { + this.annotations = annotations; + } + + @Override + public String toString() { + return getName() + ": " + getType(); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (this == obj) return true; + if (!(obj instanceof Property other)) return false; + return element.equals(other.element); + } + + @Override + public int hashCode() { + return element.hashCode(); + } + + public static final class Field extends Property { + public Field(VariableElement element) { + super(element); + } + } + + public static final class Getter extends Property { + private static final String BEAN_PREFIX = "get"; + private static final String BEAN_PREFIX_BOOL = "is"; + + private boolean stripBean = false; + + public Getter(ExecutableElement element) { + super(element); + } + + public boolean isBean() { + return getBeanPrefix() != null; + } + + @Override + public String getName() { + String name = super.getName(); + if (stripBean) { + String prefix = getBeanPrefix(); + if (prefix != null) { + return Character.toLowerCase(name.charAt(prefix.length())) + name.substring(prefix.length() + 1); + } + } + return name; + } + + @Override + public String getCallableName() { + return super.getName(); + } + + @Override + public TypeMirror getType() { + return element.getReturnType(); + } + + @ApiStatus.Internal + public void stripBean() { + stripBean = true; + } + + private String getBeanPrefix() { + String name = super.getName(); + if (element.getReturnType().getKind() == TypeKind.BOOLEAN) { + if (name.length() > BEAN_PREFIX_BOOL.length() && name.startsWith(BEAN_PREFIX_BOOL)) { + return BEAN_PREFIX_BOOL; + } + } + return name.length() > BEAN_PREFIX.length() && name.startsWith(BEAN_PREFIX) ? BEAN_PREFIX : null; + } + } + + public static sealed abstract class Param extends Property { + public Param(VariableElement element) { + super(element); + } + } + + public static final class ConstructorParam extends Param { + public ConstructorParam(VariableElement element) { + super(element); + } + } + + public static final class BuilderParam extends Param { + private final ExecutableElement method; + + public BuilderParam(ExecutableElement method) { + super(method.getParameters().get(0)); + this.method = method; + } + + @Override + public String getCallableName() { + return method.getSimpleName().toString(); + } + } +} diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Value.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Value.java new file mode 100644 index 0000000..19e89c7 --- /dev/null +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/Value.java @@ -0,0 +1,52 @@ +package io.gitlab.jfronny.gson.compile.processor.util.valueprocessor; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.TypeElement; +import java.util.Objects; +import java.util.stream.Collectors; + +public class Value { + private final ProcessingEnvironment env; + private final ConstructionSource constructionSource; + private final TypeElement element; + private Properties properties; + + public Value(ProcessingEnvironment env, ConstructionSource constructionSource) { + this.env = env; + this.constructionSource = constructionSource; + this.element = constructionSource.getTargetClass(); + } + + public Properties getProperties() throws ElementException { + return properties != null ? properties : (properties = Properties.build(env.getTypeUtils(), constructionSource)); + } + + public ConstructionSource getConstructionSource() { + return constructionSource; + } + + public TypeElement getElement() { + return element; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof Value value)) return false; + return element.equals(value.element); + } + + @Override + public int hashCode() { + return element.hashCode(); + } + + @Override + public String toString() { + try { + return element.toString() + '{' + getProperties().stream().map(Objects::toString).collect(Collectors.joining(", ")); + } catch (ElementException e) { + return element.toString(); + } + } +} diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ValueCreator.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ValueCreator.java new file mode 100644 index 0000000..c0ea858 --- /dev/null +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/util/valueprocessor/ValueCreator.java @@ -0,0 +1,154 @@ +package io.gitlab.jfronny.gson.compile.processor.util.valueprocessor; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.*; +import javax.lang.model.util.ElementFilter; +import java.util.*; + +public class ValueCreator { + private final ProcessingEnvironment env; + + public ValueCreator(ProcessingEnvironment env) { + this.env = env; + } + + public Value from(Element element) throws ElementException { + return from(element, false); + } + + /** + * Creates a [Value] from the given element. This element can be the [TypeElement] of the target class, or a + * specific constructor or factory method. If [isBuilder] is true, then the element represents the builder class, + * constructor or factory method. + */ + public Value from(Element element, boolean isBuilder) throws ElementException { + if (element instanceof TypeElement tel) { + return isBuilder ? fromBuilderClass(tel) : fromClass(tel); + } else if (element instanceof ExecutableElement xel) { + if (xel.getKind() == ElementKind.CONSTRUCTOR) { + return isBuilder ? fromBuilderConstructor(xel) : fromConstructor(xel); + } else { + return isBuilder ? fromBuilderFactory(xel) : fromFactory(xel); + } + } else throw new IllegalArgumentException("Expected TypeElement or ExecutableElement but got: " + element); + } + + /** + * Creates a [Value] from the given constructor element. ex: + * ``` + * public class Value { + * > public Value(int arg1) { ... } + * } + * ``` + */ + public Value fromConstructor(ExecutableElement constructor) { + checkKind(constructor, ElementKind.CONSTRUCTOR); + return create(new ConstructionSource.Constructor(constructor)); + } + + /** + * Creates a [Value] from the given builder's constructor element. ex: + * ``` + * public class Builder { + * > public Builder() { ... } + * public Value build() { ... } + * } + * ``` + */ + public Value fromBuilderConstructor(ExecutableElement constructor) { + checkKind(constructor, ElementKind.CONSTRUCTOR); + return create(new ConstructionSource.BuilderConstructor(env.getTypeUtils(), constructor)); + } + + /** + * Creates a [Value] from the given factory method element. ex: + * ``` + * public class Value { + * > public static Value create(int arg) { ... } + * } + * ``` + */ + public Value fromFactory(ExecutableElement factory) { + checkKind(factory, ElementKind.METHOD); + return create(new ConstructionSource.Factory(env.getTypeUtils(), factory)); + } + + /** + * Creates a [Value] from the given builder factory method element. ex: + * ``` + * public class Value { + * > public static Builder builder() { ... } + * public static class Builder { ... } + * } + * ``` + */ + public Value fromBuilderFactory(ExecutableElement builderFactory) { + checkKind(builderFactory, ElementKind.METHOD); + return create(new ConstructionSource.BuilderFactory(env.getTypeUtils(), builderFactory)); + } + + /** + * Creates a [Value] from the given class. ex: + * ``` + * > public class Value { ... } + * ``` + */ + public Value fromClass(TypeElement targetClass) throws ElementException { + ExecutableElement creator = findConstructorOrFactory(targetClass); + return creator.getKind() == ElementKind.CONSTRUCTOR ? fromConstructor(creator) : fromFactory(creator); + } + + /** + * Creates a [Value] from the given builder class. ex: + * ``` + * > public class Builder { + * public Value build() { ... } + * } + * ``` + */ + public Value fromBuilderClass(TypeElement builderClass) throws ElementException { + ExecutableElement creator = findConstructorOrFactory(builderClass); + return creator.getKind() == ElementKind.CONSTRUCTOR ? fromBuilderConstructor(creator) : fromBuilderFactory(creator); + } + + private Value create(ConstructionSource constructionSource) { + return new Value(env, constructionSource); + } + + private static ExecutableElement findConstructorOrFactory(TypeElement klazz) throws ElementException { + ExecutableElement noArgConstructor = null; + List constructors = ElementFilter.constructorsIn(klazz.getEnclosedElements()); + if (constructors.size() == 1) { + ExecutableElement constructor = constructors.get(0); + if (constructor.getParameters().isEmpty()) { + noArgConstructor = constructor; + constructors.remove(0); + } + } + for (ExecutableElement method : ElementFilter.methodsIn(klazz.getEnclosedElements())) { + Set modifiers = method.getModifiers(); + if (modifiers.contains(Modifier.STATIC) + && !modifiers.contains(Modifier.PRIVATE) + && method.getReturnType().equals(klazz.asType())) { + constructors.add(method); + } + } + if (constructors.isEmpty() && noArgConstructor != null) { + constructors.add(noArgConstructor); + } + if (constructors.size() == 1) { + return constructors.get(0); + } else { + List messages = new ArrayList<>(); + messages.add(new ElementException.Message("More than one constructor or factory method found.", klazz)); + constructors.stream().map(s -> new ElementException.Message(" " + s, s)).forEach(messages::add); + throw new ElementException(messages); + } + } + + private static void checkKind(Element element, ElementKind kind) { + if (element.getKind() != kind) { + throw new IllegalArgumentException("Expected " + kind + " but got: " + element); + } + } +} diff --git a/gson-compile-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/gson-compile-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000..2fe3ff3 --- /dev/null +++ b/gson-compile-processor/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +io.gitlab.jfronny.gson.compile.processor.GsonCompileProcessor \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..2289787 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,5 @@ +rootProject.name = "gson-compile" +include("gson-compile-core") +include("gson-compile-processor") +include("gson-compile-annotations") +include("gson-compile-example")