From 0fc1d3bce67908350dda4f049e6979564de21f83 Mon Sep 17 00:00:00 2001 From: JFronny Date: Tue, 1 Nov 2022 21:29:43 +0100 Subject: [PATCH] Support maps --- README.md | 2 +- .../java/io/gitlab/jfronny/gson/Main.java | 8 + .../compile/processor/adapter/Adapter.java | 14 +- .../compile/processor/adapter/Adapters.java | 1 + .../processor/adapter/impl/DateAdapter.java | 16 +- .../processor/adapter/impl/EnumAdapter.java | 15 +- .../processor/adapter/impl/MapAdapter.java | 188 ++++++++++++++++++ 7 files changed, 224 insertions(+), 20 deletions(-) create mode 100644 gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/impl/MapAdapter.java diff --git a/README.md b/README.md index b42d77e..f108888 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ The goal of this AP is to - Collections (Sets, Lists, Queues, Deques) - java.util.Date as iso8601 - Enums +- Maps with string, primitive, enum or UUID keys ## Used properties Use `@GPrefer` to choose one construction method if multiple are available @@ -30,7 +31,6 @@ Use `@GPrefer` to choose one construction method if multiple are available - Several utility methods in the generated class for reading from/writing to various sources ## TODO -- Maps with string, primitive or enum keys - Support for nested types from libraries - Static classes (for configs) diff --git a/gson-compile-example/src/main/java/io/gitlab/jfronny/gson/Main.java b/gson-compile-example/src/main/java/io/gitlab/jfronny/gson/Main.java index c8b060e..fa05544 100644 --- a/gson-compile-example/src/main/java/io/gitlab/jfronny/gson/Main.java +++ b/gson-compile-example/src/main/java/io/gitlab/jfronny/gson/Main.java @@ -58,6 +58,14 @@ public class Main { @GComment("Yes!") public boolean primitive; public ExamplePojo2[] recursiveTest; + + public Map map1; + + public Map map2; + + public Map map3; + + public Map map4; } @GSerializable diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/Adapter.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/Adapter.java index 5e7bc22..83ecb40 100644 --- a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/Adapter.java +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/Adapter.java @@ -31,11 +31,7 @@ public abstract class Adapter.Hydrated> { instance.other = other; instance.type = type; instance.code = code; - try { - instance.unboxedType = typeUtils.unboxedType(type); - } catch (IllegalArgumentException e) { - instance.unboxedType = type; - } + instance.unboxedType = instance.unbox(type); instance.name = propName; instance.argName = "_" + propName; instance.adapterName = "adapter_" + propName; @@ -72,5 +68,13 @@ public abstract class Adapter.Hydrated> { protected void generateWrite(CodeBlock.Builder code, TypeMirror type, String name, List annotations, Runnable writeGet) { Adapters.generateWrite(klazz, code, typeVariables, other, type, name, annotations, sourceElement, message, writeGet); } + + protected TypeMirror unbox(TypeMirror type) { + try { + return typeUtils.unboxedType(type); + } catch (IllegalArgumentException e) { + return type; + } + } } } diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/Adapters.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/Adapters.java index 24b2ac4..a9be6e6 100644 --- a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/Adapters.java +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/Adapters.java @@ -23,6 +23,7 @@ public class Adapters { new EnumAdapter(), new ArrayAdapter(), new CollectionAdapter(), + new MapAdapter(), new OtherSerializableAdapter(), new ReflectAdapter() ); diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/impl/DateAdapter.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/impl/DateAdapter.java index dbe14fd..e193b87 100644 --- a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/impl/DateAdapter.java +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/impl/DateAdapter.java @@ -40,18 +40,20 @@ public class DateAdapter extends Adapter { } 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 $T(\"Failed Parsing '\" + date + \"' as Date\", e)", Cl.GSON_SYNTAX_EXCEPTION) - .endControlFlow(); klazz.addMethod( MethodSpec.methodBuilder("parseDate") .addModifiers(Modifier.PRIVATE, Modifier.STATIC) .returns(Date.class) .addParameter(String.class, "date") - .addCode(kode.build()) + .addCode( + CodeBlock.builder() + .beginControlFlow("try") + .addStatement("return $T.parse(date, new $T(0))", Cl.GISO8601UTILS, ParsePosition.class) + .nextControlFlow("catch ($T e)", ParseException.class) + .addStatement("throw new $T(\"Failed Parsing '\" + date + \"' as Date\", e)", Cl.GSON_SYNTAX_EXCEPTION) + .endControlFlow() + .build() + ) .build() ); } diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/impl/EnumAdapter.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/impl/EnumAdapter.java index a7b0c6d..7f4760c 100644 --- a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/impl/EnumAdapter.java +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/impl/EnumAdapter.java @@ -41,19 +41,20 @@ public class EnumAdapter extends Adapter { @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()) + .addCode( + CodeBlock.builder() + .beginControlFlow("for ($1T t : $1T.values())", tel) + .addStatement("if (t.name().equals(value)) return t") + .endControlFlow() + .addStatement("return null") + .build() + ) .build() ); code.add("$N(reader.nextString())", methodName); diff --git a/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/impl/MapAdapter.java b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/impl/MapAdapter.java new file mode 100644 index 0000000..50704f3 --- /dev/null +++ b/gson-compile-processor/src/main/java/io/gitlab/jfronny/gson/compile/processor/adapter/impl/MapAdapter.java @@ -0,0 +1,188 @@ +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 io.gitlab.jfronny.gson.compile.processor.util.ValUtils; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import java.io.IOException; +import java.util.*; + +public class MapAdapter extends Adapter { + @Override + public Hydrated instantiate() { + return new Hydrated(); + } + + public class Hydrated extends Adapter.Hydrated { + private static final List> SUPPORTED = List.of(LinkedHashMap.class, HashMap.class, TreeMap.class); + + private DeclaredType type; + private TypeName implType; + private TypeMirror componentType1; + private TypeMirror componentType2; + + @Override + public boolean applies() { + return type != null; + } + + @Override + protected void afterHydrate() { + type = TypeHelper.asDeclaredType(super.type); + componentType1 = componentType2 = null; + if (type == null) return; + List typeArguments = type.getTypeArguments(); + if (typeArguments.size() != 2) { + type = null; + } else { + componentType1 = typeArguments.get(0); + if (!isValidKey(componentType1)) { + type = null; + componentType1 = null; + return; + } + componentType2 = typeArguments.get(1); + String ts = TypeHelper.asDeclaredType(typeUtils.erasure(type)).asElement().toString(); + if (Map.class.getCanonicalName().equals(ts)) { + implType = TypeName.get(SUPPORTED.get(0)); + return; + } + for (Class klazz : SUPPORTED) { + if (klazz.getCanonicalName().equals(ts)) { + implType = TypeName.get(klazz); + return; + } + } + type = null; + componentType1 = componentType2 = null; + } + } + + private boolean isValidKey(TypeMirror tm) { + if (tm.toString().equals(String.class.getCanonicalName())) return true; + if (tm.toString().equals(UUID.class.getCanonicalName())) return true; + if (unbox(tm).getKind().isPrimitive()) return true; + if (isEnum(tm)) return true; + return false; + } + + private void generateConvertKey(CodeBlock.Builder kode) { + if (componentType1.toString().equals(String.class.getCanonicalName())) { + kode.add("name"); + } + else if (componentType1.toString().equals(UUID.class.getCanonicalName())) { + kode.add("name == null ? null : $T.fromString(name)", UUID.class); + } + else if (unbox(componentType1).getKind().isPrimitive()) { + kode.add("name == null ? null : " + switch (unbox(componentType1).getKind()) { + case BOOLEAN -> "Boolean.parseBoolean(name)"; + case BYTE -> "Byte.parseByte(name)"; + case SHORT -> "Short.parseShort(name)"; + case INT -> "Integer.parseInt(name)"; + case LONG -> "Long.parseLong(name)"; + case CHAR -> "name.length() == 0 ? '\\0' : name.charAt(0)"; + case FLOAT -> "Float.parseFloat(name)"; + case DOUBLE -> "Double.parseDouble(name)"; + default -> throw new IllegalArgumentException("Unsupported primitive: " + unbox(componentType1).getKind()); + }); + } + else if (isEnum(componentType1)) { + String methodName = "readName$" + name; + boolean found = false; + for (MethodSpec spec : klazz.methodSpecs) { + if (spec.name.equals(methodName)) { + found = true; + break; + } + } + if (!found) { + klazz.addMethod( + MethodSpec.methodBuilder(methodName) + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .returns(TypeName.get(componentType1)) + .addParameter(String.class, "value") + .addCode( + CodeBlock.builder() + .beginControlFlow("for ($1T t : $1T.values())", componentType1) + .addStatement("if (t.name().equals(value)) return t") + .endControlFlow() + .addStatement("return null") + .build() + ) + .build() + ); + } + kode.add("$N(name)", methodName); + } + } + + private boolean isEnum(TypeMirror tm) { + DeclaredType declared = TypeHelper.asDeclaredType(tm); + return declared != null && declared.asElement().getKind() == ElementKind.ENUM; + } + + @Override + public void generateWrite(Runnable writeGet) { + code.addStatement("writer.beginObject()"); + code.add("for ($T.Entry<$T, $T> $N : (", Map.class, componentType1, componentType2, argName); + writeGet.run(); + code.beginControlFlow(").entrySet())") + .add("if ($N.getKey() != null || writer.getSerializeNulls()) writer.value(", argName); + if (isEnum(componentType1)) { + code.add("$N.getKey() == null ? null : $N.getKey().name()", argName, argName); + } else { + code.add("$T.toString($N.getKey())", Objects.class, argName); + } + code.addStatement(")") + .addStatement("$T value$N = $N.getValue()", componentType2, argName, argName) + .beginControlFlow("if (value$N == null)", argName) + .addStatement("if (writer.getSerializeNulls()) writer.nullValue()") + .nextControlFlow("else"); + generateWrite(code, componentType2, "value" + argName, componentType2.getAnnotationMirrors(), () -> code.add("value" + argName)); + code.endControlFlow().endControlFlow().addStatement("writer.endObject()"); + } + + @Override + public void generateRead() { + CodeBlock.Builder kode = CodeBlock.builder(); + kode.addStatement("$T map = new $T<>()", typeName, implType); + + kode.addStatement("reader.beginObject()") + .beginControlFlow("while (reader.hasNext())") + .addStatement("String name = reader.nextName()") + .beginControlFlow("if (reader.peek() == $T.NULL)", Cl.GSON_TOKEN) + .addStatement("reader.nextNull()") + .add("map.put("); + generateConvertKey(kode); + kode.addStatement(", null)") + .nextControlFlow("else") + .add("map.put("); + generateConvertKey(kode); + kode.add(", "); + generateRead(kode, componentType2, argName, componentType2.getAnnotationMirrors()); + kode.addStatement(")") + .endControlFlow() + .endControlFlow() + .addStatement("reader.endObject()") + .addStatement("return map"); + + 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); + } + } +}