From 09c501e1c4552d1b5f40bc3585603c02dd561431 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Wed, 14 Dec 2022 16:49:13 +1300 Subject: [PATCH] #39 - Adds support for types with generic parameters --- .../customer/generics/MyGenericHolder.java | 38 ++++++++++ .../generics/MyGenericPageResult.java | 50 +++++++++++++ .../generics/MyGenericHolderTest.java | 75 +++++++++++++++++++ .../generics/MyGenericPageResultTest.java | 75 +++++++++++++++++++ .../io/avaje/jsonb/generator/BeanReader.java | 8 ++ .../jsonb/generator/ComponentMetaData.java | 9 +++ .../jsonb/generator/ComponentReader.java | 19 +++-- .../io/avaje/jsonb/generator/Constants.java | 2 + .../io/avaje/jsonb/generator/FieldReader.java | 64 +++++++++++++--- .../io/avaje/jsonb/generator/Processor.java | 3 + .../jsonb/generator/SimpleAdapterWriter.java | 43 ++++++++++- .../generator/SimpleComponentWriter.java | 26 +++++-- .../io/avaje/jsonb/generator/TypeReader.java | 32 +++++--- jsonb/src/main/java/io/avaje/jsonb/Types.java | 48 +++++++++++- .../main/java/io/avaje/jsonb/core/Util.java | 36 +-------- .../java/io/avaje/jsonb/spi/MetaData.java | 10 +++ 16 files changed, 471 insertions(+), 67 deletions(-) create mode 100644 blackbox-test/src/main/java/org/example/customer/generics/MyGenericHolder.java create mode 100644 blackbox-test/src/main/java/org/example/customer/generics/MyGenericPageResult.java create mode 100644 blackbox-test/src/test/java/org/example/customer/generics/MyGenericHolderTest.java create mode 100644 blackbox-test/src/test/java/org/example/customer/generics/MyGenericPageResultTest.java diff --git a/blackbox-test/src/main/java/org/example/customer/generics/MyGenericHolder.java b/blackbox-test/src/main/java/org/example/customer/generics/MyGenericHolder.java new file mode 100644 index 00000000..08204631 --- /dev/null +++ b/blackbox-test/src/main/java/org/example/customer/generics/MyGenericHolder.java @@ -0,0 +1,38 @@ +package org.example.customer.generics; + +import io.avaje.jsonb.Json; + +@Json +public class MyGenericHolder { + + String title; + String author; + T document; + + public String getTitle() { + return title; + } + + public MyGenericHolder setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public MyGenericHolder setAuthor(String author) { + this.author = author; + return this; + } + + public T getDocument() { + return document; + } + + public MyGenericHolder setDocument(T document) { + this.document = document; + return this; + } +} diff --git a/blackbox-test/src/main/java/org/example/customer/generics/MyGenericPageResult.java b/blackbox-test/src/main/java/org/example/customer/generics/MyGenericPageResult.java new file mode 100644 index 00000000..7913cf76 --- /dev/null +++ b/blackbox-test/src/main/java/org/example/customer/generics/MyGenericPageResult.java @@ -0,0 +1,50 @@ +package org.example.customer.generics; + +import io.avaje.jsonb.Json; + +import java.util.List; + +@Json +public class MyGenericPageResult { + + int page; + int pageSize; + int totalPageCount; + List results; + + public int getPage() { + return page; + } + + public MyGenericPageResult setPage(int page) { + this.page = page; + return this; + } + + public int getPageSize() { + return pageSize; + } + + public MyGenericPageResult setPageSize(int pageSize) { + this.pageSize = pageSize; + return this; + } + + public int getTotalPageCount() { + return totalPageCount; + } + + public MyGenericPageResult setTotalPageCount(int totalPageCount) { + this.totalPageCount = totalPageCount; + return this; + } + + public List getResults() { + return results; + } + + public MyGenericPageResult setResults(List results) { + this.results = results; + return this; + } +} diff --git a/blackbox-test/src/test/java/org/example/customer/generics/MyGenericHolderTest.java b/blackbox-test/src/test/java/org/example/customer/generics/MyGenericHolderTest.java new file mode 100644 index 00000000..0e9619f4 --- /dev/null +++ b/blackbox-test/src/test/java/org/example/customer/generics/MyGenericHolderTest.java @@ -0,0 +1,75 @@ +package org.example.customer.generics; + +import io.avaje.jsonb.JsonType; +import io.avaje.jsonb.JsonView; +import io.avaje.jsonb.Jsonb; +import io.avaje.jsonb.Types; +import org.example.customer.Address; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; + +import static org.assertj.core.api.Assertions.assertThat; + +class MyGenericHolderTest { + + Jsonb jsonb = Jsonb.builder().build(); + + private static MyGenericHolder
createTestData() { + var bean = new MyGenericHolder
(); + bean.setTitle("hello").setAuthor("art").setDocument(new Address(90L, "one")); + return bean; + } + + @SuppressWarnings({"rawtypes"}) + @Test + void toJson() { + MyGenericHolder
bean = createTestData(); + + var type = jsonb.type(MyGenericHolder.class); + + String asJson = type.toJson(bean); + assertThat(asJson).isEqualTo("{\"title\":\"hello\",\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}"); + assertThat(jsonb.toJson(bean)).isEqualTo(asJson); + + MyGenericHolder pageResult = type.fromJson(asJson); + Object document = pageResult.getDocument(); + // reading via Object means the list contains LinkedHashMap + assertThat(document).isInstanceOf(LinkedHashMap.class); + LinkedHashMap asMap = (LinkedHashMap)document; + assertThat(asMap.get("street")).isEqualTo("one"); + + JsonView view = type.view("author,document(id)"); + String partialJson2 = view.toJson(bean); + // not supporting partial on the generic object (output includes street) + assertThat(partialJson2).isEqualTo("{\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}"); + } + + + @Test + void toJson_withGenericParam() { + MyGenericHolder
bean = createTestData(); + + Jsonb jsonb = Jsonb.builder().build(); + JsonType> type = jsonb.type(Types.newParameterizedType(MyGenericHolder.class, Address.class)); + + String asJson = type.toJson(bean); + assertThat(asJson).isEqualTo("{\"title\":\"hello\",\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}"); + assertThat(jsonb.toJson(bean)).isEqualTo(asJson); + + MyGenericHolder
genericResult = type.fromJson(asJson); + Address document = genericResult.getDocument(); + + assertThat(document.getId()).isEqualTo(90L); + assertThat(document.getStreet()).isEqualTo("one"); + + + JsonView> partial = type.view("author,document(*)"); + String partialJson = partial.toJson(bean); + assertThat(partialJson).isEqualTo("{\"author\":\"art\",\"document\":{\"id\":90,\"street\":\"one\"}}"); + + JsonView> partial2 = type.view("author,document(id)"); + String partialJson2 = partial2.toJson(bean); + assertThat(partialJson2).isEqualTo("{\"author\":\"art\",\"document\":{\"id\":90}}"); + } +} diff --git a/blackbox-test/src/test/java/org/example/customer/generics/MyGenericPageResultTest.java b/blackbox-test/src/test/java/org/example/customer/generics/MyGenericPageResultTest.java new file mode 100644 index 00000000..64d32cb4 --- /dev/null +++ b/blackbox-test/src/test/java/org/example/customer/generics/MyGenericPageResultTest.java @@ -0,0 +1,75 @@ +package org.example.customer.generics; + +import io.avaje.jsonb.JsonType; +import io.avaje.jsonb.JsonView; +import io.avaje.jsonb.Jsonb; +import io.avaje.jsonb.Types; +import org.example.customer.Address; +import org.junit.jupiter.api.Test; + +import java.util.LinkedHashMap; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class MyGenericPageResultTest { + + Jsonb jsonb = Jsonb.builder().build(); + + private static MyGenericPageResult
createPageTestData() { + var bean = new MyGenericPageResult
(); + bean.setPage(42).setPageSize(10).setResults(List.of(new Address(90L, "one"), new Address(91L, "two"))); + return bean; + } + + @SuppressWarnings({"rawtypes"}) + @Test + void toJson() { + MyGenericPageResult
bean = createPageTestData(); + + var type = jsonb.type(MyGenericPageResult.class); + + String asJson = type.toJson(bean); + assertThat(asJson).isEqualTo("{\"page\":42,\"pageSize\":10,\"totalPageCount\":0,\"results\":[{\"id\":90,\"street\":\"one\"},{\"id\":91,\"street\":\"two\"}]}"); + assertThat(jsonb.toJson(bean)).isEqualTo(asJson); + + MyGenericPageResult pageResult = type.fromJson(asJson); + List list = pageResult.getResults(); + assertThat(list).hasSize(2); + // reading via Object means the list contains LinkedHashMap + assertThat(list.get(0)).isInstanceOf(LinkedHashMap.class); + + JsonView partial2 = type.view("page,results(id)"); + String partialJson2 = partial2.toJson(bean); + // not supporting partial on the generic list of object (output includes street) + assertThat(partialJson2).isEqualTo("{\"page\":42,\"results\":[{\"id\":90,\"street\":\"one\"},{\"id\":91,\"street\":\"two\"}]}"); + } + + + @Test + void toJson_withGenericParam() { + MyGenericPageResult
bean = createPageTestData(); + + Jsonb jsonb = Jsonb.builder().build(); + + JsonType> type = jsonb.type(Types.newParameterizedType(MyGenericPageResult.class, Address.class)); + + String asJson = type.toJson(bean); + assertThat(asJson).isEqualTo("{\"page\":42,\"pageSize\":10,\"totalPageCount\":0,\"results\":[{\"id\":90,\"street\":\"one\"},{\"id\":91,\"street\":\"two\"}]}"); + assertThat(jsonb.toJson(bean)).isEqualTo(asJson); + + MyGenericPageResult
genericResult = type.fromJson(asJson); + List addresses = genericResult.getResults(); + + assertThat(addresses).hasSize(2); + assertThat(addresses.get(0)).isInstanceOf(Address.class); + + JsonView> partial = type.view("page,results(*)"); + String partialJson = partial.toJson(bean); + assertThat(partialJson).isEqualTo("{\"page\":42,\"results\":[{\"id\":90,\"street\":\"one\"},{\"id\":91,\"street\":\"two\"}]}"); + + JsonView> partial2 = type.view("page,results(id)"); + String partialJson2 = partial2.toJson(bean); + assertThat(partialJson2).isEqualTo("{\"page\":42,\"results\":[{\"id\":90},{\"id\":91}]}"); + } +} diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/BeanReader.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/BeanReader.java index b0173102..14a31897 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/BeanReader.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/BeanReader.java @@ -57,6 +57,10 @@ public BeanReader(TypeElement beanType, TypeElement mixInElement, ProcessingCont this.constructor = typeReader.constructor(); } + int genericTypeParamsCount() { + return typeReader.genericTypeParamsCount(); + } + @Override public String toString() { return beanType.toString(); @@ -106,6 +110,10 @@ private String shortName(Element element) { } private Set importTypes() { + if (genericTypeParamsCount() > 0) { + importTypes.add(Constants.REFLECT_TYPE); + importTypes.add(Constants.PARAMETERIZED_TYPE); + } importTypes.add(Constants.JSONB_WILD); importTypes.add(Constants.IOEXCEPTION); importTypes.add(Constants.JSONB_SPI); diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/ComponentMetaData.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/ComponentMetaData.java index fc5e92ba..5e0bebbe 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/ComponentMetaData.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/ComponentMetaData.java @@ -5,6 +5,7 @@ class ComponentMetaData { private final List allTypes = new ArrayList<>(); + private final List factoryTypes = new ArrayList<>(); private String fullName; @Override @@ -27,6 +28,10 @@ void add(String type) { allTypes.add(type); } + void addFactory(String fullName) { + factoryTypes.add(fullName); + } + void setFullName(String fullName) { this.fullName = fullName; } @@ -50,6 +55,10 @@ List all() { return allTypes; } + List allFactories() { + return factoryTypes; + } + /** * Return the package imports for the JsonAdapters and related types. */ diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/ComponentReader.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/ComponentReader.java index 389a0d02..7d3533ff 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/ComponentReader.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/ComponentReader.java @@ -19,6 +19,7 @@ class ComponentReader { private static final String META_DATA = "io.avaje.jsonb.spi.MetaData"; + private static final String META_DATA_FACTORY = "io.avaje.jsonb.spi.MetaData.Factory"; private final ProcessingContext ctx; private final ComponentMetaData componentMetaData; @@ -44,13 +45,21 @@ void read() { private void readMetaData(TypeElement moduleType) { for (AnnotationMirror annotationMirror : moduleType.getAnnotationMirrors()) { if (META_DATA.equals(annotationMirror.getAnnotationType().toString())) { - for (Map.Entry entry : annotationMirror.getElementValues().entrySet()) { - for (Object adapterEntry : (List) entry.getValue().getValue()) { - componentMetaData.add(adapterNameFromEntry(adapterEntry)); - } - } + readValues(annotationMirror).forEach(componentMetaData::add); + } else if (META_DATA_FACTORY.equals(annotationMirror.getAnnotationType().toString())) { + readValues(annotationMirror).forEach(componentMetaData::addFactory); + } + } + } + + private List readValues(AnnotationMirror annotationMirror) { + List adapterClasses = new ArrayList<>(); + for (Map.Entry entry : annotationMirror.getElementValues().entrySet()) { + for (Object adapterEntry : (List) entry.getValue().getValue()) { + adapterClasses.add(adapterNameFromEntry(adapterEntry)); } } + return adapterClasses; } private String adapterNameFromEntry(Object adapterEntry) { diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/Constants.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/Constants.java index ece5dfca..4cf95e99 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/Constants.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/Constants.java @@ -8,5 +8,7 @@ class Constants { static final String JSONB = "io.avaje.jsonb.Jsonb"; static final String IOEXCEPTION = "java.io.IOException"; static final String METHODHANDLE = "java.lang.invoke.MethodHandle"; + static final String REFLECT_TYPE = "java.lang.reflect.Type"; + static final String PARAMETERIZED_TYPE = "java.lang.reflect.ParameterizedType"; } diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/FieldReader.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/FieldReader.java index c95148d7..d0040a3b 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/FieldReader.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/FieldReader.java @@ -1,15 +1,16 @@ package io.avaje.jsonb.generator; +import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Set; -import javax.lang.model.element.Element; -import javax.lang.model.element.Modifier; - class FieldReader { private final Map subTypes = new LinkedHashMap<>(); + private final List genericTypeParams; private final boolean publicField; private final String rawType; private final GenericType genericType; @@ -28,9 +29,12 @@ class FieldReader { private MethodReader getter; private int position; private boolean constructorParam; + private boolean genericTypeParameter; + private int genericTypeParamPosition; - FieldReader(Element element, NamingConvention namingConvention, TypeSubTypeMeta subType) { + FieldReader(Element element, NamingConvention namingConvention, TypeSubTypeMeta subType, List genericTypeParams) { addSubType(subType); + this.genericTypeParams = genericTypeParams; this.fieldName = element.getSimpleName().toString(); this.propertyName = PropertyReader.name(namingConvention, fieldName, element); this.publicField = element.getModifiers().contains(Modifier.PUBLIC); @@ -58,10 +62,33 @@ class FieldReader { final String shortType = genericType.shortType(); primitive = PrimitiveUtil.isPrimitive(shortType); defaultValue = !primitive ? "null" : PrimitiveUtil.defaultValue(shortType); - final String typeWrapped = PrimitiveUtil.wrap(shortType); - adapterShortType = "JsonAdapter<" + typeWrapped + ">"; - adapterFieldName = (primitive ? "p" : "") + Util.initLower(genericType.shortName()) + "JsonAdapter"; + adapterShortType = initAdapterShortType(shortType); + adapterFieldName = (primitive ? "p" : "") + initShortName(); + } + } + + private String initAdapterShortType(String shortType) { + String typeWrapped = "JsonAdapter<" + PrimitiveUtil.wrap(shortType) + ">"; + for (int i = 0; i < genericTypeParams.size(); i++) { + String typeParam = genericTypeParams.get(i); + if (typeWrapped.contains("<" + typeParam + ">") ) { + genericTypeParameter = true; + genericTypeParamPosition = i; + typeWrapped = typeWrapped.replace("<" + typeParam + ">", ""); + } + } + return typeWrapped; + } + + private String initShortName() { + if (genericTypeParameter) { + String name = genericType.shortName(); + for (String typeParam : genericTypeParams) { + name = name.replace(typeParam, ""); + } + return Util.initLower(name) + "JsonAdapterGeneric"; } + return Util.initLower(genericType.shortName()) + "JsonAdapter"; } static String trimAnnotations(String type) { @@ -202,9 +229,21 @@ void writeConstructor(Append writer) { if (raw) { writer.append(" this.%s = jsonb.rawAdapter();", adapterFieldName).eol(); } else { - final String asType = genericType.asTypeDeclaration(); - writer.append(" this.%s = jsonb.adapter(%s);", adapterFieldName, asType).eol(); + writer.append(" this.%s = jsonb.adapter(%s);", adapterFieldName, asTypeDeclaration()).eol(); + } + } + + String asTypeDeclaration() { + String asType = genericType.asTypeDeclaration().replace("? extends ", ""); + if (genericTypeParameter) { + return genericTypeReplacement(asType, "param" + genericTypeParamPosition); } + return asType; + } + + private String genericTypeReplacement(String asType, String replaceWith) { + String typeParam = genericTypeParams.get(genericTypeParamPosition); + return asType.replace(typeParam + ".class", replaceWith); } void writeToJson(Append writer, String varName, String prefix) { @@ -306,8 +345,11 @@ void writeViewBuilder(Append writer, String shortName) { if (getter == null) { writer.append(" builder.add(\"%s\", %s, builder.field(%s.class, \"%s\"));", propertyName, adapterFieldName, shortName, fieldName).eol(); } else { - final String topType = genericType.topType(); - writer.append(" builder.add(\"%s\", %s, builder.method(%s.class, \"%s\", %s.class));", propertyName, adapterFieldName, shortName, getter.getName(), topType).eol(); + String topType = genericType.topType() + ".class"; + if (genericTypeParameter) { + topType = genericTypeReplacement(topType, "Object.class"); + } + writer.append(" builder.add(\"%s\", %s, builder.method(%s.class, \"%s\", %s));", propertyName, adapterFieldName, shortName, getter.getName(), topType).eol(); } } } diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/Processor.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/Processor.java index e466c78b..f2dbbd9d 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/Processor.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/Processor.java @@ -209,6 +209,9 @@ private void writeAdapter(TypeElement typeElement, BeanReader beanReader) { try { final SimpleAdapterWriter beanWriter = new SimpleAdapterWriter(beanReader, context); metaData.add(beanWriter.fullName()); + if (beanWriter.hasGenericFactory()) { + metaData.addFactory(beanWriter.fullName()); + } beanWriter.write(); allReaders.add(beanReader); sourceTypes.add(typeElement.getSimpleName().toString()); diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleAdapterWriter.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleAdapterWriter.java index 0b3f4799..3e414229 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleAdapterWriter.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleAdapterWriter.java @@ -11,6 +11,7 @@ class SimpleAdapterWriter { private final String adapterShortName; private final String adapterPackage; private final String adapterFullName; + private final int genericParamsCount; private Append writer; @@ -21,6 +22,7 @@ class SimpleAdapterWriter { this.adapterShortName = adapterName.shortName(); this.adapterPackage = adapterName.adapterPackage(); this.adapterFullName = adapterName.fullName(); + this.genericParamsCount = beanReader.genericTypeParamsCount(); } String fullName() { @@ -32,11 +34,16 @@ private Writer createFileWriter() throws IOException { return jfo.openWriter(); } + boolean hasGenericFactory() { + return genericParamsCount > 0; + } + void write() throws IOException { writer = new Append(createFileWriter()); writePackage(); writeImports(); writeClassStart(); + writeFactory(); writeFields(); writeConstructor(); writeToFromJson(); @@ -44,10 +51,44 @@ void write() throws IOException { writer.close(); } + private void writeFactory() { + if (genericParamsCount > 0) { + writer.append(" public static final JsonAdapter.Factory Factory = (type, jsonb) -> {").eol(); + writer.append(" if (type instanceof ParameterizedType && Types.rawType(type) == %s.class) {", adapterShortName).eol(); + writer.append(" Type[] args = Types.typeArguments(type);").eol(); + writer.append(" return new %sJsonAdapter(jsonb", adapterShortName); + for (int i = 0; i < genericParamsCount; i++) { + writer.append(", args[%d]", i); + } + writer.append(");").eol(); + writer.append(" }").eol(); + writer.append(" return null;").eol(); + writer.append(" };").eol().eol().eol(); + } + } + private void writeConstructor() { - writer.append(" public %sJsonAdapter(Jsonb jsonb) {", adapterShortName).eol(); + writer.append(" public %sJsonAdapter(Jsonb jsonb", adapterShortName); + for (int i = 0; i < genericParamsCount; i++) { + writer.append(", Type param%d", i); + } + writer.append(") {", adapterShortName).eol(); beanReader.writeConstructor(writer); writer.append(" }").eol(); + + if (genericParamsCount > 0) { + writer.eol(); + writer.append(" /**").eol(); + writer.append(" * Construct using Object for generic type parameters.").eol(); + writer.append(" */").eol(); + writer.append(" public %sJsonAdapter(Jsonb jsonb) {", adapterShortName).eol(); + writer.append(" this(jsonb"); + for (int i = 0; i < genericParamsCount; i++) { + writer.append(", Object.class"); + } + writer.append(");").eol(); + writer.append(" }").eol(); + } } private void writeToFromJson() { diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleComponentWriter.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleComponentWriter.java index 195934ea..c2c64a00 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleComponentWriter.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/SimpleComponentWriter.java @@ -53,6 +53,12 @@ void writeMetaInf() throws IOException { private void writeRegister() { writer.append(" @Override").eol(); writer.append(" public void register(Jsonb.Builder builder) {").eol(); + List strings = metaData.allFactories(); + for (String adapterFullName : strings) { + String adapterShortName = Util.shortName(adapterFullName); + String typeName = typeShortName(adapterShortName); + writer.append(" builder.add(%sJsonAdapter.Factory);", typeName).eol(); + } for (String adapterFullName : metaData.all()) { String adapterShortName = Util.shortName(adapterFullName); String typeName = typeShortName(adapterShortName); @@ -78,17 +84,27 @@ private void writeClassStart() { String fullName = metaData.fullName(); String shortName = Util.shortName(fullName); writer.append("@Generated").eol(); + List factories = metaData.allFactories(); + if (!factories.isEmpty()) { + writer.append("@MetaData.Factory({"); + writeMetaDataEntry(factories); + writer.append("})").eol(); + } writer.append("@MetaData({"); List all = metaData.all(); - for (int i = 0, size = all.size(); i < size; i++) { + writeMetaDataEntry(all); + writer.append("})").eol(); + + writer.append("public class %s implements Jsonb.GeneratedComponent {", shortName).eol().eol(); + } + + private void writeMetaDataEntry(List entries) { + for (int i = 0, size = entries.size(); i < size; i++) { if (i > 0) { writer.append(", "); } - writer.append("%s.class", Util.shortName(all.get(i))); + writer.append("%s.class", Util.shortName(entries.get(i))); } - writer.append("})").eol(); - - writer.append("public class %s implements Jsonb.GeneratedComponent {", shortName).eol().eol(); } diff --git a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/TypeReader.java b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/TypeReader.java index 95d59f92..e1f1e757 100644 --- a/jsonb-generator/src/main/java/io/avaje/jsonb/generator/TypeReader.java +++ b/jsonb-generator/src/main/java/io/avaje/jsonb/generator/TypeReader.java @@ -24,6 +24,7 @@ class TypeReader { private final TypeSubTypeReader subTypes; private final TypeElement baseType; + private final List genericTypeParams; private final ProcessingContext context; private final NamingConvention namingConvention; private final boolean hasJsonAnnotation; @@ -37,6 +38,7 @@ class TypeReader { TypeReader(TypeElement baseType, ProcessingContext context, NamingConvention namingConvention) { this.baseType = baseType; + this.genericTypeParams = initTypeParams(baseType); this.context = context; this.mixInFields = new HashMap<>(); this.namingConvention = namingConvention; @@ -44,23 +46,33 @@ class TypeReader { this.subTypes = new TypeSubTypeReader(baseType, context); } - public TypeReader( - TypeElement baseType, - TypeElement mixInType, - ProcessingContext context, - NamingConvention namingConvention) { - + public TypeReader(TypeElement baseType, TypeElement mixInType, ProcessingContext context, NamingConvention namingConvention) { this.baseType = baseType; + this.genericTypeParams = initTypeParams(baseType); this.mixInFields = - mixInType.getEnclosedElements().stream() - .filter(e -> e.getKind() == ElementKind.FIELD) - .collect(Collectors.toMap(e -> e.getSimpleName().toString(), e -> e)); + mixInType.getEnclosedElements().stream() + .filter(e -> e.getKind() == ElementKind.FIELD) + .collect(Collectors.toMap(e -> e.getSimpleName().toString(), e -> e)); this.context = context; this.namingConvention = namingConvention; this.hasJsonAnnotation = baseType.getAnnotation(Json.class) != null; this.subTypes = new TypeSubTypeReader(baseType, context); } + private List initTypeParams(TypeElement beanType) { + if (beanType.getTypeParameters().isEmpty()) { + return Collections.emptyList(); + } + return beanType.getTypeParameters() + .stream() + .map(Object::toString) + .collect(Collectors.toList()); + } + + int genericTypeParamsCount() { + return genericTypeParams.size(); + } + void read(TypeElement type) { final List localFields = new ArrayList<>(); for (Element element : type.getEnclosedElements()) { @@ -101,7 +113,7 @@ private void readField(Element element, List localFields) { element = mixInField; } if (includeField(element)) { - localFields.add(new FieldReader(element, namingConvention, currentSubType)); + localFields.add(new FieldReader(element, namingConvention, currentSubType, genericTypeParams)); } } diff --git a/jsonb/src/main/java/io/avaje/jsonb/Types.java b/jsonb/src/main/java/io/avaje/jsonb/Types.java index 0ea29cfb..d4d678a0 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/Types.java +++ b/jsonb/src/main/java/io/avaje/jsonb/Types.java @@ -17,8 +17,7 @@ import io.avaje.jsonb.core.Util; -import java.lang.reflect.ParameterizedType; -import java.lang.reflect.Type; +import java.lang.reflect.*; import java.util.List; import java.util.Map; import java.util.Set; @@ -79,4 +78,49 @@ public static ParameterizedType newParameterizedType(Type rawType, Type... typeA return Util.newParameterizedType(rawType, typeArguments); } + /** + * Return the raw type for the given potentially generic type. + */ + public static Class rawType(Type type) { + if (type instanceof Class) { + // type is a normal class. + return (Class) type; + + } else if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + + // I'm not exactly sure why getRawType() returns Type instead of Class. Neal isn't either but + // suspects some pathological case related to nested classes exists. + Type rawType = parameterizedType.getRawType(); + return (Class) rawType; + + } else if (type instanceof GenericArrayType) { + Type componentType = ((GenericArrayType) type).getGenericComponentType(); + return Array.newInstance(rawType(componentType), 0).getClass(); + + } else if (type instanceof TypeVariable) { + // We could use the variable's bounds, but that won't work if there are multiple. having a raw + // type that's more general than necessary is okay. + return Object.class; + + } else if (type instanceof WildcardType) { + return rawType(((WildcardType) type).getUpperBounds()[0]); + + } else { + String className = type == null ? "null" : type.getClass().getName(); + throw new IllegalArgumentException( + "Expected a Class, ParameterizedType, or GenericArrayType, but <" + type + "> is of type " + className); + } + } + + /** + * Return the generic type arguments expecting type to be a ParameterizedType. + */ + public static Type[] typeArguments(Type type) { + if (type instanceof ParameterizedType) { + return ((ParameterizedType) type).getActualTypeArguments(); + } + String className = type == null ? "null" : type.getClass().getName(); + throw new IllegalArgumentException("Expected ParameterizedType but <" + type + "> is of type " + className); + } } diff --git a/jsonb/src/main/java/io/avaje/jsonb/core/Util.java b/jsonb/src/main/java/io/avaje/jsonb/core/Util.java index 7b80d052..bd6ebe4a 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/core/Util.java +++ b/jsonb/src/main/java/io/avaje/jsonb/core/Util.java @@ -15,6 +15,8 @@ */ package io.avaje.jsonb.core; +import io.avaje.jsonb.Types; + import java.lang.annotation.Annotation; import java.lang.reflect.*; import java.util.*; @@ -473,39 +475,7 @@ static WildcardType supertypeOf(Type bound) { static Class rawType(Type type) { - if (type instanceof Class) { - // type is a normal class. - return (Class) type; - - } else if (type instanceof ParameterizedType) { - ParameterizedType parameterizedType = (ParameterizedType) type; - - // I'm not exactly sure why getRawType() returns Type instead of Class. Neal isn't either but - // suspects some pathological case related to nested classes exists. - Type rawType = parameterizedType.getRawType(); - return (Class) rawType; - - } else if (type instanceof GenericArrayType) { - Type componentType = ((GenericArrayType) type).getGenericComponentType(); - return Array.newInstance(rawType(componentType), 0).getClass(); - - } else if (type instanceof TypeVariable) { - // We could use the variable's bounds, but that won't work if there are multiple. having a raw - // type that's more general than necessary is okay. - return Object.class; - - } else if (type instanceof WildcardType) { - return rawType(((WildcardType) type).getUpperBounds()[0]); - - } else { - String className = type == null ? "null" : type.getClass().getName(); - throw new IllegalArgumentException( - "Expected a Class, ParameterizedType, or " - + "GenericArrayType, but <" - + type - + "> is of type " - + className); - } + return Types.rawType(type); } /** diff --git a/jsonb/src/main/java/io/avaje/jsonb/spi/MetaData.java b/jsonb/src/main/java/io/avaje/jsonb/spi/MetaData.java index 6a626270..6ac125b8 100644 --- a/jsonb/src/main/java/io/avaje/jsonb/spi/MetaData.java +++ b/jsonb/src/main/java/io/avaje/jsonb/spi/MetaData.java @@ -14,4 +14,14 @@ */ Class[] value(); + /** + * For internal use, holds metadata on generated adapters that also have factories. + */ + @interface Factory { + + /** + * The generated JsonAdapters that have a factory. + */ + Class[] value(); + } }