Add support for arrays

This commit is contained in:
Johannes Frohnmeyer 2022-11-01 13:48:23 +01:00
parent eff9c5abe7
commit 7f08e5a8a3
Signed by: Johannes
GPG Key ID: E76429612C2929F4
21 changed files with 545 additions and 401 deletions

View File

@ -14,12 +14,12 @@ The goal of this AP is to
- Comments via `@GComment`
- Records
- Nested serializable types
- Arrays
## TODO
- Arrays
- Collections (Sets, Lists, Queues, Deques)
- Maps
- Date
- Maps with string, primitive or enum keys
- Date via ISO8601Utils
- Enums
- Support for nested types from libraries
- Static classes (for configs)

View File

@ -1,23 +0,0 @@
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

@ -29,9 +29,10 @@ public class Main {
public static class ExamplePojo2 {
@GComment("Yes!")
public boolean primitive;
public ExamplePojo2[] recursiveTest;
}
@GSerializable
public record ExampleRecord(String hello, ExamplePojo2 pojo) {
public record ExampleRecord(String hello, @GComment("Sheesh") ExamplePojo2 pojo) {
}
}

View File

@ -2,13 +2,7 @@ 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 class Cl {
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");

View File

@ -3,7 +3,8 @@ 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.adapter.*;
import io.gitlab.jfronny.gson.compile.processor.adapter.Adapter;
import io.gitlab.jfronny.gson.compile.processor.adapter.Adapters;
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.Properties;
@ -17,22 +18,12 @@ import javax.lang.model.util.Elements;
import javax.tools.Diagnostic;
import java.io.*;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@SupportedAnnotationTypes2({GSerializable.class})
@SupportedOptions({"gsonCompileNoReflect"})
public class GsonCompileProcessor extends AbstractProcessor2 {
//TODO handle lists, sets, arrays, other common types (-> https://github.com/bluelinelabs/LoganSquare/blob/development/docs/TypeConverters.md)
private static final List<Adapter> ADAPTERS = List.of(
new DeclaredAdapter(),
new PrimitiveAdapter(),
new StringAdapter(),
new OtherSerializableAdapter(),
new ReflectAdapter()
);
private Messager message;
private Filer filer;
private Set<ClassName> seen;
@ -47,7 +38,7 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
elements = processingEnv.getElementUtils();
seen = new LinkedHashSet<>();
valueCreator = new ValueCreator(processingEnv);
for (Adapter adapter : ADAPTERS) {
for (Adapter adapter : Adapters.ADAPTERS) {
adapter.init(processingEnv);
}
}
@ -141,11 +132,11 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
spec.addType(
TypeSpec.classBuilder("Adapter")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.superclass(ParameterizedTypeName.get(Const.TYPE_ADAPTER, classType))
.superclass(ParameterizedTypeName.get(Cl.TYPE_ADAPTER, classType))
.addMethod(MethodSpec.methodBuilder("write")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(Const.GSON_WRITER, "writer")
.addParameter(Cl.GSON_WRITER, "writer")
.addParameter(classType, "value")
.addException(IOException.class)
.addCode(generatedClassName.simpleName() + ".write(writer, value);")
@ -153,7 +144,7 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
.addMethod(MethodSpec.methodBuilder("read")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addParameter(Const.GSON_READER, "reader")
.addParameter(Cl.GSON_READER, "reader")
.addException(IOException.class)
.returns(classType)
.addCode("return " + generatedClassName.simpleName() + ".read(reader);")
@ -170,18 +161,18 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
.build()
);
spec.addMethod(
MethodSpec.methodBuilder(Const.READ)
MethodSpec.methodBuilder("read")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Const.GSON_READER, "reader")
.addParameter(Cl.GSON_READER, "reader")
.addException(IOException.class)
.returns(classType)
.addCode("return ADAPTER.read(reader);")
.build()
);
spec.addMethod(
MethodSpec.methodBuilder(Const.READ)
MethodSpec.methodBuilder("read")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Const.GSON_READER, "writer")
.addParameter(Cl.GSON_READER, "writer")
.addParameter(classType, "value")
.addException(IOException.class)
.returns(classType)
@ -192,7 +183,7 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
private static void generateAuxiliary(TypeSpec.Builder spec, TypeName classType) {
spec.addMethod(
MethodSpec.methodBuilder(Const.READ)
MethodSpec.methodBuilder("read")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(TypeName.get(Reader.class), "in")
.addException(IOException.class)
@ -200,12 +191,12 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
.addCode("""
try ($T reader = $T.HOLDER.getGson().newJsonReader(in)) {
return read(reader);
}""", Const.GSON_READER, Const.CCORE)
}""", Cl.GSON_READER, Cl.CCORE)
.build()
);
spec.addMethod(
MethodSpec.methodBuilder(Const.READ)
MethodSpec.methodBuilder("read")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(TypeName.get(String.class), "json")
.addException(IOException.class)
@ -218,20 +209,20 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
);
spec.addMethod(
MethodSpec.methodBuilder(Const.READ)
MethodSpec.methodBuilder("read")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Const.GSON_ELEMENT, "tree")
.addParameter(Cl.GSON_ELEMENT, "tree")
.addException(IOException.class)
.returns(classType)
.addCode("""
try ($1T reader = new $1T(tree)) {
return read(reader);
}""", Const.GSON_TREE_READER)
}""", Cl.GSON_TREE_READER)
.build()
);
spec.addMethod(
MethodSpec.methodBuilder(Const.WRITE)
MethodSpec.methodBuilder("write")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Writer.class, "out")
.addParameter(classType, "value")
@ -239,7 +230,7 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
.addCode("""
try ($T writer = $T.HOLDER.getGson().newJsonWriter(out)) {
write(writer, value);
}""", Const.GSON_WRITER, Const.CCORE)
}""", Cl.GSON_WRITER, Cl.CCORE)
.build()
);
@ -262,12 +253,12 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(classType, "value")
.addException(IOException.class)
.returns(Const.GSON_ELEMENT)
.returns(Cl.GSON_ELEMENT)
.addCode("""
try ($1T writer = new $1T()) {
write(writer, value);
return writer.get();
}""", Const.GSON_TREE_WRITER)
}""", Cl.GSON_TREE_WRITER)
.build()
);
}
@ -286,21 +277,39 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
.endControlFlow();
code.addStatement("writer.beginObject()");
for (Property.Field field : properties.fields) {
generateComments(field, code);
code.addStatement("writer.name($S)", getSerializedName(field));
withAdapter(field, spec, code, typeVariables, otherAdapters, adapter -> adapter.generateWrite(() -> code.add("value.$N", field.getCallableName())));
for (Property.Field param : properties.fields) {
generateComments(param, code);
code.addStatement("writer.name($S)", getSerializedName(param));
Runnable writeGet = () -> code.add("value.$N", param.getCallableName());
if (param.getType().getKind().isPrimitive()) {
Adapters.generateWrite(param, spec, code, typeVariables, otherAdapters, message, writeGet);
} else {
code.beginControlFlow("if (value.$N == null)", param.getCallableName())
.addStatement("if (writer.getSerializeNulls()) writer.nullValue()")
.nextControlFlow("else");
Adapters.generateWrite(param, spec, code, typeVariables, otherAdapters, message, writeGet);
code.endControlFlow();
}
}
for (Property.Getter getter : properties.getters) {
generateComments(getter, code);
code.addStatement("writer.name($S)", getSerializedName(getter));
withAdapter(getter, spec, code, typeVariables, otherAdapters, adapter -> adapter.generateWrite(() -> code.add("value.$N()", getter.getCallableName())));
for (Property.Getter param : properties.getters) {
generateComments(param, code);
code.addStatement("writer.name($S)", getSerializedName(param));
if (param.getType().getKind().isPrimitive()) {
Adapters.generateWrite(param, spec, code, typeVariables, otherAdapters, message, () -> code.add("value.$N()", param.getCallableName()));
} else {
code.addStatement("$T $L$N = value.$N()", param.getType(), "$", param.getCallableName(), param.getCallableName())
.beginControlFlow("if ($L$N == null)", "$", param.getCallableName())
.addStatement("if (writer.getSerializeNulls()) writer.nullValue()")
.nextControlFlow("else");
Adapters.generateWrite(param, spec, code, typeVariables, otherAdapters, message, () -> code.add("$L$N", "$", param.getCallableName()));
code.endControlFlow();
}
}
code.addStatement("writer.endObject()");
spec.addMethod(MethodSpec.methodBuilder("write")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(Const.GSON_WRITER, "writer")
.addParameter(Cl.GSON_WRITER, "writer")
.addParameter(classType, "value")
.addException(IOException.class)
.addCode(code.build())
@ -310,7 +319,7 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
// public static T read(JsonReader reader) throws IOException
{
CodeBlock.Builder code = CodeBlock.builder();
code.beginControlFlow("if (reader.peek() == $T.NULL)", Const.GSON_TOKEN)
code.beginControlFlow("if (reader.peek() == $T.NULL)", Cl.GSON_TOKEN)
.addStatement("reader.nextNull()")
.addStatement("return null")
.endControlFlow();
@ -318,7 +327,7 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
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()));
code.addStatement("$T _$N = $L", param.getType(), param.getName(), TypeHelper.getDefaultValue(param.getType()));
}
if (isEmpty) {
code.addStatement("reader.skipValue()");
@ -328,16 +337,16 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
.beginControlFlow("switch (reader.nextName())");
for (Property<?> param : properties.names) {
if (param.getType().getKind().isPrimitive()) {
code.add("case $S -> $L = ", getSerializedName(param), Const.ARG_PREFIX + param.getName());
withAdapter(param, spec, code, typeVariables, otherAdapters, Adapter::generateRead);
code.add("case $S -> _$N = ", getSerializedName(param), param.getName());
Adapters.generateRead(param, spec, code, typeVariables, otherAdapters, message);
code.add(";\n");
} else {
code.beginControlFlow("case $S ->", getSerializedName(param))
.beginControlFlow("if (reader.peek() == $T.NULL)", Const.GSON_TOKEN)
.beginControlFlow("if (reader.peek() == $T.NULL)", Cl.GSON_TOKEN)
.addStatement("reader.nextNull()")
.addStatement("$L = null", Const.ARG_PREFIX + param.getName());
code.unindent().add("} else $L = ", Const.ARG_PREFIX + param.getName());
withAdapter(param, spec, code, typeVariables, otherAdapters, Adapter::generateRead);
.addStatement("_$N = null", param.getName());
code.unindent().add("} else _$N = ", param.getName());
Adapters.generateRead(param, spec, code, typeVariables, otherAdapters, message);
code.add(";\n")
.endControlFlow();
}
@ -352,30 +361,30 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
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(", "));
String args = properties.constructorParams.stream().map(s -> "_" + 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("return $T.$N($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(".$N(_$L)\n", param.getCallableName(), param.getName());
}
code.add(".$L();\n", builder.getBuildMethod().getSimpleName()).unindent();
code.add(".$N();\n", builder.getBuildMethod().getSimpleName()).unindent();
} else {
String args = properties.params.stream().map(s -> Const.ARG_PREFIX + s.getName()).collect(Collectors.joining(", "));
String args = properties.params.stream().map(s -> "_" + 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);
code.addStatement("return $T.$N($L)", creatorName, constructionSource.getConstructionElement().getSimpleName(), args);
}
}
spec.addMethod(MethodSpec.methodBuilder("read")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.returns(classType)
.addParameter(Const.GSON_READER, "reader")
.addParameter(Cl.GSON_READER, "reader")
.addException(IOException.class)
.addCode(code.build())
.build());
@ -384,36 +393,16 @@ public class GsonCompileProcessor extends AbstractProcessor2 {
private void generateComments(Property<?> prop, CodeBlock.Builder code) {
for (AnnotationMirror annotation : prop.getAnnotations()) {
if (annotation.getAnnotationType().asElement().toString().equals(Const.GCOMMENT.toString())) {
if (annotation.getAnnotationType().asElement().toString().equals(Cl.GCOMMENT.toString())) {
String comment = (String) annotation.getElementValues().values().iterator().next().getValue();
code.addStatement("writer.comment($S)", comment);
code.addStatement("if (writer.isLenient()) writer.comment($S)", comment);
}
}
}
private void withAdapter(Property<?> prop, TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> otherAdapters, Consumer<Adapter> action) {
for (Adapter adapter : ADAPTERS) {
adapter.hydrate(prop, klazz, code, typeVariables, otherAdapters);
if (adapter.applies()) {
action.accept(adapter);
adapter.dehydrate();
return;
} else adapter.dehydrate();
}
message.printMessage(Diagnostic.Kind.ERROR, "Could not find applicable adapter for property " + prop);
}
private static String getDefaultValue(TypeMirror type) {
return switch (type.getKind()) {
case BYTE, SHORT, INT, LONG, FLOAT, CHAR, DOUBLE -> "0";
case BOOLEAN -> "false";
default -> "null";
};
}
private static String getSerializedName(Property<?> property) {
for (AnnotationMirror annotationMirror : property.getAnnotations()) {
if (annotationMirror.getAnnotationType().asElement().toString().equals(Const.SERIALIZED_NAME.toString())) {
if (annotationMirror.getAnnotationType().asElement().toString().equals(Cl.SERIALIZED_NAME.toString())) {
return (String) annotationMirror.getElementValues().values().iterator().next().getValue();
}
}

View File

@ -10,7 +10,7 @@ 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) {
ClassName className = ClassName.get(element);
ClassName generatedClassName = ClassName.get(className.packageName(), Const.PREFIX + String.join("_", className.simpleNames()));
ClassName generatedClassName = ClassName.get(className.packageName(), "GC_" + String.join("_", className.simpleNames()));
return new SerializableClass(element, generatedClassName, with, generateAdapter);
}

View File

@ -3,8 +3,7 @@ package io.gitlab.jfronny.gson.compile.processor;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.*;
import javax.lang.model.util.SimpleTypeVisitor14;
import javax.lang.model.util.Types;
import javax.lang.model.util.*;
import java.util.*;
public class TypeHelper {
@ -41,6 +40,15 @@ public class TypeHelper {
}, null);
}
public static ArrayType asArrayType(TypeMirror type) {
return type.accept(new TypeKindVisitor14<>() {
@Override
public ArrayType visitArray(ArrayType t, Object o) {
return t;
}
}, null);
}
public static boolean isInstance(DeclaredType type, String parentClassName, Types typeUtils) {
if (type == null) return false;
TypeElement element = (TypeElement) type.asElement();
@ -57,4 +65,12 @@ public class TypeHelper {
}
return false;
}
static String getDefaultValue(TypeMirror type) {
return switch (type.getKind()) {
case BYTE, SHORT, INT, LONG, FLOAT, CHAR, DOUBLE -> "0";
case BOOLEAN -> "false";
default -> "null";
};
}
}

View File

@ -1,33 +1,23 @@
package io.gitlab.jfronny.gson.compile.processor.adapter;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.gson.compile.processor.Const;
import io.gitlab.jfronny.gson.compile.processor.SerializableClass;
import io.gitlab.jfronny.gson.compile.processor.util.valueprocessor.Property;
import javax.annotation.processing.Messager;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.AnnotationMirror;
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 {
protected Property<?> property;
protected TypeSpec.Builder klazz;
protected List<TypeVariableName> typeVariables;
protected Set<SerializableClass> other;
protected TypeMirror type;
protected TypeMirror unboxedType;
protected CodeBlock.Builder code;
protected String argName;
public abstract class Adapter<T extends Adapter<T>.Hydrated> {
protected Messager message;
protected Types typeUtils;
protected Map<String, String> options;
protected TypeName typeName;
public abstract boolean applies();
public abstract void generateWrite(Runnable writeGet);
public abstract void generateRead();
public abstract T instantiate();
public void init(ProcessingEnvironment env) {
this.message = env.getMessager();
@ -35,31 +25,53 @@ public abstract class Adapter {
this.options = env.getOptions();
}
public void hydrate(Property<?> property, TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> other) {
this.property = property;
this.klazz = klazz;
this.typeVariables = typeVariables;
this.other = other;
this.type = property.getType();
this.code = code;
public final T hydrate(TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> other, TypeMirror type, String propName, List<? extends AnnotationMirror> annotations, Element sourceElement) {
T instance = instantiate();
instance.klazz = klazz;
instance.typeVariables = typeVariables;
instance.other = other;
instance.type = type;
instance.code = code;
try {
this.unboxedType = typeUtils.unboxedType(type);
instance.unboxedType = typeUtils.unboxedType(type);
} catch (IllegalArgumentException e) {
this.unboxedType = type;
instance.unboxedType = type;
}
this.argName = Const.ARG_PREFIX + this.property.getName();
this.typeName = TypeName.get(type).box();
instance.name = propName;
instance.argName = "_" + propName;
instance.adapterName = "adapter_" + propName;
instance.typeName = TypeName.get(type).box();
instance.annotations = annotations;
instance.sourceElement = sourceElement;
instance.afterHydrate();
return instance;
}
public void dehydrate() {
this.property = null;
this.klazz = null;
this.typeVariables = null;
this.other = null;
this.type = null;
this.code = null;
this.unboxedType = null;
this.argName = null;
this.typeName = null;
public abstract class Hydrated {
protected TypeSpec.Builder klazz;
protected List<TypeVariableName> typeVariables;
protected Set<SerializableClass> other;
protected TypeMirror type;
protected TypeMirror unboxedType;
protected CodeBlock.Builder code;
protected String name;
protected String argName;
protected String adapterName;
protected TypeName typeName;
protected List<? extends AnnotationMirror> annotations;
protected Element sourceElement;
public abstract boolean applies();
public abstract void generateWrite(Runnable writeGet);
public abstract void generateRead();
protected void afterHydrate() {}
protected void generateRead(CodeBlock.Builder code, TypeMirror type, String name, List<? extends AnnotationMirror> annotations) {
Adapters.generateRead(klazz, code, typeVariables, other, type, name, annotations, sourceElement, message);
}
protected void generateWrite(CodeBlock.Builder code, TypeMirror type, String name, List<? extends AnnotationMirror> annotations, Runnable writeGet) {
Adapters.generateWrite(klazz, code, typeVariables, other, type, name, annotations, sourceElement, message, writeGet);
}
}
}

View File

@ -1,7 +1,7 @@
package io.gitlab.jfronny.gson.compile.processor.adapter;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.gson.compile.processor.Const;
import io.gitlab.jfronny.gson.compile.processor.Cl;
import io.gitlab.jfronny.gson.compile.processor.TypeHelper;
import javax.lang.model.type.TypeMirror;
@ -9,60 +9,60 @@ import java.lang.reflect.ParameterizedType;
import java.util.Iterator;
import java.util.List;
public abstract class AdapterAdapter extends Adapter {
@Override
public void generateWrite(Runnable writeGet) {
code.add(getAdapter() + ".write(writer, ");
writeGet.run();
code.add(");\n");
}
@Override
public void generateRead() {
code.add(getAdapter() + ".read(reader)");
}
private String getAdapter() {
String typeAdapterName = Const.ADAPTER_PREFIX + property.getName();
for (FieldSpec spec : klazz.fieldSpecs) {
if (spec.name.equals(typeAdapterName)) return typeAdapterName;
public abstract class AdapterAdapter<T extends AdapterAdapter<T>.Hydrated> extends Adapter<T> {
public abstract class Hydrated extends Adapter<T>.Hydrated {
@Override
public void generateWrite(Runnable writeGet) {
code.add(getAdapter() + ".write(writer, ");
writeGet.run();
code.add(");\n");
}
return createAdapter(typeAdapterName);
}
protected abstract String createAdapter(String name);
@Override
public void generateRead() {
code.add(getAdapter() + ".read(reader)");
}
protected void appendFieldTypeToken(boolean allowClassType) {
TypeMirror type = property.getType();
TypeName typeName = TypeName.get(type);
if (TypeHelper.isComplexType(type, typeUtils)) {
TypeName typeTokenType = ParameterizedTypeName.get(Const.TYPE_TOKEN, typeName);
List<? extends TypeMirror> typeParams = TypeHelper.getGenericTypes(type);
if (typeParams.isEmpty()) {
code.add("new $T() {}", typeTokenType);
} else {
code.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()));
code.add("(($T)typeToken.getType()).getActualTypeArguments()[$L]", ParameterizedType.class, typeIndex);
if (iterator.hasNext()) {
code.add(", ");
}
}
code.add(")");
private String getAdapter() {
for (FieldSpec spec : klazz.fieldSpecs) {
if (spec.name.equals(adapterName)) return adapterName;
}
} else if (TypeHelper.isGenericType(type)) {
TypeName typeTokenType = ParameterizedTypeName.get(Const.TYPE_TOKEN, typeName);
int typeIndex = typeVariables.indexOf(TypeVariableName.get(property.getType().toString()));
code.add("($T) $T.get((($T)typeToken.getType()).getActualTypeArguments()[$L])",
typeTokenType, Const.TYPE_TOKEN, ParameterizedType.class, typeIndex);
} else {
if (allowClassType) {
code.add("$T.class", typeName);
return createAdapter(adapterName);
}
protected abstract String createAdapter(String name);
protected void appendFieldTypeToken(boolean allowClassType) {
TypeName typeName = TypeName.get(type);
if (TypeHelper.isComplexType(type, typeUtils)) {
TypeName typeTokenType = ParameterizedTypeName.get(Cl.TYPE_TOKEN, typeName);
List<? extends TypeMirror> typeParams = TypeHelper.getGenericTypes(type);
if (typeParams.isEmpty()) {
code.add("new $T() {}", typeTokenType);
} else {
code.add("($T) $T.getParameterized($T.class, ", typeTokenType, Cl.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()));
code.add("(($T)typeToken.getType()).getActualTypeArguments()[$L]", ParameterizedType.class, typeIndex);
if (iterator.hasNext()) {
code.add(", ");
}
}
code.add(")");
}
} else if (TypeHelper.isGenericType(type)) {
TypeName typeTokenType = ParameterizedTypeName.get(Cl.TYPE_TOKEN, typeName);
int typeIndex = typeVariables.indexOf(TypeVariableName.get(type.toString()));
code.add("($T) $T.get((($T)typeToken.getType()).getActualTypeArguments()[$L])",
typeTokenType, Cl.TYPE_TOKEN, ParameterizedType.class, typeIndex);
} else {
code.add("TypeToken.get($T.class)", typeName);
if (allowClassType) {
code.add("$T.class", typeName);
} else {
code.add("TypeToken.get($T.class)", typeName);
}
}
}
}

View File

@ -0,0 +1,57 @@
package io.gitlab.jfronny.gson.compile.processor.adapter;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.gson.compile.processor.SerializableClass;
import io.gitlab.jfronny.gson.compile.processor.adapter.impl.*;
import io.gitlab.jfronny.gson.compile.processor.util.valueprocessor.Property;
import javax.annotation.processing.Messager;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
public class Adapters {
public static final List<Adapter<?>> ADAPTERS = List.of(
new DeclaredAdapter(),
new PrimitiveAdapter(),
new StringAdapter(),
new ArrayAdapter(),
new OtherSerializableAdapter(),
new ReflectAdapter()
);
public static void generateRead(Property<?> prop, TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> otherAdapters, Messager message) {
withAdapter(prop, klazz, code, typeVariables, otherAdapters, message, Adapter.Hydrated::generateRead);
}
public static void generateWrite(Property<?> prop, TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> otherAdapters, Messager message, Runnable writeGet) {
withAdapter(prop, klazz, code, typeVariables, otherAdapters, message, adapter -> adapter.generateWrite(writeGet));
}
private static void withAdapter(Property<?> prop, TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> otherAdapters, Messager message, Consumer<Adapter<?>.Hydrated> action) {
withAdapter(klazz, code, typeVariables, otherAdapters, prop.getType(), prop.getName(), prop.getAnnotations(), prop.getElement(), message, action);
}
public static void generateRead(TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> other, TypeMirror type, String propName, List<? extends AnnotationMirror> annotations, Element sourceElement, Messager message) {
withAdapter(klazz, code, typeVariables, other, type, propName, annotations, sourceElement, message, Adapter.Hydrated::generateRead);
}
public static void generateWrite(TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> other, TypeMirror type, String propName, List<? extends AnnotationMirror> annotations, Element sourceElement, Messager message, Runnable writeGet) {
withAdapter(klazz, code, typeVariables, other, type, propName, annotations, sourceElement, message, adapter -> adapter.generateWrite(writeGet));
}
private static void withAdapter(TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> other, TypeMirror type, String propName, List<? extends AnnotationMirror> annotations, Element sourceElement, Messager message, Consumer<Adapter<?>.Hydrated> action) {
for (Adapter<?> adapter : Adapters.ADAPTERS) {
Adapter<?>.Hydrated hydrated = adapter.hydrate(klazz, code, typeVariables, other, type, propName, annotations, sourceElement);
if (hydrated.applies()) {
action.accept(hydrated);
return;
}
}
message.printMessage(Diagnostic.Kind.ERROR, "Could not find applicable adapter for property " + propName, sourceElement);
}
}

View File

@ -1,70 +0,0 @@
package io.gitlab.jfronny.gson.compile.processor.adapter;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.gson.compile.processor.*;
import io.gitlab.jfronny.gson.compile.processor.util.valueprocessor.Property;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.tools.Diagnostic;
import java.util.*;
public class DeclaredAdapter extends AdapterAdapter {
private DeclaredType typeAdapterClass;
@Override
public boolean applies() {
return typeAdapterClass != null;
}
@Override
public void hydrate(Property<?> property, TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> other) {
super.hydrate(property, klazz, code, typeVariables, other);
this.typeAdapterClass = findTypeAdapterClass(property.getAnnotations());
}
@Override
public void dehydrate() {
super.dehydrate();
typeAdapterClass = null;
}
@Override
protected String createAdapter(String typeAdapterName) {
if (TypeHelper.isInstance(typeAdapterClass, Const.TYPE_ADAPTER.toString(), typeUtils)) {
klazz.addField(
FieldSpec.builder(TypeName.get(typeAdapterClass), typeAdapterName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("new $T()", typeAdapterClass)
.build()
);
} else if (TypeHelper.isInstance(typeAdapterClass, Const.TYPE_ADAPTER_FACTORY.toString(), typeUtils)) {
TypeName typeAdapterType = ParameterizedTypeName.get(Const.TYPE_ADAPTER, TypeName.get(type).box());
CodeBlock.Builder block = CodeBlock.builder();
block.add("new $T().create($T.HOLDER.getGson(), ", typeAdapterClass, Const.CCORE);
appendFieldTypeToken(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.", property.getElement());
return typeAdapterName;
}
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;
}
}

View File

@ -1,39 +0,0 @@
package io.gitlab.jfronny.gson.compile.processor.adapter;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.gson.compile.processor.SerializableClass;
import io.gitlab.jfronny.gson.compile.processor.util.valueprocessor.Property;
import java.util.List;
import java.util.Set;
public class OtherSerializableAdapter extends AdapterAdapter {
private String adapter;
@Override
public boolean applies() {
return adapter != null;
}
@Override
protected String createAdapter(String name) {
return adapter;
}
@Override
public void hydrate(Property<?> property, TypeSpec.Builder klazz, CodeBlock.Builder code, List<TypeVariableName> typeVariables, Set<SerializableClass> other) {
super.hydrate(property, klazz, code, typeVariables, other);
for (SerializableClass adapter : other) {
if (TypeName.get(adapter.classElement().asType()).equals(typeName)) {
// Use self-made adapter
this.adapter = adapter.generatedClassName().toString();
}
}
}
@Override
public void dehydrate() {
super.dehydrate();
this.adapter = null;
}
}

View File

@ -1,39 +0,0 @@
package io.gitlab.jfronny.gson.compile.processor.adapter;
public class PrimitiveAdapter extends Adapter {
@Override
public boolean applies() {
return unboxedType.getKind().isPrimitive();
}
@Override
public void generateWrite(Runnable writeGet) {
if (type.equals(unboxedType)) {
code.add("writer.value(");
writeGet.run();
code.addStatement(");\n");
} else {
code.add("$T $L = ", type, argName);
writeGet.run();
code.add(";\n");
code.beginControlFlow("if ($L == null)", argName)
.addStatement("writer.nullValue()")
.endControlFlow("else writer.value($L)", argName);
}
}
@Override
public void generateRead() {
code.add(switch (unboxedType.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: " + unboxedType.getKind());
});
}
}

View File

@ -1,31 +0,0 @@
package io.gitlab.jfronny.gson.compile.processor.adapter;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.gson.compile.processor.Const;
import javax.lang.model.element.Modifier;
import javax.tools.Diagnostic;
public class ReflectAdapter extends AdapterAdapter {
@Override
public boolean applies() {
return !options.containsKey("gsonCompileNoReflect");
}
@Override
protected String createAdapter(String typeAdapterName) {
message.printMessage(Diagnostic.Kind.WARNING, "Falling back to adapter detection for unsupported type " + type, property.getElement());
TypeName typeAdapterType = ParameterizedTypeName.get(Const.TYPE_ADAPTER, typeName);
CodeBlock.Builder block = CodeBlock.builder();
block.add("$T.HOLDER.getGson().getAdapter(", Const.CCORE);
appendFieldTypeToken(true);
block.add(")");
klazz.addField(
FieldSpec.builder(typeAdapterType, typeAdapterName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer(block.build())
.build()
);
return typeAdapterName;
}
}

View File

@ -1,23 +0,0 @@
package io.gitlab.jfronny.gson.compile.processor.adapter;
public class StringAdapter extends Adapter {
@Override
public boolean applies() {
return type.toString().equals(String.class.getCanonicalName());
}
@Override
public void generateWrite(Runnable writeGet) {
code.add("$T $L = ", type, argName);
writeGet.run();
code.add(";\n");
code.beginControlFlow("if ($L == null)", argName)
.addStatement("writer.nullValue()")
.endControlFlow("else writer.value($L)", argName);
}
@Override
public void generateRead() {
code.add("reader.nextString()");
}
}

View File

@ -0,0 +1,85 @@
package io.gitlab.jfronny.gson.compile.processor.adapter.impl;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.gson.compile.processor.Cl;
import io.gitlab.jfronny.gson.compile.processor.TypeHelper;
import io.gitlab.jfronny.gson.compile.processor.adapter.Adapter;
import javax.lang.model.element.Modifier;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.TypeMirror;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
public class ArrayAdapter extends Adapter<ArrayAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends Adapter<ArrayAdapter.Hydrated>.Hydrated {
private ArrayType type;
private TypeMirror componentType;
@Override
public boolean applies() {
return type != null;
}
@Override
protected void afterHydrate() {
type = TypeHelper.asArrayType(super.type);
componentType = type == null ? null : type.getComponentType();
}
@Override
public void generateWrite(Runnable writeGet) {
code.add("for ($T $N : ", componentType, argName);
writeGet.run();
code.beginControlFlow(")")
.beginControlFlow("if ($N == null)", argName)
.addStatement("if (writer.getSerializeNulls()) writer.nullValue()")
.nextControlFlow("else");
generateWrite(code, componentType, argName, componentType.getAnnotationMirrors(), () -> code.add(argName));
code.endControlFlow().endControlFlow();
}
@Override
public void generateRead() {
CodeBlock.Builder kode = CodeBlock.builder();
// Coerce
kode.beginControlFlow("if (reader.isLenient() && reader.peek() != $T.BEGIN_ARRAY)", Cl.GSON_TOKEN)
.add("return new $T[] { ", componentType);
generateRead(kode, componentType, argName, componentType.getAnnotationMirrors());
kode.add(" };\n").endControlFlow();
kode.addStatement("$T<$T> list = new $T<>()", List.class, componentType, ArrayList.class)
.addStatement("reader.beginArray()")
.beginControlFlow("while (reader.hasNext())")
.beginControlFlow("if (reader.peek() == $T.NULL)", Cl.GSON_TOKEN)
.addStatement("reader.nextNull()")
.addStatement("list.add(null)")
.nextControlFlow("else")
.add("list.add(");
generateRead(kode, componentType, argName, componentType.getAnnotationMirrors());
kode.add(");\n")
.endControlFlow()
.endControlFlow()
.addStatement("reader.endArray()")
.addStatement("return list.toArray($T[]::new)", componentType);
String methodName = "read$" + name;
klazz.addMethod(
MethodSpec.methodBuilder(methodName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC)
.returns(typeName)
.addParameter(Cl.GSON_READER, "reader")
.addException(IOException.class)
.addCode(kode.build())
.build()
);
code.add("$N(reader)", methodName);
}
}
}

View File

@ -0,0 +1,72 @@
package io.gitlab.jfronny.gson.compile.processor.adapter.impl;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.gson.compile.processor.Cl;
import io.gitlab.jfronny.gson.compile.processor.TypeHelper;
import io.gitlab.jfronny.gson.compile.processor.adapter.AdapterAdapter;
import javax.lang.model.element.*;
import javax.lang.model.type.DeclaredType;
import javax.tools.Diagnostic;
import java.util.List;
import java.util.Map;
public class DeclaredAdapter extends AdapterAdapter<DeclaredAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends AdapterAdapter<Hydrated>.Hydrated {
private DeclaredType typeAdapterClass;
@Override
public boolean applies() {
return typeAdapterClass != null;
}
@Override
protected void afterHydrate() {
this.typeAdapterClass = findTypeAdapterClass(annotations);
}
@Override
protected String createAdapter(String typeAdapterName) {
if (TypeHelper.isInstance(typeAdapterClass, Cl.TYPE_ADAPTER.toString(), typeUtils)) {
klazz.addField(
FieldSpec.builder(TypeName.get(typeAdapterClass), typeAdapterName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer("new $T()", typeAdapterClass)
.build()
);
} else if (TypeHelper.isInstance(typeAdapterClass, Cl.TYPE_ADAPTER_FACTORY.toString(), typeUtils)) {
TypeName typeAdapterType = ParameterizedTypeName.get(Cl.TYPE_ADAPTER, TypeName.get(type).box());
CodeBlock.Builder block = CodeBlock.builder();
block.add("new $T().create($T.HOLDER.getGson(), ", typeAdapterClass, Cl.CCORE);
appendFieldTypeToken(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.", sourceElement);
return typeAdapterName;
}
private static DeclaredType findTypeAdapterClass(List<? extends AnnotationMirror> annotations) {
for (AnnotationMirror annotation : annotations) {
String typeName = annotation.getAnnotationType().toString();
if (typeName.equals(Cl.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;
}
}
}

View File

@ -0,0 +1,36 @@
package io.gitlab.jfronny.gson.compile.processor.adapter.impl;
import com.squareup.javapoet.TypeName;
import io.gitlab.jfronny.gson.compile.processor.SerializableClass;
import io.gitlab.jfronny.gson.compile.processor.adapter.AdapterAdapter;
public class OtherSerializableAdapter extends AdapterAdapter<OtherSerializableAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends AdapterAdapter<Hydrated>.Hydrated {
private String adapter;
@Override
public boolean applies() {
return adapter != null;
}
@Override
protected String createAdapter(String name) {
return adapter;
}
@Override
protected void afterHydrate() {
for (SerializableClass adapter : other) {
if (TypeName.get(adapter.classElement().asType()).equals(typeName)) {
// Use self-made adapter
this.adapter = adapter.generatedClassName().toString();
}
}
}
}
}

View File

@ -0,0 +1,39 @@
package io.gitlab.jfronny.gson.compile.processor.adapter.impl;
import io.gitlab.jfronny.gson.compile.processor.adapter.Adapter;
public class PrimitiveAdapter extends Adapter<PrimitiveAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends Adapter<Hydrated>.Hydrated {
@Override
public boolean applies() {
return unboxedType.getKind().isPrimitive();
}
@Override
public void generateWrite(Runnable writeGet) {
code.add("writer.value(");
writeGet.run();
code.add(");\n");
}
@Override
public void generateRead() {
code.add(switch (unboxedType.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: " + unboxedType.getKind());
});
}
}
}

View File

@ -0,0 +1,39 @@
package io.gitlab.jfronny.gson.compile.processor.adapter.impl;
import com.squareup.javapoet.*;
import io.gitlab.jfronny.gson.compile.processor.Cl;
import io.gitlab.jfronny.gson.compile.processor.adapter.AdapterAdapter;
import javax.lang.model.element.Modifier;
import javax.tools.Diagnostic;
public class ReflectAdapter extends AdapterAdapter<ReflectAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends AdapterAdapter<Hydrated>.Hydrated {
@Override
public boolean applies() {
return !options.containsKey("gsonCompileNoReflect");
}
@Override
protected String createAdapter(String typeAdapterName) {
message.printMessage(Diagnostic.Kind.WARNING, "Falling back to adapter detection for unsupported type " + type, sourceElement);
TypeName typeAdapterType = ParameterizedTypeName.get(Cl.TYPE_ADAPTER, typeName);
CodeBlock.Builder block = CodeBlock.builder();
block.add("$T.HOLDER.getGson().getAdapter(", Cl.CCORE);
appendFieldTypeToken(true);
block.add(")");
klazz.addField(
FieldSpec.builder(typeAdapterType, typeAdapterName)
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.initializer(block.build())
.build()
);
return typeAdapterName;
}
}
}

View File

@ -0,0 +1,29 @@
package io.gitlab.jfronny.gson.compile.processor.adapter.impl;
import io.gitlab.jfronny.gson.compile.processor.adapter.Adapter;
public class StringAdapter extends Adapter<StringAdapter.Hydrated> {
@Override
public Hydrated instantiate() {
return new Hydrated();
}
public class Hydrated extends Adapter<Hydrated>.Hydrated {
@Override
public boolean applies() {
return type.toString().equals(String.class.getCanonicalName());
}
@Override
public void generateWrite(Runnable writeGet) {
code.add("writer.value(");
writeGet.run();
code.add(");\n");
}
@Override
public void generateRead() {
code.add("reader.nextString()");
}
}
}