Compare commits

...

5 Commits

Author SHA1 Message Date
Johannes Frohnmeyer ad238ac2b3
Parse enums 2022-11-01 17:26:32 +01:00
Johannes Frohnmeyer fe2b1fbe24
Parse dates 2022-11-01 16:58:16 +01:00
Johannes Frohnmeyer ca86f98ae4
Fix missing field/setters in read() 2022-11-01 16:47:00 +01:00
Johannes Frohnmeyer 47c77a116b
Support builders 2022-11-01 15:39:50 +01:00
Johannes Frohnmeyer cf795cdedc
Implement GPrefer 2022-11-01 15:24:59 +01:00
13 changed files with 363 additions and 115 deletions

View File

@ -6,26 +6,35 @@ The goal of this AP is to
- Support json5 through gson-comments
- Be compile-time where possible (ideally compatible with proguard)
## Currently supported
- Primitive types
- Adapter generation
- Utility methods
- Strict no-reflection enforcement via `-AgsonCompileNoReflect`
- Comments via `@GComment`
## Supported types
- Primitives (and boxes)
- Records
- Nested serializable types
- Arrays
- Collections (Sets, Lists, Queues, Deques)
- java.util.Date as iso8601
- Enums
## Used properties
Use `@GPrefer` to choose one construction method if multiple are available
- Builders (referenced in `@GSerializable`)
- Factory functions
- Constructors
- Getters/Setters
- Direct fields (public non-static only)
## Additional features
- Support for generating Gson adapters to hook into the gson system
- Optional, strict no-reflection enforcement via `-AgsonCompileNoReflect`
- Comments via `@GComment`
- Several utility methods in the generated class for reading from/writing to various sources
## TODO
- Maps with string, primitive or enum keys
- Date via ISO8601Utils
- Enums
- Support for nested types from libraries
- Static classes (for configs)
- GPrefer to bypass builder/constructor recovery
## Credit
The Gson-Compile processor is based on [gsonvalue](https://github.com/evant/gsonvalue) and [value-processor](https://github.com/evant/value-processor) by Eva Tatarka.
The API was inspired by [kotlinx.serialization](https://github.com/Kotlin/kotlinx.serialization) by Jetbrains
The serialization is powered by [my fork](https://gitlab.com/JFronny/gson-comments) of [gson](https://github.com/google/gson) by Google
The serialization is powered by [my fork](https://gitlab.com/JFronny/gson-comments) of [gson](https://github.com/google/gson) by Google

View File

@ -5,6 +5,18 @@ import java.lang.annotation.*;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface GSerializable {
/**
* @return The class implementing this types serialization/deserialization. Must have static read/write methods
*/
Class<?> with() default void.class;
/**
* @return The builder class to use for creating this type. Incompatible with custom serialization
*/
Class<?> builder() default void.class;
/**
* @return Whether to generate an adapter class to use with normal gson adapter resolution
*/
boolean generateAdapter() default false;
}

View File

@ -1,8 +1,7 @@
package io.gitlab.jfronny.gson;
import io.gitlab.jfronny.gson.annotations.SerializedName;
import io.gitlab.jfronny.gson.compile.annotations.GComment;
import io.gitlab.jfronny.gson.compile.annotations.GSerializable;
import io.gitlab.jfronny.gson.compile.annotations.*;
import java.util.*;
@ -32,6 +31,10 @@ public class Main {
public Queue<String> queue;
public Date date;
public EeE eEe;
public void setJoe(String joe) {
}
@ -49,5 +52,39 @@ public class Main {
@GSerializable
public record ExampleRecord(String hello, @GComment("Sheesh") ExamplePojo2 pojo) {
@GPrefer
public ExampleRecord(String yes) {
this(yes, null);
}
}
@GSerializable(builder = Example4.Builder.class)
public static class Example4 {
public String someField;
public boolean shesh;
public static class Builder {
private String someField;
private boolean shesh;
public Builder(String someField) {
this.someField = someField;
}
public Builder setShesh(boolean shesh) {
this.shesh = shesh;
return this;
}
public Example4 build() {
Example4 e = new Example4();
e.someField = someField;
return e;
}
}
}
public enum EeE {
Yes, Yay, Aaee;
}
}

View File

@ -16,5 +16,6 @@ public class Cl {
public static final ClassName SERIALIZED_NAME = ClassName.get("io.gitlab.jfronny.gson.annotations", "SerializedName");
public static final ClassName GCOMMENT = ClassName.get("io.gitlab.jfronny.gson.compile.annotations", "GComment");
public static final ClassName GISO8601UTILS = ClassName.get("io.gitlab.jfronny.gson.internal.bind.util", "ISO8601Utils");
public static final ClassName CCORE = ClassName.get("io.gitlab.jfronny.gson.compile.core", "CCore");
}

View File

@ -18,7 +18,6 @@ import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import java.io.*;
import java.util.*;
import java.util.stream.Collectors;
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@SupportedAnnotationTypes2({GSerializable.class})
@ -50,31 +49,39 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
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();
try {
if (mirror.getAnnotationType().toString().equals(GSerializable.class.getCanonicalName())) {
var bld = new Object() {
TypeMirror with = null;
TypeMirror builder = 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 "builder" -> {
if (bld.builder != null) throw new IllegalArgumentException("Duplicate annotation parameter: builder");
bld.builder = (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);
}
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!");
});
if (bld.with == null) throw new IllegalArgumentException("Missing annotation parameter: with");
if (bld.builder == null) throw new IllegalArgumentException("Missing annotation parameter: with");
if (bld.generateAdapter == null) throw new IllegalArgumentException("Missing annotation parameter: generateAdapter");
toGenerate.add(SerializableClass.of((TypeElement) element, bld.with, bld.generateAdapter));
toGenerate.add(SerializableClass.of((TypeElement) element, bld.with, bld.builder, bld.generateAdapter));
}
} catch (ElementException e) {
e.printMessage(message);
}
}
}
@ -85,8 +92,10 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
for (SerializableClass toProcess : toGenerate) {
try {
process(toProcess, toGenerate);
} catch (IOException | ElementException e) {
} catch (IOException e) {
message.printMessage(Diagnostic.Kind.ERROR, "GsonCompile threw an exception: " + StringFormatter.toString(e), toProcess.classElement());
} catch (ElementException e) {
e.printMessage(message);
}
}
return false;
@ -115,7 +124,7 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
if (toProcess.generateAdapter()) {
generateDelegatingAdapter(spec, classType, toProcess.generatedClassName());
}
generateSerialisation(spec, classType, toProcess.classElement(), typeVariables, other);
generateSerialisation(spec, toProcess, typeVariables, other);
}
generateAuxiliary(spec, classType);
@ -154,19 +163,13 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
}
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("read")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Cl.GSON_READER, "reader")
.addException(IOException.class)
.returns(classType)
.addCode("return ADAPTER.read(reader);")
.addCode("return $T.read(reader);", adapter)
.build()
);
spec.addMethod(
@ -176,7 +179,7 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
.addParameter(classType, "value")
.addException(IOException.class)
.returns(classType)
.addCode("ADAPTER.write(reader, value);")
.addCode("$T.write(reader, value);", adapter)
.build()
);
}
@ -263,8 +266,8 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
);
}
private void generateSerialisation(TypeSpec.Builder spec, TypeName classType, TypeElement classElement, List<TypeVariableName> typeVariables, Set<SerializableClass> otherAdapters) throws ElementException {
Value value = valueCreator.from(classElement);
private void generateSerialisation(TypeSpec.Builder spec, SerializableClass self, List<TypeVariableName> typeVariables, Set<SerializableClass> otherAdapters) throws ElementException {
Value value = self.builder() == null ? valueCreator.from(self.classElement(), false) : valueCreator.from(TypeHelper.asDeclaredType(self.builder()).asElement(), true);
ConstructionSource constructionSource = value.getConstructionSource();
Properties properties = value.getProperties();
@ -278,6 +281,7 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
code.addStatement("writer.beginObject()");
for (Property.Field param : properties.fields) {
if (Properties.containsName(properties.getters, param)) continue;
generateComments(param, code);
code.addStatement("writer.name($S)", getSerializedName(param));
Runnable writeGet = () -> code.add("value.$N", param.getCallableName());
@ -310,7 +314,7 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
spec.addMethod(MethodSpec.methodBuilder("write")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Cl.GSON_WRITER, "writer")
.addParameter(classType, "value")
.addParameter(self.getTypeName(), "value")
.addException(IOException.class)
.addCode(code.build())
.build());
@ -359,32 +363,48 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
.addStatement("reader.endObject()");
}
code.addStatement("$T result;", self.getTypeName());
ClassName creatorName = ClassName.get((TypeElement) constructionSource.getConstructionElement().getEnclosingElement());
if (constructionSource instanceof ConstructionSource.Builder builder) {
String args = properties.constructorParams.stream().map(s -> "_" + s.getName()).collect(Collectors.joining(", "));
StringBuilder args = new StringBuilder();
for (Property.ConstructorParam param : properties.constructorParams) {
args.append(", _").append(param.getName());
}
code.add("$T builder = ", builder.getBuilderClass());
if (constructionSource.isConstructor()) {
code.add("return new $T($L)", builder.getBuilderClass(), args);
code.add("new $T($L)", builder.getBuilderClass(), args.length() > 0 ? args.substring(2) : "");
} else {
code.add("return $T.$N($L)", creatorName, classElement.getSimpleName(), args);
code.add("$T.$N($L)", creatorName, self.classElement().getSimpleName(), args.length() > 0 ? args.substring(2) : "");
}
code.add("\n").indent();
for (Property.BuilderParam param : properties.builderParams) {
code.add(".$N(_$L)\n", param.getCallableName(), param.getName());
code.add(";\n");
for (Property.Setter param : properties.builderParams) {
code.addStatement("builder.$N(_$N)", param.getCallableName(), param.getName());
}
code.add(".$N();\n", builder.getBuildMethod().getSimpleName()).unindent();
code.addStatement("result = builder.$N()", builder.getBuildMethod().getSimpleName());
} else {
String args = properties.params.stream().map(s -> "_" + s.getName()).collect(Collectors.joining(", "));
StringBuilder args = new StringBuilder();
for (Property.Param param : properties.params) {
args.append(", _").append(param.getName());
}
if (constructionSource.isConstructor()) {
code.addStatement("return new $T($L)", classType, args);
code.addStatement("result = new $T($L)", self.getTypeName(), args.length() > 0 ? args.substring(2) : "");
} else {
code.addStatement("return $T.$N($L)", creatorName, constructionSource.getConstructionElement().getSimpleName(), args);
code.addStatement("result = $T.$N($L)", creatorName, constructionSource.getConstructionElement().getSimpleName(), args.length() > 0 ? args.substring(2) : "");
}
}
//TODO manually set fields and setters if not in constructor
for (Property.Setter setter : properties.setters) {
code.addStatement("result.$N(_$N)", setter.getCallableName(), setter.getName());
}
for (Property.Field field : properties.fields) {
if (Properties.containsName(properties.setters, field)) continue;
if (Properties.containsName(properties.params, field)) continue;
code.addStatement("result.$N = _$N", field.getName(), field.getCallableName());
}
code.addStatement("return result");
spec.addMethod(MethodSpec.methodBuilder("read")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(classType)
.returns(self.getTypeName())
.addParameter(Cl.GSON_READER, "reader")
.addException(IOException.class)
.addCode(code.build())

View File

@ -2,16 +2,17 @@ package io.gitlab.jfronny.gson.compile.processor;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.TypeName;
import io.gitlab.jfronny.gson.compile.processor.util.valueprocessor.ElementException;
import org.jetbrains.annotations.Nullable;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.TypeMirror;
public record SerializableClass(TypeElement classElement, ClassName generatedClassName, @Nullable TypeMirror adapter, boolean generateAdapter) {
public static SerializableClass of(TypeElement element, @Nullable TypeMirror with, boolean generateAdapter) {
public record SerializableClass(TypeElement classElement, ClassName generatedClassName, @Nullable TypeMirror adapter, @Nullable TypeMirror builder, boolean generateAdapter) {
public static SerializableClass of(TypeElement element, @Nullable TypeMirror with, @Nullable TypeMirror builder, boolean generateAdapter) throws ElementException {
ClassName className = ClassName.get(element);
ClassName generatedClassName = ClassName.get(className.packageName(), "GC_" + String.join("_", className.simpleNames()));
return new SerializableClass(element, generatedClassName, with, generateAdapter);
return new SerializableClass(element, generatedClassName, voidToNull(with), voidToNull(builder), generateAdapter).validate();
}
public ClassName getClassName() {
@ -21,4 +22,13 @@ public record SerializableClass(TypeElement classElement, ClassName generatedCla
public TypeName getTypeName() {
return TypeName.get(classElement.asType());
}
private SerializableClass validate() throws ElementException {
if (adapter != null && builder != null) throw new ElementException("@Serializable with both an adapter and a builder. This is unsupported!", classElement);
return this;
}
private static TypeMirror voidToNull(TypeMirror type) {
return type == null || type.toString().equals("void") ? null : type;
}
}

View File

@ -10,7 +10,6 @@ import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import java.util.*;
import java.util.function.Consumer;
public abstract class Adapter<T extends Adapter<T>.Hydrated> {
protected Messager message;

View File

@ -19,6 +19,8 @@ public class Adapters {
new DeclaredAdapter(),
new PrimitiveAdapter(),
new StringAdapter(),
new DateAdapter(),
new EnumAdapter(),
new ArrayAdapter(),
new CollectionAdapter(),
new OtherSerializableAdapter(),

View File

@ -0,0 +1,62 @@
package io.gitlab.jfronny.gson.compile.processor.adapter.impl;
import com.squareup.javapoet.CodeBlock;
import com.squareup.javapoet.MethodSpec;
import io.gitlab.jfronny.gson.compile.processor.Cl;
import io.gitlab.jfronny.gson.compile.processor.adapter.Adapter;
import javax.lang.model.element.Modifier;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Date;
public class DateAdapter extends Adapter<DateAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends Adapter<Hydrated>.Hydrated {
@Override
public boolean applies() {
return type.toString().equals(Date.class.getCanonicalName());
}
@Override
public void generateWrite(Runnable writeGet) {
code.add("writer.value($T.format(", Cl.GISO8601UTILS);
writeGet.run();
code.add("));\n");
}
@Override
public void generateRead() {
boolean found = false;
for (MethodSpec spec : klazz.methodSpecs) {
if (spec.name.equals("parseDate")) {
found = true;
break;
}
}
if (!found) {
CodeBlock.Builder kode = CodeBlock.builder();
kode.beginControlFlow("try")
.addStatement("return $T.parse(date, new $T(0))", Cl.GISO8601UTILS, ParsePosition.class)
.nextControlFlow("catch ($T e)", ParseException.class)
.addStatement("throw new JsonSyntaxException(\"Failed Parsing '\" + date + \"' as Date\", e)")
.endControlFlow();
klazz.addMethod(
MethodSpec.methodBuilder("parseDate")
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.returns(Date.class)
.addParameter(String.class, "date")
.addCode(kode.build())
.build()
);
}
code.add("parseDate(reader.nextString())");
}
}
}

View File

@ -0,0 +1,62 @@
package io.gitlab.jfronny.gson.compile.processor.adapter.impl;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.gson.compile.processor.TypeHelper;
import io.gitlab.jfronny.gson.compile.processor.adapter.Adapter;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.DeclaredType;
public class EnumAdapter extends Adapter<EnumAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends Adapter<Hydrated>.Hydrated {
private DeclaredType tel;
@Override
protected void afterHydrate() {
super.afterHydrate();
tel = null;
DeclaredType declared = TypeHelper.asDeclaredType(type);
if (declared == null) return;
if (declared.asElement().getKind() != ElementKind.ENUM) return;
tel = declared;
}
@Override
public boolean applies() {
return tel != null;
}
@Override
public void generateWrite(Runnable writeGet) {
code.add("writer.value(");
writeGet.run();
code.add(".name());\n");
}
@Override
public void generateRead() {
CodeBlock.Builder kode = CodeBlock.builder();
kode.beginControlFlow("for ($1T t : $1T.values())", tel)
.addStatement("if (t.name().equals(value)) return t")
.endControlFlow()
.addStatement("return null");
String methodName = "read$" + name;
klazz.addMethod(
MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.returns(TypeName.get(tel))
.addParameter(String.class, "value")
.addCode(kode.build())
.build()
);
code.add("$N(reader.nextString())", methodName);
}
}
}

View File

@ -14,8 +14,9 @@ public class Properties extends DelegateList<Property<?>> {
public final List<Property.Param> params;
public final List<Property.Field> fields;
public final List<Property.Getter> getters;
public final List<Property.Setter> setters;
public final List<Property.ConstructorParam> constructorParams;
public final List<Property.BuilderParam> builderParams;
public final List<Property.Setter> builderParams;
public static Properties build(Types types, ConstructionSource constructionSource) throws ElementException {
Builder builder = new Builder(types);
@ -27,27 +28,29 @@ public class Properties extends DelegateList<Property<?>> {
if (constructionSource instanceof ConstructionSource.Builder csb) {
var builderClass = csb.getBuilderClass();
for (ExecutableElement method : ElementFilter.methodsIn(builderClass.getEnclosedElements())) {
builder.addBuilderParam(builderClass.asType(), method);
builder.addBuilderParam(method);
}
}
var targetClass = constructionSource.getTargetClass();
builder.addFieldsAndGetters(targetClass);
builder.addFieldsAndAccessors(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) {
List<Property.Param> params,
List<Property.Field> fields,
List<Property.Getter> getters,
List<Property.Setter> setters,
List<Property.ConstructorParam> constructorParams,
List<Property.Setter> builderParams) {
super(names);
this.names = Objects.requireNonNull(names);
this.params = Objects.requireNonNull(params);
this.fields = Objects.requireNonNull(fields);
this.getters = Objects.requireNonNull(getters);
this.setters = Objects.requireNonNull(setters);
this.constructorParams = Objects.requireNonNull(constructorParams);
this.builderParams = Objects.requireNonNull(builderParams);
}
@ -77,17 +80,19 @@ public class Properties extends DelegateList<Property<?>> {
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.Setter> setters = new ArrayList<>();
public final List<Property.ConstructorParam> constructorParams = new ArrayList<>();
public final List<Property.BuilderParam> builderParams = new ArrayList<>();
public final List<Property.Setter> builderParams = new ArrayList<>();
public Builder(Types types) {
this.types = types;
}
public void addFieldsAndGetters(TypeElement targetClass) {
// getters
public void addFieldsAndAccessors(TypeElement targetClass) {
// accessors
for (ExecutableElement method : ElementFilter.methodsIn(targetClass.getEnclosedElements())) {
addGetter(targetClass, method);
addSetter(targetClass, method);
}
// fields
@ -96,12 +101,12 @@ public class Properties extends DelegateList<Property<?>> {
}
for (TypeMirror superInterface : targetClass.getInterfaces()) {
addFieldsAndGetters((TypeElement) types.asElement(superInterface));
addFieldsAndAccessors((TypeElement) types.asElement(superInterface));
}
TypeMirror superclass = targetClass.getSuperclass();
if (superclass.getKind() != TypeKind.NONE && !superclass.toString().equals("java.lang.Object")) {
addFieldsAndGetters((TypeElement) types.asElement(superclass));
addFieldsAndAccessors((TypeElement) types.asElement(superclass));
}
}
@ -117,6 +122,18 @@ public class Properties extends DelegateList<Property<?>> {
getters.add(new Property.Getter(method));
}
public void addSetter(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().size() != 1
|| isMethodToSkip(classElement, method)) {
return;
}
setters.add(new Property.Setter(method));
}
public void addField(VariableElement field) {
Set<Modifier> modifiers = field.getModifiers();
if (modifiers.contains(Modifier.STATIC)) return;
@ -129,9 +146,9 @@ public class Properties extends DelegateList<Property<?>> {
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);
public void addBuilderParam(ExecutableElement method) {
if (method.getParameters().size() == 1 && method.getSimpleName().toString().startsWith("set")) {
Property.Setter prop = new Property.Setter(method);
builderParams.add(prop);
params.add(prop);
}
@ -140,13 +157,16 @@ public class Properties extends DelegateList<Property<?>> {
public Properties build() throws ElementException {
stripBeans(getters);
removeExtraBuilders();
removeExtraSetters();
removeGettersForTransientFields();
mergeSerializeNames(params, fields, getters);
removeSettersForTransientFields();
mergeSerializeNames(params, fields, getters, setters);
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);
setters.stream().filter(f -> !containsName(names, f)).forEach(names::add);
return new Properties(names, params, fields, getters, setters, constructorParams, builderParams);
}
private void stripBeans(List<Property.Getter> getters) {
@ -159,7 +179,7 @@ public class Properties extends DelegateList<Property<?>> {
private void removeExtraBuilders() {
for (int i = builderParams.size() - 1; i >= 0; i--) {
Property.BuilderParam builderParam = builderParams.get(i);
Property.Setter builderParam = builderParams.get(i);
if (containsName(constructorParams, builderParam)) {
builderParams.remove(i);
params.remove(builderParam);
@ -167,26 +187,29 @@ public class Properties extends DelegateList<Property<?>> {
}
}
private void removeExtraSetters() {
setters.removeIf(s -> containsName(constructorParams, s));
}
private void removeExtraFields() {
for (int i = fields.size() - 1; i >= 0; i--) {
Property.Field field = fields.get(i);
fields.removeIf(field -> {
Set<Modifier> modifiers = field.element.getModifiers();
if (modifiers.contains(Modifier.PRIVATE)
|| modifiers.contains(Modifier.TRANSIENT)
|| containsName(getters, field)) {
fields.remove(i);
}
}
return modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.TRANSIENT);
});
}
private void removeGettersForTransientFields() {
for (int i = getters.size() - 1; i >= 0; i--) {
Property.Getter getter = getters.get(i);
getters.removeIf(getter -> {
Property<?> field = findName(fields, getter);
if (field != null && field.element.getModifiers().contains(Modifier.TRANSIENT)) {
getters.remove(i);
}
}
return field != null && field.element.getModifiers().contains(Modifier.TRANSIENT);
});
}
private void removeSettersForTransientFields() {
getters.removeIf(getter -> {
Property<?> field = findName(fields, getter);
return field != null && field.element.getModifiers().contains(Modifier.TRANSIENT);
});
}
private boolean isMethodToSkip(TypeElement classElement, ExecutableElement method) {
@ -235,11 +258,11 @@ public class Properties extends DelegateList<Property<?>> {
}
}
private static <N extends Property<?>> N findName(List<N> names, Property<?> property) {
public 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) {
public static boolean containsName(List<? extends Property<?>> properties, Property<?> property) {
return findName(properties, property) != null;
}

View File

@ -144,12 +144,19 @@ public abstract sealed class Property<T extends Element> {
}
}
public static final class BuilderParam extends Param {
public static final class Setter extends Param {
private final ExecutableElement method;
private final String name;
public BuilderParam(ExecutableElement method) {
public Setter(ExecutableElement method) {
super(method.getParameters().get(0));
this.method = method;
name = Character.toLowerCase(method.getSimpleName().toString().charAt(3)) + method.getSimpleName().toString().substring(4);
}
@Override
public String getName() {
return name;
}
@Override

View File

@ -1,5 +1,7 @@
package io.gitlab.jfronny.gson.compile.processor.util.valueprocessor;
import io.gitlab.jfronny.gson.compile.annotations.GPrefer;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.*;
import javax.lang.model.util.ElementFilter;
@ -12,10 +14,6 @@ public class ValueCreator {
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,
@ -133,17 +131,23 @@ public class ValueCreator {
constructors.add(method);
}
}
if (constructors.isEmpty() && noArgConstructor != null) {
constructors.add(noArgConstructor);
if (constructors.isEmpty()) {
if (noArgConstructor != null) return noArgConstructor;
else throw new ElementException("Lacking constructor or factory method", klazz);
}
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);
if (constructors.size() == 1) return constructors.get(0);
if (noArgConstructor != null) constructors.add(noArgConstructor);
List<ExecutableElement> preferred = new ArrayList<>();
for (ExecutableElement constructor : constructors) {
if (constructor.getAnnotationsByType(GPrefer.class).length != 0) {
preferred.add(constructor);
}
}
if (preferred.size() == 1) return preferred.get(0);
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) {