Support for 1d classes of primitives

This commit is contained in:
Johannes Frohnmeyer 2022-10-31 20:52:48 +01:00
commit e2f59601c0
Signed by: Johannes
GPG Key ID: E76429612C2929F4
24 changed files with 1921 additions and 0 deletions

39
.gitignore vendored Normal file
View File

@ -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

2
build.gradle.kts Normal file
View File

@ -0,0 +1,2 @@
group = "io.gitlab.jfronny"
version = "1.0-SNAPSHOT"

View File

@ -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>("test") {
useJUnitPlatform()
}

View File

@ -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 {
}

View File

@ -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;
}

View File

@ -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>("test") {
useJUnitPlatform()
}

View File

@ -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();
}

View File

@ -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<String>() {
@Override
public void write(JsonWriter jsonWriter, String s) throws IOException {
}
@Override
public String read(JsonReader jsonReader) throws IOException {
return null;
}
};
}
}

View File

@ -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"))
}

View File

@ -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;
}
}

View File

@ -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>("test") {
useJUnitPlatform()
}

View File

@ -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");
}

View File

@ -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<ClassName> 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<? extends TypeElement> annotations, RoundEnvironment roundEnvironment) {
Set<ToProcess> 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<TypeVariableName> 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<TypeVariableName> 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<TypeVariableName> 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<TypeVariableName> 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<TypeVariableName> 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<TypeVariableName> typeVariables, boolean allowClassType) {
TypeMirror type = property.getType();
TypeName typeName = TypeName.get(type);
if (isComplexType(type)) {
TypeName typeTokenType = ParameterizedTypeName.get(Const.TYPE_TOKEN, typeName);
List<? extends TypeMirror> 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<? extends TypeMirror> 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<? extends TypeMirror> getGenericTypes(TypeMirror type) {
DeclaredType declaredType = asDeclaredType(type);
if (declaredType == null) {
return Collections.emptyList();
}
ArrayList<TypeMirror> 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<? extends AnnotationMirror> annotations) {
for (AnnotationMirror annotation : annotations) {
String typeName = annotation.getAnnotationType().toString();
if (typeName.equals(Const.JSON_ADAPTER.toString())) {
Map<? extends ExecutableElement, ? extends AnnotationValue> 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);
}
}

View File

@ -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<String> 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());
}
}

View File

@ -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<T> implements List<T> {
private final List<T> delegate;
public DelegateList(List<T> delegate) {
Objects.requireNonNull(delegate);
this.delegate = delegate instanceof DelegateList<T> 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<T> iterator() {
return delegate.iterator();
}
@Override
public void forEach(Consumer<? super T> action) {
delegate.forEach(action);
}
@NotNull
@Override
public Object[] toArray() {
return delegate.toArray();
}
@NotNull
@Override
public <T1> T1[] toArray(@NotNull T1[] t1s) {
return delegate.toArray(t1s);
}
@Override
public <T1> T1[] toArray(IntFunction<T1[]> 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<? extends T> collection) {
return delegate.addAll(collection);
}
@Override
public boolean addAll(int i, @NotNull Collection<? extends T> collection) {
return delegate.addAll(i, collection);
}
@Override
public boolean removeAll(@NotNull Collection<?> collection) {
return delegate.removeAll(collection);
}
@Override
public boolean removeIf(Predicate<? super T> filter) {
return delegate.removeIf(filter);
}
@Override
public boolean retainAll(@NotNull Collection<?> collection) {
return delegate.retainAll(collection);
}
@Override
public void replaceAll(UnaryOperator<T> operator) {
delegate.replaceAll(operator);
}
@Override
public void sort(Comparator<? super T> 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<T> listIterator() {
return delegate.listIterator();
}
@NotNull
@Override
public ListIterator<T> listIterator(int i) {
return delegate.listIterator(i);
}
@NotNull
@Override
public List<T> subList(int i, int i1) {
return delegate.subList(i, i1);
}
@Override
public Spliterator<T> spliterator() {
return delegate.spliterator();
}
@Override
public Stream<T> stream() {
return delegate.stream();
}
@Override
public Stream<T> 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<T> unwrap() {
return delegate instanceof DelegateList<T> dl ? dl.unwrap() : delegate;
}
}

View File

@ -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();
}

View File

@ -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");
}
}

View File

@ -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<Message> messages;
public ElementException(String message, Element element) {
this(List.of(new Message(message, element)));
}
public ElementException(List<Message> 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) {}
}

View File

@ -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<Property<?>> {
public final List<Property<?>> names;
public final List<Property.Param> params;
public final List<Property.Field> fields;
public final List<Property.Getter> getters;
public final List<Property.ConstructorParam> constructorParams;
public final List<Property.BuilderParam> 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<Property<?>> names,
List<Property.Param> params,
List<Property.Field> fields,
List<Property.Getter> getters,
List<Property.ConstructorParam> constructorParams,
List<Property.BuilderParam> 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<String> METHODS_TO_SKIP = Set.of("hashCode", "toString", "clone");
private final Types types;
public final List<Property<?>> names = new ArrayList<>();
public final List<Property.Param> params = new ArrayList<>();
public final List<Property.Field> fields = new ArrayList<>();
public final List<Property.Getter> getters = new ArrayList<>();
public final List<Property.ConstructorParam> constructorParams = new ArrayList<>();
public final List<Property.BuilderParam> 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<Modifier> 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<Modifier> 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<Property.Getter> 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<Modifier> 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<AnnotationMirror> 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<? extends Property<?>>... 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 extends Property<?>> N findName(List<N> names, Property<?> property) {
return names.stream().filter(n -> n.getName().equals(property.getName())).findFirst().orElse(null);
}
private static boolean containsName(List<? extends Property<?>> 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"));
}
}

View File

@ -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<T extends Element> {
protected final T element;
private List<? extends AnnotationMirror> 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<? extends AnnotationMirror> getAnnotations() {
return annotations;
}
public void setAnnotations(List<? extends AnnotationMirror> 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<VariableElement> {
public Field(VariableElement element) {
super(element);
}
}
public static final class Getter extends Property<ExecutableElement> {
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<VariableElement> {
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();
}
}
}

View File

@ -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();
}
}
}

View File

@ -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<ExecutableElement> 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<Modifier> 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<ElementException.Message> 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);
}
}
}

View File

@ -0,0 +1 @@
io.gitlab.jfronny.gson.compile.processor.GsonCompileProcessor

5
settings.gradle.kts Normal file
View File

@ -0,0 +1,5 @@
rootProject.name = "gson-compile"
include("gson-compile-core")
include("gson-compile-processor")
include("gson-compile-annotations")
include("gson-compile-example")