From cc998ca018848beeee12eb76a3949f65427d0e50 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 9 Jul 2019 23:40:03 +0300 Subject: [PATCH 01/20] Generate Jsonb ContextResolver --- .../jsonb/deployment/JsonbConfig.java | 61 +++++++ .../deployment/ResteasyJsonbProcessor.java | 149 ++++++++++++++++++ .../ResteasyServerCommonProcessor.java | 4 +- 3 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java new file mode 100644 index 0000000000000..07d8d14d73ab5 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java @@ -0,0 +1,61 @@ +package io.quarkus.resteasy.jsonb.deployment; + +import static io.quarkus.runtime.annotations.ConfigPhase.BUILD_TIME; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import javax.json.bind.config.PropertyOrderStrategy; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = BUILD_TIME) +public class JsonbConfig { + + static final Set ALLOWED_PROPERTY_ORDER_VALUES = Collections.unmodifiableSet( + new HashSet<>(Arrays.asList(PropertyOrderStrategy.LEXICOGRAPHICAL, PropertyOrderStrategy.ANY, + PropertyOrderStrategy.REVERSE))); + + /** + * default locale to use + */ + @ConfigItem(defaultValue = "") + String locale; + + /** + * default date format to use + */ + @ConfigItem(defaultValue = "") + String dateFormat; + + /** + * defines whether or not null values are serialized + */ + @ConfigItem(defaultValue = "false") + boolean serializeNullValues; + + /** + * defines the order in which the properties appear in the json output + */ + @ConfigItem(defaultValue = PropertyOrderStrategy.LEXICOGRAPHICAL) + String propertyOrderStrategy; + + /** + * encoding to use when deserializing data + */ + @ConfigItem(defaultValue = "") + String encoding; + + /** + * specified whether unknown properties will cause deserialization to fail + */ + @ConfigItem(defaultValue = "true") + boolean failOnUnknownProperties; + + boolean isValidPropertyOrderStrategy() { + return ALLOWED_PROPERTY_ORDER_VALUES.contains(propertyOrderStrategy.toUpperCase()); + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index 724cc4c022501..decb17a0764be 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -1,14 +1,163 @@ package io.quarkus.resteasy.jsonb.deployment; +import java.util.Collection; +import java.util.Locale; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; + +import org.eclipse.yasson.YassonProperties; +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; + import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.common.deployment.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.server.common.deployment.ResteasyServerCommonProcessor; public class ResteasyJsonbProcessor { + private static final String CONTEXT_RESOLVER = "io.quarkus.jsonb.QuarkusJsonbContextResolver"; + @BuildStep(providesCapabilities = Capabilities.RESTEASY_JSON_EXTENSION) void build(BuildProducer feature) { feature.produce(new FeatureBuildItem(FeatureBuildItem.RESTEASY_JSONB)); } + + JsonbConfig jsonbConfig; + + @BuildStep + void generateJsonbContextResolver(CombinedIndexBuildItem combinedIndexBuildItem, + BuildProducer generatedClass, + BuildProducer jaxrsProvider) { + IndexView index = combinedIndexBuildItem.getIndex(); + + validateConfiguration(); + + ClassOutput classOutput = new ClassOutput() { + @Override + public void write(String name, byte[] data) { + generatedClass.produce(new GeneratedClassBuildItem(true, name, data)); + } + }; + + for (DotName annotationType : ResteasyServerCommonProcessor.METHOD_ANNOTATIONS) { + Collection instances = index.getAnnotations(annotationType); + for (AnnotationInstance instance : instances) { + MethodInfo method = instance.target().asMethod(); + if (ResteasyServerCommonProcessor.isReflectionDeclarationRequiredFor(method.returnType())) { + // TODO generate a serializer + } + } + } + + generateJsonbContextResolver(classOutput); + jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(CONTEXT_RESOLVER)); + } + + private void validateConfiguration() { + if (!jsonbConfig.isValidPropertyOrderStrategy()) { + throw new IllegalArgumentException( + "quarkus.jsonb.property-order-strategy can only be one of " + JsonbConfig.ALLOWED_PROPERTY_ORDER_VALUES); + } + } + + private void generateJsonbContextResolver(ClassOutput classOutput) { + try (ClassCreator cc = ClassCreator.builder() + .classOutput(classOutput).className(CONTEXT_RESOLVER) + .interfaces(ContextResolver.class) + .signature("Ljava/lang/Object;Ljavax/ws/rs/ext/ContextResolver;") + .build()) { + + cc.addAnnotation(Provider.class); + + try (MethodCreator getContext = cc.getMethodCreator("getContext", Jsonb.class, Class.class)) { + final Class jsonbConfigClass = javax.json.bind.JsonbConfig.class; + + //create the JsonbConfig object + MethodDescriptor configCtor = MethodDescriptor.ofConstructor(jsonbConfigClass); + ResultHandle config = getContext.newInstance(configCtor); + + //handle locale + ResultHandle locale = null; + if (!jsonbConfig.locale.isEmpty()) { + locale = getContext.invokeStaticMethod( + MethodDescriptor.ofMethod(Locale.class, "forLanguageTag", Locale.class, String.class), + getContext.load(jsonbConfig.locale)); + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withLocale", jsonbConfigClass, Locale.class), + config, locale); + } + + // handle date format + if (!jsonbConfig.dateFormat.isEmpty()) { + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withDateFormat", jsonbConfigClass, String.class, + Locale.class), + config, + getContext.load(jsonbConfig.dateFormat), + locale != null ? locale : getContext.loadNull()); + } + + // handle serializeNullValues + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withNullValues", jsonbConfigClass, Boolean.class), + config, + getContext.invokeStaticMethod( + MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), + getContext.load(jsonbConfig.serializeNullValues))); + + // handle propertyOrderStrategy + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withPropertyOrderStrategy", jsonbConfigClass, + String.class), + config, getContext.load(jsonbConfig.propertyOrderStrategy.toUpperCase())); + + // handle encoding + if (!jsonbConfig.encoding.isEmpty()) { + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withEncoding", jsonbConfigClass, + String.class), + config, getContext.load(jsonbConfig.encoding)); + } + + // handle failOnUnknownProperties + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "setProperty", jsonbConfigClass, String.class, + Object.class), + config, + getContext.load(YassonProperties.FAIL_ON_UNKNOWN_PROPERTIES), + getContext.invokeStaticMethod( + MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), + getContext.load(jsonbConfig.failOnUnknownProperties))); + + //create Jsonb from JsonbBuilder#create using the previously created config + ResultHandle result = getContext.invokeStaticMethod( + MethodDescriptor.ofMethod(JsonbBuilder.class, "create", Jsonb.class, jsonbConfigClass), config); + getContext.returnValue(result); + } + + try (MethodCreator bridgeGetContext = cc.getMethodCreator("getContext", Object.class, Class.class)) { + MethodDescriptor getContext = MethodDescriptor.ofMethod(CONTEXT_RESOLVER, "getContext", "javax.json.bind.Jsonb", + "java.lang.Class"); + ResultHandle result = bridgeGetContext.invokeVirtualMethod(getContext, bridgeGetContext.getThis(), + bridgeGetContext.getMethodParam(0)); + bridgeGetContext.returnValue(result); + } + } + } + } diff --git a/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java b/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java index 4eedd22e9367a..87e963b0e85f4 100755 --- a/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java +++ b/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java @@ -88,7 +88,7 @@ public class ResteasyServerCommonProcessor { private static final DotName JSONB_ANNOTATION = DotName.createSimple("javax.json.bind.annotation.JsonbAnnotation"); - private static final DotName[] METHOD_ANNOTATIONS = { + public static final DotName[] METHOD_ANNOTATIONS = { ResteasyDotNames.GET, ResteasyDotNames.HEAD, ResteasyDotNames.DELETE, @@ -722,7 +722,7 @@ private static void scanMethodParameters(DotName annotationType, } } - private static boolean isReflectionDeclarationRequiredFor(Type type) { + public static boolean isReflectionDeclarationRequiredFor(Type type) { DotName className = getClassName(type); return className != null && !ResteasyDotNames.TYPES_IGNORED_FOR_REFLECTION.contains(className); From 999e1ab9b143f6027f22db825fdfc4e388649d63 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Sun, 14 Jul 2019 20:55:41 +0300 Subject: [PATCH 02/20] Add basic functionality for generating serializers --- .../deployment/AdditionalClassGenerator.java | 202 +++++++++++ .../jsonb/deployment/CollectionUtil.java | 44 +++ .../resteasy/jsonb/deployment/DotNames.java | 49 +++ .../resteasy/jsonb/deployment/JandexUtil.java | 100 ++++++ .../jsonb/deployment/JsonbConfig.java | 17 +- .../jsonb/deployment/PropertyUtil.java | 79 +++++ .../deployment/ResteasyJsonbProcessor.java | 240 ++++++++----- .../SerializationClassInspector.java | 254 ++++++++++++++ .../AbstractDatetimeSerializerGenerator.java | 34 ++ ...AbstractNumberTypeSerializerGenerator.java | 62 ++++ .../AbstractTypeSerializerGenerator.java | 35 ++ .../CollectionTypeSerializerGenerator.java | 75 +++++ .../GlobalSerializationConfig.java | 35 ++ .../IntegerTypeSerializerGenerator.java | 33 ++ .../LocalDateTimeSerializerGenerator.java | 56 +++ .../ObjectTypeSerializerGenerator.java | 318 ++++++++++++++++++ .../serializers/SerializerGeneratorUtil.java | 27 ++ .../StringTypeSerializerGenerator.java | 25 ++ .../serializers/TypeSerializerGenerator.java | 95 ++++++ .../TypeSerializerGeneratorRegistry.java | 45 +++ .../LocalDateTimeSerializerHelper.java | 26 ++ 21 files changed, 1756 insertions(+), 95 deletions(-) create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/CollectionUtil.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JandexUtil.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/PropertyUtil.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractDatetimeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractNumberTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/CollectionTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/GlobalSerializationConfig.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/IntegerTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/LocalDateTimeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/SerializerGeneratorUtil.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/StringTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java create mode 100644 extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/LocalDateTimeSerializerHelper.java diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java new file mode 100644 index 0000000000000..65abcf99c79f5 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java @@ -0,0 +1,202 @@ +package io.quarkus.resteasy.jsonb.deployment; + +import java.lang.reflect.Modifier; +import java.util.List; +import java.util.Locale; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.json.bind.annotation.JsonbDateFormat; +import javax.json.bind.serializer.JsonbSerializer; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; + +import org.eclipse.yasson.YassonProperties; +import org.eclipse.yasson.internal.serializer.JsonbDateFormatter; + +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; + +public class AdditionalClassGenerator { + + public static final String QUARKUS_CONTEXT_RESOLVER = "io.quarkus.jsonb.QuarkusJsonbContextResolver"; + public static final String QUARKUS_DEFAULT_DATE_FORMATTER_PROVIDER = "io.quarkus.jsonb.QuarkusDefaultJsonbDateFormatterProvider"; + public static final String QUARKUS_DEFAULT_LOCALE_PROVIDER = "io.quarkus.jsonb.QuarkusDefaultJsonbLocaleProvider"; + + private final JsonbConfig jsonbConfig; + + public AdditionalClassGenerator(JsonbConfig jsonbConfig) { + this.jsonbConfig = jsonbConfig; + } + + void generateDefaultLocaleProvider(ClassOutput classOutput) { + try (ClassCreator cc = ClassCreator.builder() + .classOutput(classOutput).className(QUARKUS_DEFAULT_LOCALE_PROVIDER) + .build()) { + + FieldDescriptor instance = cc.getFieldCreator("INSTANCE", Locale.class) + .setModifiers(Modifier.FINAL | Modifier.STATIC | Modifier.PRIVATE) + .getFieldDescriptor(); + + try (MethodCreator clinit = cc.getMethodCreator("", void.class)) { + clinit.setModifiers(Modifier.STATIC); + + ResultHandle locale; + if (jsonbConfig.locale.isPresent()) { + locale = clinit.invokeStaticMethod( + MethodDescriptor.ofMethod(Locale.class, "forLanguageTag", Locale.class, String.class), + clinit.load(jsonbConfig.locale.get())); + } else { + locale = clinit.invokeStaticMethod( + MethodDescriptor.ofMethod(Locale.class, "getDefault", Locale.class)); + } + + clinit.writeStaticField(instance, locale); + clinit.returnValue(null); + } + + try (MethodCreator get = cc.getMethodCreator("get", Locale.class)) { + get.setModifiers(Modifier.STATIC | Modifier.PUBLIC); + + get.returnValue(get.readStaticField(instance)); + } + } + } + + void generateJsonbDefaultJsonbDateFormatterProvider(ClassOutput classOutput) { + try (ClassCreator cc = ClassCreator.builder() + .classOutput(classOutput).className(QUARKUS_DEFAULT_DATE_FORMATTER_PROVIDER) + .build()) { + + FieldDescriptor instance = cc.getFieldCreator("INSTANCE", JsonbDateFormatter.class) + .setModifiers(Modifier.FINAL | Modifier.STATIC | Modifier.PRIVATE) + .getFieldDescriptor(); + + try (MethodCreator clinit = cc.getMethodCreator("", void.class)) { + clinit.setModifiers(Modifier.STATIC); + + ResultHandle locale = clinit.invokeStaticMethod( + MethodDescriptor.ofMethod(QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", Locale.class)); + + ResultHandle format = clinit.load(jsonbConfig.dateFormat.orElse(JsonbDateFormat.DEFAULT_FORMAT)); + + ResultHandle jsonbDateFormatter = clinit.newInstance( + MethodDescriptor.ofConstructor(JsonbDateFormatter.class, String.class, String.class), + format, locale); + + clinit.writeStaticField(instance, jsonbDateFormatter); + clinit.returnValue(null); + } + + try (MethodCreator get = cc.getMethodCreator("get", JsonbDateFormatter.class)) { + get.setModifiers(Modifier.STATIC | Modifier.PUBLIC); + + get.returnValue(get.readStaticField(instance)); + } + } + } + + void generateJsonbContextResolver(ClassOutput classOutput, List generatedSerializers) { + try (ClassCreator cc = ClassCreator.builder() + .classOutput(classOutput).className(QUARKUS_CONTEXT_RESOLVER) + .interfaces(ContextResolver.class) + .signature("Ljava/lang/Object;Ljavax/ws/rs/ext/ContextResolver;") + .build()) { + + cc.addAnnotation(Provider.class); + + try (MethodCreator getContext = cc.getMethodCreator("getContext", Jsonb.class, Class.class)) { + final Class jsonbConfigClass = javax.json.bind.JsonbConfig.class; + + //create the JsonbConfig object + MethodDescriptor configCtor = MethodDescriptor.ofConstructor(jsonbConfigClass); + ResultHandle config = getContext.newInstance(configCtor); + + //handle locale + ResultHandle locale = null; + if (jsonbConfig.locale.isPresent()) { + locale = getContext.invokeStaticMethod( + MethodDescriptor.ofMethod(QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", Locale.class)); + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withLocale", jsonbConfigClass, Locale.class), + config, locale); + } + + // handle date format + if (jsonbConfig.dateFormat.isPresent()) { + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withDateFormat", jsonbConfigClass, String.class, + Locale.class), + config, + getContext.load(jsonbConfig.dateFormat.get()), + locale != null ? locale : getContext.loadNull()); + } + + // handle serializeNullValues + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withNullValues", jsonbConfigClass, Boolean.class), + config, + getContext.invokeStaticMethod( + MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), + getContext.load(jsonbConfig.serializeNullValues))); + + // handle propertyOrderStrategy + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withPropertyOrderStrategy", jsonbConfigClass, + String.class), + config, getContext.load(jsonbConfig.propertyOrderStrategy.toUpperCase())); + + // handle encoding + if (jsonbConfig.encoding.isPresent()) { + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withEncoding", jsonbConfigClass, + String.class), + config, getContext.load(jsonbConfig.encoding.get())); + } + + // handle failOnUnknownProperties + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "setProperty", jsonbConfigClass, String.class, + Object.class), + config, + getContext.load(YassonProperties.FAIL_ON_UNKNOWN_PROPERTIES), + getContext.invokeStaticMethod( + MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), + getContext.load(jsonbConfig.failOnUnknownProperties))); + + // add generated serializers to config + if (!generatedSerializers.isEmpty()) { + ResultHandle serializersArray = getContext.newArray(JsonbSerializer.class, + getContext.load(generatedSerializers.size())); + for (int i = 0; i < generatedSerializers.size(); i++) { + ResultHandle serializer = getContext + .newInstance(MethodDescriptor.ofConstructor(generatedSerializers.get(i))); + getContext.writeArrayValue(serializersArray, getContext.load(i), serializer); + } + getContext.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withSerializers", jsonbConfigClass, + JsonbSerializer[].class), + config, serializersArray); + } + + //create Jsonb from JsonbBuilder#create using the previously created config + ResultHandle result = getContext.invokeStaticMethod( + MethodDescriptor.ofMethod(JsonbBuilder.class, "create", Jsonb.class, jsonbConfigClass), config); + getContext.returnValue(result); + } + + try (MethodCreator bridgeGetContext = cc.getMethodCreator("getContext", Object.class, Class.class)) { + MethodDescriptor getContext = MethodDescriptor.ofMethod(QUARKUS_CONTEXT_RESOLVER, "getContext", + "javax.json.bind.Jsonb", + "java.lang.Class"); + ResultHandle result = bridgeGetContext.invokeVirtualMethod(getContext, bridgeGetContext.getThis(), + bridgeGetContext.getMethodParam(0)); + bridgeGetContext.returnValue(result); + } + } + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/CollectionUtil.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/CollectionUtil.java new file mode 100644 index 0000000000000..195841f4003db --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/CollectionUtil.java @@ -0,0 +1,44 @@ +package io.quarkus.resteasy.jsonb.deployment; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.jboss.jandex.DotName; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.Type; + +public final class CollectionUtil { + + private static final Set SUPPORTED_TYPES = new HashSet<>( + Arrays.asList(DotNames.COLLECTION, DotNames.LIST, DotNames.SET)); + + private CollectionUtil() { + } + + // TODO come up with a better way of determining if the type is supported + public static boolean isCollection(DotName dotName) { + return SUPPORTED_TYPES.contains(dotName); + } + + /** + * @return the generic type of a collection + */ + public static Type getGenericType(Type type) { + if (!isCollection(type.name())) { + return null; + } + + if (!(type instanceof ParameterizedType)) { + return null; + } + + ParameterizedType parameterizedType = type.asParameterizedType(); + + if (parameterizedType.arguments().size() != 1) { + return null; + } + + return parameterizedType.arguments().get(0); + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java new file mode 100644 index 0000000000000..329c23d137a08 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java @@ -0,0 +1,49 @@ +package io.quarkus.resteasy.jsonb.deployment; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.json.bind.annotation.JsonbDateFormat; +import javax.json.bind.annotation.JsonbNillable; +import javax.json.bind.annotation.JsonbNumberFormat; +import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbPropertyOrder; +import javax.json.bind.annotation.JsonbTransient; +import javax.json.bind.annotation.JsonbTypeAdapter; +import javax.json.bind.annotation.JsonbTypeSerializer; +import javax.json.bind.annotation.JsonbVisibility; +import javax.ws.rs.ext.ContextResolver; + +import org.jboss.jandex.DotName; + +public final class DotNames { + + private DotNames() { + } + + public static final DotName OBJECT = DotName.createSimple(Object.class.getName()); + public static final DotName STRING = DotName.createSimple(String.class.getName()); + public static final DotName INTEGER = DotName.createSimple(Integer.class.getName()); + public static final DotName LOCAL_DATE_TIME = DotName.createSimple(LocalDateTime.class.getName()); + + public static final DotName COLLECTION = DotName.createSimple(Collection.class.getName()); + public static final DotName LIST = DotName.createSimple(List.class.getName()); + public static final DotName SET = DotName.createSimple(Set.class.getName()); + + public static final DotName MAP = DotName.createSimple(Map.class.getName()); + + public static final DotName CONTEXT_RESOLVER = DotName.createSimple(ContextResolver.class.getName()); + + public static final DotName JSONB_TRANSIENT = DotName.createSimple(JsonbTransient.class.getName()); + public static final DotName JSONB_PROPERTY = DotName.createSimple(JsonbProperty.class.getName()); + public static final DotName JSONB_TYPE_SERIALIZER = DotName.createSimple(JsonbTypeSerializer.class.getName()); + public static final DotName JSONB_TYPE_ADAPTER = DotName.createSimple(JsonbTypeAdapter.class.getName()); + public static final DotName JSONB_VISIBILITY = DotName.createSimple(JsonbVisibility.class.getName()); + public static final DotName JSONB_NILLABLE = DotName.createSimple(JsonbNillable.class.getName()); + public static final DotName JSONB_PROPERTY_ORDER = DotName.createSimple(JsonbPropertyOrder.class.getName()); + public static final DotName JSONB_NUMBER_FORMAT = DotName.createSimple(JsonbNumberFormat.class.getName()); + public static final DotName JSONB_DATE_FORMAT = DotName.createSimple(JsonbDateFormat.class.getName()); +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JandexUtil.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JandexUtil.java new file mode 100644 index 0000000000000..5c42e0d904fe4 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JandexUtil.java @@ -0,0 +1,100 @@ +package io.quarkus.resteasy.jsonb.deployment; + +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; + +public final class JandexUtil { + + private JandexUtil() { + } + + // includes annotations on superclasses, all interfaces and package-info + public static Map getEffectiveClassAnnotations(DotName classDotName, IndexView index) { + Map result = new HashMap<>(); + getEffectiveClassAnnotationsRec(classDotName, index, result); + + // we need these because they could contain jsonb annotations that alter the default behavior for all + // classes in the package + Collection annotationsFromPackage = getAnnotationsOfPackage(classDotName, index); + if (!annotationsFromPackage.isEmpty()) { + for (AnnotationInstance packageAnnotation : annotationsFromPackage) { + if (!result.containsKey(packageAnnotation.name())) { + result.put(packageAnnotation.name(), packageAnnotation); + } + } + } + return result; + } + + private static void getEffectiveClassAnnotationsRec(DotName classDotName, IndexView index, + Map collected) { + // annotations previously collected have higher "priority" so we need to make sure we don't add them again + ClassInfo classInfo = index.getClassByName(classDotName); + if (classInfo == null) { + return; + } + + Collection newInstances = classInfo.classAnnotations(); + for (AnnotationInstance newInstance : newInstances) { + if (!collected.containsKey(newInstance.name())) { + collected.put(newInstance.name(), newInstance); + } + } + + // collect annotations from the super type until we reach object + if (!DotNames.OBJECT.equals(classInfo.superName())) { + getEffectiveClassAnnotationsRec(classInfo.superName(), index, collected); + } + + // collect annotations from all interfaces + for (DotName interfaceDotName : classInfo.interfaceNames()) { + getEffectiveClassAnnotationsRec(interfaceDotName, index, collected); + } + } + + private static Collection getAnnotationsOfPackage(DotName classDotName, IndexView index) { + String className = classDotName.toString(); + if (!className.contains(".")) { + return Collections.emptyList(); + } + int i = className.lastIndexOf('.'); + String packageName = className.substring(0, i); + ClassInfo packageClassInfo = index.getClassByName(DotName.createSimple(packageName + ".package-info")); + if (packageClassInfo == null) { + return Collections.emptyList(); + } + + return packageClassInfo.classAnnotations(); + } + + // determine whether the class contains any interface (however far up the tree) that contains a default method + public static boolean containsInterfacesWithDefaultMethods(ClassInfo classInfo, IndexView index) { + List interfaceNames = classInfo.interfaceNames(); + for (DotName interfaceName : interfaceNames) { + ClassInfo interfaceClassInfo = index.getClassByName(interfaceName); + if (interfaceClassInfo == null) { + continue; + } + final List methods = interfaceClassInfo.methods(); + for (MethodInfo method : methods) { + // essentially the same as java.lang.reflect.Method#isDefault + if (((method.flags() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC)) { + return true; + } + } + return containsInterfacesWithDefaultMethods(interfaceClassInfo, index); + } + return false; + } + +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java index 07d8d14d73ab5..813dd31295326 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java @@ -5,6 +5,7 @@ import java.util.Arrays; import java.util.Collections; import java.util.HashSet; +import java.util.Optional; import java.util.Set; import javax.json.bind.config.PropertyOrderStrategy; @@ -22,14 +23,14 @@ public class JsonbConfig { /** * default locale to use */ - @ConfigItem(defaultValue = "") - String locale; + @ConfigItem + Optional locale; /** * default date format to use */ - @ConfigItem(defaultValue = "") - String dateFormat; + @ConfigItem + Optional dateFormat; /** * defines whether or not null values are serialized @@ -43,11 +44,13 @@ public class JsonbConfig { @ConfigItem(defaultValue = PropertyOrderStrategy.LEXICOGRAPHICAL) String propertyOrderStrategy; + // DESERIALIZER RELATED PROPERTIES + /** - * encoding to use when deserializing data + * encoding to use when de-serializing data */ - @ConfigItem(defaultValue = "") - String encoding; + @ConfigItem + Optional encoding; /** * specified whether unknown properties will cause deserialization to fail diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/PropertyUtil.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/PropertyUtil.java new file mode 100644 index 0000000000000..c6f2875c897af --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/PropertyUtil.java @@ -0,0 +1,79 @@ +package io.quarkus.resteasy.jsonb.deployment; + +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.MethodInfo; + +public final class PropertyUtil { + + private PropertyUtil() { + } + + private static final String IS_PREFIX = "is"; + private static final String GET_PREFIX = "get"; + + /** + * @return a Map where the keys are the getters and the values are the corresponding fields + */ + static Map getGetterMethods(ClassInfo classInfo) { + List allMethods = classInfo.methods(); + Map result = new HashMap<>(); + for (MethodInfo method : allMethods) { + if (isGetter(method)) { + result.put(method, classInfo.field(toFieldName(method))); + } + } + return result; + } + + static List getPublicFieldsWithoutGetters(ClassInfo classInfo, Collection getters) { + Set getterPropertyNames = new HashSet<>(); + for (MethodInfo getter : getters) { + getterPropertyNames.add(toFieldName(getter)); + } + List allFields = classInfo.fields(); + List result = new ArrayList<>(allFields.size()); + for (FieldInfo field : allFields) { + if (Modifier.isPublic(field.flags()) && !getterPropertyNames.contains(field.name())) { + result.add(field); + } + } + return result; + } + + private static boolean isGetter(MethodInfo m) { + return (m.name().startsWith(GET_PREFIX) || m.name().startsWith(IS_PREFIX)) && m.parameters().size() == 0; + } + + /** + * Returns the corresponding property name + * Assumes that the input is a getter + */ + public static String toFieldName(MethodInfo getter) { + String name = getter.name(); + return lowerFirstLetter(name.substring(name.startsWith(IS_PREFIX) ? 2 : 3)); + } + + private static String lowerFirstLetter(String name) { + if (name.length() == 0) { + //methods named get() or set() + return name; + } + if (name.length() > 1 && Character.isUpperCase(name.charAt(1)) && + Character.isUpperCase(name.charAt(0))) { + return name; + } + char[] chars = name.toCharArray(); + chars[0] = Character.toLowerCase(chars[0]); + return new String(chars); + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index decb17a0764be..be971c016e0d5 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -1,18 +1,26 @@ package io.quarkus.resteasy.jsonb.deployment; +import java.util.ArrayList; import java.util.Collection; -import java.util.Locale; +import java.util.HashSet; +import java.util.List; +import java.util.Set; import javax.json.bind.Jsonb; -import javax.json.bind.JsonbBuilder; -import javax.ws.rs.ext.ContextResolver; +import javax.json.bind.serializer.JsonbSerializer; +import javax.json.bind.serializer.SerializationContext; +import javax.json.stream.JsonGenerator; import javax.ws.rs.ext.Provider; -import org.eclipse.yasson.YassonProperties; +import io.quarkus.deployment.builditem.substrate.RuntimeInitializedClassBuildItem; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.Type; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; @@ -26,10 +34,14 @@ import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.resteasy.common.deployment.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.jsonb.deployment.serializers.GlobalSerializationConfig; +import io.quarkus.resteasy.jsonb.deployment.serializers.TypeSerializerGenerator; +import io.quarkus.resteasy.jsonb.deployment.serializers.TypeSerializerGeneratorRegistry; import io.quarkus.resteasy.server.common.deployment.ResteasyServerCommonProcessor; public class ResteasyJsonbProcessor { + @BuildStep(providesCapabilities = Capabilities.RESTEASY_JSON_EXTENSION) private static final String CONTEXT_RESOLVER = "io.quarkus.jsonb.QuarkusJsonbContextResolver"; @BuildStep(providesCapabilities = Capabilities.RESTEASY_JSON_EXTENSION) @@ -39,14 +51,32 @@ void build(BuildProducer feature) { JsonbConfig jsonbConfig; + /* + * If possible we are going to create a serializer for the class + * indicated by returnType + * We only create serializers for types we are 100% sure we can handle + * Whenever we encounter something we can't handle, + * we don't create a serializer and therefore fallback to + * jsonb to do it's runtime reflection work + */ @BuildStep - void generateJsonbContextResolver(CombinedIndexBuildItem combinedIndexBuildItem, - BuildProducer generatedClass, - BuildProducer jaxrsProvider) { + void generateClasses(CombinedIndexBuildItem combinedIndexBuildItem, + BuildProducer generatedClass, + BuildProducer jaxrsProvider, + BuildProducer runtimeClasses) { IndexView index = combinedIndexBuildItem.getIndex(); + // if the user has declared a custom ContextResolver for Jsonb, we don't generate anything + if (hasCustomContextResolverBeenDeclared(index)) { + return; + } + validateConfiguration(); + SerializationClassInspector serializationClassInspector = new SerializationClassInspector(index); + TypeSerializerGeneratorRegistry typeSerializerGeneratorRegistry = new TypeSerializerGeneratorRegistry( + serializationClassInspector); + ClassOutput classOutput = new ClassOutput() { @Override public void write(String name, byte[] data) { @@ -54,18 +84,84 @@ public void write(String name, byte[] data) { } }; + Set serializerCandidates = new HashSet<>(); for (DotName annotationType : ResteasyServerCommonProcessor.METHOD_ANNOTATIONS) { - Collection instances = index.getAnnotations(annotationType); - for (AnnotationInstance instance : instances) { - MethodInfo method = instance.target().asMethod(); - if (ResteasyServerCommonProcessor.isReflectionDeclarationRequiredFor(method.returnType())) { - // TODO generate a serializer + Collection jaxrsMethodInstances = index.getAnnotations(annotationType); + for (AnnotationInstance jaxrsMethodInstance : jaxrsMethodInstances) { + MethodInfo method = jaxrsMethodInstance.target().asMethod(); + Type returnType = method.returnType(); + if (!ResteasyServerCommonProcessor.isReflectionDeclarationRequiredFor(returnType) + || returnType.name().toString().startsWith("java.lang")) { + continue; + } + + if (returnType instanceof ClassType) { + serializerCandidates.add(returnType.asClassType()); + continue; + } + + // don't generate serializers for collection types since it would override the default ones + // we do however want to generate serializers for types that are captured by collections or Maps + if (CollectionUtil.isCollection(returnType.name())) { + Type genericType = CollectionUtil.getGenericType(returnType); + if (genericType instanceof ClassType) { + serializerCandidates.add(genericType.asClassType()); + } } } } - generateJsonbContextResolver(classOutput); - jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(CONTEXT_RESOLVER)); + List generatedSerializers = new ArrayList<>(); + for (ClassType type : serializerCandidates) { + String generatedSerializerClassName = generateSerializerForClassType(type, + typeSerializerGeneratorRegistry, + classOutput); + if (generatedSerializerClassName != null) { + generatedSerializers.add(generatedSerializerClassName); + } + } + + AdditionalClassGenerator additionalClassGenerator = new AdditionalClassGenerator(jsonbConfig); + additionalClassGenerator.generateDefaultLocaleProvider(classOutput); + additionalClassGenerator.generateJsonbDefaultJsonbDateFormatterProvider(classOutput); + additionalClassGenerator.generateJsonbContextResolver(classOutput, generatedSerializers); + + jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(AdditionalClassGenerator.QUARKUS_CONTEXT_RESOLVER)); + + // ensure that the default locale is read at runtime + runtimeClasses.produce(new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_DEFAULT_LOCALE_PROVIDER)); + runtimeClasses.produce(new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_DEFAULT_DATE_FORMATTER_PROVIDER)); + } + + private boolean hasCustomContextResolverBeenDeclared(IndexView index) { + for (ClassInfo contextResolver : index.getAllKnownImplementors(DotNames.CONTEXT_RESOLVER)) { + if (contextResolver.classAnnotation(DotName.createSimple(Provider.class.getName())) == null) { + continue; + } + + for (Type interfacesType : contextResolver.interfaceTypes()) { + if (!DotNames.CONTEXT_RESOLVER.equals(interfacesType.name())) { + continue; + } + + // make sure we are only dealing with implementations that have set the generic type of ContextResolver + if (!(interfacesType instanceof ParameterizedType)) { + continue; + } + + List contextResolverGenericArguments = interfacesType.asParameterizedType().arguments(); + if (contextResolverGenericArguments.size() != 1) { + continue; // shouldn't ever happen + } + + Type firstGenericType = contextResolverGenericArguments.get(0); + if ((firstGenericType instanceof ClassType) && + firstGenericType.asClassType().name().equals(DotName.createSimple(Jsonb.class.getName()))) { + return true; + } + } + } + return false; } private void validateConfiguration() { @@ -75,89 +171,57 @@ private void validateConfiguration() { } } - private void generateJsonbContextResolver(ClassOutput classOutput) { + /** + * @return The full name of the generated class or null if a serializer could not be generated + */ + private String generateSerializerForClassType(ClassType classType, TypeSerializerGeneratorRegistry registry, + ClassOutput classOutput) { + if (!registry.getObjectSerializer().supports(classType, registry)) { + return null; + } + + DotName classDotName = classType.name(); + String generatedSerializerName = "io.quarkus.jsonb.serializers." + classDotName.withoutPackagePrefix() + "Serializer"; try (ClassCreator cc = ClassCreator.builder() - .classOutput(classOutput).className(CONTEXT_RESOLVER) - .interfaces(ContextResolver.class) - .signature("Ljava/lang/Object;Ljavax/ws/rs/ext/ContextResolver;") + .classOutput(classOutput).className(generatedSerializerName) + .interfaces(JsonbSerializer.class) + .signature(String.format("Ljava/lang/Object;Ljavax/json/bind/serializer/JsonbSerializer;", + classDotName.toString()).replace('.', '/')) .build()) { - cc.addAnnotation(Provider.class); + // actual implementation of serialize method + try (MethodCreator serialize = cc.getMethodCreator("serialize", "void", classDotName.toString(), + JsonGenerator.class.getName(), SerializationContext.class.getName())) { + ResultHandle object = serialize.getMethodParam(0); + ResultHandle jsonGenerator = serialize.getMethodParam(1); - try (MethodCreator getContext = cc.getMethodCreator("getContext", Jsonb.class, Class.class)) { - final Class jsonbConfigClass = javax.json.bind.JsonbConfig.class; + // delegate to object serializer + registry.getObjectSerializer().generate( + new TypeSerializerGenerator.GenerateContext(classType, serialize, jsonGenerator, object, registry, + getGlobalConfig(), false, null)); - //create the JsonbConfig object - MethodDescriptor configCtor = MethodDescriptor.ofConstructor(jsonbConfigClass); - ResultHandle config = getContext.newInstance(configCtor); - - //handle locale - ResultHandle locale = null; - if (!jsonbConfig.locale.isEmpty()) { - locale = getContext.invokeStaticMethod( - MethodDescriptor.ofMethod(Locale.class, "forLanguageTag", Locale.class, String.class), - getContext.load(jsonbConfig.locale)); - getContext.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "withLocale", jsonbConfigClass, Locale.class), - config, locale); - } - - // handle date format - if (!jsonbConfig.dateFormat.isEmpty()) { - getContext.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "withDateFormat", jsonbConfigClass, String.class, - Locale.class), - config, - getContext.load(jsonbConfig.dateFormat), - locale != null ? locale : getContext.loadNull()); - } - - // handle serializeNullValues - getContext.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "withNullValues", jsonbConfigClass, Boolean.class), - config, - getContext.invokeStaticMethod( - MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), - getContext.load(jsonbConfig.serializeNullValues))); - - // handle propertyOrderStrategy - getContext.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "withPropertyOrderStrategy", jsonbConfigClass, - String.class), - config, getContext.load(jsonbConfig.propertyOrderStrategy.toUpperCase())); - - // handle encoding - if (!jsonbConfig.encoding.isEmpty()) { - getContext.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "withEncoding", jsonbConfigClass, - String.class), - config, getContext.load(jsonbConfig.encoding)); - } - - // handle failOnUnknownProperties - getContext.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "setProperty", jsonbConfigClass, String.class, - Object.class), - config, - getContext.load(YassonProperties.FAIL_ON_UNKNOWN_PROPERTIES), - getContext.invokeStaticMethod( - MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), - getContext.load(jsonbConfig.failOnUnknownProperties))); - - //create Jsonb from JsonbBuilder#create using the previously created config - ResultHandle result = getContext.invokeStaticMethod( - MethodDescriptor.ofMethod(JsonbBuilder.class, "create", Jsonb.class, jsonbConfigClass), config); - getContext.returnValue(result); + serialize.returnValue(null); } - try (MethodCreator bridgeGetContext = cc.getMethodCreator("getContext", Object.class, Class.class)) { - MethodDescriptor getContext = MethodDescriptor.ofMethod(CONTEXT_RESOLVER, "getContext", "javax.json.bind.Jsonb", - "java.lang.Class"); - ResultHandle result = bridgeGetContext.invokeVirtualMethod(getContext, bridgeGetContext.getThis(), - bridgeGetContext.getMethodParam(0)); - bridgeGetContext.returnValue(result); + // bridge method + try (MethodCreator bridgeSerialize = cc.getMethodCreator("serialize", "void", Object.class, JsonGenerator.class, + SerializationContext.class)) { + MethodDescriptor serialize = MethodDescriptor.ofMethod(generatedSerializerName, "serialize", "void", + classDotName.toString(), + JsonGenerator.class.getName(), SerializationContext.class.getName()); + ResultHandle castedObject = bridgeSerialize.checkCast(bridgeSerialize.getMethodParam(0), + classDotName.toString()); + bridgeSerialize.invokeVirtualMethod(serialize, bridgeSerialize.getThis(), + castedObject, bridgeSerialize.getMethodParam(1), bridgeSerialize.getMethodParam(2)); + bridgeSerialize.returnValue(null); } } + + return generatedSerializerName; } + private GlobalSerializationConfig getGlobalConfig() { + return new GlobalSerializationConfig( + jsonbConfig.locale, jsonbConfig.dateFormat, jsonbConfig.serializeNullValues, jsonbConfig.propertyOrderStrategy); + } } diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java new file mode 100644 index 0000000000000..d79762893f401 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java @@ -0,0 +1,254 @@ +package io.quarkus.resteasy.jsonb.deployment; + +import java.lang.reflect.Modifier; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.Type; + +public class SerializationClassInspector { + + private final Map classInspectionResultMap = new HashMap<>(); + + private final IndexView index; + + public SerializationClassInspector(IndexView index) { + this.index = index; + } + + public Result inspect(DotName classDotName) { + ClassInfo classInfo = index.getClassByName(classDotName); + if (classInfo == null) { + return SerializationClassInspector.Result.notPossible(classInfo); + } + + if (!Modifier.isPublic(classInfo.flags())) { + return SerializationClassInspector.Result.notPossible(classInfo); + } + + if (Modifier.isInterface(classInfo.flags()) || !index.getAllKnownSubclasses(classDotName).isEmpty()) { + // if the class is an interface or is subclassed we ignore it because json-b + // adds all the properties of the implementation or subclasses (which we can't know) + // TODO investigate if we could relax these constraints by checking if there + // there are no implementations or subclasses that contain properties other than those + // of the interface or class + return SerializationClassInspector.Result.notPossible(classInfo); + } + + if (classInspectionResultMap.containsKey(classInfo.name())) { + return classInspectionResultMap.get(classInfo.name()); + } + + if (!DotNames.OBJECT.equals(classInfo.superName())) { + // for now don't handle classes with super types other than object, too many corner cases + return SerializationClassInspector.Result.notPossible(classInfo); + } + + if (JandexUtil.containsInterfacesWithDefaultMethods(classInfo, index)) { + // for now don't handle default methods either + return SerializationClassInspector.Result.notPossible(classInfo); + } + + Map effectiveClassAnnotations = JandexUtil.getEffectiveClassAnnotations(classInfo.name(), + index); + + if (effectiveClassAnnotations.containsKey(DotNames.JSONB_TYPE_SERIALIZER)) { + // we don't need to do anything since the type already has a serializer + return SerializationClassInspector.Result.notPossible(classInfo); + } + if (effectiveClassAnnotations.containsKey(DotNames.JSONB_TYPE_ADAPTER)) { + // for now we don't handle adapters at all + return SerializationClassInspector.Result.notPossible(classInfo); + } + if (effectiveClassAnnotations.containsKey(DotNames.JSONB_VISIBILITY)) { + // for now we don't handle visibility + return SerializationClassInspector.Result.notPossible(classInfo); + } + + Map getters = PropertyUtil.getGetterMethods(classInfo); + if (removeTransientGetters(getters)) { + return SerializationClassInspector.Result.notPossible(classInfo); + } + for (MethodInfo methodInfo : getters.keySet()) { + // currently we don't support generating serializers that contains items of the same class + if (hasReferenceToClassType(methodInfo.returnType(), classDotName)) { + return Result.notPossible(classInfo); + } + } + + List fields = PropertyUtil.getPublicFieldsWithoutGetters(classInfo, getters.keySet()); + if (removeTransientFields(fields)) { + return SerializationClassInspector.Result.notPossible(classInfo); + } + for (FieldInfo field : fields) { + if (hasReferenceToClassType(field.type(), classDotName)) { + return Result.notPossible(classInfo); + } + } + + SerializationClassInspector.Result result = SerializationClassInspector.Result.possible(classInfo, + effectiveClassAnnotations, getters, fields); + classInspectionResultMap.put(classInfo.name(), result); + return result; + } + + /** + * Removes fields annotated with @JsonbTransient + * + * @return true if the serializer can't be generated + */ + private boolean removeTransientFields(List fields) { + // TODO handle @JsonbTransient meta-annotations + + Iterator fieldsIterator = fields.iterator(); + while (fieldsIterator.hasNext()) { + FieldInfo next = fieldsIterator.next(); + + if (next.hasAnnotation(DotNames.JSONB_TRANSIENT)) { + int jsonbAnnotationCount = 0; + for (AnnotationInstance annotation : next.annotations()) { + if (annotation.name().toString().contains("json.bind")) { + jsonbAnnotationCount++; + } + } + if (jsonbAnnotationCount > 1) { + // we bail out and let jsonb handle this case at runtime (which will end up throwing an exception) + return true; + } else { + // the field was annotated with the @JsonbTransient so we ignore it + fieldsIterator.remove(); + } + } + } + return false; + } + + /** + * Removes getters annotated with @JsonbTransient + * + * @return true if the serializer can't be generated + */ + private boolean removeTransientGetters(Map getters) { + // TODO handle @JsonbTransient meta-annotations + + Iterator> iterator = getters.entrySet().iterator(); + while (iterator.hasNext()) { + Map.Entry next = iterator.next(); + + if (next.getKey().hasAnnotation(DotNames.JSONB_TRANSIENT)) { + int jsonbAnnotationCount = 0; + for (AnnotationInstance annotation : next.getKey().annotations()) { + if (annotation.target().kind() != AnnotationTarget.Kind.METHOD) { // we only care about the annotations on the method itself + continue; + } + if (annotation.name().toString().contains("json.bind")) { + jsonbAnnotationCount++; + } + } + if (jsonbAnnotationCount > 1) { + // we bail out and let jsonb handle this case at runtime (which will end up throwing an exception) + return true; + } else { + // the field was annotated with the @JsonbTransient so we ignore it + iterator.remove(); + } + } + if (next.getValue() != null && next.getValue().hasAnnotation(DotNames.JSONB_TRANSIENT)) { + int jsonbAnnotationCount = 0; + for (AnnotationInstance annotation : next.getValue().annotations()) { + if (annotation.name().toString().contains("json.bind")) { + jsonbAnnotationCount++; + } + } + if (jsonbAnnotationCount > 1) { + // we bail out and let jsonb handle this case at runtime (which will end up throwing an exception) + return true; + } else { + // the field was annotated with the @JsonbTransient so we ignore it + iterator.remove(); + } + } + } + return false; + } + + // check if type is the same as the class type or is a generic type that references it + private boolean hasReferenceToClassType(Type type, DotName classDotName) { + if (type.name().equals(classDotName)) { + // we don't support serializing types that contain serializable items of the same class + return true; + } else if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = type.asParameterizedType(); + for (Type argumentType : parameterizedType.arguments()) { + if (hasReferenceToClassType(argumentType, classDotName)) { + return true; + } + } + } + return false; + } + + public IndexView getIndex() { + return index; + } + + public static class Result { + private final ClassInfo classInfo; + private final boolean isPossible; + private final Map effectiveClassAnnotations; + private final Map getters; + private final Collection visibleFieldsWithoutGetters; + + private Result(ClassInfo classInfo, boolean isPossible, + Map effectiveClassAnnotations, + Map getters, Collection visibleFieldsWithoutGetters) { + this.classInfo = classInfo; + this.isPossible = isPossible; + this.effectiveClassAnnotations = effectiveClassAnnotations; + this.getters = getters; + this.visibleFieldsWithoutGetters = visibleFieldsWithoutGetters; + } + + public static Result notPossible(ClassInfo classInfo) { + return new Result(classInfo, false, Collections.emptyMap(), Collections.emptyMap(), + Collections.emptyList()); + } + + public static Result possible(ClassInfo classInfo, Map effectiveClassAnnotations, + Map getters, Collection visibleFieldsWithoutGetters) { + return new Result(classInfo, true, effectiveClassAnnotations, getters, visibleFieldsWithoutGetters); + } + + public ClassInfo getClassInfo() { + return classInfo; + } + + public boolean isPossible() { + return isPossible; + } + + public Map getEffectiveClassAnnotations() { + return effectiveClassAnnotations; + } + + public Map getGetters() { + return getters; + } + + public Collection getVisibleFieldsWithoutGetters() { + return visibleFieldsWithoutGetters; + } + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractDatetimeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractDatetimeSerializerGenerator.java new file mode 100644 index 0000000000000..d8c2ec9209e81 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractDatetimeSerializerGenerator.java @@ -0,0 +1,34 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import javax.json.bind.annotation.JsonbDateFormat; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; + +import io.quarkus.resteasy.jsonb.deployment.DotNames; + +public abstract class AbstractDatetimeSerializerGenerator extends AbstractTypeSerializerGenerator { + + protected abstract void doGenerate(GenerateContext context, String format, String locale); + + @Override + protected void generateNotNull(GenerateContext context) { + String format = JsonbDateFormat.DEFAULT_FORMAT; + String locale = null; + if (context.getEffectivePropertyAnnotations().containsKey(DotNames.JSONB_DATE_FORMAT)) { + AnnotationInstance jsonbDateFormatInstance = context.getEffectivePropertyAnnotations() + .get(DotNames.JSONB_DATE_FORMAT); + AnnotationValue formatValue = jsonbDateFormatInstance.value(); + if (formatValue != null) { + format = formatValue.asString(); + } + + AnnotationValue localeValue = jsonbDateFormatInstance.value("locale"); + if (localeValue != null) { + locale = localeValue.asString(); + } + } + + doGenerate(context, format, locale); + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractNumberTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractNumberTypeSerializerGenerator.java new file mode 100644 index 0000000000000..f6978a07fcc92 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractNumberTypeSerializerGenerator.java @@ -0,0 +1,62 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import java.text.DecimalFormat; +import java.text.NumberFormat; +import java.util.Locale; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.deployment.DotNames; + +public abstract class AbstractNumberTypeSerializerGenerator extends AbstractTypeSerializerGenerator { + + protected abstract void generateUnformatted(GenerateContext context); + + @Override + public void generateNotNull(GenerateContext context) { + if (context.getEffectivePropertyAnnotations().containsKey(DotNames.JSONB_NUMBER_FORMAT)) { + AnnotationInstance jsonbNumberFormatInstance = context.getEffectivePropertyAnnotations() + .get(DotNames.JSONB_NUMBER_FORMAT); + + String format = ""; // the default value of the @JsonbTransient annotation + AnnotationValue formatValue = jsonbNumberFormatInstance.value(); + if (formatValue != null) { + format = formatValue.asString(); + } + + String locale = null; + AnnotationValue localeValue = jsonbNumberFormatInstance.value("locale"); + if (localeValue != null) { + locale = localeValue.asString(); + } + + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + + ResultHandle localeHandle = SerializerGeneratorUtil.getLocaleHandle(locale, bytecodeCreator); + ResultHandle numberFormatHandle = bytecodeCreator + .invokeStaticMethod( + MethodDescriptor.ofMethod(NumberFormat.class, "getInstance", NumberFormat.class, Locale.class), + localeHandle); + ResultHandle decimalNumberFormatHandle = bytecodeCreator.checkCast(numberFormatHandle, DecimalFormat.class); + bytecodeCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(DecimalFormat.class, "applyPattern", void.class, String.class), + decimalNumberFormatHandle, bytecodeCreator.load(format)); + ResultHandle formattedValue = bytecodeCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(DecimalFormat.class, "format", String.class, Object.class), + decimalNumberFormatHandle, context.getCurrentItem()); + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "write", JsonGenerator.class, String.class), + context.getJsonGenerator(), + formattedValue); + } else { + generateUnformatted(context); + } + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractTypeSerializerGenerator.java new file mode 100644 index 0000000000000..141febdeca9bc --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractTypeSerializerGenerator.java @@ -0,0 +1,35 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import javax.json.stream.JsonGenerator; + +import io.quarkus.gizmo.BranchResult; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; + +public abstract class AbstractTypeSerializerGenerator implements TypeSerializerGenerator { + + protected abstract void generateNotNull(GenerateContext context); + + @Override + public void generate(GenerateContext context) { + if (context.isNullChecked()) { + // null has already been checked in the previous level (when checking whether to write the key or not) + generateNotNull(context); + } else { + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + BytecodeCreator ifScope = bytecodeCreator.createScope(); + BranchResult currentItemNullBranch = ifScope.ifNull(context.getCurrentItem()); + + BytecodeCreator currentItemNull = currentItemNullBranch.trueBranch(); + currentItemNull.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeNull", JsonGenerator.class), + context.getJsonGenerator()); + currentItemNull.breakScope(ifScope); + + BytecodeCreator currentIemNotNull = currentItemNullBranch.falseBranch(); + generateNotNull(context.changeByteCodeCreator(currentIemNotNull)); + currentIemNotNull.breakScope(ifScope); + } + } + +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/CollectionTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/CollectionTypeSerializerGenerator.java new file mode 100644 index 0000000000000..39adb09e98558 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/CollectionTypeSerializerGenerator.java @@ -0,0 +1,75 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import java.util.Collection; +import java.util.Iterator; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BranchResult; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.deployment.CollectionUtil; + +public class CollectionTypeSerializerGenerator extends AbstractTypeSerializerGenerator { + + @Override + public boolean supports(Type type, TypeSerializerGeneratorRegistry registry) { + if (!CollectionUtil.isCollection(type.name())) { + return false; + } + + Type genericType = CollectionUtil.getGenericType(type); + if (genericType == null) { + return false; + } + return (registry.correspondingTypeSerializer(genericType) != null); + } + + @Override + public void generateNotNull(GenerateContext context) { + Type genericType = CollectionUtil.getGenericType(context.getType()); + if (genericType == null) { + throw new IllegalStateException("Could not generate serializer for collection type " + context.getType()); + } + + TypeSerializerGenerator genericTypeSerializerGenerator = context.getRegistry().correspondingTypeSerializer(genericType); + if (genericTypeSerializerGenerator == null) { + throw new IllegalStateException("Could not generate serializer for generic type " + genericType.name() + + " of collection type" + context.getType().name()); + } + + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + ResultHandle jsonGenerator = context.getJsonGenerator(); + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeStartArray", JsonGenerator.class), + jsonGenerator); + + ResultHandle iterator = bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Collection.class, "iterator", Iterator.class), + context.getCurrentItem()); + + BytecodeCreator loop = bytecodeCreator.createScope(); + + ResultHandle hasNext = loop.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Iterator.class, "hasNext", boolean.class), + iterator); + BranchResult branchResult = loop.ifNonZero(hasNext); + BytecodeCreator hasNextBranch = branchResult.trueBranch(); + BytecodeCreator noNextBranch = branchResult.falseBranch(); + ResultHandle next = hasNextBranch.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Iterator.class, "next", Object.class), + iterator); + genericTypeSerializerGenerator.generate(context.changeItem(hasNextBranch, genericType, next, false)); + hasNextBranch.continueScope(loop); + + noNextBranch.breakScope(loop); + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeEnd", JsonGenerator.class), jsonGenerator); + } + +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/GlobalSerializationConfig.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/GlobalSerializationConfig.java new file mode 100644 index 0000000000000..b612f209a6566 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/GlobalSerializationConfig.java @@ -0,0 +1,35 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import java.util.Optional; + +public class GlobalSerializationConfig { + + private final Optional locale; + private final Optional dateFormat; + private final boolean serializeNullValues; + private final String propertyOrderStrategy; + + public GlobalSerializationConfig(Optional locale, Optional dateFormat, boolean serializeNullValues, + String propertyOrderStrategy) { + this.locale = locale; + this.dateFormat = dateFormat; + this.serializeNullValues = serializeNullValues; + this.propertyOrderStrategy = propertyOrderStrategy; + } + + public Optional getLocale() { + return locale; + } + + public Optional getDateFormat() { + return dateFormat; + } + + public boolean isSerializeNullValues() { + return serializeNullValues; + } + + public String getPropertyOrderStrategy() { + return propertyOrderStrategy; + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/IntegerTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/IntegerTypeSerializerGenerator.java new file mode 100644 index 0000000000000..dee7897b0722b --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/IntegerTypeSerializerGenerator.java @@ -0,0 +1,33 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.deployment.DotNames; + +public class IntegerTypeSerializerGenerator extends AbstractNumberTypeSerializerGenerator { + + @Override + public boolean supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.INTEGER.equals(type.name()); + } + + @Override + public void generateUnformatted(GenerateContext context) { + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + + ResultHandle intValueHandle = bytecodeCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(Integer.class, "intValue", int.class), + context.getCurrentItem()); + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "write", JsonGenerator.class, int.class), + context.getJsonGenerator(), + intValueHandle); + } + +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/LocalDateTimeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/LocalDateTimeSerializerGenerator.java new file mode 100644 index 0000000000000..4b48296fa2cc1 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/LocalDateTimeSerializerGenerator.java @@ -0,0 +1,56 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import java.time.LocalDateTime; +import java.util.Locale; + +import javax.json.bind.annotation.JsonbDateFormat; +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.deployment.DotNames; +import io.quarkus.resteasy.jsonb.runtime.serializers.LocalDateTimeSerializerHelper; + +public class LocalDateTimeSerializerGenerator extends AbstractDatetimeSerializerGenerator { + + @Override + public boolean supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.LOCAL_DATE_TIME.equals(type.name()); + } + + @Override + protected void doGenerate(GenerateContext context, String format, String locale) { + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + + ResultHandle localeHandle = SerializerGeneratorUtil.getLocaleHandle(locale, bytecodeCreator); + ResultHandle stringValueHandle = getStringValueResultHandle(context, format, localeHandle); + + context.getBytecodeCreator().invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "write", JsonGenerator.class, String.class), + context.getJsonGenerator(), + stringValueHandle); + } + + private ResultHandle getStringValueResultHandle(GenerateContext context, String format, ResultHandle localeHandle) { + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + if (JsonbDateFormat.DEFAULT_FORMAT.equals(format)) { + return bytecodeCreator.invokeStaticMethod( + MethodDescriptor.ofMethod(LocalDateTimeSerializerHelper.class, "defaultFormat", String.class, + LocalDateTime.class, Locale.class), + context.getCurrentItem(), localeHandle); + } else if (JsonbDateFormat.TIME_IN_MILLIS.equals(format)) { + return bytecodeCreator.invokeStaticMethod( + MethodDescriptor.ofMethod(LocalDateTimeSerializerHelper.class, "timeInMillisFormat", String.class, + LocalDateTime.class), + context.getCurrentItem()); + } + + return bytecodeCreator.invokeStaticMethod( + MethodDescriptor.ofMethod(LocalDateTimeSerializerHelper.class, "customFormat", String.class, + LocalDateTime.class, String.class, Locale.class), + context.getCurrentItem(), bytecodeCreator.load(format), localeHandle); + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java new file mode 100644 index 0000000000000..5f88c2ceff4e7 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java @@ -0,0 +1,318 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.SortedMap; +import java.util.TreeMap; + +import javax.json.bind.config.PropertyOrderStrategy; +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ArrayType; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.deployment.DotNames; +import io.quarkus.resteasy.jsonb.deployment.PropertyUtil; +import io.quarkus.resteasy.jsonb.deployment.SerializationClassInspector; + +public class ObjectTypeSerializerGenerator extends AbstractTypeSerializerGenerator { + + @Override + public boolean supports(Type type, TypeSerializerGeneratorRegistry registry) { + if (type instanceof ArrayType) { + return false; + } + + if (type.name().toString().startsWith("java")) { + return false; + } + + final SerializationClassInspector.Result inspectionsResult = registry.getInspector().inspect(type.name()); + if (!inspectionsResult.isPossible()) { + return false; + } + + for (MethodInfo getter : inspectionsResult.getGetters().keySet()) { + if (registry.correspondingTypeSerializer(getter.returnType()) == null) { + return false; + } + } + + for (FieldInfo field : inspectionsResult.getVisibleFieldsWithoutGetters()) { + if (registry.correspondingTypeSerializer(field.type()) == null) { + return false; + } + } + + return true; + } + + @Override + protected void generateNotNull(GenerateContext context) { + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + ResultHandle jsonGenerator = context.getJsonGenerator(); + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeStartObject", JsonGenerator.class), jsonGenerator); + + TypeSerializerGeneratorRegistry serializerRegistry = context.getRegistry(); + DotName classDotNate = context.getType().name(); + SerializationClassInspector.Result inspectionResult = serializerRegistry.getInspector() + .inspect(classDotNate); + if (!inspectionResult.isPossible()) { + // should never happen when used property (meaning that supports is called before this method) + throw new IllegalStateException("Could not generate serializer for " + classDotNate); + } + + // instead of generating the bytecode for each property right away, we instead introduce + // a Generator interface that will do the job on demand + // this allows us to add the keys from both getters and fields and have them both sorted + SortedMap> propertyNameToGenerator = PropertyOrderStrategy.REVERSE + .equalsIgnoreCase(context.getGlobalConfig().getPropertyOrderStrategy()) + ? new TreeMap<>(Collections.reverseOrder()) + : new TreeMap<>(); + Map defaultToFinaKeyName = new HashMap<>(); + Map finalToDefaultKeyName = new HashMap<>(); + + for (Map.Entry entry : inspectionResult.getGetters().entrySet()) { + MethodInfo getterMethodInfo = entry.getKey(); + FieldInfo fieldInfo = entry.getValue(); + Type returnType = getterMethodInfo.returnType(); + TypeSerializerGenerator getterTypeSerializerGenerator = serializerRegistry.correspondingTypeSerializer(returnType); + if (getterTypeSerializerGenerator == null) { + throw new IllegalStateException("Could not generate serializer for getter " + getterMethodInfo.name() + + " of type " + classDotNate); + } + + String defaultKeyName = PropertyUtil.toFieldName(getterMethodInfo); + Map effectiveGetterAnnotations = getEffectiveGetterAnnotations(getterMethodInfo, + fieldInfo, serializerRegistry.getInspector()); + String finalKeyName = getFinalKeyName(defaultKeyName, effectiveGetterAnnotations); + + defaultToFinaKeyName.put(defaultKeyName, finalKeyName); + finalToDefaultKeyName.put(finalKeyName, defaultKeyName); + + boolean isNillable = isPropertyNillable(effectiveGetterAnnotations.get(DotNames.JSONB_PROPERTY), + context.getGlobalConfig(), inspectionResult); + + propertyNameToGenerator.put( + finalKeyName, + new GetterGenerator(new GeneratorInput<>( + context, getterMethodInfo, fieldInfo, getterTypeSerializerGenerator, finalKeyName, isNillable))); + } + + // TODO serialize fields + + // TODO handle @JsonbPropertyOrder meta-annotations + if (inspectionResult.getEffectiveClassAnnotations().containsKey(DotNames.JSONB_PROPERTY_ORDER)) { + LinkedHashSet customOrder = new LinkedHashSet<>(); + AnnotationInstance annotationInstance = inspectionResult.getEffectiveClassAnnotations() + .get(DotNames.JSONB_PROPERTY_ORDER); + AnnotationValue value = annotationInstance.value(); + if (value != null) { + customOrder.addAll(Arrays.asList(value.asStringArray())); + } + + // JSON-B specifies that the values of the @JsonbPropertyOrder annotation are the original java property names + // before any customizations are applied + for (String propertyName : customOrder) { + if (defaultToFinaKeyName.containsKey(propertyName)) { + String finalKeyName = defaultToFinaKeyName.get(propertyName); + if (propertyNameToGenerator.containsKey(finalKeyName)) { + propertyNameToGenerator.get(finalKeyName).generate(); + } + } + } + // go through the properties and serialize the ones that weren't already serialized + for (Map.Entry> entry : propertyNameToGenerator.entrySet()) { + String defaultName = finalToDefaultKeyName.get(entry.getKey()); + if (!customOrder.contains(defaultName)) { + entry.getValue().generate(); + } + } + } else { + // at this point the properties are sorted so we can just generate the bytecode + for (Generator generator : propertyNameToGenerator.values()) { + generator.generate(); + } + } + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeEnd", JsonGenerator.class), jsonGenerator); + } + + private String getFinalKeyName(String defaultKeyName, Map effectiveGetterAnnotations) { + String finalKeyName = defaultKeyName; + if (effectiveGetterAnnotations.containsKey(DotNames.JSONB_PROPERTY)) { + AnnotationInstance instance = effectiveGetterAnnotations.get(DotNames.JSONB_PROPERTY); + AnnotationValue value = instance.value(); + if (value != null) { + String valueStr = value.asString(); + if ((valueStr != null) && !valueStr.isEmpty()) { + finalKeyName = valueStr; + } + } + } + return finalKeyName; + } + + /** + * Determine if the property is nillable. + * Priorities are from highest to lowest: + * - @JsonbProperty on the method + * - @JsonbProperty on the field + * - @JsonbNillable on the class (or anywhere in the class hierarchy if it's not directly on the class) + * - @JsonbNillable on the package + * - global configuration + */ + private boolean isPropertyNillable(AnnotationInstance jsonbPropertyInstance, GlobalSerializationConfig globalConfig, + SerializationClassInspector.Result inspectionResult) { + boolean isNillable = globalConfig.isSerializeNullValues(); // use the global configuration as the default + if (jsonbPropertyInstance != null) { + AnnotationValue value = jsonbPropertyInstance.value("nillable"); + if (value != null) { + // use whatever was specified on the method or field + isNillable = value.asBoolean(); + } + } else if (inspectionResult.getEffectiveClassAnnotations().containsKey(DotNames.JSONB_NILLABLE)) { + isNillable = true; // set the default value of @JsonbNillable since it was used + AnnotationValue value = inspectionResult.getEffectiveClassAnnotations().get(DotNames.JSONB_NILLABLE).value(); + if (value != null) { + isNillable = value.asBoolean(); + } + } + return isNillable; + } + + private static void writeKey(BytecodeCreator bytecodeCreator, ResultHandle jsonGenerator, String finalNameOfKey) { + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeKey", JsonGenerator.class, String.class), + jsonGenerator, + bytecodeCreator.load(finalNameOfKey)); + } + + private static Map getEffectiveGetterAnnotations(MethodInfo getterMethodInfo, + FieldInfo fieldInfo, SerializationClassInspector inspector) { + Map result = new HashMap<>(); + for (AnnotationInstance annotationInstance : getterMethodInfo.annotations()) { + result.put(annotationInstance.name(), annotationInstance); + } + + if (fieldInfo != null) { + for (AnnotationInstance annotationInstance : fieldInfo.annotations()) { + if (!result.containsKey(annotationInstance.name())) { + result.put(annotationInstance.name(), annotationInstance); + } + } + } + + Map effectiveClassAnnotations = inspector.inspect(getterMethodInfo.declaringClass().name()) + .getEffectiveClassAnnotations(); + for (DotName classAnnotationDotName : effectiveClassAnnotations.keySet()) { + if (!result.containsKey(classAnnotationDotName)) { + result.put(classAnnotationDotName, effectiveClassAnnotations.get(classAnnotationDotName)); + } + } + + return result; + } + + private static class GeneratorInput { + private final GenerateContext context; + private final T instanceInfo; + private final AnnotationTarget associatedInstanceInfo; + private final TypeSerializerGenerator typeSerializerGenerator; + private final String finalKeyName; + private final boolean isNillable; + + GeneratorInput(GenerateContext context, T instanceInfo, AnnotationTarget associatedInstanceInfo, + TypeSerializerGenerator typeSerializerGenerator, String finalKeyName, boolean isNillable) { + this.context = context; + this.instanceInfo = instanceInfo; + this.associatedInstanceInfo = associatedInstanceInfo; + this.typeSerializerGenerator = typeSerializerGenerator; + this.finalKeyName = finalKeyName; + this.isNillable = isNillable; + } + + GenerateContext getContext() { + return context; + } + + T getInstanceInfo() { + return instanceInfo; + } + + AnnotationTarget getAssociatedInstanceInfo() { + return associatedInstanceInfo; + } + + TypeSerializerGenerator getTypeSerializerGenerator() { + return typeSerializerGenerator; + } + + String getFinalKeyName() { + return finalKeyName; + } + + boolean isNillable() { + return isNillable; + } + } + + private interface Generator> { + void generate(); + } + + private static class GetterGenerator implements Generator> { + + private GeneratorInput input; + + public GetterGenerator(GeneratorInput input) { + this.input = input; + } + + @Override + public void generate() { + BytecodeCreator bytecodeCreator = input.getContext().getBytecodeCreator(); + ResultHandle jsonGenerator = input.getContext().getJsonGenerator(); + DotName classDotNate = input.getContext().getType().name(); + MethodInfo getterMethodInfo = input.getInstanceInfo(); + Type returnType = getterMethodInfo.returnType(); + TypeSerializerGenerator getterTypeSerializerGenerator = input.getTypeSerializerGenerator(); + + // ResultHandle of the getter method + ResultHandle getter = bytecodeCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(classDotNate.toString(), getterMethodInfo.name(), + returnType.name().toString()), + input.getContext().getCurrentItem()); + + Map effectivePropertyAnnotations = getEffectiveGetterAnnotations(getterMethodInfo, + (FieldInfo) input.getAssociatedInstanceInfo(), input.getContext().getRegistry().getInspector()); + if (input.isNillable()) { + writeKey(bytecodeCreator, jsonGenerator, input.getFinalKeyName()); + getterTypeSerializerGenerator.generate(input.getContext().changeItem( + bytecodeCreator, returnType, getter, false, effectivePropertyAnnotations)); + } else { + // in this case we only write the property and value if the value is not null + BytecodeCreator getterNotNull = bytecodeCreator.ifNull(getter).falseBranch(); + + writeKey(getterNotNull, jsonGenerator, input.getFinalKeyName()); + getterTypeSerializerGenerator.generate(input.getContext().changeItem(getterNotNull, + returnType, getter, true, effectivePropertyAnnotations)); + } + } + } + +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/SerializerGeneratorUtil.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/SerializerGeneratorUtil.java new file mode 100644 index 0000000000000..39bbba9be3b05 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/SerializerGeneratorUtil.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import java.util.Locale; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.deployment.AdditionalClassGenerator; + +final class SerializerGeneratorUtil { + + private SerializerGeneratorUtil() { + } + + static ResultHandle getLocaleHandle(String locale, BytecodeCreator bytecodeCreator) { + if (locale == null) { + // just use the default locale + return bytecodeCreator + .invokeStaticMethod(MethodDescriptor.ofMethod(AdditionalClassGenerator.QUARKUS_DEFAULT_LOCALE_PROVIDER, + "get", Locale.class)); + } + + return bytecodeCreator.invokeStaticMethod( + MethodDescriptor.ofMethod(Locale.class, "forLanguageTag", Locale.class, String.class), + bytecodeCreator.load(locale)); + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/StringTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/StringTypeSerializerGenerator.java new file mode 100644 index 0000000000000..7ce3b1b6b1973 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/StringTypeSerializerGenerator.java @@ -0,0 +1,25 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.resteasy.jsonb.deployment.DotNames; + +public class StringTypeSerializerGenerator extends AbstractTypeSerializerGenerator { + + @Override + public boolean supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.STRING.equals(type.name()); + } + + @Override + protected void generateNotNull(GenerateContext context) { + context.getBytecodeCreator().invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "write", JsonGenerator.class, String.class), + context.getJsonGenerator(), + context.getCurrentItem()); + } + +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java new file mode 100644 index 0000000000000..a1d3d9bf30dc1 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java @@ -0,0 +1,95 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import java.util.Map; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.ResultHandle; + +public interface TypeSerializerGenerator { + + boolean supports(Type type, TypeSerializerGeneratorRegistry registry); + + void generate(GenerateContext context); + + class GenerateContext { + private final Type type; + private final BytecodeCreator bytecodeCreator; + private final ResultHandle jsonGenerator; + private final ResultHandle currentItem; + private final TypeSerializerGeneratorRegistry registry; + private final GlobalSerializationConfig globalConfig; + + // only used when the context is built for a property + private final boolean nullChecked; + private final Map effectivePropertyAnnotations; + + public GenerateContext(Type type, BytecodeCreator bytecodeCreator, ResultHandle jsonGenerator, + ResultHandle currentItem, + TypeSerializerGeneratorRegistry registry, GlobalSerializationConfig globalConfig, + boolean nullChecked, Map effectivePropertyAnnotations) { + this.type = type; + this.bytecodeCreator = bytecodeCreator; + this.jsonGenerator = jsonGenerator; + this.currentItem = currentItem; + this.registry = registry; + this.globalConfig = globalConfig; + this.nullChecked = nullChecked; + this.effectivePropertyAnnotations = effectivePropertyAnnotations; + } + + Type getType() { + return type; + } + + BytecodeCreator getBytecodeCreator() { + return bytecodeCreator; + } + + ResultHandle getJsonGenerator() { + return jsonGenerator; + } + + ResultHandle getCurrentItem() { + return currentItem; + } + + TypeSerializerGeneratorRegistry getRegistry() { + return registry; + } + + GlobalSerializationConfig getGlobalConfig() { + return globalConfig; + } + + boolean isNullChecked() { + return nullChecked; + } + + Map getEffectivePropertyAnnotations() { + return effectivePropertyAnnotations; + } + + GenerateContext changeItem(BytecodeCreator newBytecodeCreator, Type newType, ResultHandle newCurrentItem, + boolean newNullChecked) { + return new GenerateContext(newType, newBytecodeCreator, jsonGenerator, newCurrentItem, registry, + globalConfig, + newNullChecked, effectivePropertyAnnotations); + } + + GenerateContext changeItem(BytecodeCreator newBytecodeCreator, Type newType, + ResultHandle newCurrentItem, boolean newNullChecked, + Map newEffectivePropertyAnnotations) { + return new GenerateContext(newType, newBytecodeCreator, jsonGenerator, newCurrentItem, registry, + globalConfig, newNullChecked, newEffectivePropertyAnnotations); + } + + GenerateContext changeByteCodeCreator(BytecodeCreator newBytecodeCreator) { + return new GenerateContext(type, newBytecodeCreator, jsonGenerator, currentItem, registry, + globalConfig, nullChecked, effectivePropertyAnnotations); + } + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java new file mode 100644 index 0000000000000..468baea7336c4 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java @@ -0,0 +1,45 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import java.util.Arrays; +import java.util.List; + +import org.jboss.jandex.IndexView; +import org.jboss.jandex.Type; + +import io.quarkus.resteasy.jsonb.deployment.SerializationClassInspector; + +public final class TypeSerializerGeneratorRegistry { + + private final TypeSerializerGenerator objectSerializer = new ObjectTypeSerializerGenerator(); + private final List typeSerializerGenerators = Arrays.asList(new StringTypeSerializerGenerator(), + new IntegerTypeSerializerGenerator(), new LocalDateTimeSerializerGenerator(), + objectSerializer, + new CollectionTypeSerializerGenerator()); + + private final SerializationClassInspector inspector; + + public TypeSerializerGeneratorRegistry(SerializationClassInspector inspector) { + this.inspector = inspector; + } + + public TypeSerializerGenerator correspondingTypeSerializer(Type type) { + for (TypeSerializerGenerator typeSerializerGenerator : typeSerializerGenerators) { + if (typeSerializerGenerator.supports(type, this)) { + return typeSerializerGenerator; + } + } + return null; + } + + public IndexView getIndex() { + return inspector.getIndex(); + } + + public SerializationClassInspector getInspector() { + return inspector; + } + + public TypeSerializerGenerator getObjectSerializer() { + return objectSerializer; + } +} diff --git a/extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/LocalDateTimeSerializerHelper.java b/extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/LocalDateTimeSerializerHelper.java new file mode 100644 index 0000000000000..6378a175175da --- /dev/null +++ b/extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/LocalDateTimeSerializerHelper.java @@ -0,0 +1,26 @@ +package io.quarkus.resteasy.jsonb.runtime.serializers; + +import static org.eclipse.yasson.internal.serializer.AbstractDateTimeSerializer.UTC; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeFormatterBuilder; +import java.util.Locale; + +public final class LocalDateTimeSerializerHelper { + + private LocalDateTimeSerializerHelper() { + } + + public static String defaultFormat(LocalDateTime localDateTime, Locale locale) { + return DateTimeFormatter.ISO_LOCAL_DATE_TIME.withLocale(locale).format(localDateTime); + } + + public static String timeInMillisFormat(LocalDateTime localDateTime) { + return String.valueOf(localDateTime.atZone(UTC).toInstant().toEpochMilli()); + } + + public static String customFormat(LocalDateTime localDateTime, String format, Locale locale) { + return new DateTimeFormatterBuilder().appendPattern(format).toFormatter(locale).withZone(UTC).format(localDateTime); + } +} From e2a1a5af81e14693657ab0fa6dca1d9595e86d2f Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Mon, 15 Jul 2019 22:30:44 +0300 Subject: [PATCH 03/20] Add property that controls jsonb configuration and serializer generation --- .../resteasy/jsonb/deployment/JsonbConfig.java | 8 ++++++++ .../jsonb/deployment/ResteasyJsonbProcessor.java | 15 ++++++++++----- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java index 813dd31295326..53be33756cffe 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java @@ -20,6 +20,14 @@ public class JsonbConfig { new HashSet<>(Arrays.asList(PropertyOrderStrategy.LEXICOGRAPHICAL, PropertyOrderStrategy.ANY, PropertyOrderStrategy.REVERSE))); + /** + * If enabled, Quarkus will a create a JAX-RS resolves that configures JSON-B with the properties + * specified here + * It will also attempt to generate serializers for JAX-RS return types + */ + @ConfigItem(defaultValue = "false") + boolean enabled; + /** * default locale to use */ diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index be971c016e0d5..8269618549d6d 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -12,7 +12,6 @@ import javax.json.stream.JsonGenerator; import javax.ws.rs.ext.Provider; -import io.quarkus.deployment.builditem.substrate.RuntimeInitializedClassBuildItem; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassType; @@ -28,6 +27,7 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.deployment.builditem.substrate.RuntimeInitializedClassBuildItem; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.MethodCreator; @@ -61,11 +61,15 @@ void build(BuildProducer feature) { */ @BuildStep void generateClasses(CombinedIndexBuildItem combinedIndexBuildItem, - BuildProducer generatedClass, - BuildProducer jaxrsProvider, - BuildProducer runtimeClasses) { + BuildProducer generatedClass, + BuildProducer jaxrsProvider, + BuildProducer runtimeClasses) { IndexView index = combinedIndexBuildItem.getIndex(); + if (!jsonbConfig.enabled) { + return; + } + // if the user has declared a custom ContextResolver for Jsonb, we don't generate anything if (hasCustomContextResolverBeenDeclared(index)) { return; @@ -130,7 +134,8 @@ public void write(String name, byte[] data) { // ensure that the default locale is read at runtime runtimeClasses.produce(new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_DEFAULT_LOCALE_PROVIDER)); - runtimeClasses.produce(new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_DEFAULT_DATE_FORMATTER_PROVIDER)); + runtimeClasses.produce( + new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_DEFAULT_DATE_FORMATTER_PROVIDER)); } private boolean hasCustomContextResolverBeenDeclared(IndexView index) { From f8ac5637c024993d5efae2d51f5f7c2f1b61ad86 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 18 Jul 2019 13:50:47 +0300 Subject: [PATCH 04/20] Ensure that the same Jsonb instance is used from ContextResolver --- .../deployment/AdditionalClassGenerator.java | 73 ++++++++++--------- .../deployment/ResteasyJsonbProcessor.java | 13 +++- 2 files changed, 47 insertions(+), 39 deletions(-) diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java index 65abcf99c79f5..05c9e175f8cf8 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java @@ -42,8 +42,7 @@ void generateDefaultLocaleProvider(ClassOutput classOutput) { .setModifiers(Modifier.FINAL | Modifier.STATIC | Modifier.PRIVATE) .getFieldDescriptor(); - try (MethodCreator clinit = cc.getMethodCreator("", void.class)) { - clinit.setModifiers(Modifier.STATIC); + try (MethodCreator clinit = cc.getMethodCreator("", void.class).setModifiers(Modifier.STATIC)) { ResultHandle locale; if (jsonbConfig.locale.isPresent()) { @@ -76,8 +75,7 @@ void generateJsonbDefaultJsonbDateFormatterProvider(ClassOutput classOutput) { .setModifiers(Modifier.FINAL | Modifier.STATIC | Modifier.PRIVATE) .getFieldDescriptor(); - try (MethodCreator clinit = cc.getMethodCreator("", void.class)) { - clinit.setModifiers(Modifier.STATIC); + try (MethodCreator clinit = cc.getMethodCreator("", void.class).setModifiers(Modifier.STATIC)) { ResultHandle locale = clinit.invokeStaticMethod( MethodDescriptor.ofMethod(QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", Locale.class)); @@ -109,93 +107,98 @@ void generateJsonbContextResolver(ClassOutput classOutput, List generate cc.addAnnotation(Provider.class); - try (MethodCreator getContext = cc.getMethodCreator("getContext", Jsonb.class, Class.class)) { + FieldDescriptor instance = cc.getFieldCreator("INSTANCE", Jsonb.class) + .setModifiers(Modifier.FINAL | Modifier.STATIC | Modifier.PRIVATE) + .getFieldDescriptor(); + + try (MethodCreator clinit = cc.getMethodCreator("", void.class).setModifiers(Modifier.STATIC)) { final Class jsonbConfigClass = javax.json.bind.JsonbConfig.class; //create the JsonbConfig object MethodDescriptor configCtor = MethodDescriptor.ofConstructor(jsonbConfigClass); - ResultHandle config = getContext.newInstance(configCtor); + ResultHandle config = clinit.newInstance(configCtor); //handle locale ResultHandle locale = null; if (jsonbConfig.locale.isPresent()) { - locale = getContext.invokeStaticMethod( + locale = clinit.invokeStaticMethod( MethodDescriptor.ofMethod(QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", Locale.class)); - getContext.invokeVirtualMethod( + clinit.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withLocale", jsonbConfigClass, Locale.class), config, locale); } // handle date format if (jsonbConfig.dateFormat.isPresent()) { - getContext.invokeVirtualMethod( + clinit.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withDateFormat", jsonbConfigClass, String.class, Locale.class), config, - getContext.load(jsonbConfig.dateFormat.get()), - locale != null ? locale : getContext.loadNull()); + clinit.load(jsonbConfig.dateFormat.get()), + locale != null ? locale : clinit.loadNull()); } // handle serializeNullValues - getContext.invokeVirtualMethod( + clinit.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withNullValues", jsonbConfigClass, Boolean.class), config, - getContext.invokeStaticMethod( + clinit.invokeStaticMethod( MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), - getContext.load(jsonbConfig.serializeNullValues))); + clinit.load(jsonbConfig.serializeNullValues))); // handle propertyOrderStrategy - getContext.invokeVirtualMethod( + clinit.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withPropertyOrderStrategy", jsonbConfigClass, String.class), - config, getContext.load(jsonbConfig.propertyOrderStrategy.toUpperCase())); + config, clinit.load(jsonbConfig.propertyOrderStrategy.toUpperCase())); // handle encoding if (jsonbConfig.encoding.isPresent()) { - getContext.invokeVirtualMethod( + clinit.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withEncoding", jsonbConfigClass, String.class), - config, getContext.load(jsonbConfig.encoding.get())); + config, clinit.load(jsonbConfig.encoding.get())); } // handle failOnUnknownProperties - getContext.invokeVirtualMethod( + clinit.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "setProperty", jsonbConfigClass, String.class, Object.class), config, - getContext.load(YassonProperties.FAIL_ON_UNKNOWN_PROPERTIES), - getContext.invokeStaticMethod( + clinit.load(YassonProperties.FAIL_ON_UNKNOWN_PROPERTIES), + clinit.invokeStaticMethod( MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), - getContext.load(jsonbConfig.failOnUnknownProperties))); + clinit.load(jsonbConfig.failOnUnknownProperties))); // add generated serializers to config if (!generatedSerializers.isEmpty()) { - ResultHandle serializersArray = getContext.newArray(JsonbSerializer.class, - getContext.load(generatedSerializers.size())); + ResultHandle serializersArray = clinit.newArray(JsonbSerializer.class, + clinit.load(generatedSerializers.size())); for (int i = 0; i < generatedSerializers.size(); i++) { - ResultHandle serializer = getContext + ResultHandle serializer = clinit .newInstance(MethodDescriptor.ofConstructor(generatedSerializers.get(i))); - getContext.writeArrayValue(serializersArray, getContext.load(i), serializer); + clinit.writeArrayValue(serializersArray, clinit.load(i), serializer); } - getContext.invokeVirtualMethod( + clinit.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withSerializers", jsonbConfigClass, JsonbSerializer[].class), config, serializersArray); } //create Jsonb from JsonbBuilder#create using the previously created config - ResultHandle result = getContext.invokeStaticMethod( + ResultHandle jsonb = clinit.invokeStaticMethod( MethodDescriptor.ofMethod(JsonbBuilder.class, "create", Jsonb.class, jsonbConfigClass), config); - getContext.returnValue(result); + + clinit.writeStaticField(instance, jsonb); + clinit.returnValue(null); + } + + try (MethodCreator getContext = cc.getMethodCreator("getContext", Jsonb.class, Class.class)) { + getContext.returnValue(getContext.readStaticField(instance)); } try (MethodCreator bridgeGetContext = cc.getMethodCreator("getContext", Object.class, Class.class)) { - MethodDescriptor getContext = MethodDescriptor.ofMethod(QUARKUS_CONTEXT_RESOLVER, "getContext", - "javax.json.bind.Jsonb", - "java.lang.Class"); - ResultHandle result = bridgeGetContext.invokeVirtualMethod(getContext, bridgeGetContext.getThis(), - bridgeGetContext.getMethodParam(0)); - bridgeGetContext.returnValue(result); + bridgeGetContext.returnValue(bridgeGetContext.readStaticField(instance)); } } } diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index 8269618549d6d..87baed144bd48 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -132,10 +132,15 @@ public void write(String name, byte[] data) { jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(AdditionalClassGenerator.QUARKUS_CONTEXT_RESOLVER)); - // ensure that the default locale is read at runtime - runtimeClasses.produce(new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_DEFAULT_LOCALE_PROVIDER)); - runtimeClasses.produce( - new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_DEFAULT_DATE_FORMATTER_PROVIDER)); + // ensure that the default locale is read at runtime when it's not set in the configuration (meaning the system default is needed) + if (!jsonbConfig.locale.isPresent()) { + runtimeClasses + .produce(new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_DEFAULT_LOCALE_PROVIDER)); + runtimeClasses.produce( + new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_DEFAULT_DATE_FORMATTER_PROVIDER)); + runtimeClasses.produce( + new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_CONTEXT_RESOLVER)); + } } private boolean hasCustomContextResolverBeenDeclared(IndexView index) { From 264ff1208eb528cbb5cd968efd86ddcb04ec02ad Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 18 Jul 2019 18:35:25 +0300 Subject: [PATCH 05/20] Ensure that serializers are used by Yasson without any reflection on Types --- .../deployment/AdditionalClassGenerator.java | 61 +++-- .../deployment/ResteasyJsonbProcessor.java | 9 +- .../serializers/QuarkusJsonbBinding.java | 238 ++++++++++++++++++ .../SimpleContainerSerializerProvider.java | 20 ++ 4 files changed, 310 insertions(+), 18 deletions(-) create mode 100644 extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/QuarkusJsonbBinding.java create mode 100644 extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/SimpleContainerSerializerProvider.java diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java index 05c9e175f8cf8..3181708fd7581 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java @@ -1,17 +1,20 @@ package io.quarkus.resteasy.jsonb.deployment; import java.lang.reflect.Modifier; -import java.util.List; import java.util.Locale; +import java.util.Map; import javax.json.bind.Jsonb; -import javax.json.bind.JsonbBuilder; import javax.json.bind.annotation.JsonbDateFormat; import javax.json.bind.serializer.JsonbSerializer; +import javax.json.spi.JsonProvider; import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.Provider; import org.eclipse.yasson.YassonProperties; +import org.eclipse.yasson.internal.JsonbContext; +import org.eclipse.yasson.internal.MappingContext; +import org.eclipse.yasson.internal.serializer.ContainerSerializerProvider; import org.eclipse.yasson.internal.serializer.JsonbDateFormatter; import io.quarkus.gizmo.ClassCreator; @@ -20,6 +23,8 @@ import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.runtime.serializers.QuarkusJsonbBinding; +import io.quarkus.resteasy.jsonb.runtime.serializers.SimpleContainerSerializerProvider; public class AdditionalClassGenerator { @@ -98,7 +103,7 @@ void generateJsonbDefaultJsonbDateFormatterProvider(ClassOutput classOutput) { } } - void generateJsonbContextResolver(ClassOutput classOutput, List generatedSerializers) { + void generateJsonbContextResolver(ClassOutput classOutput, Map typeToGeneratedSerializers) { try (ClassCreator cc = ClassCreator.builder() .classOutput(classOutput).className(QUARKUS_CONTEXT_RESOLVER) .interfaces(ContextResolver.class) @@ -114,9 +119,18 @@ void generateJsonbContextResolver(ClassOutput classOutput, List generate try (MethodCreator clinit = cc.getMethodCreator("", void.class).setModifiers(Modifier.STATIC)) { final Class jsonbConfigClass = javax.json.bind.JsonbConfig.class; - //create the JsonbConfig object - MethodDescriptor configCtor = MethodDescriptor.ofConstructor(jsonbConfigClass); - ResultHandle config = clinit.newInstance(configCtor); + // create the JsonbConfig object + ResultHandle config = clinit.newInstance(MethodDescriptor.ofConstructor(jsonbConfigClass)); + + // create the jsonbContext object + ResultHandle provider = clinit + .invokeStaticMethod(MethodDescriptor.ofMethod(JsonProvider.class, "provider", JsonProvider.class)); + ResultHandle jsonbContext = clinit.newInstance( + MethodDescriptor.ofConstructor(JsonbContext.class, jsonbConfigClass, JsonProvider.class), + config, provider); + ResultHandle mappingContext = clinit.invokeVirtualMethod( + MethodDescriptor.ofMethod(JsonbContext.class, "getMappingContext", MappingContext.class), + jsonbContext); //handle locale ResultHandle locale = null; @@ -171,13 +185,32 @@ void generateJsonbContextResolver(ClassOutput classOutput, List generate clinit.load(jsonbConfig.failOnUnknownProperties))); // add generated serializers to config - if (!generatedSerializers.isEmpty()) { + if (!typeToGeneratedSerializers.isEmpty()) { ResultHandle serializersArray = clinit.newArray(JsonbSerializer.class, - clinit.load(generatedSerializers.size())); - for (int i = 0; i < generatedSerializers.size(); i++) { + clinit.load(typeToGeneratedSerializers.size())); + int i = 0; + for (Map.Entry entry : typeToGeneratedSerializers.entrySet()) { + ResultHandle serializer = clinit - .newInstance(MethodDescriptor.ofConstructor(generatedSerializers.get(i))); + .newInstance(MethodDescriptor.ofConstructor(entry.getValue())); + + // build up the serializers array that will be passed to JsonbConfig clinit.writeArrayValue(serializersArray, clinit.load(i), serializer); + + ResultHandle clazz = clinit.invokeStaticMethod( + MethodDescriptor.ofMethod(Class.class, "forName", Class.class, String.class), + clinit.load(entry.getKey())); + + // add a ContainerSerializerProvider for the serializer + ResultHandle serializerProvider = clinit.newInstance( + MethodDescriptor.ofConstructor(SimpleContainerSerializerProvider.class, JsonbSerializer.class), + serializer); + clinit.invokeVirtualMethod( + MethodDescriptor.ofMethod(MappingContext.class, "addSerializerProvider", void.class, + Class.class, ContainerSerializerProvider.class), + mappingContext, clazz, serializerProvider); + + i++; } clinit.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withSerializers", jsonbConfigClass, @@ -185,11 +218,11 @@ void generateJsonbContextResolver(ClassOutput classOutput, List generate config, serializersArray); } - //create Jsonb from JsonbBuilder#create using the previously created config - ResultHandle jsonb = clinit.invokeStaticMethod( - MethodDescriptor.ofMethod(JsonbBuilder.class, "create", Jsonb.class, jsonbConfigClass), config); - + // create jsonb from QuarkusJsonbBinding + ResultHandle jsonb = clinit.newInstance( + MethodDescriptor.ofConstructor(QuarkusJsonbBinding.class, JsonbContext.class), jsonbContext); clinit.writeStaticField(instance, jsonb); + clinit.returnValue(null); } diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index 87baed144bd48..a0bf1d2633d32 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -1,9 +1,10 @@ package io.quarkus.resteasy.jsonb.deployment; -import java.util.ArrayList; import java.util.Collection; +import java.util.HashMap; import java.util.HashSet; import java.util.List; +import java.util.Map; import java.util.Set; import javax.json.bind.Jsonb; @@ -115,20 +116,20 @@ public void write(String name, byte[] data) { } } - List generatedSerializers = new ArrayList<>(); + Map typeToGeneratedSerializers = new HashMap<>(); for (ClassType type : serializerCandidates) { String generatedSerializerClassName = generateSerializerForClassType(type, typeSerializerGeneratorRegistry, classOutput); if (generatedSerializerClassName != null) { - generatedSerializers.add(generatedSerializerClassName); + typeToGeneratedSerializers.put(type.name().toString(), generatedSerializerClassName); } } AdditionalClassGenerator additionalClassGenerator = new AdditionalClassGenerator(jsonbConfig); additionalClassGenerator.generateDefaultLocaleProvider(classOutput); additionalClassGenerator.generateJsonbDefaultJsonbDateFormatterProvider(classOutput); - additionalClassGenerator.generateJsonbContextResolver(classOutput, generatedSerializers); + additionalClassGenerator.generateJsonbContextResolver(classOutput, typeToGeneratedSerializers); jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(AdditionalClassGenerator.QUARKUS_CONTEXT_RESOLVER)); diff --git a/extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/QuarkusJsonbBinding.java b/extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/QuarkusJsonbBinding.java new file mode 100644 index 0000000000000..b6004e22e9c06 --- /dev/null +++ b/extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/QuarkusJsonbBinding.java @@ -0,0 +1,238 @@ +package io.quarkus.resteasy.jsonb.runtime.serializers; + +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.StringReader; +import java.io.StringWriter; +import java.io.Writer; +import java.lang.reflect.Type; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import javax.json.JsonStructure; +import javax.json.bind.JsonbConfig; +import javax.json.bind.JsonbException; +import javax.json.stream.JsonGenerator; +import javax.json.stream.JsonParser; + +import org.eclipse.yasson.YassonJsonb; +import org.eclipse.yasson.internal.JsonbContext; +import org.eclipse.yasson.internal.JsonbRiParser; +import org.eclipse.yasson.internal.Marshaller; +import org.eclipse.yasson.internal.Unmarshaller; +import org.eclipse.yasson.internal.jsonstructure.JsonGeneratorToStructureAdapter; +import org.eclipse.yasson.internal.jsonstructure.JsonStructureToParserAdapter; +import org.eclipse.yasson.internal.properties.MessageKeys; +import org.eclipse.yasson.internal.properties.Messages; + +/** + * Used only so we can use a pre-configured JsonbContext + * + * The reason a pre-configured JsonbContext is needed is so we can add our ContainerSerializerProvider + * to JsonbContext's mappingContext. + * + * If we don't do this, Yasson recreates the serializer (which involved doing reflection) + * for every request since the mappingContext does not contain the proper ContainerSerializerProvider classes + * + * So this class is basically the same as JsonBinding but has the jsonbContext passed into it instead + * creating it on it's own + */ +public class QuarkusJsonbBinding implements YassonJsonb { + + private final JsonbContext jsonbContext; + + public QuarkusJsonbBinding(JsonbContext jsonbContext) { + this.jsonbContext = jsonbContext; + } + + private T deserialize(final Type type, final JsonParser parser, final Unmarshaller unmarshaller) { + return unmarshaller.deserialize(type, parser); + } + + @Override + public T fromJson(String str, Class type) throws JsonbException { + final JsonParser parser = new JsonbRiParser(jsonbContext.getJsonProvider().createParser(new StringReader(str))); + final Unmarshaller unmarshaller = new Unmarshaller(jsonbContext); + return deserialize(type, parser, unmarshaller); + } + + @Override + public T fromJson(String str, Type type) throws JsonbException { + JsonParser parser = new JsonbRiParser(jsonbContext.getJsonProvider().createParser(new StringReader(str))); + Unmarshaller unmarshaller = new Unmarshaller(jsonbContext); + return deserialize(type, parser, unmarshaller); + } + + @Override + public T fromJson(Reader reader, Class type) throws JsonbException { + JsonParser parser = new JsonbRiParser(jsonbContext.getJsonProvider().createParser(reader)); + Unmarshaller unmarshaller = new Unmarshaller(jsonbContext); + return deserialize(type, parser, unmarshaller); + } + + @Override + public T fromJson(Reader reader, Type type) throws JsonbException { + JsonParser parser = new JsonbRiParser(jsonbContext.getJsonProvider().createParser(reader)); + Unmarshaller unmarshaller = new Unmarshaller(jsonbContext); + return deserialize(type, parser, unmarshaller); + } + + @Override + public T fromJson(InputStream stream, Class clazz) throws JsonbException { + Unmarshaller unmarshaller = new Unmarshaller(jsonbContext); + return deserialize(clazz, inputStreamParser(stream), unmarshaller); + } + + @Override + public T fromJson(InputStream stream, Type type) throws JsonbException { + Unmarshaller unmarshaller = new Unmarshaller(jsonbContext); + return deserialize(type, inputStreamParser(stream), unmarshaller); + } + + @Override + public T fromJsonStructure(JsonStructure jsonStructure, Class type) throws JsonbException { + JsonParser parser = new JsonbRiParser(new JsonStructureToParserAdapter(jsonStructure)); + return deserialize(type, parser, new Unmarshaller(jsonbContext)); + } + + @Override + public T fromJsonStructure(JsonStructure jsonStructure, Type runtimeType) throws JsonbException { + JsonParser parser = new JsonbRiParser(new JsonStructureToParserAdapter(jsonStructure)); + return deserialize(runtimeType, parser, new Unmarshaller(jsonbContext)); + } + + private JsonParser inputStreamParser(InputStream stream) { + return new JsonbRiParser(jsonbContext.getJsonProvider() + .createParserFactory(createJsonpProperties(jsonbContext.getConfig())) + .createParser(stream, + Charset.forName((String) jsonbContext.getConfig().getProperty(JsonbConfig.ENCODING).orElse("UTF-8")))); + } + + @Override + public String toJson(Object object) throws JsonbException { + StringWriter writer = new StringWriter(); + final JsonGenerator generator = writerGenerator(writer); + new Marshaller(jsonbContext).marshall(object, generator); + return writer.toString(); + } + + @Override + public String toJson(Object object, Type type) throws JsonbException { + StringWriter writer = new StringWriter(); + final JsonGenerator generator = writerGenerator(writer); + new Marshaller(jsonbContext, type).marshall(object, generator); + return writer.toString(); + } + + @Override + public void toJson(Object object, Writer writer) throws JsonbException { + final Marshaller marshaller = new Marshaller(jsonbContext); + marshaller.marshall(object, writerGenerator(writer)); + } + + @Override + public void toJson(Object object, Type type, Writer writer) throws JsonbException { + final Marshaller marshaller = new Marshaller(jsonbContext, type); + marshaller.marshall(object, writerGenerator(writer)); + } + + private JsonGenerator writerGenerator(Writer writer) { + Map factoryProperties = createJsonpProperties(jsonbContext.getConfig()); + if (factoryProperties.isEmpty()) { + return jsonbContext.getJsonProvider().createGenerator(writer); + } + return jsonbContext.getJsonProvider().createGeneratorFactory(factoryProperties).createGenerator(writer); + } + + @Override + public void toJson(Object object, OutputStream stream) throws JsonbException { + final Marshaller marshaller = new Marshaller(jsonbContext); + marshaller.marshall(object, streamGenerator(stream)); + } + + @Override + public void toJson(Object object, Type type, OutputStream stream) throws JsonbException { + final Marshaller marshaller = new Marshaller(jsonbContext, type); + marshaller.marshall(object, streamGenerator(stream)); + } + + @Override + public T fromJson(JsonParser jsonParser, Class type) throws JsonbException { + Unmarshaller unmarshaller = new Unmarshaller(jsonbContext); + return unmarshaller.deserialize(type, new JsonbRiParser(jsonParser)); + } + + @Override + public T fromJson(JsonParser jsonParser, Type runtimeType) throws JsonbException { + Unmarshaller unmarshaller = new Unmarshaller(jsonbContext); + return unmarshaller.deserialize(runtimeType, new JsonbRiParser(jsonParser)); + } + + @Override + public void toJson(Object object, JsonGenerator jsonGenerator) throws JsonbException { + final Marshaller marshaller = new Marshaller(jsonbContext); + marshaller.marshallWithoutClose(object, jsonGenerator); + } + + @Override + public void toJson(Object object, Type runtimeType, JsonGenerator jsonGenerator) throws JsonbException { + final Marshaller marshaller = new Marshaller(jsonbContext, runtimeType); + marshaller.marshallWithoutClose(object, jsonGenerator); + } + + @Override + public JsonStructure toJsonStructure(Object object) throws JsonbException { + JsonGeneratorToStructureAdapter structureGenerator = new JsonGeneratorToStructureAdapter( + jsonbContext.getJsonProvider()); + final Marshaller marshaller = new Marshaller(jsonbContext); + marshaller.marshall(object, structureGenerator); + return structureGenerator.getRootStructure(); + } + + @Override + public JsonStructure toJsonStructure(Object object, Type runtimeType) throws JsonbException { + JsonGeneratorToStructureAdapter structureGenerator = new JsonGeneratorToStructureAdapter( + jsonbContext.getJsonProvider()); + final Marshaller marshaller = new Marshaller(jsonbContext, runtimeType); + marshaller.marshall(object, structureGenerator); + return structureGenerator.getRootStructure(); + } + + private JsonGenerator streamGenerator(OutputStream stream) { + Map factoryProperties = createJsonpProperties(jsonbContext.getConfig()); + final String encoding = (String) jsonbContext.getConfig().getProperty(JsonbConfig.ENCODING).orElse("UTF-8"); + return jsonbContext.getJsonProvider().createGeneratorFactory(factoryProperties).createGenerator(stream, + Charset.forName(encoding)); + } + + @Override + public void close() throws Exception { + jsonbContext.getComponentInstanceCreator().close(); + } + + /** + * Propagates properties from JsonbConfig to JSONP generator / parser factories. + * + * @param jsonbConfig jsonb config + * @return properties for JSONP generator / parser + */ + protected Map createJsonpProperties(JsonbConfig jsonbConfig) { + //JSONP 1.0 actually ignores the value, just checks the key is present. Only set if JsonbConfig.FORMATTING is true. + final Optional property = jsonbConfig.getProperty(JsonbConfig.FORMATTING); + final Map factoryProperties = new HashMap<>(); + if (property.isPresent()) { + final Object value = property.get(); + if (!(value instanceof Boolean)) { + throw new JsonbException(Messages.getMessage(MessageKeys.JSONB_CONFIG_FORMATTING_ILLEGAL_VALUE)); + } + if ((Boolean) value) { + factoryProperties.put(JsonGenerator.PRETTY_PRINTING, Boolean.TRUE); + } + return factoryProperties; + } + return factoryProperties; + } +} diff --git a/extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/SimpleContainerSerializerProvider.java b/extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/SimpleContainerSerializerProvider.java new file mode 100644 index 0000000000000..173955255c748 --- /dev/null +++ b/extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/SimpleContainerSerializerProvider.java @@ -0,0 +1,20 @@ +package io.quarkus.resteasy.jsonb.runtime.serializers; + +import javax.json.bind.serializer.JsonbSerializer; + +import org.eclipse.yasson.internal.model.JsonbPropertyInfo; +import org.eclipse.yasson.internal.serializer.ContainerSerializerProvider; + +public class SimpleContainerSerializerProvider implements ContainerSerializerProvider { + + private final JsonbSerializer serializer; + + public SimpleContainerSerializerProvider(JsonbSerializer serializer) { + this.serializer = serializer; + } + + @Override + public JsonbSerializer provideSerializer(JsonbPropertyInfo propertyInfo) { + return serializer; + } +} From 37f2f64b4504d7c71fab0de9627ba8d043200bd6 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 18 Jul 2019 19:18:21 +0300 Subject: [PATCH 06/20] Remove use of static initializers --- .../deployment/AdditionalClassGenerator.java | 144 ++++++++++-------- .../deployment/ResteasyJsonbProcessor.java | 14 +- 2 files changed, 79 insertions(+), 79 deletions(-) diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java index 3181708fd7581..af06360ce64a6 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java @@ -17,6 +17,8 @@ import org.eclipse.yasson.internal.serializer.ContainerSerializerProvider; import org.eclipse.yasson.internal.serializer.JsonbDateFormatter; +import io.quarkus.gizmo.BranchResult; +import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.FieldDescriptor; @@ -44,29 +46,30 @@ void generateDefaultLocaleProvider(ClassOutput classOutput) { .build()) { FieldDescriptor instance = cc.getFieldCreator("INSTANCE", Locale.class) - .setModifiers(Modifier.FINAL | Modifier.STATIC | Modifier.PRIVATE) + .setModifiers(Modifier.STATIC | Modifier.PRIVATE) .getFieldDescriptor(); - try (MethodCreator clinit = cc.getMethodCreator("", void.class).setModifiers(Modifier.STATIC)) { + try (MethodCreator get = cc.getMethodCreator("get", Locale.class)) { + get.setModifiers(Modifier.STATIC | Modifier.PUBLIC); + + BranchResult branchResult = get.ifNull(get.readStaticField(instance)); + + BytecodeCreator instanceNotNull = branchResult.falseBranch(); + instanceNotNull.returnValue(instanceNotNull.readStaticField(instance)); + BytecodeCreator instanceNull = branchResult.trueBranch(); ResultHandle locale; if (jsonbConfig.locale.isPresent()) { - locale = clinit.invokeStaticMethod( + locale = instanceNull.invokeStaticMethod( MethodDescriptor.ofMethod(Locale.class, "forLanguageTag", Locale.class, String.class), - clinit.load(jsonbConfig.locale.get())); + instanceNull.load(jsonbConfig.locale.get())); } else { - locale = clinit.invokeStaticMethod( + locale = instanceNull.invokeStaticMethod( MethodDescriptor.ofMethod(Locale.class, "getDefault", Locale.class)); } - clinit.writeStaticField(instance, locale); - clinit.returnValue(null); - } - - try (MethodCreator get = cc.getMethodCreator("get", Locale.class)) { - get.setModifiers(Modifier.STATIC | Modifier.PUBLIC); - - get.returnValue(get.readStaticField(instance)); + instanceNull.writeStaticField(instance, locale); + instanceNull.returnValue(locale); } } } @@ -77,28 +80,28 @@ void generateJsonbDefaultJsonbDateFormatterProvider(ClassOutput classOutput) { .build()) { FieldDescriptor instance = cc.getFieldCreator("INSTANCE", JsonbDateFormatter.class) - .setModifiers(Modifier.FINAL | Modifier.STATIC | Modifier.PRIVATE) + .setModifiers(Modifier.STATIC | Modifier.PRIVATE) .getFieldDescriptor(); - try (MethodCreator clinit = cc.getMethodCreator("", void.class).setModifiers(Modifier.STATIC)) { + try (MethodCreator get = cc.getMethodCreator("get", JsonbDateFormatter.class)) { + get.setModifiers(Modifier.STATIC | Modifier.PUBLIC); + + BranchResult branchResult = get.ifNull(get.readStaticField(instance)); + + BytecodeCreator instanceNotNull = branchResult.falseBranch(); + instanceNotNull.returnValue(instanceNotNull.readStaticField(instance)); - ResultHandle locale = clinit.invokeStaticMethod( + BytecodeCreator instanceNull = branchResult.trueBranch(); + ResultHandle locale = instanceNull.invokeStaticMethod( MethodDescriptor.ofMethod(QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", Locale.class)); - ResultHandle format = clinit.load(jsonbConfig.dateFormat.orElse(JsonbDateFormat.DEFAULT_FORMAT)); + ResultHandle format = instanceNull.load(jsonbConfig.dateFormat.orElse(JsonbDateFormat.DEFAULT_FORMAT)); - ResultHandle jsonbDateFormatter = clinit.newInstance( + ResultHandle jsonbDateFormatter = instanceNull.newInstance( MethodDescriptor.ofConstructor(JsonbDateFormatter.class, String.class, String.class), format, locale); - - clinit.writeStaticField(instance, jsonbDateFormatter); - clinit.returnValue(null); - } - - try (MethodCreator get = cc.getMethodCreator("get", JsonbDateFormatter.class)) { - get.setModifiers(Modifier.STATIC | Modifier.PUBLIC); - - get.returnValue(get.readStaticField(instance)); + instanceNull.writeStaticField(instance, jsonbDateFormatter); + instanceNull.returnValue(jsonbDateFormatter); } } } @@ -113,124 +116,133 @@ void generateJsonbContextResolver(ClassOutput classOutput, Map t cc.addAnnotation(Provider.class); FieldDescriptor instance = cc.getFieldCreator("INSTANCE", Jsonb.class) - .setModifiers(Modifier.FINAL | Modifier.STATIC | Modifier.PRIVATE) + .setModifiers(Modifier.STATIC | Modifier.PRIVATE) .getFieldDescriptor(); - try (MethodCreator clinit = cc.getMethodCreator("", void.class).setModifiers(Modifier.STATIC)) { - final Class jsonbConfigClass = javax.json.bind.JsonbConfig.class; + try (MethodCreator getContext = cc.getMethodCreator("getContext", Jsonb.class, Class.class)) { + BranchResult branchResult = getContext.ifNull(getContext.readStaticField(instance)); + + BytecodeCreator instanceNotNull = branchResult.falseBranch(); + instanceNotNull.returnValue(instanceNotNull.readStaticField(instance)); + + BytecodeCreator instanceNull = branchResult.trueBranch(); + + Class jsonbConfigClass = javax.json.bind.JsonbConfig.class; // create the JsonbConfig object - ResultHandle config = clinit.newInstance(MethodDescriptor.ofConstructor(jsonbConfigClass)); + ResultHandle config = instanceNull.newInstance(MethodDescriptor.ofConstructor(jsonbConfigClass)); // create the jsonbContext object - ResultHandle provider = clinit + ResultHandle provider = instanceNull .invokeStaticMethod(MethodDescriptor.ofMethod(JsonProvider.class, "provider", JsonProvider.class)); - ResultHandle jsonbContext = clinit.newInstance( + ResultHandle jsonbContext = instanceNull.newInstance( MethodDescriptor.ofConstructor(JsonbContext.class, jsonbConfigClass, JsonProvider.class), config, provider); - ResultHandle mappingContext = clinit.invokeVirtualMethod( + ResultHandle mappingContext = instanceNull.invokeVirtualMethod( MethodDescriptor.ofMethod(JsonbContext.class, "getMappingContext", MappingContext.class), jsonbContext); //handle locale ResultHandle locale = null; if (jsonbConfig.locale.isPresent()) { - locale = clinit.invokeStaticMethod( + locale = instanceNull.invokeStaticMethod( MethodDescriptor.ofMethod(QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", Locale.class)); - clinit.invokeVirtualMethod( + instanceNull.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withLocale", jsonbConfigClass, Locale.class), config, locale); } // handle date format if (jsonbConfig.dateFormat.isPresent()) { - clinit.invokeVirtualMethod( + instanceNull.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withDateFormat", jsonbConfigClass, String.class, Locale.class), config, - clinit.load(jsonbConfig.dateFormat.get()), - locale != null ? locale : clinit.loadNull()); + instanceNull.load(jsonbConfig.dateFormat.get()), + locale != null ? locale : instanceNull.loadNull()); } // handle serializeNullValues - clinit.invokeVirtualMethod( + instanceNull.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withNullValues", jsonbConfigClass, Boolean.class), config, - clinit.invokeStaticMethod( + instanceNull.invokeStaticMethod( MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), - clinit.load(jsonbConfig.serializeNullValues))); + instanceNull.load(jsonbConfig.serializeNullValues))); // handle propertyOrderStrategy - clinit.invokeVirtualMethod( + instanceNull.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withPropertyOrderStrategy", jsonbConfigClass, String.class), - config, clinit.load(jsonbConfig.propertyOrderStrategy.toUpperCase())); + config, instanceNull.load(jsonbConfig.propertyOrderStrategy.toUpperCase())); // handle encoding if (jsonbConfig.encoding.isPresent()) { - clinit.invokeVirtualMethod( + instanceNull.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withEncoding", jsonbConfigClass, String.class), - config, clinit.load(jsonbConfig.encoding.get())); + config, instanceNull.load(jsonbConfig.encoding.get())); } // handle failOnUnknownProperties - clinit.invokeVirtualMethod( + instanceNull.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "setProperty", jsonbConfigClass, String.class, Object.class), config, - clinit.load(YassonProperties.FAIL_ON_UNKNOWN_PROPERTIES), - clinit.invokeStaticMethod( + instanceNull.load(YassonProperties.FAIL_ON_UNKNOWN_PROPERTIES), + instanceNull.invokeStaticMethod( MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), - clinit.load(jsonbConfig.failOnUnknownProperties))); + instanceNull.load(jsonbConfig.failOnUnknownProperties))); // add generated serializers to config if (!typeToGeneratedSerializers.isEmpty()) { - ResultHandle serializersArray = clinit.newArray(JsonbSerializer.class, - clinit.load(typeToGeneratedSerializers.size())); + ResultHandle serializersArray = instanceNull.newArray(JsonbSerializer.class, + instanceNull.load(typeToGeneratedSerializers.size())); int i = 0; for (Map.Entry entry : typeToGeneratedSerializers.entrySet()) { - ResultHandle serializer = clinit + ResultHandle serializer = instanceNull .newInstance(MethodDescriptor.ofConstructor(entry.getValue())); // build up the serializers array that will be passed to JsonbConfig - clinit.writeArrayValue(serializersArray, clinit.load(i), serializer); + instanceNull.writeArrayValue(serializersArray, instanceNull.load(i), serializer); - ResultHandle clazz = clinit.invokeStaticMethod( + ResultHandle clazz = instanceNull.invokeStaticMethod( MethodDescriptor.ofMethod(Class.class, "forName", Class.class, String.class), - clinit.load(entry.getKey())); + instanceNull.load(entry.getKey())); // add a ContainerSerializerProvider for the serializer - ResultHandle serializerProvider = clinit.newInstance( + ResultHandle serializerProvider = instanceNull.newInstance( MethodDescriptor.ofConstructor(SimpleContainerSerializerProvider.class, JsonbSerializer.class), serializer); - clinit.invokeVirtualMethod( + instanceNull.invokeVirtualMethod( MethodDescriptor.ofMethod(MappingContext.class, "addSerializerProvider", void.class, Class.class, ContainerSerializerProvider.class), mappingContext, clazz, serializerProvider); i++; } - clinit.invokeVirtualMethod( + instanceNull.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withSerializers", jsonbConfigClass, JsonbSerializer[].class), config, serializersArray); } // create jsonb from QuarkusJsonbBinding - ResultHandle jsonb = clinit.newInstance( + ResultHandle jsonb = instanceNull.newInstance( MethodDescriptor.ofConstructor(QuarkusJsonbBinding.class, JsonbContext.class), jsonbContext); - clinit.writeStaticField(instance, jsonb); - clinit.returnValue(null); - } - - try (MethodCreator getContext = cc.getMethodCreator("getContext", Jsonb.class, Class.class)) { - getContext.returnValue(getContext.readStaticField(instance)); + instanceNull.writeStaticField(instance, jsonb); + instanceNull.returnValue(jsonb); } try (MethodCreator bridgeGetContext = cc.getMethodCreator("getContext", Object.class, Class.class)) { + MethodDescriptor getContext = MethodDescriptor.ofMethod(QUARKUS_CONTEXT_RESOLVER, "getContext", + "javax.json.bind.Jsonb", + "java.lang.Class"); + ResultHandle result = bridgeGetContext.invokeVirtualMethod(getContext, bridgeGetContext.getThis(), + bridgeGetContext.getMethodParam(0)); + bridgeGetContext.returnValue(result); bridgeGetContext.returnValue(bridgeGetContext.readStaticField(instance)); } } diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index a0bf1d2633d32..e09d2e6fbfaa5 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -28,7 +28,6 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; -import io.quarkus.deployment.builditem.substrate.RuntimeInitializedClassBuildItem; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.MethodCreator; @@ -63,8 +62,7 @@ void build(BuildProducer feature) { @BuildStep void generateClasses(CombinedIndexBuildItem combinedIndexBuildItem, BuildProducer generatedClass, - BuildProducer jaxrsProvider, - BuildProducer runtimeClasses) { + BuildProducer jaxrsProvider) { IndexView index = combinedIndexBuildItem.getIndex(); if (!jsonbConfig.enabled) { @@ -132,16 +130,6 @@ public void write(String name, byte[] data) { additionalClassGenerator.generateJsonbContextResolver(classOutput, typeToGeneratedSerializers); jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(AdditionalClassGenerator.QUARKUS_CONTEXT_RESOLVER)); - - // ensure that the default locale is read at runtime when it's not set in the configuration (meaning the system default is needed) - if (!jsonbConfig.locale.isPresent()) { - runtimeClasses - .produce(new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_DEFAULT_LOCALE_PROVIDER)); - runtimeClasses.produce( - new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_DEFAULT_DATE_FORMATTER_PROVIDER)); - runtimeClasses.produce( - new RuntimeInitializedClassBuildItem(AdditionalClassGenerator.QUARKUS_CONTEXT_RESOLVER)); - } } private boolean hasCustomContextResolverBeenDeclared(IndexView index) { From 8f4e7b5c1f3f9701518bf4867b60ace1c232563f Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Thu, 18 Jul 2019 21:58:20 +0300 Subject: [PATCH 07/20] Fix minor date issue --- .../resteasy/jsonb/deployment/AdditionalClassGenerator.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java index af06360ce64a6..1947e5b8d28f2 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java @@ -95,11 +95,15 @@ void generateJsonbDefaultJsonbDateFormatterProvider(ClassOutput classOutput) { ResultHandle locale = instanceNull.invokeStaticMethod( MethodDescriptor.ofMethod(QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", Locale.class)); + ResultHandle localeStr = instanceNull.invokeVirtualMethod( + MethodDescriptor.ofMethod(Locale.class, "toLanguageTag", String.class), + locale); + ResultHandle format = instanceNull.load(jsonbConfig.dateFormat.orElse(JsonbDateFormat.DEFAULT_FORMAT)); ResultHandle jsonbDateFormatter = instanceNull.newInstance( MethodDescriptor.ofConstructor(JsonbDateFormatter.class, String.class, String.class), - format, locale); + format, localeStr); instanceNull.writeStaticField(instance, jsonbDateFormatter); instanceNull.returnValue(jsonbDateFormatter); } From 545f5ae72111232f647b5ce9a8a5aad4ff1c73ae Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 19 Jul 2019 11:08:04 +0300 Subject: [PATCH 08/20] Prevent reflection registration for classes with serializers --- .../deployment/ResteasyJsonbProcessor.java | 7 +++++- ...ReturnTypesWithoutReflectionBuildItem.java | 16 +++++++++++++ .../ResteasyServerCommonProcessor.java | 24 +++++++++++++------ 3 files changed, 39 insertions(+), 8 deletions(-) create mode 100644 extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyAdditionalReturnTypesWithoutReflectionBuildItem.java diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index e09d2e6fbfaa5..ed394c49c932b 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -37,6 +37,7 @@ import io.quarkus.resteasy.jsonb.deployment.serializers.GlobalSerializationConfig; import io.quarkus.resteasy.jsonb.deployment.serializers.TypeSerializerGenerator; import io.quarkus.resteasy.jsonb.deployment.serializers.TypeSerializerGeneratorRegistry; +import io.quarkus.resteasy.server.common.deployment.ResteasyAdditionalReturnTypesWithoutReflectionBuildItem; import io.quarkus.resteasy.server.common.deployment.ResteasyServerCommonProcessor; public class ResteasyJsonbProcessor { @@ -62,7 +63,8 @@ void build(BuildProducer feature) { @BuildStep void generateClasses(CombinedIndexBuildItem combinedIndexBuildItem, BuildProducer generatedClass, - BuildProducer jaxrsProvider) { + BuildProducer jaxrsProvider, + BuildProducer typesWithoutReflection) { IndexView index = combinedIndexBuildItem.getIndex(); if (!jsonbConfig.enabled) { @@ -130,6 +132,9 @@ public void write(String name, byte[] data) { additionalClassGenerator.generateJsonbContextResolver(classOutput, typeToGeneratedSerializers); jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(AdditionalClassGenerator.QUARKUS_CONTEXT_RESOLVER)); + for (String type : typeToGeneratedSerializers.keySet()) { + typesWithoutReflection.produce(new ResteasyAdditionalReturnTypesWithoutReflectionBuildItem(type)); + } } private boolean hasCustomContextResolverBeenDeclared(IndexView index) { diff --git a/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyAdditionalReturnTypesWithoutReflectionBuildItem.java b/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyAdditionalReturnTypesWithoutReflectionBuildItem.java new file mode 100644 index 0000000000000..cc0227247f291 --- /dev/null +++ b/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyAdditionalReturnTypesWithoutReflectionBuildItem.java @@ -0,0 +1,16 @@ +package io.quarkus.resteasy.server.common.deployment; + +import io.quarkus.builder.item.MultiBuildItem; + +public final class ResteasyAdditionalReturnTypesWithoutReflectionBuildItem extends MultiBuildItem { + + private final String className; + + public ResteasyAdditionalReturnTypesWithoutReflectionBuildItem(String className) { + this.className = className; + } + + public String getClassName() { + return className; + } +} diff --git a/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java b/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java index 87e963b0e85f4..cbcc43ba66a90 100755 --- a/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java +++ b/extensions/resteasy-server-common/deployment/src/main/java/io/quarkus/resteasy/server/common/deployment/ResteasyServerCommonProcessor.java @@ -167,7 +167,9 @@ public void build( List additionalJaxRsResourceMethodParamAnnotations, JaxrsProvidersToRegisterBuildItem jaxrsProvidersToRegisterBuildItem, CombinedIndexBuildItem combinedIndexBuildItem, - BeanArchiveIndexBuildItem beanArchiveIndexBuildItem) throws Exception { + BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, + List ignoreReflectionRegistrationBuildItems) + throws Exception { IndexView index = combinedIndexBuildItem.getIndex(); resource.produce(new SubstrateResourceBuildItem("META-INF/services/javax.ws.rs.client.ClientBuilder")); @@ -258,7 +260,7 @@ public void build( registerContextProxyDefinitions(index, proxyDefinition); registerReflectionForSerialization(reflectiveClass, reflectiveHierarchy, combinedIndexBuildItem, - beanArchiveIndexBuildItem, additionalJaxRsResourceMethodAnnotations); + beanArchiveIndexBuildItem, additionalJaxRsResourceMethodAnnotations, ignoreReflectionRegistrationBuildItems); for (ClassInfo implementation : index.getAllKnownImplementors(ResteasyDotNames.DYNAMIC_FEATURE)) { reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, implementation.name().toString())); @@ -671,10 +673,16 @@ private static void registerReflectionForSerialization(BuildProducer reflectiveHierarchy, CombinedIndexBuildItem combinedIndexBuildItem, BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, - List additionalJaxRsResourceMethodAnnotations) { + List additionalJaxRsResourceMethodAnnotations, + List returnTypesWithoutReflectionBuiltItem) { IndexView index = combinedIndexBuildItem.getIndex(); IndexView beanArchiveIndex = beanArchiveIndexBuildItem.getIndex(); + Set returnTypesWithoutReflection = new HashSet<>(); + for (ResteasyAdditionalReturnTypesWithoutReflectionBuildItem item : returnTypesWithoutReflectionBuiltItem) { + returnTypesWithoutReflection.add(item.getClassName()); + } + // This is probably redundant with the automatic resolution we do just below but better be safe for (AnnotationInstance annotation : index.getAnnotations(JSONB_ANNOTATION)) { if (annotation.target().kind() == AnnotationTarget.Kind.CLASS) { @@ -692,8 +700,8 @@ private static void registerReflectionForSerialization(BuildProducer reflectiveHierarchy, IndexView index) { + BuildProducer reflectiveHierarchy, IndexView index, + Set returnTypesWithoutReflection) { Collection instances = index.getAnnotations(annotationType); for (AnnotationInstance instance : instances) { if (instance.target().kind() != Kind.METHOD) { continue; } MethodInfo method = instance.target().asMethod(); - if (isReflectionDeclarationRequiredFor(method.returnType())) { + if (isReflectionDeclarationRequiredFor(method.returnType()) && + !returnTypesWithoutReflection.contains(method.returnType().name().toString())) { reflectiveHierarchy.produce(new ReflectiveHierarchyBuildItem(method.returnType(), index)); } for (short i = 0; i < method.parameters().size(); i++) { From 7387daa5bfe693d131cbaf35fb7725339d3f6d12 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 19 Jul 2019 20:13:11 +0300 Subject: [PATCH 09/20] Lift restriction of having to know how to serialize all properties --- .../deployment/ResteasyJsonbProcessor.java | 65 ++++++++++++++++--- .../CollectionTypeSerializerGenerator.java | 11 ++-- .../IntegerTypeSerializerGenerator.java | 4 +- .../LocalDateTimeSerializerGenerator.java | 4 +- .../ObjectTypeSerializerGenerator.java | 38 ++++++++--- .../StringTypeSerializerGenerator.java | 4 +- .../serializers/TypeSerializerGenerator.java | 24 +++++-- .../TypeSerializerGeneratorRegistry.java | 3 +- .../serializers/UnhandledTypeGenerator.java | 64 ++++++++++++++++++ .../UnhandledTypeGeneratorUtil.java | 48 ++++++++++++++ 10 files changed, 230 insertions(+), 35 deletions(-) create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/UnhandledTypeGenerator.java create mode 100644 extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/UnhandledTypeGeneratorUtil.java diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index ed394c49c932b..cc6a8371996d3 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.jsonb.deployment; +import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; @@ -117,12 +118,16 @@ public void write(String name, byte[] data) { } Map typeToGeneratedSerializers = new HashMap<>(); + List typesThatDontNeedReflection = new ArrayList<>(); for (ClassType type : serializerCandidates) { - String generatedSerializerClassName = generateSerializerForClassType(type, + GenerationResult generationResult = generateSerializerForClassType(type, typeSerializerGeneratorRegistry, classOutput); - if (generatedSerializerClassName != null) { - typeToGeneratedSerializers.put(type.name().toString(), generatedSerializerClassName); + if (generationResult.isGenerated()) { + typeToGeneratedSerializers.put(type.name().toString(), generationResult.getClassName()); + if (!generationResult.isNeedsReflection()) { + typesThatDontNeedReflection.add(type.name().toString()); + } } } @@ -132,7 +137,7 @@ public void write(String name, byte[] data) { additionalClassGenerator.generateJsonbContextResolver(classOutput, typeToGeneratedSerializers); jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(AdditionalClassGenerator.QUARKUS_CONTEXT_RESOLVER)); - for (String type : typeToGeneratedSerializers.keySet()) { + for (String type : typesThatDontNeedReflection) { typesWithoutReflection.produce(new ResteasyAdditionalReturnTypesWithoutReflectionBuildItem(type)); } } @@ -178,10 +183,11 @@ private void validateConfiguration() { /** * @return The full name of the generated class or null if a serializer could not be generated */ - private String generateSerializerForClassType(ClassType classType, TypeSerializerGeneratorRegistry registry, + private GenerationResult generateSerializerForClassType(ClassType classType, TypeSerializerGeneratorRegistry registry, ClassOutput classOutput) { - if (!registry.getObjectSerializer().supports(classType, registry)) { - return null; + TypeSerializerGenerator.Supported supported = registry.getObjectSerializer().supports(classType, registry); + if (supported == TypeSerializerGenerator.Supported.UNSUPPORTED) { + return GenerationResult.notGenerated(); } DotName classDotName = classType.name(); @@ -198,10 +204,12 @@ private String generateSerializerForClassType(ClassType classType, TypeSerialize JsonGenerator.class.getName(), SerializationContext.class.getName())) { ResultHandle object = serialize.getMethodParam(0); ResultHandle jsonGenerator = serialize.getMethodParam(1); + ResultHandle serializationContext = serialize.getMethodParam(2); // delegate to object serializer registry.getObjectSerializer().generate( - new TypeSerializerGenerator.GenerateContext(classType, serialize, jsonGenerator, object, registry, + new TypeSerializerGenerator.GenerateContext(classType, serialize, jsonGenerator, serializationContext, + object, registry, getGlobalConfig(), false, null)); serialize.returnValue(null); @@ -221,11 +229,50 @@ private String generateSerializerForClassType(ClassType classType, TypeSerialize } } - return generatedSerializerName; + return supported == TypeSerializerGenerator.Supported.FULLY + ? GenerationResult.noReflectionNeeded(generatedSerializerName) + : GenerationResult.reflectionNeeded(generatedSerializerName); } private GlobalSerializationConfig getGlobalConfig() { return new GlobalSerializationConfig( jsonbConfig.locale, jsonbConfig.dateFormat, jsonbConfig.serializeNullValues, jsonbConfig.propertyOrderStrategy); } + + static class GenerationResult { + private final boolean generated; + private final String className; + private final boolean needsReflection; + + private GenerationResult(boolean generated, String className, boolean needsReflection) { + this.generated = generated; + this.className = className; + this.needsReflection = needsReflection; + } + + static GenerationResult notGenerated() { + return new GenerationResult(false, null, false); + } + + static GenerationResult noReflectionNeeded(String className) { + return new GenerationResult(true, className, false); + } + + static GenerationResult reflectionNeeded(String className) { + return new GenerationResult(true, className, true); + } + + boolean isGenerated() { + return generated; + } + + String getClassName() { + return className; + } + + boolean isNeedsReflection() { + return needsReflection; + } + } + } diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/CollectionTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/CollectionTypeSerializerGenerator.java index 39adb09e98558..8768f1dd1e6a5 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/CollectionTypeSerializerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/CollectionTypeSerializerGenerator.java @@ -16,16 +16,19 @@ public class CollectionTypeSerializerGenerator extends AbstractTypeSerializerGenerator { @Override - public boolean supports(Type type, TypeSerializerGeneratorRegistry registry) { + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { if (!CollectionUtil.isCollection(type.name())) { - return false; + return Supported.UNSUPPORTED; } Type genericType = CollectionUtil.getGenericType(type); if (genericType == null) { - return false; + return Supported.UNSUPPORTED; } - return (registry.correspondingTypeSerializer(genericType) != null); + + TypeSerializerGenerator typeSerializerGenerator = registry.correspondingTypeSerializer(genericType); + return typeSerializerGenerator != null ? typeSerializerGenerator.supports(genericType, registry) + : Supported.UNSUPPORTED; } @Override diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/IntegerTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/IntegerTypeSerializerGenerator.java index dee7897b0722b..327f4661984f4 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/IntegerTypeSerializerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/IntegerTypeSerializerGenerator.java @@ -12,8 +12,8 @@ public class IntegerTypeSerializerGenerator extends AbstractNumberTypeSerializerGenerator { @Override - public boolean supports(Type type, TypeSerializerGeneratorRegistry registry) { - return DotNames.INTEGER.equals(type.name()); + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.INTEGER.equals(type.name()) ? Supported.FULLY : Supported.UNSUPPORTED; } @Override diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/LocalDateTimeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/LocalDateTimeSerializerGenerator.java index 4b48296fa2cc1..cc99831182aa5 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/LocalDateTimeSerializerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/LocalDateTimeSerializerGenerator.java @@ -17,8 +17,8 @@ public class LocalDateTimeSerializerGenerator extends AbstractDatetimeSerializerGenerator { @Override - public boolean supports(Type type, TypeSerializerGeneratorRegistry registry) { - return DotNames.LOCAL_DATE_TIME.equals(type.name()); + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.LOCAL_DATE_TIME.equals(type.name()) ? Supported.FULLY : Supported.UNSUPPORTED; } @Override diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java index 5f88c2ceff4e7..b622c7db71e28 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java @@ -15,6 +15,7 @@ import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ArrayType; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; import org.jboss.jandex.MethodInfo; @@ -30,33 +31,46 @@ public class ObjectTypeSerializerGenerator extends AbstractTypeSerializerGenerator { @Override - public boolean supports(Type type, TypeSerializerGeneratorRegistry registry) { + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { if (type instanceof ArrayType) { - return false; + return Supported.UNSUPPORTED; } if (type.name().toString().startsWith("java")) { - return false; + return Supported.UNSUPPORTED; } final SerializationClassInspector.Result inspectionsResult = registry.getInspector().inspect(type.name()); if (!inspectionsResult.isPossible()) { - return false; + return Supported.UNSUPPORTED; } + boolean foundUnhandledType = false; for (MethodInfo getter : inspectionsResult.getGetters().keySet()) { if (registry.correspondingTypeSerializer(getter.returnType()) == null) { - return false; + if (canUseUnhandledTypeGenerator(getter.returnType())) { + foundUnhandledType = true; + } else { + return Supported.UNSUPPORTED; + } } } for (FieldInfo field : inspectionsResult.getVisibleFieldsWithoutGetters()) { if (registry.correspondingTypeSerializer(field.type()) == null) { - return false; + if (canUseUnhandledTypeGenerator(field.type())) { + foundUnhandledType = true; + } else { + return Supported.UNSUPPORTED; + } } } - return true; + return foundUnhandledType ? Supported.WITH_UNHANDLED : Supported.FULLY; + } + + private boolean canUseUnhandledTypeGenerator(Type type) { + return type instanceof ClassType; } @Override @@ -88,14 +102,18 @@ protected void generateNotNull(GenerateContext context) { for (Map.Entry entry : inspectionResult.getGetters().entrySet()) { MethodInfo getterMethodInfo = entry.getKey(); FieldInfo fieldInfo = entry.getValue(); + String defaultKeyName = PropertyUtil.toFieldName(getterMethodInfo); Type returnType = getterMethodInfo.returnType(); TypeSerializerGenerator getterTypeSerializerGenerator = serializerRegistry.correspondingTypeSerializer(returnType); if (getterTypeSerializerGenerator == null) { - throw new IllegalStateException("Could not generate serializer for getter " + getterMethodInfo.name() - + " of type " + classDotNate); + if (canUseUnhandledTypeGenerator(returnType)) { + getterTypeSerializerGenerator = new UnhandledTypeGenerator(context.getType(), defaultKeyName); + } else { + throw new IllegalStateException("Could not generate serializer for getter " + getterMethodInfo.name() + + " of type " + classDotNate); + } } - String defaultKeyName = PropertyUtil.toFieldName(getterMethodInfo); Map effectiveGetterAnnotations = getEffectiveGetterAnnotations(getterMethodInfo, fieldInfo, serializerRegistry.getInspector()); String finalKeyName = getFinalKeyName(defaultKeyName, effectiveGetterAnnotations); diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/StringTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/StringTypeSerializerGenerator.java index 7ce3b1b6b1973..1138e471e532b 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/StringTypeSerializerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/StringTypeSerializerGenerator.java @@ -10,8 +10,8 @@ public class StringTypeSerializerGenerator extends AbstractTypeSerializerGenerator { @Override - public boolean supports(Type type, TypeSerializerGeneratorRegistry registry) { - return DotNames.STRING.equals(type.name()); + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.STRING.equals(type.name()) ? Supported.FULLY : Supported.UNSUPPORTED; } @Override diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java index a1d3d9bf30dc1..00eb1abd39c18 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java @@ -11,7 +11,13 @@ public interface TypeSerializerGenerator { - boolean supports(Type type, TypeSerializerGeneratorRegistry registry); + Supported supports(Type type, TypeSerializerGeneratorRegistry registry); + + enum Supported { + FULLY, + WITH_UNHANDLED, + UNSUPPORTED + } void generate(GenerateContext context); @@ -19,6 +25,7 @@ class GenerateContext { private final Type type; private final BytecodeCreator bytecodeCreator; private final ResultHandle jsonGenerator; + private final ResultHandle serializationContext; private final ResultHandle currentItem; private final TypeSerializerGeneratorRegistry registry; private final GlobalSerializationConfig globalConfig; @@ -28,12 +35,13 @@ class GenerateContext { private final Map effectivePropertyAnnotations; public GenerateContext(Type type, BytecodeCreator bytecodeCreator, ResultHandle jsonGenerator, - ResultHandle currentItem, + ResultHandle serializationContext, ResultHandle currentItem, TypeSerializerGeneratorRegistry registry, GlobalSerializationConfig globalConfig, boolean nullChecked, Map effectivePropertyAnnotations) { this.type = type; this.bytecodeCreator = bytecodeCreator; this.jsonGenerator = jsonGenerator; + this.serializationContext = serializationContext; this.currentItem = currentItem; this.registry = registry; this.globalConfig = globalConfig; @@ -53,6 +61,10 @@ ResultHandle getJsonGenerator() { return jsonGenerator; } + ResultHandle getSerializationContext() { + return serializationContext; + } + ResultHandle getCurrentItem() { return currentItem; } @@ -75,7 +87,8 @@ Map getEffectivePropertyAnnotations() { GenerateContext changeItem(BytecodeCreator newBytecodeCreator, Type newType, ResultHandle newCurrentItem, boolean newNullChecked) { - return new GenerateContext(newType, newBytecodeCreator, jsonGenerator, newCurrentItem, registry, + return new GenerateContext(newType, newBytecodeCreator, jsonGenerator, serializationContext, newCurrentItem, + registry, globalConfig, newNullChecked, effectivePropertyAnnotations); } @@ -83,12 +96,13 @@ GenerateContext changeItem(BytecodeCreator newBytecodeCreator, Type newType, Res GenerateContext changeItem(BytecodeCreator newBytecodeCreator, Type newType, ResultHandle newCurrentItem, boolean newNullChecked, Map newEffectivePropertyAnnotations) { - return new GenerateContext(newType, newBytecodeCreator, jsonGenerator, newCurrentItem, registry, + return new GenerateContext(newType, newBytecodeCreator, jsonGenerator, serializationContext, newCurrentItem, + registry, globalConfig, newNullChecked, newEffectivePropertyAnnotations); } GenerateContext changeByteCodeCreator(BytecodeCreator newBytecodeCreator) { - return new GenerateContext(type, newBytecodeCreator, jsonGenerator, currentItem, registry, + return new GenerateContext(type, newBytecodeCreator, jsonGenerator, serializationContext, currentItem, registry, globalConfig, nullChecked, effectivePropertyAnnotations); } } diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java index 468baea7336c4..5c91e0f9eaf66 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java @@ -11,6 +11,7 @@ public final class TypeSerializerGeneratorRegistry { private final TypeSerializerGenerator objectSerializer = new ObjectTypeSerializerGenerator(); + private final List typeSerializerGenerators = Arrays.asList(new StringTypeSerializerGenerator(), new IntegerTypeSerializerGenerator(), new LocalDateTimeSerializerGenerator(), objectSerializer, @@ -24,7 +25,7 @@ public TypeSerializerGeneratorRegistry(SerializationClassInspector inspector) { public TypeSerializerGenerator correspondingTypeSerializer(Type type) { for (TypeSerializerGenerator typeSerializerGenerator : typeSerializerGenerators) { - if (typeSerializerGenerator.supports(type, this)) { + if (typeSerializerGenerator.supports(type, this) != TypeSerializerGenerator.Supported.UNSUPPORTED) { return typeSerializerGenerator; } } diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/UnhandledTypeGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/UnhandledTypeGenerator.java new file mode 100644 index 0000000000000..b796842ca7133 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/UnhandledTypeGenerator.java @@ -0,0 +1,64 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import javax.json.bind.serializer.JsonbSerializer; +import javax.json.bind.serializer.SerializationContext; +import javax.json.stream.JsonGenerator; + +import org.eclipse.yasson.internal.Marshaller; +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.runtime.serializers.UnhandledTypeGeneratorUtil; + +/** + * Generator that simply delegates to Yasson + * + * Important notes: + * 1) Results in reflection being done on the enclosing type in order to populate the metadata needed by Yasson + * 2) Doesn't handle generic types + */ +public class UnhandledTypeGenerator extends AbstractTypeSerializerGenerator { + + private final Type enclosingType; + private final String propertyName; + + UnhandledTypeGenerator(Type enclosingType, String propertyName) { + this.enclosingType = enclosingType; + this.propertyName = propertyName; + } + + // won't ever be called because this class isn't part of the TypeSerializerGeneratorRegistry + // it is instead constructed on demand by ObjectTypeSerializerGenerator + @Override + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + throw new IllegalStateException("Should not be called"); + } + + @Override + protected void generateNotNull(GenerateContext context) { + // we generate pretty much the same code as Yasson's ObjectSerializer#marshallProperty + + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + ResultHandle jsonGenerator = context.getJsonGenerator(); + ResultHandle serializationContext = context.getSerializationContext(); + + ResultHandle marshaller = bytecodeCreator.checkCast(serializationContext, Marshaller.class); + ResultHandle enclosingTypeClass = bytecodeCreator.invokeStaticMethod( + MethodDescriptor.ofMethod(Class.class, "forName", Class.class, String.class), + bytecodeCreator.load(enclosingType.name().toString())); + + ResultHandle propertyCachedSerializer = bytecodeCreator.invokeStaticMethod( + MethodDescriptor.ofMethod(UnhandledTypeGeneratorUtil.class, "getSerializerForUnhandledType", + JsonbSerializer.class, Marshaller.class, Class.class, Object.class, String.class), + marshaller, enclosingTypeClass, + context.getCurrentItem(), bytecodeCreator.load(propertyName)); + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonbSerializer.class, "serialize", void.class, Object.class, JsonGenerator.class, + SerializationContext.class), + propertyCachedSerializer, context.getCurrentItem(), jsonGenerator, context.getSerializationContext()); + } + +} diff --git a/extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/UnhandledTypeGeneratorUtil.java b/extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/UnhandledTypeGeneratorUtil.java new file mode 100644 index 0000000000000..47df6eed5b492 --- /dev/null +++ b/extensions/resteasy-jsonb/runtime/src/main/java/io/quarkus/resteasy/jsonb/runtime/serializers/UnhandledTypeGeneratorUtil.java @@ -0,0 +1,48 @@ +package io.quarkus.resteasy.jsonb.runtime.serializers; + +import java.util.HashMap; +import java.util.Map; + +import javax.json.bind.serializer.JsonbSerializer; + +import org.eclipse.yasson.internal.Marshaller; +import org.eclipse.yasson.internal.model.PropertyModel; +import org.eclipse.yasson.internal.serializer.SerializerBuilder; + +public final class UnhandledTypeGeneratorUtil { + + private static final Map> CACHE = new HashMap<>(); + + private UnhandledTypeGeneratorUtil() { + } + + /** + * Use Yasson to generate a serializer for a property whose type we don't yet handle + *

+ * The third param is a string to avoid having to load the class when it's not needed + *

+ * used by UnhandledTypeGenerator + */ + public static JsonbSerializer getSerializerForUnhandledType(Marshaller marshaller, Class enclosingClass, + Object propertyValue, String propertyName) throws ClassNotFoundException { + PropertyModel propertyModel = marshaller.getMappingContext().getOrCreateClassModel(enclosingClass) + .getPropertyModel(propertyName); + JsonbSerializer powerUnitSerializer = propertyModel.getPropertySerializer(); + if (powerUnitSerializer != null) { + return powerUnitSerializer; + } + + Class propertyClass = propertyValue.getClass(); + String cacheKey = enclosingClass.getName() + "-" + propertyName; + if (CACHE.containsKey(cacheKey)) { + return CACHE.get(cacheKey); + } + + powerUnitSerializer = new SerializerBuilder(marshaller.getJsonbContext()) + .withObjectClass(propertyClass) + .withCustomization(propertyModel.getCustomization()) + .build(); + CACHE.put(cacheKey, powerUnitSerializer); + return powerUnitSerializer; + } +} From 01a7756d470c7d83f1fe2add1a451d1261505016 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Sat, 20 Jul 2019 23:41:32 +0300 Subject: [PATCH 10/20] Separate RESTEasy specific from generic concerns --- .../JsonbSupportClassGenerator.java | 99 ++++++++++ ....java => ResteasyJsonbClassGenerator.java} | 83 +-------- .../deployment/ResteasyJsonbProcessor.java | 171 ++++-------------- .../deployment/SerializerClassGenerator.java | 118 ++++++++++++ .../serializers/SerializerGeneratorUtil.java | 4 +- 5 files changed, 262 insertions(+), 213 deletions(-) create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbSupportClassGenerator.java rename extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/{AdditionalClassGenerator.java => ResteasyJsonbClassGenerator.java} (68%) create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializerClassGenerator.java diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbSupportClassGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbSupportClassGenerator.java new file mode 100644 index 0000000000000..31cfae23edd70 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbSupportClassGenerator.java @@ -0,0 +1,99 @@ +package io.quarkus.resteasy.jsonb.deployment; + +import java.lang.reflect.Modifier; +import java.util.Locale; + +import javax.json.bind.annotation.JsonbDateFormat; + +import org.eclipse.yasson.internal.serializer.JsonbDateFormatter; + +import io.quarkus.gizmo.BranchResult; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; + +public class JsonbSupportClassGenerator { + + public static final String QUARKUS_DEFAULT_DATE_FORMATTER_PROVIDER = "io.quarkus.jsonb.QuarkusDefaultJsonbDateFormatterProvider"; + public static final String QUARKUS_DEFAULT_LOCALE_PROVIDER = "io.quarkus.jsonb.QuarkusDefaultJsonbLocaleProvider"; + + private final JsonbConfig jsonbConfig; + + public JsonbSupportClassGenerator(JsonbConfig jsonbConfig) { + this.jsonbConfig = jsonbConfig; + } + + public void generateDefaultLocaleProvider(ClassOutput classOutput) { + try (ClassCreator cc = ClassCreator.builder() + .classOutput(classOutput).className(QUARKUS_DEFAULT_LOCALE_PROVIDER) + .build()) { + + FieldDescriptor instance = cc.getFieldCreator("INSTANCE", Locale.class) + .setModifiers(Modifier.STATIC | Modifier.PRIVATE) + .getFieldDescriptor(); + + try (MethodCreator get = cc.getMethodCreator("get", Locale.class)) { + get.setModifiers(Modifier.STATIC | Modifier.PUBLIC); + + BranchResult branchResult = get.ifNull(get.readStaticField(instance)); + + BytecodeCreator instanceNotNull = branchResult.falseBranch(); + instanceNotNull.returnValue(instanceNotNull.readStaticField(instance)); + + BytecodeCreator instanceNull = branchResult.trueBranch(); + ResultHandle locale; + if (jsonbConfig.locale.isPresent()) { + locale = instanceNull.invokeStaticMethod( + MethodDescriptor.ofMethod(Locale.class, "forLanguageTag", Locale.class, String.class), + instanceNull.load(jsonbConfig.locale.get())); + } else { + locale = instanceNull.invokeStaticMethod( + MethodDescriptor.ofMethod(Locale.class, "getDefault", Locale.class)); + } + + instanceNull.writeStaticField(instance, locale); + instanceNull.returnValue(locale); + } + } + } + + public void generateJsonbDefaultJsonbDateFormatterProvider(ClassOutput classOutput) { + try (ClassCreator cc = ClassCreator.builder() + .classOutput(classOutput).className(QUARKUS_DEFAULT_DATE_FORMATTER_PROVIDER) + .build()) { + + FieldDescriptor instance = cc.getFieldCreator("INSTANCE", JsonbDateFormatter.class) + .setModifiers(Modifier.STATIC | Modifier.PRIVATE) + .getFieldDescriptor(); + + try (MethodCreator get = cc.getMethodCreator("get", JsonbDateFormatter.class)) { + get.setModifiers(Modifier.STATIC | Modifier.PUBLIC); + + BranchResult branchResult = get.ifNull(get.readStaticField(instance)); + + BytecodeCreator instanceNotNull = branchResult.falseBranch(); + instanceNotNull.returnValue(instanceNotNull.readStaticField(instance)); + + BytecodeCreator instanceNull = branchResult.trueBranch(); + ResultHandle locale = instanceNull.invokeStaticMethod( + MethodDescriptor.ofMethod(QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", Locale.class)); + + ResultHandle localeStr = instanceNull.invokeVirtualMethod( + MethodDescriptor.ofMethod(Locale.class, "toLanguageTag", String.class), + locale); + + ResultHandle format = instanceNull.load(jsonbConfig.dateFormat.orElse(JsonbDateFormat.DEFAULT_FORMAT)); + + ResultHandle jsonbDateFormatter = instanceNull.newInstance( + MethodDescriptor.ofConstructor(JsonbDateFormatter.class, String.class, String.class), + format, localeStr); + instanceNull.writeStaticField(instance, jsonbDateFormatter); + instanceNull.returnValue(jsonbDateFormatter); + } + } + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java similarity index 68% rename from extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java rename to extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java index 1947e5b8d28f2..67700ccd1261f 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/AdditionalClassGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java @@ -5,7 +5,6 @@ import java.util.Map; import javax.json.bind.Jsonb; -import javax.json.bind.annotation.JsonbDateFormat; import javax.json.bind.serializer.JsonbSerializer; import javax.json.spi.JsonProvider; import javax.ws.rs.ext.ContextResolver; @@ -15,7 +14,6 @@ import org.eclipse.yasson.internal.JsonbContext; import org.eclipse.yasson.internal.MappingContext; import org.eclipse.yasson.internal.serializer.ContainerSerializerProvider; -import org.eclipse.yasson.internal.serializer.JsonbDateFormatter; import io.quarkus.gizmo.BranchResult; import io.quarkus.gizmo.BytecodeCreator; @@ -28,88 +26,16 @@ import io.quarkus.resteasy.jsonb.runtime.serializers.QuarkusJsonbBinding; import io.quarkus.resteasy.jsonb.runtime.serializers.SimpleContainerSerializerProvider; -public class AdditionalClassGenerator { +class ResteasyJsonbClassGenerator { - public static final String QUARKUS_CONTEXT_RESOLVER = "io.quarkus.jsonb.QuarkusJsonbContextResolver"; - public static final String QUARKUS_DEFAULT_DATE_FORMATTER_PROVIDER = "io.quarkus.jsonb.QuarkusDefaultJsonbDateFormatterProvider"; - public static final String QUARKUS_DEFAULT_LOCALE_PROVIDER = "io.quarkus.jsonb.QuarkusDefaultJsonbLocaleProvider"; + static final String QUARKUS_CONTEXT_RESOLVER = "io.quarkus.jsonb.QuarkusJsonbContextResolver"; private final JsonbConfig jsonbConfig; - public AdditionalClassGenerator(JsonbConfig jsonbConfig) { + public ResteasyJsonbClassGenerator(JsonbConfig jsonbConfig) { this.jsonbConfig = jsonbConfig; } - void generateDefaultLocaleProvider(ClassOutput classOutput) { - try (ClassCreator cc = ClassCreator.builder() - .classOutput(classOutput).className(QUARKUS_DEFAULT_LOCALE_PROVIDER) - .build()) { - - FieldDescriptor instance = cc.getFieldCreator("INSTANCE", Locale.class) - .setModifiers(Modifier.STATIC | Modifier.PRIVATE) - .getFieldDescriptor(); - - try (MethodCreator get = cc.getMethodCreator("get", Locale.class)) { - get.setModifiers(Modifier.STATIC | Modifier.PUBLIC); - - BranchResult branchResult = get.ifNull(get.readStaticField(instance)); - - BytecodeCreator instanceNotNull = branchResult.falseBranch(); - instanceNotNull.returnValue(instanceNotNull.readStaticField(instance)); - - BytecodeCreator instanceNull = branchResult.trueBranch(); - ResultHandle locale; - if (jsonbConfig.locale.isPresent()) { - locale = instanceNull.invokeStaticMethod( - MethodDescriptor.ofMethod(Locale.class, "forLanguageTag", Locale.class, String.class), - instanceNull.load(jsonbConfig.locale.get())); - } else { - locale = instanceNull.invokeStaticMethod( - MethodDescriptor.ofMethod(Locale.class, "getDefault", Locale.class)); - } - - instanceNull.writeStaticField(instance, locale); - instanceNull.returnValue(locale); - } - } - } - - void generateJsonbDefaultJsonbDateFormatterProvider(ClassOutput classOutput) { - try (ClassCreator cc = ClassCreator.builder() - .classOutput(classOutput).className(QUARKUS_DEFAULT_DATE_FORMATTER_PROVIDER) - .build()) { - - FieldDescriptor instance = cc.getFieldCreator("INSTANCE", JsonbDateFormatter.class) - .setModifiers(Modifier.STATIC | Modifier.PRIVATE) - .getFieldDescriptor(); - - try (MethodCreator get = cc.getMethodCreator("get", JsonbDateFormatter.class)) { - get.setModifiers(Modifier.STATIC | Modifier.PUBLIC); - - BranchResult branchResult = get.ifNull(get.readStaticField(instance)); - - BytecodeCreator instanceNotNull = branchResult.falseBranch(); - instanceNotNull.returnValue(instanceNotNull.readStaticField(instance)); - - BytecodeCreator instanceNull = branchResult.trueBranch(); - ResultHandle locale = instanceNull.invokeStaticMethod( - MethodDescriptor.ofMethod(QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", Locale.class)); - - ResultHandle localeStr = instanceNull.invokeVirtualMethod( - MethodDescriptor.ofMethod(Locale.class, "toLanguageTag", String.class), - locale); - - ResultHandle format = instanceNull.load(jsonbConfig.dateFormat.orElse(JsonbDateFormat.DEFAULT_FORMAT)); - - ResultHandle jsonbDateFormatter = instanceNull.newInstance( - MethodDescriptor.ofConstructor(JsonbDateFormatter.class, String.class, String.class), - format, localeStr); - instanceNull.writeStaticField(instance, jsonbDateFormatter); - instanceNull.returnValue(jsonbDateFormatter); - } - } - } - void generateJsonbContextResolver(ClassOutput classOutput, Map typeToGeneratedSerializers) { try (ClassCreator cc = ClassCreator.builder() .classOutput(classOutput).className(QUARKUS_CONTEXT_RESOLVER) @@ -150,7 +76,8 @@ void generateJsonbContextResolver(ClassOutput classOutput, Map t ResultHandle locale = null; if (jsonbConfig.locale.isPresent()) { locale = instanceNull.invokeStaticMethod( - MethodDescriptor.ofMethod(QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", Locale.class)); + MethodDescriptor.ofMethod(JsonbSupportClassGenerator.QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", + Locale.class)); instanceNull.invokeVirtualMethod( MethodDescriptor.ofMethod(jsonbConfigClass, "withLocale", jsonbConfigClass, Locale.class), config, locale); diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index cc6a8371996d3..65c91462cd800 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -9,9 +9,6 @@ import java.util.Set; import javax.json.bind.Jsonb; -import javax.json.bind.serializer.JsonbSerializer; -import javax.json.bind.serializer.SerializationContext; -import javax.json.stream.JsonGenerator; import javax.ws.rs.ext.Provider; import org.jboss.jandex.AnnotationInstance; @@ -29,14 +26,8 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; -import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; -import io.quarkus.gizmo.MethodCreator; -import io.quarkus.gizmo.MethodDescriptor; -import io.quarkus.gizmo.ResultHandle; import io.quarkus.resteasy.common.deployment.ResteasyJaxrsProviderBuildItem; -import io.quarkus.resteasy.jsonb.deployment.serializers.GlobalSerializationConfig; -import io.quarkus.resteasy.jsonb.deployment.serializers.TypeSerializerGenerator; import io.quarkus.resteasy.jsonb.deployment.serializers.TypeSerializerGeneratorRegistry; import io.quarkus.resteasy.server.common.deployment.ResteasyAdditionalReturnTypesWithoutReflectionBuildItem; import io.quarkus.resteasy.server.common.deployment.ResteasyServerCommonProcessor; @@ -90,37 +81,14 @@ public void write(String name, byte[] data) { } }; - Set serializerCandidates = new HashSet<>(); - for (DotName annotationType : ResteasyServerCommonProcessor.METHOD_ANNOTATIONS) { - Collection jaxrsMethodInstances = index.getAnnotations(annotationType); - for (AnnotationInstance jaxrsMethodInstance : jaxrsMethodInstances) { - MethodInfo method = jaxrsMethodInstance.target().asMethod(); - Type returnType = method.returnType(); - if (!ResteasyServerCommonProcessor.isReflectionDeclarationRequiredFor(returnType) - || returnType.name().toString().startsWith("java.lang")) { - continue; - } + Set serializerCandidates = determineSerializationCandidates(index); - if (returnType instanceof ClassType) { - serializerCandidates.add(returnType.asClassType()); - continue; - } - - // don't generate serializers for collection types since it would override the default ones - // we do however want to generate serializers for types that are captured by collections or Maps - if (CollectionUtil.isCollection(returnType.name())) { - Type genericType = CollectionUtil.getGenericType(returnType); - if (genericType instanceof ClassType) { - serializerCandidates.add(genericType.asClassType()); - } - } - } - } + SerializerClassGenerator serializerClassGenerator = new SerializerClassGenerator(jsonbConfig); Map typeToGeneratedSerializers = new HashMap<>(); List typesThatDontNeedReflection = new ArrayList<>(); for (ClassType type : serializerCandidates) { - GenerationResult generationResult = generateSerializerForClassType(type, + SerializerClassGenerator.Result generationResult = serializerClassGenerator.generateSerializerForClassType(type, typeSerializerGeneratorRegistry, classOutput); if (generationResult.isGenerated()) { @@ -131,12 +99,14 @@ public void write(String name, byte[] data) { } } - AdditionalClassGenerator additionalClassGenerator = new AdditionalClassGenerator(jsonbConfig); - additionalClassGenerator.generateDefaultLocaleProvider(classOutput); - additionalClassGenerator.generateJsonbDefaultJsonbDateFormatterProvider(classOutput); - additionalClassGenerator.generateJsonbContextResolver(classOutput, typeToGeneratedSerializers); + JsonbSupportClassGenerator jsonbSupportClassGenerator = new JsonbSupportClassGenerator(jsonbConfig); + jsonbSupportClassGenerator.generateDefaultLocaleProvider(classOutput); + jsonbSupportClassGenerator.generateJsonbDefaultJsonbDateFormatterProvider(classOutput); + + ResteasyJsonbClassGenerator resteasyJsonbClassGenerator = new ResteasyJsonbClassGenerator(jsonbConfig); + resteasyJsonbClassGenerator.generateJsonbContextResolver(classOutput, typeToGeneratedSerializers); - jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(AdditionalClassGenerator.QUARKUS_CONTEXT_RESOLVER)); + jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(ResteasyJsonbClassGenerator.QUARKUS_CONTEXT_RESOLVER)); for (String type : typesThatDontNeedReflection) { typesWithoutReflection.produce(new ResteasyAdditionalReturnTypesWithoutReflectionBuildItem(type)); } @@ -173,105 +143,40 @@ private boolean hasCustomContextResolverBeenDeclared(IndexView index) { return false; } - private void validateConfiguration() { - if (!jsonbConfig.isValidPropertyOrderStrategy()) { - throw new IllegalArgumentException( - "quarkus.jsonb.property-order-strategy can only be one of " + JsonbConfig.ALLOWED_PROPERTY_ORDER_VALUES); - } - } - - /** - * @return The full name of the generated class or null if a serializer could not be generated - */ - private GenerationResult generateSerializerForClassType(ClassType classType, TypeSerializerGeneratorRegistry registry, - ClassOutput classOutput) { - TypeSerializerGenerator.Supported supported = registry.getObjectSerializer().supports(classType, registry); - if (supported == TypeSerializerGenerator.Supported.UNSUPPORTED) { - return GenerationResult.notGenerated(); - } - - DotName classDotName = classType.name(); - String generatedSerializerName = "io.quarkus.jsonb.serializers." + classDotName.withoutPackagePrefix() + "Serializer"; - try (ClassCreator cc = ClassCreator.builder() - .classOutput(classOutput).className(generatedSerializerName) - .interfaces(JsonbSerializer.class) - .signature(String.format("Ljava/lang/Object;Ljavax/json/bind/serializer/JsonbSerializer;", - classDotName.toString()).replace('.', '/')) - .build()) { - - // actual implementation of serialize method - try (MethodCreator serialize = cc.getMethodCreator("serialize", "void", classDotName.toString(), - JsonGenerator.class.getName(), SerializationContext.class.getName())) { - ResultHandle object = serialize.getMethodParam(0); - ResultHandle jsonGenerator = serialize.getMethodParam(1); - ResultHandle serializationContext = serialize.getMethodParam(2); - - // delegate to object serializer - registry.getObjectSerializer().generate( - new TypeSerializerGenerator.GenerateContext(classType, serialize, jsonGenerator, serializationContext, - object, registry, - getGlobalConfig(), false, null)); + private Set determineSerializationCandidates(IndexView index) { + Set serializerCandidates = new HashSet<>(); + for (DotName annotationType : ResteasyServerCommonProcessor.METHOD_ANNOTATIONS) { + Collection jaxrsMethodInstances = index.getAnnotations(annotationType); + for (AnnotationInstance jaxrsMethodInstance : jaxrsMethodInstances) { + MethodInfo method = jaxrsMethodInstance.target().asMethod(); + Type returnType = method.returnType(); + if (!ResteasyServerCommonProcessor.isReflectionDeclarationRequiredFor(returnType) + || returnType.name().toString().startsWith("java.lang")) { + continue; + } - serialize.returnValue(null); - } + if (returnType instanceof ClassType) { + serializerCandidates.add(returnType.asClassType()); + continue; + } - // bridge method - try (MethodCreator bridgeSerialize = cc.getMethodCreator("serialize", "void", Object.class, JsonGenerator.class, - SerializationContext.class)) { - MethodDescriptor serialize = MethodDescriptor.ofMethod(generatedSerializerName, "serialize", "void", - classDotName.toString(), - JsonGenerator.class.getName(), SerializationContext.class.getName()); - ResultHandle castedObject = bridgeSerialize.checkCast(bridgeSerialize.getMethodParam(0), - classDotName.toString()); - bridgeSerialize.invokeVirtualMethod(serialize, bridgeSerialize.getThis(), - castedObject, bridgeSerialize.getMethodParam(1), bridgeSerialize.getMethodParam(2)); - bridgeSerialize.returnValue(null); + // we don't generate serializers for collection types since it would override the default ones + // we do however want to generate serializers for types that are captured by collections or Maps + if (CollectionUtil.isCollection(returnType.name())) { + Type genericType = CollectionUtil.getGenericType(returnType); + if (genericType instanceof ClassType) { + serializerCandidates.add(genericType.asClassType()); + } + } } } - - return supported == TypeSerializerGenerator.Supported.FULLY - ? GenerationResult.noReflectionNeeded(generatedSerializerName) - : GenerationResult.reflectionNeeded(generatedSerializerName); + return serializerCandidates; } - private GlobalSerializationConfig getGlobalConfig() { - return new GlobalSerializationConfig( - jsonbConfig.locale, jsonbConfig.dateFormat, jsonbConfig.serializeNullValues, jsonbConfig.propertyOrderStrategy); - } - - static class GenerationResult { - private final boolean generated; - private final String className; - private final boolean needsReflection; - - private GenerationResult(boolean generated, String className, boolean needsReflection) { - this.generated = generated; - this.className = className; - this.needsReflection = needsReflection; - } - - static GenerationResult notGenerated() { - return new GenerationResult(false, null, false); - } - - static GenerationResult noReflectionNeeded(String className) { - return new GenerationResult(true, className, false); - } - - static GenerationResult reflectionNeeded(String className) { - return new GenerationResult(true, className, true); - } - - boolean isGenerated() { - return generated; - } - - String getClassName() { - return className; - } - - boolean isNeedsReflection() { - return needsReflection; + private void validateConfiguration() { + if (!jsonbConfig.isValidPropertyOrderStrategy()) { + throw new IllegalArgumentException( + "quarkus.jsonb.property-order-strategy can only be one of " + JsonbConfig.ALLOWED_PROPERTY_ORDER_VALUES); } } diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializerClassGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializerClassGenerator.java new file mode 100644 index 0000000000000..7d46fc61d45e9 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializerClassGenerator.java @@ -0,0 +1,118 @@ +package io.quarkus.resteasy.jsonb.deployment; + +import javax.json.bind.serializer.JsonbSerializer; +import javax.json.bind.serializer.SerializationContext; +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.ClassType; +import org.jboss.jandex.DotName; + +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.deployment.serializers.GlobalSerializationConfig; +import io.quarkus.resteasy.jsonb.deployment.serializers.TypeSerializerGenerator; +import io.quarkus.resteasy.jsonb.deployment.serializers.TypeSerializerGeneratorRegistry; + +public class SerializerClassGenerator { + + private final JsonbConfig jsonbConfig; + + public SerializerClassGenerator(JsonbConfig jsonbConfig) { + this.jsonbConfig = jsonbConfig; + } + + public Result generateSerializerForClassType(ClassType classType, TypeSerializerGeneratorRegistry registry, + ClassOutput classOutput) { + TypeSerializerGenerator.Supported supported = registry.getObjectSerializer().supports(classType, registry); + if (supported == TypeSerializerGenerator.Supported.UNSUPPORTED) { + return Result.notGenerated(); + } + + DotName classDotName = classType.name(); + String generatedSerializerName = "io.quarkus.jsonb.serializers." + classDotName.withoutPackagePrefix() + "Serializer"; + try (ClassCreator cc = ClassCreator.builder() + .classOutput(classOutput).className(generatedSerializerName) + .interfaces(JsonbSerializer.class) + .signature(String.format("Ljava/lang/Object;Ljavax/json/bind/serializer/JsonbSerializer;", + classDotName.toString()).replace('.', '/')) + .build()) { + + // actual implementation of serialize method + try (MethodCreator serialize = cc.getMethodCreator("serialize", "void", classDotName.toString(), + JsonGenerator.class.getName(), SerializationContext.class.getName())) { + ResultHandle object = serialize.getMethodParam(0); + ResultHandle jsonGenerator = serialize.getMethodParam(1); + ResultHandle serializationContext = serialize.getMethodParam(2); + + // delegate to object serializer + registry.getObjectSerializer().generate( + new TypeSerializerGenerator.GenerateContext(classType, serialize, jsonGenerator, serializationContext, + object, registry, + getGlobalConfig(), false, null)); + + serialize.returnValue(null); + } + + // bridge method + try (MethodCreator bridgeSerialize = cc.getMethodCreator("serialize", "void", Object.class, JsonGenerator.class, + SerializationContext.class)) { + MethodDescriptor serialize = MethodDescriptor.ofMethod(generatedSerializerName, "serialize", "void", + classDotName.toString(), + JsonGenerator.class.getName(), SerializationContext.class.getName()); + ResultHandle castedObject = bridgeSerialize.checkCast(bridgeSerialize.getMethodParam(0), + classDotName.toString()); + bridgeSerialize.invokeVirtualMethod(serialize, bridgeSerialize.getThis(), + castedObject, bridgeSerialize.getMethodParam(1), bridgeSerialize.getMethodParam(2)); + bridgeSerialize.returnValue(null); + } + } + + return supported == TypeSerializerGenerator.Supported.FULLY + ? Result.noReflectionNeeded(generatedSerializerName) + : Result.reflectionNeeded(generatedSerializerName); + } + + private GlobalSerializationConfig getGlobalConfig() { + return new GlobalSerializationConfig( + jsonbConfig.locale, jsonbConfig.dateFormat, jsonbConfig.serializeNullValues, jsonbConfig.propertyOrderStrategy); + } + + static class Result { + private final boolean generated; + private final String className; + private final boolean needsReflection; + + private Result(boolean generated, String className, boolean needsReflection) { + this.generated = generated; + this.className = className; + this.needsReflection = needsReflection; + } + + static Result notGenerated() { + return new Result(false, null, false); + } + + static Result noReflectionNeeded(String className) { + return new Result(true, className, false); + } + + static Result reflectionNeeded(String className) { + return new Result(true, className, true); + } + + boolean isGenerated() { + return generated; + } + + String getClassName() { + return className; + } + + boolean isNeedsReflection() { + return needsReflection; + } + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/SerializerGeneratorUtil.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/SerializerGeneratorUtil.java index 39bbba9be3b05..60cc83358ad26 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/SerializerGeneratorUtil.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/SerializerGeneratorUtil.java @@ -5,7 +5,7 @@ import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; -import io.quarkus.resteasy.jsonb.deployment.AdditionalClassGenerator; +import io.quarkus.resteasy.jsonb.deployment.JsonbSupportClassGenerator; final class SerializerGeneratorUtil { @@ -16,7 +16,7 @@ static ResultHandle getLocaleHandle(String locale, BytecodeCreator bytecodeCreat if (locale == null) { // just use the default locale return bytecodeCreator - .invokeStaticMethod(MethodDescriptor.ofMethod(AdditionalClassGenerator.QUARKUS_DEFAULT_LOCALE_PROVIDER, + .invokeStaticMethod(MethodDescriptor.ofMethod(JsonbSupportClassGenerator.QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", Locale.class)); } From 8b833d27d00d853e552122ccbf3fb84535b80bad Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Sun, 21 Jul 2019 00:08:41 +0300 Subject: [PATCH 11/20] Add more type serializers --- .../resteasy/jsonb/deployment/DotNames.java | 12 +++ .../resteasy/jsonb/deployment/MapUtil.java | 62 +++++++++++ .../AbstractTypeSerializerGenerator.java | 6 +- .../BigDecimalTypeSerializerGenerator.java | 28 +++++ .../BooleanTypeSerializerGenerator.java | 32 ++++++ .../LongTypeSerializerGenerator.java | 33 ++++++ .../MapTypeSerializerGenerator.java | 102 ++++++++++++++++++ .../ObjectArrayTypeSerializerGenerator.java | 85 +++++++++++++++ .../ObjectTypeSerializerGenerator.java | 12 ++- .../OptionalTypeSerializerGenerator.java | 89 +++++++++++++++ ...imitiveBooleanTypeSerializerGenerator.java | 27 +++++ .../PrimitiveIntTypeSerializerGenerator.java | 27 +++++ .../PrimitiveLongTypeSerializerGenerator.java | 27 +++++ .../TypeSerializerGeneratorRegistry.java | 9 +- 14 files changed, 546 insertions(+), 5 deletions(-) create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/MapUtil.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/BigDecimalTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/BooleanTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/LongTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/MapTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectArrayTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/OptionalTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/PrimitiveBooleanTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/PrimitiveIntTypeSerializerGenerator.java create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/PrimitiveLongTypeSerializerGenerator.java diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java index 329c23d137a08..dde6dfbb79639 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java @@ -1,9 +1,12 @@ package io.quarkus.resteasy.jsonb.deployment; +import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.Collection; +import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import javax.json.bind.annotation.JsonbDateFormat; @@ -26,14 +29,23 @@ private DotNames() { public static final DotName OBJECT = DotName.createSimple(Object.class.getName()); public static final DotName STRING = DotName.createSimple(String.class.getName()); + public static final DotName PRIMITIVE_BOOLEAN = DotName.createSimple(boolean.class.getName()); + public static final DotName PRIMITIVE_INT = DotName.createSimple(int.class.getName()); + public static final DotName PRIMITIVE_LONG = DotName.createSimple(long.class.getName()); + public static final DotName BOOLEAN = DotName.createSimple(Boolean.class.getName()); public static final DotName INTEGER = DotName.createSimple(Integer.class.getName()); + public static final DotName LONG = DotName.createSimple(Long.class.getName()); + public static final DotName BIG_DECIMAL = DotName.createSimple(BigDecimal.class.getName()); public static final DotName LOCAL_DATE_TIME = DotName.createSimple(LocalDateTime.class.getName()); public static final DotName COLLECTION = DotName.createSimple(Collection.class.getName()); public static final DotName LIST = DotName.createSimple(List.class.getName()); public static final DotName SET = DotName.createSimple(Set.class.getName()); + public static final DotName OPTIONAL = DotName.createSimple(Optional.class.getName()); + public static final DotName MAP = DotName.createSimple(Map.class.getName()); + public static final DotName HASHMAP = DotName.createSimple(HashMap.class.getName()); public static final DotName CONTEXT_RESOLVER = DotName.createSimple(ContextResolver.class.getName()); diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/MapUtil.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/MapUtil.java new file mode 100644 index 0000000000000..dcdf5b9371da9 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/MapUtil.java @@ -0,0 +1,62 @@ +package io.quarkus.resteasy.jsonb.deployment; + +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +import org.jboss.jandex.DotName; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.Type; + +public final class MapUtil { + + private static final Set SUPPORTED_TYPES = new HashSet<>( + Arrays.asList(DotNames.MAP, DotNames.HASHMAP)); + + private MapUtil() { + } + + // TODO come up with a better way of determining if the type is supported + public static boolean isMap(DotName dotName) { + return SUPPORTED_TYPES.contains(dotName); + } + + /** + * @return the generic type of a collection + */ + public static MapTypes getGenericType(Type type) { + if (!isMap(type.name())) { + return null; + } + + if (!(type instanceof ParameterizedType)) { + return null; + } + + ParameterizedType parameterizedType = type.asParameterizedType(); + + if (parameterizedType.arguments().size() != 2) { + return null; + } + + return new MapTypes(parameterizedType.arguments().get(0), parameterizedType.arguments().get(1)); + } + + public static class MapTypes { + private final Type keyType; + private final Type valueType; + + public MapTypes(Type keyType, Type valueType) { + this.keyType = keyType; + this.valueType = valueType; + } + + public Type getKeyType() { + return keyType; + } + + public Type getValueType() { + return valueType; + } + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractTypeSerializerGenerator.java index 141febdeca9bc..6b61c77dd6b0c 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractTypeSerializerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractTypeSerializerGenerator.java @@ -2,6 +2,8 @@ import javax.json.stream.JsonGenerator; +import org.jboss.jandex.PrimitiveType; + import io.quarkus.gizmo.BranchResult; import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.MethodDescriptor; @@ -12,8 +14,8 @@ public abstract class AbstractTypeSerializerGenerator implements TypeSerializerG @Override public void generate(GenerateContext context) { - if (context.isNullChecked()) { - // null has already been checked in the previous level (when checking whether to write the key or not) + if (context.isNullChecked() // null has already been checked in the previous level (when checking whether to write the key or not) + || (context.getType() instanceof PrimitiveType)) { generateNotNull(context); } else { BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/BigDecimalTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/BigDecimalTypeSerializerGenerator.java new file mode 100644 index 0000000000000..9faddb0a1a605 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/BigDecimalTypeSerializerGenerator.java @@ -0,0 +1,28 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.resteasy.jsonb.deployment.DotNames; + +public class BigDecimalTypeSerializerGenerator extends AbstractNumberTypeSerializerGenerator { + + @Override + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.BIG_DECIMAL.equals(type.name()) ? Supported.FULLY : Supported.UNSUPPORTED; + } + + @Override + public void generateUnformatted(GenerateContext context) { + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "write", JsonGenerator.class, int.class), + context.getJsonGenerator(), + context.getCurrentItem()); + } + +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/BooleanTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/BooleanTypeSerializerGenerator.java new file mode 100644 index 0000000000000..354a5b13b6b4d --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/BooleanTypeSerializerGenerator.java @@ -0,0 +1,32 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.deployment.DotNames; + +public class BooleanTypeSerializerGenerator extends AbstractTypeSerializerGenerator { + + @Override + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.BOOLEAN.equals(type.name()) ? Supported.FULLY : Supported.UNSUPPORTED; + } + + @Override + protected void generateNotNull(GenerateContext context) { + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + + ResultHandle booleanValueHandle = bytecodeCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(Boolean.class, "booleanValue", boolean.class), + context.getCurrentItem()); + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "write", JsonGenerator.class, boolean.class), + context.getJsonGenerator(), + booleanValueHandle); + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/LongTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/LongTypeSerializerGenerator.java new file mode 100644 index 0000000000000..64b5c482aa9d5 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/LongTypeSerializerGenerator.java @@ -0,0 +1,33 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.deployment.DotNames; + +public class LongTypeSerializerGenerator extends AbstractNumberTypeSerializerGenerator { + + @Override + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.LONG.equals(type.name()) ? Supported.FULLY : Supported.UNSUPPORTED; + } + + @Override + public void generateUnformatted(GenerateContext context) { + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + + ResultHandle intValueHandle = bytecodeCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(Long.class, "longValue", long.class), + context.getCurrentItem()); + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "write", JsonGenerator.class, long.class), + context.getJsonGenerator(), + intValueHandle); + } + +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/MapTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/MapTypeSerializerGenerator.java new file mode 100644 index 0000000000000..e495e3f3b7cf0 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/MapTypeSerializerGenerator.java @@ -0,0 +1,102 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BranchResult; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.deployment.DotNames; +import io.quarkus.resteasy.jsonb.deployment.MapUtil; + +public class MapTypeSerializerGenerator extends AbstractTypeSerializerGenerator { + @Override + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + if (!MapUtil.isMap(type.name())) { + return Supported.UNSUPPORTED; + } + + MapUtil.MapTypes genericTypes = MapUtil.getGenericType(type); + if (genericTypes == null) { + return Supported.UNSUPPORTED; + } + + if (!DotNames.STRING.equals(genericTypes.getKeyType().name())) { + return Supported.UNSUPPORTED; + } + + TypeSerializerGenerator typeSerializerGenerator = registry.correspondingTypeSerializer(genericTypes.getValueType()); + return typeSerializerGenerator != null ? typeSerializerGenerator.supports(genericTypes.getValueType(), registry) + : Supported.UNSUPPORTED; + } + + @Override + protected void generateNotNull(GenerateContext context) { + MapUtil.MapTypes genericTypes = MapUtil.getGenericType(context.getType()); + if (genericTypes == null) { + throw new IllegalStateException("Could not generate serializer for collection type " + context.getType()); + } + + Type genericTypeOfValue = genericTypes.getValueType(); + TypeSerializerGenerator genericTypeSerializerGenerator = context.getRegistry() + .correspondingTypeSerializer(genericTypeOfValue); + if (genericTypeSerializerGenerator == null) { + throw new IllegalStateException("Could not generate serializer for generic type " + genericTypeOfValue.name() + + " of collection type" + context.getType().name()); + } + + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + ResultHandle jsonGenerator = context.getJsonGenerator(); + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeStartObject", JsonGenerator.class), + jsonGenerator); + + ResultHandle entrySet = bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Map.class, "entrySet", Set.class), + context.getCurrentItem()); + ResultHandle iterator = bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Set.class, "iterator", Iterator.class), + entrySet); + + BytecodeCreator loop = bytecodeCreator.createScope(); + + ResultHandle hasNext = loop.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Iterator.class, "hasNext", boolean.class), + iterator); + BranchResult branchResult = loop.ifNonZero(hasNext); + BytecodeCreator hasNextBranch = branchResult.trueBranch(); + BytecodeCreator noNextBranch = branchResult.falseBranch(); + + ResultHandle next = hasNextBranch.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Iterator.class, "next", Object.class), + iterator); + ResultHandle key = hasNextBranch.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Map.Entry.class, "getKey", Object.class), + next); + ResultHandle value = hasNextBranch.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Map.Entry.class, "getValue", Object.class), + next); + + ResultHandle keyAsString = hasNextBranch.invokeVirtualMethod( + MethodDescriptor.ofMethod(Object.class, "toString", String.class), + key); + hasNextBranch.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeKey", JsonGenerator.class, String.class), + jsonGenerator, keyAsString); + genericTypeSerializerGenerator.generate(context.changeItem(hasNextBranch, genericTypeOfValue, value, false)); + + hasNextBranch.continueScope(loop); + + noNextBranch.breakScope(loop); + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeEnd", JsonGenerator.class), jsonGenerator); + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectArrayTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectArrayTypeSerializerGenerator.java new file mode 100644 index 0000000000000..c10ef7940c1b1 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectArrayTypeSerializerGenerator.java @@ -0,0 +1,85 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.ArrayType; +import org.jboss.jandex.PrimitiveType; +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BranchResult; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; + +public class ObjectArrayTypeSerializerGenerator extends AbstractTypeSerializerGenerator { + + @Override + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + if (!(type instanceof ArrayType)) { + return Supported.UNSUPPORTED; + } + + Type componentType = type.asArrayType().component(); + if (componentType instanceof PrimitiveType) { + return Supported.UNSUPPORTED; + } + + TypeSerializerGenerator typeSerializerGenerator = registry.correspondingTypeSerializer(componentType); + return typeSerializerGenerator != null ? typeSerializerGenerator.supports(componentType, registry) + : Supported.UNSUPPORTED; + } + + @Override + public void generateNotNull(GenerateContext context) { + if (!(context.getType() instanceof ArrayType)) { + throw new IllegalStateException(context.getType().name() + " is not an array type"); + } + + Type componentType = context.getType().asArrayType().component(); + TypeSerializerGenerator genericTypeSerializerGenerator = context.getRegistry() + .correspondingTypeSerializer(componentType); + if (genericTypeSerializerGenerator == null) { + throw new IllegalStateException("Could not generate serializer for generic type " + componentType.name() + + " of collection type" + context.getType().name()); + } + + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + ResultHandle jsonGenerator = context.getJsonGenerator(); + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeStartArray", JsonGenerator.class), + jsonGenerator); + + ResultHandle asList = bytecodeCreator.invokeStaticMethod( + MethodDescriptor.ofMethod(Arrays.class, "asList", List.class, Object[].class), + context.getCurrentItem()); + + ResultHandle iterator = bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(List.class, "iterator", Iterator.class), + asList); + + BytecodeCreator loop = bytecodeCreator.createScope(); + + ResultHandle hasNext = loop.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Iterator.class, "hasNext", boolean.class), + iterator); + BranchResult branchResult = loop.ifNonZero(hasNext); + BytecodeCreator hasNextBranch = branchResult.trueBranch(); + BytecodeCreator noNextBranch = branchResult.falseBranch(); + ResultHandle next = hasNextBranch.invokeInterfaceMethod( + MethodDescriptor.ofMethod(Iterator.class, "next", Object.class), + iterator); + genericTypeSerializerGenerator.generate(context.changeItem(hasNextBranch, componentType, next, false)); + hasNextBranch.continueScope(loop); + + noNextBranch.breakScope(loop); + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeEnd", JsonGenerator.class), jsonGenerator); + } + +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java index b622c7db71e28..36680d0ad8500 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java @@ -5,6 +5,7 @@ import java.util.HashMap; import java.util.LinkedHashSet; import java.util.Map; +import java.util.Optional; import java.util.SortedMap; import java.util.TreeMap; @@ -70,7 +71,9 @@ public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { } private boolean canUseUnhandledTypeGenerator(Type type) { - return type instanceof ClassType; + // Parameterized types are unsupported because we don't have the proper Yasson metadata + // to tell Yasson the serializer to use + return type instanceof ClassType || type instanceof ArrayType; } @Override @@ -325,6 +328,13 @@ public void generate() { } else { // in this case we only write the property and value if the value is not null BytecodeCreator getterNotNull = bytecodeCreator.ifNull(getter).falseBranch(); + if (DotNames.OPTIONAL.equals(returnType.name())) { + ResultHandle isPresent = getterNotNull.invokeVirtualMethod( + MethodDescriptor.ofMethod(Optional.class, "isPresent", boolean.class), + getter); + + getterNotNull = getterNotNull.ifNonZero(isPresent).trueBranch(); + } writeKey(getterNotNull, jsonGenerator, input.getFinalKeyName()); getterTypeSerializerGenerator.generate(input.getContext().changeItem(getterNotNull, diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/OptionalTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/OptionalTypeSerializerGenerator.java new file mode 100644 index 0000000000000..630fd672b7eb1 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/OptionalTypeSerializerGenerator.java @@ -0,0 +1,89 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import java.util.List; +import java.util.Optional; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BranchResult; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.deployment.DotNames; + +public class OptionalTypeSerializerGenerator extends AbstractTypeSerializerGenerator { + + @Override + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + if (!DotNames.OPTIONAL.equals(type.name())) { + return Supported.UNSUPPORTED; + } + + if (!(type instanceof ParameterizedType)) { + return Supported.UNSUPPORTED; + } + + List typeArguments = type.asParameterizedType().arguments(); + if (typeArguments.size() != 1) { + return Supported.UNSUPPORTED; + } + + Type genericType = typeArguments.get(0); + TypeSerializerGenerator typeSerializerGenerator = registry.correspondingTypeSerializer(genericType); + return typeSerializerGenerator != null ? typeSerializerGenerator.supports(genericType, registry) + : Supported.UNSUPPORTED; + } + + @Override + protected void generateNotNull(GenerateContext context) { + if (!(context.getType() instanceof ParameterizedType)) { + throw new IllegalStateException("Could not generate serializer for type " + context.getType()); + } + + List arguments = context.getType().asParameterizedType().arguments(); + if (arguments.size() != 1) { + throw new IllegalStateException( + "Could not generate serializer for type " + context.getType() + " with generic arguments" + arguments); + } + + Type genericType = arguments.get(0); + TypeSerializerGenerator genericTypeSerializerGenerator = context.getRegistry() + .correspondingTypeSerializer(genericType); + if (genericTypeSerializerGenerator == null) { + throw new IllegalStateException("Could not generate serializer for generic type " + genericType.name() + + " of collection type" + context.getType().name()); + } + + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + if (context.isNullChecked()) { + doSerialize(context, genericType, genericTypeSerializerGenerator, bytecodeCreator); + } else { + ResultHandle isPresent = bytecodeCreator.invokeVirtualMethod( + MethodDescriptor.ofMethod(Optional.class, "isPresent", boolean.class), + context.getCurrentItem()); + + BytecodeCreator ifScope = bytecodeCreator.createScope(); + BranchResult isPresentBranch = ifScope.ifNonZero(isPresent); + BytecodeCreator isPresentFalse = isPresentBranch.falseBranch(); + isPresentFalse.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeNull", JsonGenerator.class), + context.getJsonGenerator()); + isPresentFalse.breakScope(ifScope); + + BytecodeCreator isPresentTrue = isPresentBranch.trueBranch(); + doSerialize(context, genericType, genericTypeSerializerGenerator, isPresentTrue); + isPresentTrue.breakScope(ifScope); + } + } + + private void doSerialize(GenerateContext context, Type genericType, TypeSerializerGenerator genericTypeSerializerGenerator, + BytecodeCreator isPresentTrue) { + ResultHandle item = isPresentTrue.invokeVirtualMethod( + MethodDescriptor.ofMethod(Optional.class, "get", Object.class), + context.getCurrentItem()); + genericTypeSerializerGenerator.generate(context.changeItem(isPresentTrue, genericType, item, true)); + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/PrimitiveBooleanTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/PrimitiveBooleanTypeSerializerGenerator.java new file mode 100644 index 0000000000000..00ff64c61f2d3 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/PrimitiveBooleanTypeSerializerGenerator.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.resteasy.jsonb.deployment.DotNames; + +public class PrimitiveBooleanTypeSerializerGenerator extends AbstractNumberTypeSerializerGenerator { + + @Override + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.PRIMITIVE_BOOLEAN.equals(type.name()) ? Supported.FULLY : Supported.UNSUPPORTED; + } + + @Override + public void generateUnformatted(GenerateContext context) { + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "write", JsonGenerator.class, boolean.class), + context.getJsonGenerator(), + context.getCurrentItem()); + } + +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/PrimitiveIntTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/PrimitiveIntTypeSerializerGenerator.java new file mode 100644 index 0000000000000..c9fd79e1e8e90 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/PrimitiveIntTypeSerializerGenerator.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.resteasy.jsonb.deployment.DotNames; + +public class PrimitiveIntTypeSerializerGenerator extends AbstractNumberTypeSerializerGenerator { + + @Override + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.PRIMITIVE_INT.equals(type.name()) ? Supported.FULLY : Supported.UNSUPPORTED; + } + + @Override + public void generateUnformatted(GenerateContext context) { + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "write", JsonGenerator.class, int.class), + context.getJsonGenerator(), + context.getCurrentItem()); + } + +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/PrimitiveLongTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/PrimitiveLongTypeSerializerGenerator.java new file mode 100644 index 0000000000000..d1d844237df50 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/PrimitiveLongTypeSerializerGenerator.java @@ -0,0 +1,27 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import javax.json.stream.JsonGenerator; + +import org.jboss.jandex.Type; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.resteasy.jsonb.deployment.DotNames; + +public class PrimitiveLongTypeSerializerGenerator extends AbstractNumberTypeSerializerGenerator { + + @Override + public Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.PRIMITIVE_LONG.equals(type.name()) ? Supported.FULLY : Supported.UNSUPPORTED; + } + + @Override + public void generateUnformatted(GenerateContext context) { + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "write", JsonGenerator.class, long.class), + context.getJsonGenerator(), + context.getCurrentItem()); + } + +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java index 5c91e0f9eaf66..4bfe381a39a89 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java @@ -13,9 +13,14 @@ public final class TypeSerializerGeneratorRegistry { private final TypeSerializerGenerator objectSerializer = new ObjectTypeSerializerGenerator(); private final List typeSerializerGenerators = Arrays.asList(new StringTypeSerializerGenerator(), - new IntegerTypeSerializerGenerator(), new LocalDateTimeSerializerGenerator(), + new PrimitiveIntTypeSerializerGenerator(), new PrimitiveLongTypeSerializerGenerator(), + new PrimitiveBooleanTypeSerializerGenerator(), + new BooleanTypeSerializerGenerator(), new IntegerTypeSerializerGenerator(), new LongTypeSerializerGenerator(), + new BigDecimalTypeSerializerGenerator(), + new LocalDateTimeSerializerGenerator(), objectSerializer, - new CollectionTypeSerializerGenerator()); + new ObjectArrayTypeSerializerGenerator(), new CollectionTypeSerializerGenerator(), + new MapTypeSerializerGenerator(), new OptionalTypeSerializerGenerator()); private final SerializationClassInspector inspector; From 54d7aee5b4bf5c219b14a9e084108e4cb2209f90 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 23 Jul 2019 01:13:26 +0300 Subject: [PATCH 12/20] Add support for serializing public fields --- .../ObjectTypeSerializerGenerator.java | 113 ++++++++++++++++-- 1 file changed, 106 insertions(+), 7 deletions(-) diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java index 36680d0ad8500..882a08eca0031 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java @@ -16,6 +16,7 @@ import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ArrayType; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; @@ -23,6 +24,7 @@ import org.jboss.jandex.Type; import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.FieldDescriptor; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; import io.quarkus.resteasy.jsonb.deployment.DotNames; @@ -93,15 +95,17 @@ protected void generateNotNull(GenerateContext context) { } // instead of generating the bytecode for each property right away, we instead introduce - // a Generator interface that will do the job on demand + // a Generator interface that will do the job on lazily // this allows us to add the keys from both getters and fields and have them both sorted + // using the proper strategy SortedMap> propertyNameToGenerator = PropertyOrderStrategy.REVERSE .equalsIgnoreCase(context.getGlobalConfig().getPropertyOrderStrategy()) ? new TreeMap<>(Collections.reverseOrder()) - : new TreeMap<>(); + : new TreeMap<>(); //use lexicographical order by default Map defaultToFinaKeyName = new HashMap<>(); Map finalToDefaultKeyName = new HashMap<>(); + // setup getter generation for (Map.Entry entry : inspectionResult.getGetters().entrySet()) { MethodInfo getterMethodInfo = entry.getKey(); FieldInfo fieldInfo = entry.getValue(); @@ -133,9 +137,38 @@ protected void generateNotNull(GenerateContext context) { context, getterMethodInfo, fieldInfo, getterTypeSerializerGenerator, finalKeyName, isNillable))); } - // TODO serialize fields + // setup field generation + for (FieldInfo fieldInfo : inspectionResult.getVisibleFieldsWithoutGetters()) { + Type fieldType = fieldInfo.type(); + String defaultKeyName = fieldInfo.name(); + TypeSerializerGenerator getterTypeSerializerGenerator = serializerRegistry.correspondingTypeSerializer(fieldType); + if (getterTypeSerializerGenerator == null) { + if (canUseUnhandledTypeGenerator(fieldType)) { + getterTypeSerializerGenerator = new UnhandledTypeGenerator(context.getType(), defaultKeyName); + } else { + throw new IllegalStateException("Could not generate serializer for field " + defaultKeyName + + " of type " + classDotNate); + } + } + + Map effectiveGetterAnnotations = getEffectiveFieldAnnotations(fieldInfo, + serializerRegistry.getInspector()); + String finalKeyName = getFinalKeyName(defaultKeyName, effectiveGetterAnnotations); + + defaultToFinaKeyName.put(defaultKeyName, finalKeyName); + finalToDefaultKeyName.put(finalKeyName, defaultKeyName); + + boolean isNillable = isPropertyNillable(effectiveGetterAnnotations.get(DotNames.JSONB_PROPERTY), + context.getGlobalConfig(), inspectionResult); + + propertyNameToGenerator.put( + finalKeyName, + new FieldGenerator(new GeneratorInput<>( + context, fieldInfo, null, getterTypeSerializerGenerator, finalKeyName, isNillable))); + } // TODO handle @JsonbPropertyOrder meta-annotations + // setup the properties in the correct order if the @JsonbPropertyOrder annotation is used if (inspectionResult.getEffectiveClassAnnotations().containsKey(DotNames.JSONB_PROPERTY_ORDER)) { LinkedHashSet customOrder = new LinkedHashSet<>(); AnnotationInstance annotationInstance = inspectionResult.getEffectiveClassAnnotations() @@ -238,15 +271,35 @@ private static Map getEffectiveGetterAnnotations(Me } } - Map effectiveClassAnnotations = inspector.inspect(getterMethodInfo.declaringClass().name()) + addEffectiveClassAnnotations(inspector, getterMethodInfo.declaringClass(), result); + + return result; + } + + private static Map getEffectiveFieldAnnotations(FieldInfo fieldInfo, + SerializationClassInspector inspector) { + Map result = new HashMap<>(); + + for (AnnotationInstance annotationInstance : fieldInfo.annotations()) { + if (!result.containsKey(annotationInstance.name())) { + result.put(annotationInstance.name(), annotationInstance); + } + } + + addEffectiveClassAnnotations(inspector, fieldInfo.declaringClass(), result); + + return result; + } + + private static void addEffectiveClassAnnotations(SerializationClassInspector inspector, ClassInfo classInfo, + Map result) { + Map effectiveClassAnnotations = inspector.inspect(classInfo.name()) .getEffectiveClassAnnotations(); for (DotName classAnnotationDotName : effectiveClassAnnotations.keySet()) { if (!result.containsKey(classAnnotationDotName)) { result.put(classAnnotationDotName, effectiveClassAnnotations.get(classAnnotationDotName)); } } - - return result; } private static class GeneratorInput { @@ -300,7 +353,7 @@ private static class GetterGenerator implements Generator input; - public GetterGenerator(GeneratorInput input) { + GetterGenerator(GeneratorInput input) { this.input = input; } @@ -343,4 +396,50 @@ public void generate() { } } + private static class FieldGenerator implements Generator> { + + private GeneratorInput input; + + FieldGenerator(GeneratorInput input) { + this.input = input; + } + + @Override + public void generate() { + BytecodeCreator bytecodeCreator = input.getContext().getBytecodeCreator(); + ResultHandle jsonGenerator = input.getContext().getJsonGenerator(); + DotName classDotNate = input.getContext().getType().name(); + FieldInfo fieldInfo = input.getInstanceInfo(); + Type fieldType = fieldInfo.type(); + TypeSerializerGenerator fieldTypeSerializerGenerator = input.getTypeSerializerGenerator(); + + ResultHandle field = bytecodeCreator.readInstanceField( + FieldDescriptor.of(classDotNate.toString(), fieldInfo.name(), + fieldType.name().toString()), + input.getContext().getCurrentItem()); + + Map effectivePropertyAnnotations = getEffectiveFieldAnnotations(fieldInfo, + input.getContext().getRegistry().getInspector()); + if (input.isNillable()) { + writeKey(bytecodeCreator, jsonGenerator, input.getFinalKeyName()); + fieldTypeSerializerGenerator.generate(input.getContext().changeItem( + bytecodeCreator, fieldType, field, false, effectivePropertyAnnotations)); + } else { + // in this case we only write the property and value if the value is not null + BytecodeCreator fieldNotNull = bytecodeCreator.ifNull(field).falseBranch(); + if (DotNames.OPTIONAL.equals(fieldType.name())) { + ResultHandle isPresent = fieldNotNull.invokeVirtualMethod( + MethodDescriptor.ofMethod(Optional.class, "isPresent", boolean.class), + field); + + fieldNotNull = fieldNotNull.ifNonZero(isPresent).trueBranch(); + } + + writeKey(fieldNotNull, jsonGenerator, input.getFinalKeyName()); + fieldTypeSerializerGenerator.generate(input.getContext().changeItem(fieldNotNull, + fieldType, field, true, effectivePropertyAnnotations)); + } + } + } + } From 34f0050ac8cdf6531fc625c22c2eb6615a6efb76 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 23 Jul 2019 23:47:10 +0300 Subject: [PATCH 13/20] Provide ability to generate serializer for single impl interface --- .../deployment/ResteasyJsonbProcessor.java | 5 ++- .../SerializationClassInspector.java | 21 +++++++++-- .../deployment/SerializerClassGenerator.java | 37 ++++++++++++------- .../ObjectTypeSerializerGenerator.java | 14 ++++++- .../serializers/TypeSerializerGenerator.java | 5 +++ 5 files changed, 61 insertions(+), 21 deletions(-) diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index 65c91462cd800..767fd508d7fa4 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -92,9 +92,10 @@ public void write(String name, byte[] data) { typeSerializerGeneratorRegistry, classOutput); if (generationResult.isGenerated()) { - typeToGeneratedSerializers.put(type.name().toString(), generationResult.getClassName()); + typeToGeneratedSerializers.put(generationResult.getClassActuallyUsed().toString(), + generationResult.getGeneratedClassName()); if (!generationResult.isNeedsReflection()) { - typesThatDontNeedReflection.add(type.name().toString()); + typesThatDontNeedReflection.add(generationResult.getClassActuallyUsed().toString()); } } } diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java index d79762893f401..52e032adae0a2 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java @@ -38,12 +38,25 @@ public Result inspect(DotName classDotName) { return SerializationClassInspector.Result.notPossible(classInfo); } - if (Modifier.isInterface(classInfo.flags()) || !index.getAllKnownSubclasses(classDotName).isEmpty()) { + if (Modifier.isInterface(classInfo.flags())) { + Collection allKnownImplementors = index.getAllKnownImplementors(classInfo.name()); + if (allKnownImplementors.size() != 1) { + // when the type is an interface than we can correctly generate a serializer at build time + // if there is a single implementation + // TODO investigate if this can possible be relaxed by checking and comparing all fields, getters and + // class annotations of the implementations + return SerializationClassInspector.Result.notPossible(classInfo); + } else { + return inspect(allKnownImplementors.iterator().next().name()); + } + } + + if (!index.getAllKnownSubclasses(classDotName).isEmpty()) { // if the class is an interface or is subclassed we ignore it because json-b // adds all the properties of the implementation or subclasses (which we can't know) // TODO investigate if we could relax these constraints by checking if there - // there are no implementations or subclasses that contain properties other than those - // of the interface or class + // there are no implementations or subclasses that contain properties other than those + // of the interface or class return SerializationClassInspector.Result.notPossible(classInfo); } @@ -52,7 +65,7 @@ public Result inspect(DotName classDotName) { } if (!DotNames.OBJECT.equals(classInfo.superName())) { - // for now don't handle classes with super types other than object, too many corner cases + // for now don't handle classes with super types other than object because there are too many corner cases return SerializationClassInspector.Result.notPossible(classInfo); } diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializerClassGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializerClassGenerator.java index 7d46fc61d45e9..f6d4aa3bdbb02 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializerClassGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializerClassGenerator.java @@ -31,7 +31,12 @@ public Result generateSerializerForClassType(ClassType classType, TypeSerializer return Result.notGenerated(); } - DotName classDotName = classType.name(); + // use the inspection result because it gives us the actual type that we need to generate a serializer for + // this is needed in case the input is an interface in which case we actually generate a serializer for the single + // implementation + SerializationClassInspector.Result inspectionResult = registry.getInspector().inspect(classType.name()); + + DotName classDotName = inspectionResult.getClassInfo().name(); String generatedSerializerName = "io.quarkus.jsonb.serializers." + classDotName.withoutPackagePrefix() + "Serializer"; try (ClassCreator cc = ClassCreator.builder() .classOutput(classOutput).className(generatedSerializerName) @@ -71,8 +76,8 @@ public Result generateSerializerForClassType(ClassType classType, TypeSerializer } return supported == TypeSerializerGenerator.Supported.FULLY - ? Result.noReflectionNeeded(generatedSerializerName) - : Result.reflectionNeeded(generatedSerializerName); + ? Result.noReflectionNeeded(classDotName, generatedSerializerName) + : Result.reflectionNeeded(classDotName, generatedSerializerName); } private GlobalSerializationConfig getGlobalConfig() { @@ -82,33 +87,39 @@ private GlobalSerializationConfig getGlobalConfig() { static class Result { private final boolean generated; - private final String className; + private final DotName classActuallyUsed; // will be the input class if that was a regular class or the single implementation if it was an interface + private final String generatedClassName; private final boolean needsReflection; - private Result(boolean generated, String className, boolean needsReflection) { + private Result(boolean generated, DotName classActuallyUsed, String generatedClassName, boolean needsReflection) { this.generated = generated; - this.className = className; + this.classActuallyUsed = classActuallyUsed; + this.generatedClassName = generatedClassName; this.needsReflection = needsReflection; } static Result notGenerated() { - return new Result(false, null, false); + return new Result(false, null, null, false); } - static Result noReflectionNeeded(String className) { - return new Result(true, className, false); + static Result noReflectionNeeded(DotName classActuallyUsed, String generatedClassName) { + return new Result(true, classActuallyUsed, generatedClassName, false); } - static Result reflectionNeeded(String className) { - return new Result(true, className, true); + static Result reflectionNeeded(DotName classActuallyUsed, String generatedClassName) { + return new Result(true, classActuallyUsed, generatedClassName, true); } boolean isGenerated() { return generated; } - String getClassName() { - return className; + public DotName getClassActuallyUsed() { + return classActuallyUsed; + } + + String getGeneratedClassName() { + return generatedClassName; } boolean isNeedsReflection() { diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java index 882a08eca0031..355cca53223bc 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java @@ -1,5 +1,6 @@ package io.quarkus.resteasy.jsonb.deployment.serializers; +import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; @@ -82,8 +83,6 @@ private boolean canUseUnhandledTypeGenerator(Type type) { protected void generateNotNull(GenerateContext context) { BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); ResultHandle jsonGenerator = context.getJsonGenerator(); - bytecodeCreator.invokeInterfaceMethod( - MethodDescriptor.ofMethod(JsonGenerator.class, "writeStartObject", JsonGenerator.class), jsonGenerator); TypeSerializerGeneratorRegistry serializerRegistry = context.getRegistry(); DotName classDotNate = context.getType().name(); @@ -94,6 +93,17 @@ protected void generateNotNull(GenerateContext context) { throw new IllegalStateException("Could not generate serializer for " + classDotNate); } + // if the type is an interface, we need to cast to the actual type that will be used + ClassInfo classInfo = context.getRegistry().getIndex().getClassByName(context.getType().name()); + if (Modifier.isInterface(classInfo.flags())) { + ClassInfo concreteType = inspectionResult.getClassInfo(); + ResultHandle castedToConcrete = bytecodeCreator.checkCast(context.getCurrentItem(), concreteType.name().toString()); + context = context.changeItem(Type.create(concreteType.name(), Type.Kind.CLASS), castedToConcrete); + } + + bytecodeCreator.invokeInterfaceMethod( + MethodDescriptor.ofMethod(JsonGenerator.class, "writeStartObject", JsonGenerator.class), jsonGenerator); + // instead of generating the bytecode for each property right away, we instead introduce // a Generator interface that will do the job on lazily // this allows us to add the keys from both getters and fields and have them both sorted diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java index 00eb1abd39c18..023d511d6bb6a 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java @@ -85,6 +85,11 @@ Map getEffectivePropertyAnnotations() { return effectivePropertyAnnotations; } + GenerateContext changeItem(Type newType, ResultHandle newItem) { + return new GenerateContext(newType, bytecodeCreator, jsonGenerator, serializationContext, newItem, registry, + globalConfig, nullChecked, effectivePropertyAnnotations); + } + GenerateContext changeItem(BytecodeCreator newBytecodeCreator, Type newType, ResultHandle newCurrentItem, boolean newNullChecked) { return new GenerateContext(newType, newBytecodeCreator, jsonGenerator, serializationContext, newCurrentItem, From 218fd13a413007d69bb1a97dc6b1f8bd06a98e21 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 24 Jul 2019 00:33:55 +0300 Subject: [PATCH 14/20] Provide ability to generate serializer for subclasses --- .../SerializationClassInspector.java | 147 ++++++++++++++---- 1 file changed, 121 insertions(+), 26 deletions(-) diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java index 52e032adae0a2..70bf5072f4fbf 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java @@ -1,16 +1,20 @@ package io.quarkus.resteasy.jsonb.deployment; import java.lang.reflect.Modifier; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; import org.jboss.jandex.FieldInfo; import org.jboss.jandex.IndexView; @@ -21,7 +25,6 @@ public class SerializationClassInspector { private final Map classInspectionResultMap = new HashMap<>(); - private final IndexView index; public SerializationClassInspector(IndexView index) { @@ -29,6 +32,10 @@ public SerializationClassInspector(IndexView index) { } public Result inspect(DotName classDotName) { + return inspect(classDotName, true); + } + + private Result inspect(DotName classDotName, boolean checkSubtypes) { ClassInfo classInfo = index.getClassByName(classDotName); if (classInfo == null) { return SerializationClassInspector.Result.notPossible(classInfo); @@ -44,19 +51,19 @@ public Result inspect(DotName classDotName) { // when the type is an interface than we can correctly generate a serializer at build time // if there is a single implementation // TODO investigate if this can possible be relaxed by checking and comparing all fields, getters and - // class annotations of the implementations + // class annotations of the implementations or perhaps by generating serializers for all implementations? return SerializationClassInspector.Result.notPossible(classInfo); } else { return inspect(allKnownImplementors.iterator().next().name()); } } - if (!index.getAllKnownSubclasses(classDotName).isEmpty()) { - // if the class is an interface or is subclassed we ignore it because json-b + if (checkSubtypes && !index.getAllKnownSubclasses(classDotName).isEmpty()) { + // if the class is subclassed we ignore it because json-b // adds all the properties of the implementation or subclasses (which we can't know) // TODO investigate if we could relax these constraints by checking if there - // there are no implementations or subclasses that contain properties other than those - // of the interface or class + // there are no subclasses that contain properties other than those + // of the interface or class. Another idea is to generate serializers for all subclasses return SerializationClassInspector.Result.notPossible(classInfo); } @@ -64,9 +71,13 @@ public Result inspect(DotName classDotName) { return classInspectionResultMap.get(classInfo.name()); } + Result superClassResult = null; if (!DotNames.OBJECT.equals(classInfo.superName())) { - // for now don't handle classes with super types other than object because there are too many corner cases - return SerializationClassInspector.Result.notPossible(classInfo); + superClassResult = inspect(classInfo.superName(), false); + if (!superClassResult.isPossible()) { + return SerializationClassInspector.Result.notPossible(classInfo); + } + classInspectionResultMap.put(classInfo.superName(), superClassResult); } if (JandexUtil.containsInterfacesWithDefaultMethods(classInfo, index)) { @@ -94,25 +105,25 @@ public Result inspect(DotName classDotName) { if (removeTransientGetters(getters)) { return SerializationClassInspector.Result.notPossible(classInfo); } - for (MethodInfo methodInfo : getters.keySet()) { - // currently we don't support generating serializers that contains items of the same class - if (hasReferenceToClassType(methodInfo.returnType(), classDotName)) { - return Result.notPossible(classInfo); - } - } List fields = PropertyUtil.getPublicFieldsWithoutGetters(classInfo, getters.keySet()); if (removeTransientFields(fields)) { return SerializationClassInspector.Result.notPossible(classInfo); } - for (FieldInfo field : fields) { - if (hasReferenceToClassType(field.type(), classDotName)) { - return Result.notPossible(classInfo); - } + + // we don't support generating a serializer when the class to be serialized is referenced as a field + // or getter + if (hasCyclicReferences(getters.keySet(), fields, classDotName)) { + return SerializationClassInspector.Result.notPossible(classInfo); } SerializationClassInspector.Result result = SerializationClassInspector.Result.possible(classInfo, effectiveClassAnnotations, getters, fields); + + if (superClassResult != null) { + result = result.merge(superClassResult); + } + classInspectionResultMap.put(classInfo.name(), result); return result; } @@ -197,19 +208,60 @@ private boolean removeTransientGetters(Map getters) { return false; } - // check if type is the same as the class type or is a generic type that references it - private boolean hasReferenceToClassType(Type type, DotName classDotName) { - if (type.name().equals(classDotName)) { - // we don't support serializing types that contain serializable items of the same class - return true; + // checks whether the methods or fields contain any references back to the class + // this check is recursive meaning that the methods and fields are also checked in turn for cyclic references + private boolean hasCyclicReferences(Collection methods, Collection fields, DotName classDotName) { + return hasCyclicReferences(methods, fields, Collections.singleton(classDotName)); + } + + private boolean hasCyclicReferences(Collection methods, Collection fields, Set candidates) { + Set additionalTypesToCheck = new HashSet<>(); + // first check if any of the fields or methods contain direct references to the candidates + for (MethodInfo method : methods) { + if (containedInCandidates(method.returnType(), candidates, additionalTypesToCheck)) { + return true; + } + } + for (FieldInfo field : fields) { + if (containedInCandidates(field.type(), candidates, additionalTypesToCheck)) { + return true; + } + } + // now recursively check the class types of fields and methods to see if any of their + // fields or methods contain references + for (DotName dotName : additionalTypesToCheck) { + ClassInfo classInfo = index.getClassByName(dotName); + if (classInfo == null) { + continue; + } + Set newMethods = PropertyUtil.getGetterMethods(classInfo).keySet(); + List newFields = PropertyUtil.getPublicFieldsWithoutGetters(classInfo, newMethods); + Set newCandidates = new HashSet<>(candidates); + newCandidates.add(classInfo.name()); + if (hasCyclicReferences(newMethods, newFields, newCandidates)) { + return true; + } + } + + return false; + } + + private boolean containedInCandidates(Type type, Set candidates, Set additionalTypesToCheck) { + if (type instanceof ClassType) { + if (candidates.contains(type.name())) { + return true; + } else { + additionalTypesToCheck.add(type.name()); + } } else if (type instanceof ParameterizedType) { - ParameterizedType parameterizedType = type.asParameterizedType(); - for (Type argumentType : parameterizedType.arguments()) { - if (hasReferenceToClassType(argumentType, classDotName)) { + List argumentTypes = type.asParameterizedType().arguments(); + for (Type argumentType : argumentTypes) { + if (containedInCandidates(argumentType, candidates, additionalTypesToCheck)) { return true; } } } + return false; } @@ -263,5 +315,48 @@ public Map getGetters() { public Collection getVisibleFieldsWithoutGetters() { return visibleFieldsWithoutGetters; } + + /** + * Merge info from other result with lower priority, which means that info from this object + * takes precedence if conflicting data exists + */ + public Result merge(Result lowerPriorityResult) { + if (!(this.isPossible && lowerPriorityResult.isPossible)) { + throw new IllegalArgumentException("merge can only be used on Result objects who have isPossible = true"); + } + + Map finalEffectiveClassAnnotations = new HashMap<>(effectiveClassAnnotations); + for (DotName dotName : lowerPriorityResult.getEffectiveClassAnnotations().keySet()) { + if (!finalEffectiveClassAnnotations.containsKey(dotName)) { + finalEffectiveClassAnnotations.put(dotName, + lowerPriorityResult.getEffectiveClassAnnotations().get(dotName)); + } + } + + Map finalGetters = new HashMap<>(getters); + Set getterNames = new HashSet<>(getters.size()); + for (MethodInfo methodInfo : getters.keySet()) { + getterNames.add(methodInfo.name()); + } + for (MethodInfo methodInfo : lowerPriorityResult.getGetters().keySet()) { + if (!getterNames.contains(methodInfo.name())) { + finalGetters.put(methodInfo, lowerPriorityResult.getGetters().get(methodInfo)); + } + } + + Collection finalVisibleFieldsWithoutGetters = new ArrayList<>(visibleFieldsWithoutGetters); + Set fieldNames = new HashSet<>(visibleFieldsWithoutGetters.size()); + for (FieldInfo fieldInfo : visibleFieldsWithoutGetters) { + fieldNames.add(fieldInfo.name()); + } + for (FieldInfo fieldInfo : lowerPriorityResult.getVisibleFieldsWithoutGetters()) { + if (!fieldNames.contains(fieldInfo.name())) { + finalVisibleFieldsWithoutGetters.add(fieldInfo); + } + } + + return new Result(this.classInfo, true, finalEffectiveClassAnnotations, finalGetters, + finalVisibleFieldsWithoutGetters); + } } } From 6d9804e26dd133fb85f6df5c2bd945f1df4c0dc3 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 24 Jul 2019 01:28:20 +0300 Subject: [PATCH 15/20] Ensure serializers are only generated for json producing jax-rs methods --- .../deployment/ResteasyJsonbProcessor.java | 35 +++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index 767fd508d7fa4..bb72a8c72e10a 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -9,9 +9,11 @@ import java.util.Set; import javax.json.bind.Jsonb; +import javax.ws.rs.core.MediaType; import javax.ws.rs.ext.Provider; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassType; import org.jboss.jandex.DotName; @@ -34,10 +36,9 @@ public class ResteasyJsonbProcessor { - @BuildStep(providesCapabilities = Capabilities.RESTEASY_JSON_EXTENSION) - private static final String CONTEXT_RESOLVER = "io.quarkus.jsonb.QuarkusJsonbContextResolver"; + private static final DotName JAX_RS_PRODUCES = DotName.createSimple("javax.ws.rs.Produces"); - @BuildStep(providesCapabilities = Capabilities.RESTEASY_JSON_EXTENSION) + @BuildStep void build(BuildProducer feature) { feature.produce(new FeatureBuildItem(FeatureBuildItem.RESTEASY_JSONB)); } @@ -150,6 +151,11 @@ private Set determineSerializationCandidates(IndexView index) { Collection jaxrsMethodInstances = index.getAnnotations(annotationType); for (AnnotationInstance jaxrsMethodInstance : jaxrsMethodInstances) { MethodInfo method = jaxrsMethodInstance.target().asMethod(); + + if (!producesJson(method)) { + continue; + } + Type returnType = method.returnType(); if (!ResteasyServerCommonProcessor.isReflectionDeclarationRequiredFor(returnType) || returnType.name().toString().startsWith("java.lang")) { @@ -174,6 +180,29 @@ private Set determineSerializationCandidates(IndexView index) { return serializerCandidates; } + private boolean producesJson(MethodInfo method) { + AnnotationInstance produces = method.annotation(JAX_RS_PRODUCES); + if (produces == null) { + method.declaringClass().classAnnotation(JAX_RS_PRODUCES); + } + if (produces == null) { + return false; + } + + AnnotationValue value = produces.value(); + if (value == null) { + return false; + } + + for (String mediaTypeStr : value.asStringArray()) { + MediaType mediaType = MediaType.valueOf(mediaTypeStr); + if (MediaType.APPLICATION_JSON_TYPE.equals(mediaType)) { + return true; + } + } + return false; + } + private void validateConfiguration() { if (!jsonbConfig.isValidPropertyOrderStrategy()) { throw new IllegalArgumentException( From 9fe01ae64e9228cf66e31ee3764171a53b3d6cdb Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 24 Jul 2019 20:59:31 +0300 Subject: [PATCH 16/20] Add tests for jsonb handling --- integration-tests/pom.xml | 1 + integration-tests/resteasy-jsonb/pom.xml | 112 ++++++++++++++++++ .../io/quarkus/it/resteasy/jsonb/Animal.java | 21 ++++ .../io/quarkus/it/resteasy/jsonb/Cat.java | 17 +++ .../it/resteasy/jsonb/CatResource.java | 18 +++ .../io/quarkus/it/resteasy/jsonb/Coffee.java | 97 +++++++++++++++ .../it/resteasy/jsonb/CoffeeResource.java | 25 ++++ .../io/quarkus/it/resteasy/jsonb/Country.java | 31 +++++ .../io/quarkus/it/resteasy/jsonb/Greeter.java | 14 +++ .../it/resteasy/jsonb/GreeterResource.java | 15 +++ .../quarkus/it/resteasy/jsonb/Greeting.java | 21 ++++ .../io/quarkus/it/resteasy/jsonb/HasName.java | 6 + .../it/resteasy/jsonb/HasNameResource.java | 15 +++ .../io/quarkus/it/resteasy/jsonb/Person.java | 22 ++++ .../io/quarkus/it/resteasy/jsonb/Seller.java | 20 ++++ .../src/main/resources/application.properties | 1 + .../it/resteasy/jsonb/ComplexObjectTest.java | 47 ++++++++ .../jsonb/InterfaceImplementationTest.java | 24 ++++ .../io/quarkus/it/resteasy/jsonb/JaxRsIT.java | 7 ++ .../quarkus/it/resteasy/jsonb/JaxRsTest.java | 41 +++++++ .../jsonb/SerializerNotGeneratedTest.java | 24 ++++ .../it/resteasy/jsonb/SubclassTest.java | 24 ++++ .../quarkus/it/resteasy/jsonb/TestUtil.java | 48 ++++++++ 23 files changed, 651 insertions(+) create mode 100644 integration-tests/resteasy-jsonb/pom.xml create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Animal.java create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Cat.java create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/CatResource.java create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Coffee.java create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/CoffeeResource.java create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Country.java create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Greeter.java create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/GreeterResource.java create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Greeting.java create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/HasName.java create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/HasNameResource.java create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Person.java create mode 100644 integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Seller.java create mode 100644 integration-tests/resteasy-jsonb/src/main/resources/application.properties create mode 100644 integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/ComplexObjectTest.java create mode 100644 integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/InterfaceImplementationTest.java create mode 100644 integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/JaxRsIT.java create mode 100644 integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/JaxRsTest.java create mode 100644 integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/SerializerNotGeneratedTest.java create mode 100644 integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/SubclassTest.java create mode 100644 integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/TestUtil.java diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index a75e03ceb5113..7d07f72339c8d 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -57,6 +57,7 @@ mongodb-client jackson resteasy-jackson + resteasy-jsonb jgit virtual-http artemis-core diff --git a/integration-tests/resteasy-jsonb/pom.xml b/integration-tests/resteasy-jsonb/pom.xml new file mode 100644 index 0000000000000..147fc604e5ec9 --- /dev/null +++ b/integration-tests/resteasy-jsonb/pom.xml @@ -0,0 +1,112 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + ../ + + + quarkus-integration-test-resteasy-jsonb + Quarkus - Integration Tests - RESTEasy JSON-B + + + + io.quarkus + quarkus-resteasy-jsonb + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.assertj + assertj-core + test + + + org.eclipse.microprofile.reactive-streams-operators + microprofile-reactive-streams-operators-api + 1.0 + compile + + + + + + + ${project.groupId} + quarkus-maven-plugin + ${project.version} + + + + build + + + + + + + + + + native-image + + + native + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + ${project.groupId} + quarkus-maven-plugin + ${project.version} + + + native-image + + native-image + + + true + true + ${graalvmHome} + + + + + + + + + diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Animal.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Animal.java new file mode 100644 index 0000000000000..cf328aca6777d --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Animal.java @@ -0,0 +1,21 @@ +package io.quarkus.it.resteasy.jsonb; + +import javax.json.bind.annotation.JsonbNumberFormat; +import javax.json.bind.annotation.JsonbProperty; + +public class Animal { + + @JsonbProperty(nillable = false) + public final String color; + private final int age; + + public Animal(String color, int age) { + this.color = color; + this.age = age; + } + + @JsonbNumberFormat(value = "0.00") + public int getAge() { + return this.age; + } +} diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Cat.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Cat.java new file mode 100644 index 0000000000000..810da6a072510 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Cat.java @@ -0,0 +1,17 @@ +package io.quarkus.it.resteasy.jsonb; + +// used to show that properties from the superclass are used +public class Cat extends Animal { + + private final String breed; + + public Cat(String color, int age, String breed) { + super(color, age); + this.breed = breed; + } + + public String getBreed() { + return breed; + } + +} diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/CatResource.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/CatResource.java new file mode 100644 index 0000000000000..7f57096ab4687 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/CatResource.java @@ -0,0 +1,18 @@ +package io.quarkus.it.resteasy.jsonb; + +import java.util.Collections; +import java.util.List; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +@Path("/cat") +public class CatResource { + + @GET + @Produces("application/json") + public List cats() { + return Collections.singletonList(new Cat("Grey", 1, "Scottish Fold")); + } +} diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Coffee.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Coffee.java new file mode 100644 index 0000000000000..d6c5f601b1a64 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Coffee.java @@ -0,0 +1,97 @@ +package io.quarkus.it.resteasy.jsonb; + +import java.util.Collection; +import java.util.Map; +import java.util.Optional; + +import javax.json.bind.annotation.JsonbNillable; +import javax.json.bind.annotation.JsonbNumberFormat; +import javax.json.bind.annotation.JsonbProperty; + +@JsonbNillable // allow null values be default +public class Coffee { + + // used to test both the name and the formatting + // also because of the upper-case name, this will be the first value in the json output + @JsonbProperty("ID") + @JsonbNumberFormat(value = "#,#00.0#;(#,#00.0#)", locale = "en_US") + private Integer id; + + private String name; + + // used to show that nillable extends to empty optional as well + @JsonbProperty(value = "other-name", nillable = false) + private Optional otherName = Optional.empty(); + + private Country countryOfOrigin; + + // used to test that primitives use their default value and that public fields with no getters are added + public boolean enabled; + + @JsonbProperty(nillable = false) + private Collection sellers; + + @JsonbProperty("similar") + public Map similarCoffees; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Optional getOtherName() { + return otherName; + } + + public void setOtherName(Optional otherName) { + this.otherName = otherName; + } + + // used to show that @JsonbProperty can be added to the getters as well + @JsonbProperty(value = "origin", nillable = true) + public Country getCountryOfOrigin() { + return countryOfOrigin; + } + + public void setCountryOfOrigin(Country countryOfOrigin) { + this.countryOfOrigin = countryOfOrigin; + } + + public Collection getSellers() { + return sellers; + } + + public void setSellers(Collection sellers) { + this.sellers = sellers; + } + + public Map getSimilarCoffees() { + return similarCoffees; + } + + public void setSimilarCoffees(Map similarCoffees) { + this.similarCoffees = similarCoffees; + } + + // used to verify that null fields don't end up in the output when the value is not nillable + @JsonbProperty(nillable = false) + public String getDummyNullValue() { + return null; + } + + // used to verify that this will end up the json output since the class is annotated with @JsonNillable + public Long getNullLongValue() { + return null; + } +} diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/CoffeeResource.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/CoffeeResource.java new file mode 100644 index 0000000000000..165dc38511313 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/CoffeeResource.java @@ -0,0 +1,25 @@ +package io.quarkus.it.resteasy.jsonb; + +import java.util.Arrays; +import java.util.Collections; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +@Path("/coffee") +public class CoffeeResource { + + @GET + @Produces("application/json") + public Coffee coffee() { + Coffee coffee = new Coffee(); + coffee.setId(1); + coffee.setName("Robusta"); + coffee.setCountryOfOrigin(new Country(1003, "Ethiopia", "ETH")); + coffee.setSellers(Arrays.asList(new Seller("Carrefour", new Country(1001, "France", "FRA")), + new Seller("Wallmart", new Country(1002, "USA", "USA")))); + coffee.similarCoffees = Collections.singletonMap("arabica", 50); + return coffee; + } +} diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Country.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Country.java new file mode 100644 index 0000000000000..ab2885acb7372 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Country.java @@ -0,0 +1,31 @@ +package io.quarkus.it.resteasy.jsonb; + +import javax.json.bind.annotation.JsonbProperty; +import javax.json.bind.annotation.JsonbPropertyOrder; + +@JsonbPropertyOrder({ "iso3" }) // used to test that the ordering works properly when +public class Country { + + private final int id; + private final String name; + @JsonbProperty("iso") + private final String iso3; + + public Country(int id, String name, String iso3) { + this.id = id; + this.name = name; + this.iso3 = iso3; + } + + public int getId() { + return id; + } + + public String getName() { + return name; + } + + public String getIso3() { + return iso3; + } +} diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Greeter.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Greeter.java new file mode 100644 index 0000000000000..90159b6e34663 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Greeter.java @@ -0,0 +1,14 @@ +package io.quarkus.it.resteasy.jsonb; + +public interface Greeter { + + void sayHello(); + + static class Default implements Greeter { + @Override + public void sayHello() { + + } + } + +} diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/GreeterResource.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/GreeterResource.java new file mode 100644 index 0000000000000..9bfd836a3438b --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/GreeterResource.java @@ -0,0 +1,15 @@ +package io.quarkus.it.resteasy.jsonb; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +@Path("/greeter") +public class GreeterResource { + + @GET + @Produces("application/json") + public Greeter greeting() { + return new Greeting("hello"); + } +} diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Greeting.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Greeting.java new file mode 100644 index 0000000000000..f3ab42dcb1328 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Greeting.java @@ -0,0 +1,21 @@ +package io.quarkus.it.resteasy.jsonb; + +// the fact that there are 2 implementation of Greeter (which is used as the return type in the JAX-RS resource) +// ensures that no serializer is generated +public class Greeting implements Greeter { + + private final String message; + + public Greeting(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + @Override + public void sayHello() { + + } +} diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/HasName.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/HasName.java new file mode 100644 index 0000000000000..e9e51d070fb48 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/HasName.java @@ -0,0 +1,6 @@ +package io.quarkus.it.resteasy.jsonb; + +public interface HasName { + + String getName(); +} diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/HasNameResource.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/HasNameResource.java new file mode 100644 index 0000000000000..8909ac920563b --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/HasNameResource.java @@ -0,0 +1,15 @@ +package io.quarkus.it.resteasy.jsonb; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; + +@Path("/hasName") +public class HasNameResource { + + @GET + @Produces("application/json") + public HasName hasName() { + return new Person("Alice", 40); + } +} diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Person.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Person.java new file mode 100644 index 0000000000000..838c8fd64b0f6 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Person.java @@ -0,0 +1,22 @@ +package io.quarkus.it.resteasy.jsonb; + +// used to show that when there is a single implementation of an interface, a serializer is generated +public class Person implements HasName { + + private final String name; + private final int age; + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + @Override + public String getName() { + return name; + } + + public int getAge() { + return age; + } +} diff --git a/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Seller.java b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Seller.java new file mode 100644 index 0000000000000..e2453b23b7782 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/java/io/quarkus/it/resteasy/jsonb/Seller.java @@ -0,0 +1,20 @@ +package io.quarkus.it.resteasy.jsonb; + +public class Seller { + + private final String name; + private final Country country; + + public Seller(String name, Country country) { + this.name = name; + this.country = country; + } + + public String getName() { + return name; + } + + public Country getCountry() { + return country; + } +} diff --git a/integration-tests/resteasy-jsonb/src/main/resources/application.properties b/integration-tests/resteasy-jsonb/src/main/resources/application.properties new file mode 100644 index 0000000000000..8361d340439d9 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/main/resources/application.properties @@ -0,0 +1 @@ +quarkus.jsonb.enabled=true diff --git a/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/ComplexObjectTest.java b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/ComplexObjectTest.java new file mode 100644 index 0000000000000..d88fdc0627ebf --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/ComplexObjectTest.java @@ -0,0 +1,47 @@ +package io.quarkus.it.resteasy.jsonb; + +import static io.quarkus.it.resteasy.jsonb.TestUtil.getConfiguredJsonb; +import static io.quarkus.it.resteasy.jsonb.TestUtil.getConfiguredJsonbSerializers; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import javax.json.bind.serializer.JsonbSerializer; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class ComplexObjectTest { + + @Test + public void testJsonbResolverCreated() { + assertThat(getConfiguredJsonb()).isNotNull(); + } + + @Test + public void testJsonbConfigContainsCoffeeSerializer() { + List configuredJsonbSerializers = getConfiguredJsonbSerializers(); + assertThat(configuredJsonbSerializers).anySatisfy(s -> { + assertThat(s.getClass().getName()).contains("Coffee"); + }); + } + + @Test + public void testSerialization() { + Coffee coffee = new Coffee(); + coffee.setId(1); + coffee.setName("Robusta"); + coffee.setCountryOfOrigin(new Country(1003, "Ethiopia", "ETH")); + coffee.setSellers(Arrays.asList(new Seller("Carrefour", new Country(1001, "France", "FRA")), + new Seller("Wallmart", new Country(1002, "USA", "USA")))); + coffee.similarCoffees = Collections.singletonMap("arabica", 50); + String jsonStr = getConfiguredJsonb().toJson(coffee); + + assertThat(jsonStr).isEqualTo( + "{\"ID\":\"01.0\",\"enabled\":false,\"name\":\"Robusta\",\"nullLongValue\":null,\"origin\":{\"iso\":\"ETH\",\"id\":1003,\"name\":\"Ethiopia\"},\"sellers\":[{\"country\":{\"iso\":\"FRA\",\"id\":1001,\"name\":\"France\"},\"name\":\"Carrefour\"},{\"country\":{\"iso\":\"USA\",\"id\":1002,\"name\":\"USA\"},\"name\":\"Wallmart\"}],\"similar\":{\"arabica\":50}}"); + } +} diff --git a/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/InterfaceImplementationTest.java b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/InterfaceImplementationTest.java new file mode 100644 index 0000000000000..089409033f0a6 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/InterfaceImplementationTest.java @@ -0,0 +1,24 @@ +package io.quarkus.it.resteasy.jsonb; + +import static io.quarkus.it.resteasy.jsonb.TestUtil.getConfiguredJsonbSerializers; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import javax.json.bind.serializer.JsonbSerializer; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class InterfaceImplementationTest { + + @Test + public void testJsonbConfigContainsPersonSerializer() { + List configuredJsonbSerializers = getConfiguredJsonbSerializers(); + assertThat(configuredJsonbSerializers).anySatisfy(s -> { + assertThat(s.getClass().getName()).contains("Person"); + }); + } +} diff --git a/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/JaxRsIT.java b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/JaxRsIT.java new file mode 100644 index 0000000000000..91e040f7b294f --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/JaxRsIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.resteasy.jsonb; + +import io.quarkus.test.junit.SubstrateTest; + +@SubstrateTest +public class JaxRsIT extends JaxRsTest { +} diff --git a/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/JaxRsTest.java b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/JaxRsTest.java new file mode 100644 index 0000000000000..b901fb7ecc0a2 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/JaxRsTest.java @@ -0,0 +1,41 @@ +package io.quarkus.it.resteasy.jsonb; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.core.StringContains.containsString; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +public class JaxRsTest { + + @Test + public void testComplexObject() { + RestAssured.when().get("/coffee").then() + .statusCode(200) + .body(containsString("Robusta"), containsString("Ethiopia")); + } + + @Test + public void testImplementationClass() { + RestAssured.when().get("/hasName").then() + .statusCode(200) + .body(is("{\"age\":40,\"name\":\"Alice\"}")); + } + + @Test + public void testJaxRsResourceResult() { + RestAssured.when().get("/cat").then() + .statusCode(200) + .body(is("[{\"age\":\"1.00\",\"color\":\"Grey\",\"breed\":\"Scottish Fold\"}]")); + } + + @Test + public void testPojoThatHasNoSerializer() { + RestAssured.when().get("/greeter").then() + .statusCode(200) + .body(is("{\"message\":\"hello\"}")); + } +} diff --git a/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/SerializerNotGeneratedTest.java b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/SerializerNotGeneratedTest.java new file mode 100644 index 0000000000000..107bd251e6166 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/SerializerNotGeneratedTest.java @@ -0,0 +1,24 @@ +package io.quarkus.it.resteasy.jsonb; + +import static io.quarkus.it.resteasy.jsonb.TestUtil.getConfiguredJsonbSerializers; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import javax.json.bind.serializer.JsonbSerializer; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class SerializerNotGeneratedTest { + + @Test + public void testJsonbConfigContainsPersonSerializer() { + List configuredJsonbSerializers = getConfiguredJsonbSerializers(); + assertThat(configuredJsonbSerializers).noneSatisfy(s -> { + assertThat(s.getClass().getName()).contains("Greeting"); + }); + } +} diff --git a/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/SubclassTest.java b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/SubclassTest.java new file mode 100644 index 0000000000000..3112f54ce7b47 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/SubclassTest.java @@ -0,0 +1,24 @@ +package io.quarkus.it.resteasy.jsonb; + +import static io.quarkus.it.resteasy.jsonb.TestUtil.getConfiguredJsonbSerializers; +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import javax.json.bind.serializer.JsonbSerializer; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class SubclassTest { + + @Test + public void testJsonbConfigContainsPersonSerializer() { + List configuredJsonbSerializers = getConfiguredJsonbSerializers(); + assertThat(configuredJsonbSerializers).anySatisfy(s -> { + assertThat(s.getClass().getName()).contains("Cat"); + }); + } +} diff --git a/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/TestUtil.java b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/TestUtil.java new file mode 100644 index 0000000000000..f07a92434f8a6 --- /dev/null +++ b/integration-tests/resteasy-jsonb/src/test/java/io/quarkus/it/resteasy/jsonb/TestUtil.java @@ -0,0 +1,48 @@ +package io.quarkus.it.resteasy.jsonb; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbConfig; +import javax.json.bind.serializer.JsonbSerializer; + +import org.eclipse.yasson.internal.JsonbContext; + +import io.quarkus.resteasy.jsonb.runtime.serializers.QuarkusJsonbBinding; + +final class TestUtil { + + static Jsonb getConfiguredJsonb() { + try { + Class jsonbResolverClass = Class.forName("io.quarkus.jsonb.QuarkusJsonbContextResolver"); + Object jsonbResolverObject = jsonbResolverClass.newInstance(); + Method getContext = jsonbResolverClass.getMethod("getContext", Class.class); + return (Jsonb) getContext.invoke(jsonbResolverObject, Jsonb.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + static JsonbConfig getConfiguredJsonbConfig() { + try { + QuarkusJsonbBinding configuredJsonb = (QuarkusJsonbBinding) getConfiguredJsonb(); + Field jsonbContextField = configuredJsonb.getClass().getDeclaredField("jsonbContext"); + jsonbContextField.setAccessible(true); + JsonbContext jsonbContext = (JsonbContext) jsonbContextField.get(configuredJsonb); + return jsonbContext.getConfig(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + static List getConfiguredJsonbSerializers() { + JsonbConfig jsonbConfig = getConfiguredJsonbConfig(); + Optional property = jsonbConfig.getProperty(JsonbConfig.SERIALIZERS); + return property.map(o -> Arrays.asList((JsonbSerializer[]) o)).orElse(Collections.emptyList()); + } +} From 7f5309c3b94ea085eb4a5e7e274a4e081b15a1c5 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Sat, 27 Jul 2019 01:51:16 +0300 Subject: [PATCH 17/20] Make Jsonb a bean --- .../resteasy/jsonb/deployment/DotNames.java | 2 + .../JsonbBeanProducerGenerator.java | 160 ++++++++++++++++++ .../ResteasyJsonbClassGenerator.java | 143 +++------------- .../deployment/ResteasyJsonbProcessor.java | 70 ++++++-- 4 files changed, 238 insertions(+), 137 deletions(-) create mode 100644 extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbBeanProducerGenerator.java diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java index dde6dfbb79639..ee9255ea9d3e4 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java @@ -9,6 +9,7 @@ import java.util.Optional; import java.util.Set; +import javax.json.bind.Jsonb; import javax.json.bind.annotation.JsonbDateFormat; import javax.json.bind.annotation.JsonbNillable; import javax.json.bind.annotation.JsonbNumberFormat; @@ -49,6 +50,7 @@ private DotNames() { public static final DotName CONTEXT_RESOLVER = DotName.createSimple(ContextResolver.class.getName()); + public static final DotName JSONB = DotName.createSimple(Jsonb.class.getName()); public static final DotName JSONB_TRANSIENT = DotName.createSimple(JsonbTransient.class.getName()); public static final DotName JSONB_PROPERTY = DotName.createSimple(JsonbProperty.class.getName()); public static final DotName JSONB_TYPE_SERIALIZER = DotName.createSimple(JsonbTypeSerializer.class.getName()); diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbBeanProducerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbBeanProducerGenerator.java new file mode 100644 index 0000000000000..a187970afb257 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbBeanProducerGenerator.java @@ -0,0 +1,160 @@ +package io.quarkus.resteasy.jsonb.deployment; + +import java.util.Locale; +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Singleton; +import javax.json.bind.Jsonb; +import javax.json.bind.serializer.JsonbSerializer; +import javax.json.spi.JsonProvider; + +import org.eclipse.yasson.YassonProperties; +import org.eclipse.yasson.internal.JsonbContext; +import org.eclipse.yasson.internal.MappingContext; +import org.eclipse.yasson.internal.serializer.ContainerSerializerProvider; + +import io.quarkus.arc.DefaultBean; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.resteasy.jsonb.runtime.serializers.QuarkusJsonbBinding; +import io.quarkus.resteasy.jsonb.runtime.serializers.SimpleContainerSerializerProvider; + +public class JsonbBeanProducerGenerator { + + public static String JSONB_PRODUCER = "io.quarkus.jsonb.JsonbProducer"; + + private final JsonbConfig jsonbConfig; + + public JsonbBeanProducerGenerator(JsonbConfig jsonbConfig) { + this.jsonbConfig = jsonbConfig; + } + + void generateJsonbContextResolver(ClassOutput classOutput, Map typeToGeneratedSerializers) { + try (ClassCreator cc = ClassCreator.builder() + .classOutput(classOutput).className(JSONB_PRODUCER) + .build()) { + + cc.addAnnotation(ApplicationScoped.class); + + try (MethodCreator createJsonb = cc.getMethodCreator("createJsonb", Jsonb.class)) { + + createJsonb.addAnnotation(Singleton.class); + createJsonb.addAnnotation(Produces.class); + createJsonb.addAnnotation(DefaultBean.class); + + Class jsonbConfigClass = javax.json.bind.JsonbConfig.class; + + // create the JsonbConfig object + ResultHandle config = createJsonb.newInstance(MethodDescriptor.ofConstructor(jsonbConfigClass)); + + // create the jsonbContext object + ResultHandle provider = createJsonb + .invokeStaticMethod(MethodDescriptor.ofMethod(JsonProvider.class, "provider", JsonProvider.class)); + ResultHandle jsonbContext = createJsonb.newInstance( + MethodDescriptor.ofConstructor(JsonbContext.class, jsonbConfigClass, JsonProvider.class), + config, provider); + ResultHandle mappingContext = createJsonb.invokeVirtualMethod( + MethodDescriptor.ofMethod(JsonbContext.class, "getMappingContext", MappingContext.class), + jsonbContext); + + //handle locale + ResultHandle locale = null; + if (jsonbConfig.locale.isPresent()) { + locale = createJsonb.invokeStaticMethod( + MethodDescriptor.ofMethod(JsonbSupportClassGenerator.QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", + Locale.class)); + createJsonb.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withLocale", jsonbConfigClass, Locale.class), + config, locale); + } + + // handle date format + if (jsonbConfig.dateFormat.isPresent()) { + createJsonb.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withDateFormat", jsonbConfigClass, String.class, + Locale.class), + config, + createJsonb.load(jsonbConfig.dateFormat.get()), + locale != null ? locale : createJsonb.loadNull()); + } + + // handle serializeNullValues + createJsonb.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withNullValues", jsonbConfigClass, Boolean.class), + config, + createJsonb.invokeStaticMethod( + MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), + createJsonb.load(jsonbConfig.serializeNullValues))); + + // handle propertyOrderStrategy + createJsonb.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withPropertyOrderStrategy", jsonbConfigClass, + String.class), + config, createJsonb.load(jsonbConfig.propertyOrderStrategy.toUpperCase())); + + // handle encoding + if (jsonbConfig.encoding.isPresent()) { + createJsonb.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withEncoding", jsonbConfigClass, + String.class), + config, createJsonb.load(jsonbConfig.encoding.get())); + } + + // handle failOnUnknownProperties + createJsonb.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "setProperty", jsonbConfigClass, String.class, + Object.class), + config, + createJsonb.load(YassonProperties.FAIL_ON_UNKNOWN_PROPERTIES), + createJsonb.invokeStaticMethod( + MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), + createJsonb.load(jsonbConfig.failOnUnknownProperties))); + + // add generated serializers to config + if (!typeToGeneratedSerializers.isEmpty()) { + ResultHandle serializersArray = createJsonb.newArray(JsonbSerializer.class, + createJsonb.load(typeToGeneratedSerializers.size())); + int i = 0; + for (Map.Entry entry : typeToGeneratedSerializers.entrySet()) { + + ResultHandle serializer = createJsonb + .newInstance(MethodDescriptor.ofConstructor(entry.getValue())); + + // build up the serializers array that will be passed to JsonbConfig + createJsonb.writeArrayValue(serializersArray, createJsonb.load(i), serializer); + + ResultHandle clazz = createJsonb.invokeStaticMethod( + MethodDescriptor.ofMethod(Class.class, "forName", Class.class, String.class), + createJsonb.load(entry.getKey())); + + // add a ContainerSerializerProvider for the serializer + ResultHandle serializerProvider = createJsonb.newInstance( + MethodDescriptor.ofConstructor(SimpleContainerSerializerProvider.class, JsonbSerializer.class), + serializer); + createJsonb.invokeVirtualMethod( + MethodDescriptor.ofMethod(MappingContext.class, "addSerializerProvider", void.class, + Class.class, ContainerSerializerProvider.class), + mappingContext, clazz, serializerProvider); + + i++; + } + createJsonb.invokeVirtualMethod( + MethodDescriptor.ofMethod(jsonbConfigClass, "withSerializers", jsonbConfigClass, + JsonbSerializer[].class), + config, serializersArray); + } + + // create jsonb from QuarkusJsonbBinding + ResultHandle jsonb = createJsonb.newInstance( + MethodDescriptor.ofConstructor(QuarkusJsonbBinding.class, JsonbContext.class), jsonbContext); + + createJsonb.returnValue(jsonb); + } + } + } +} diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java index 67700ccd1261f..14cc781393d4f 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java @@ -1,20 +1,15 @@ package io.quarkus.resteasy.jsonb.deployment; +import java.lang.annotation.Annotation; import java.lang.reflect.Modifier; -import java.util.Locale; -import java.util.Map; import javax.json.bind.Jsonb; -import javax.json.bind.serializer.JsonbSerializer; -import javax.json.spi.JsonProvider; import javax.ws.rs.ext.ContextResolver; import javax.ws.rs.ext.Provider; -import org.eclipse.yasson.YassonProperties; -import org.eclipse.yasson.internal.JsonbContext; -import org.eclipse.yasson.internal.MappingContext; -import org.eclipse.yasson.internal.serializer.ContainerSerializerProvider; - +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InstanceHandle; import io.quarkus.gizmo.BranchResult; import io.quarkus.gizmo.BytecodeCreator; import io.quarkus.gizmo.ClassCreator; @@ -23,20 +18,12 @@ import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; -import io.quarkus.resteasy.jsonb.runtime.serializers.QuarkusJsonbBinding; -import io.quarkus.resteasy.jsonb.runtime.serializers.SimpleContainerSerializerProvider; class ResteasyJsonbClassGenerator { static final String QUARKUS_CONTEXT_RESOLVER = "io.quarkus.jsonb.QuarkusJsonbContextResolver"; - private final JsonbConfig jsonbConfig; - - public ResteasyJsonbClassGenerator(JsonbConfig jsonbConfig) { - this.jsonbConfig = jsonbConfig; - } - - void generateJsonbContextResolver(ClassOutput classOutput, Map typeToGeneratedSerializers) { + void generateJsonbContextResolver(ClassOutput classOutput) { try (ClassCreator cc = ClassCreator.builder() .classOutput(classOutput).className(QUARKUS_CONTEXT_RESOLVER) .interfaces(ContextResolver.class) @@ -57,111 +44,21 @@ void generateJsonbContextResolver(ClassOutput classOutput, Map t BytecodeCreator instanceNull = branchResult.trueBranch(); - Class jsonbConfigClass = javax.json.bind.JsonbConfig.class; - - // create the JsonbConfig object - ResultHandle config = instanceNull.newInstance(MethodDescriptor.ofConstructor(jsonbConfigClass)); - - // create the jsonbContext object - ResultHandle provider = instanceNull - .invokeStaticMethod(MethodDescriptor.ofMethod(JsonProvider.class, "provider", JsonProvider.class)); - ResultHandle jsonbContext = instanceNull.newInstance( - MethodDescriptor.ofConstructor(JsonbContext.class, jsonbConfigClass, JsonProvider.class), - config, provider); - ResultHandle mappingContext = instanceNull.invokeVirtualMethod( - MethodDescriptor.ofMethod(JsonbContext.class, "getMappingContext", MappingContext.class), - jsonbContext); - - //handle locale - ResultHandle locale = null; - if (jsonbConfig.locale.isPresent()) { - locale = instanceNull.invokeStaticMethod( - MethodDescriptor.ofMethod(JsonbSupportClassGenerator.QUARKUS_DEFAULT_LOCALE_PROVIDER, "get", - Locale.class)); - instanceNull.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "withLocale", jsonbConfigClass, Locale.class), - config, locale); - } - - // handle date format - if (jsonbConfig.dateFormat.isPresent()) { - instanceNull.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "withDateFormat", jsonbConfigClass, String.class, - Locale.class), - config, - instanceNull.load(jsonbConfig.dateFormat.get()), - locale != null ? locale : instanceNull.loadNull()); - } - - // handle serializeNullValues - instanceNull.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "withNullValues", jsonbConfigClass, Boolean.class), - config, - instanceNull.invokeStaticMethod( - MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), - instanceNull.load(jsonbConfig.serializeNullValues))); - - // handle propertyOrderStrategy - instanceNull.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "withPropertyOrderStrategy", jsonbConfigClass, - String.class), - config, instanceNull.load(jsonbConfig.propertyOrderStrategy.toUpperCase())); - - // handle encoding - if (jsonbConfig.encoding.isPresent()) { - instanceNull.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "withEncoding", jsonbConfigClass, - String.class), - config, instanceNull.load(jsonbConfig.encoding.get())); - } - - // handle failOnUnknownProperties - instanceNull.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "setProperty", jsonbConfigClass, String.class, - Object.class), - config, - instanceNull.load(YassonProperties.FAIL_ON_UNKNOWN_PROPERTIES), - instanceNull.invokeStaticMethod( - MethodDescriptor.ofMethod(Boolean.class, "valueOf", Boolean.class, boolean.class), - instanceNull.load(jsonbConfig.failOnUnknownProperties))); - - // add generated serializers to config - if (!typeToGeneratedSerializers.isEmpty()) { - ResultHandle serializersArray = instanceNull.newArray(JsonbSerializer.class, - instanceNull.load(typeToGeneratedSerializers.size())); - int i = 0; - for (Map.Entry entry : typeToGeneratedSerializers.entrySet()) { - - ResultHandle serializer = instanceNull - .newInstance(MethodDescriptor.ofConstructor(entry.getValue())); - - // build up the serializers array that will be passed to JsonbConfig - instanceNull.writeArrayValue(serializersArray, instanceNull.load(i), serializer); - - ResultHandle clazz = instanceNull.invokeStaticMethod( - MethodDescriptor.ofMethod(Class.class, "forName", Class.class, String.class), - instanceNull.load(entry.getKey())); - - // add a ContainerSerializerProvider for the serializer - ResultHandle serializerProvider = instanceNull.newInstance( - MethodDescriptor.ofConstructor(SimpleContainerSerializerProvider.class, JsonbSerializer.class), - serializer); - instanceNull.invokeVirtualMethod( - MethodDescriptor.ofMethod(MappingContext.class, "addSerializerProvider", void.class, - Class.class, ContainerSerializerProvider.class), - mappingContext, clazz, serializerProvider); - - i++; - } - instanceNull.invokeVirtualMethod( - MethodDescriptor.ofMethod(jsonbConfigClass, "withSerializers", jsonbConfigClass, - JsonbSerializer[].class), - config, serializersArray); - } - - // create jsonb from QuarkusJsonbBinding - ResultHandle jsonb = instanceNull.newInstance( - MethodDescriptor.ofConstructor(QuarkusJsonbBinding.class, JsonbContext.class), jsonbContext); + ResultHandle arcContainer = instanceNull + .invokeStaticMethod(MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class)); + + ResultHandle jsonbClass = instanceNull.invokeStaticMethod( + MethodDescriptor.ofMethod(Class.class, "forName", Class.class, String.class), + instanceNull.load(Jsonb.class.getName())); + ResultHandle instanceHandle = instanceNull.invokeInterfaceMethod( + MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class, + Annotation[].class), + arcContainer, jsonbClass, instanceNull.loadNull()); + ResultHandle get = instanceNull.invokeInterfaceMethod( + MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class), + instanceHandle); + + ResultHandle jsonb = instanceNull.checkCast(get, Jsonb.class); instanceNull.writeStaticField(instance, jsonb); instanceNull.returnValue(jsonb); diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index bb72a8c72e10a..44bea1ade60ad 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -13,6 +13,7 @@ import javax.ws.rs.ext.Provider; import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.ClassType; @@ -22,6 +23,8 @@ import org.jboss.jandex.ParameterizedType; import org.jboss.jandex.Type; +import io.quarkus.arc.deployment.GeneratedBeanBuildItem; +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; @@ -38,7 +41,7 @@ public class ResteasyJsonbProcessor { private static final DotName JAX_RS_PRODUCES = DotName.createSimple("javax.ws.rs.Produces"); - @BuildStep + @BuildStep(providesCapabilities = Capabilities.RESTEASY_JSON_EXTENSION) void build(BuildProducer feature) { feature.produce(new FeatureBuildItem(FeatureBuildItem.RESTEASY_JSONB)); } @@ -57,7 +60,9 @@ void build(BuildProducer feature) { void generateClasses(CombinedIndexBuildItem combinedIndexBuildItem, BuildProducer generatedClass, BuildProducer jaxrsProvider, - BuildProducer typesWithoutReflection) { + BuildProducer typesWithoutReflection, + BuildProducer generatedBean, + BuildProducer unremovableBean) { IndexView index = combinedIndexBuildItem.getIndex(); if (!jsonbConfig.enabled) { @@ -65,16 +70,10 @@ void generateClasses(CombinedIndexBuildItem combinedIndexBuildItem, } // if the user has declared a custom ContextResolver for Jsonb, we don't generate anything - if (hasCustomContextResolverBeenDeclared(index)) { + if (hasCustomContextResolverBeenSupplied(index)) { return; } - validateConfiguration(); - - SerializationClassInspector serializationClassInspector = new SerializationClassInspector(index); - TypeSerializerGeneratorRegistry typeSerializerGeneratorRegistry = new TypeSerializerGeneratorRegistry( - serializationClassInspector); - ClassOutput classOutput = new ClassOutput() { @Override public void write(String name, byte[] data) { @@ -82,6 +81,27 @@ public void write(String name, byte[] data) { } }; + // we generate a context resolver which pulls the jsonb bean out of Arc + // this is done regardless of whether the user has configured a bean or not + // because we always want the jsonb bean to be used by RESTEasy + ResteasyJsonbClassGenerator resteasyJsonbClassGenerator = new ResteasyJsonbClassGenerator(); + resteasyJsonbClassGenerator.generateJsonbContextResolver(classOutput); + jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(ResteasyJsonbClassGenerator.QUARKUS_CONTEXT_RESOLVER)); + + // we need to make user supplied jsonb producer beans unremovable since there are injection points + Set userSuppliedProducers = getUserSuppliedJsonbProducerBeans(index); + if (!userSuppliedProducers.isEmpty()) { + unremovableBean.produce(new UnremovableBeanBuildItem( + new UnremovableBeanBuildItem.BeanClassNamesExclusion(userSuppliedProducers))); + return; + } + + validateConfiguration(); + + SerializationClassInspector serializationClassInspector = new SerializationClassInspector(index); + TypeSerializerGeneratorRegistry typeSerializerGeneratorRegistry = new TypeSerializerGeneratorRegistry( + serializationClassInspector); + Set serializerCandidates = determineSerializationCandidates(index); SerializerClassGenerator serializerClassGenerator = new SerializerClassGenerator(jsonbConfig); @@ -101,20 +121,27 @@ public void write(String name, byte[] data) { } } + JsonbBeanProducerGenerator jsonbBeanProducerGenerator = new JsonbBeanProducerGenerator(jsonbConfig); + jsonbBeanProducerGenerator.generateJsonbContextResolver(new ClassOutput() { + @Override + public void write(String name, byte[] data) { + generatedBean.produce(new GeneratedBeanBuildItem(name, data)); + } + }, typeToGeneratedSerializers); + + unremovableBean.produce(new UnremovableBeanBuildItem( + new UnremovableBeanBuildItem.BeanClassNameExclusion(JsonbBeanProducerGenerator.JSONB_PRODUCER))); + JsonbSupportClassGenerator jsonbSupportClassGenerator = new JsonbSupportClassGenerator(jsonbConfig); jsonbSupportClassGenerator.generateDefaultLocaleProvider(classOutput); jsonbSupportClassGenerator.generateJsonbDefaultJsonbDateFormatterProvider(classOutput); - ResteasyJsonbClassGenerator resteasyJsonbClassGenerator = new ResteasyJsonbClassGenerator(jsonbConfig); - resteasyJsonbClassGenerator.generateJsonbContextResolver(classOutput, typeToGeneratedSerializers); - - jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(ResteasyJsonbClassGenerator.QUARKUS_CONTEXT_RESOLVER)); for (String type : typesThatDontNeedReflection) { typesWithoutReflection.produce(new ResteasyAdditionalReturnTypesWithoutReflectionBuildItem(type)); } } - private boolean hasCustomContextResolverBeenDeclared(IndexView index) { + private boolean hasCustomContextResolverBeenSupplied(IndexView index) { for (ClassInfo contextResolver : index.getAllKnownImplementors(DotNames.CONTEXT_RESOLVER)) { if (contextResolver.classAnnotation(DotName.createSimple(Provider.class.getName())) == null) { continue; @@ -145,6 +172,21 @@ private boolean hasCustomContextResolverBeenDeclared(IndexView index) { return false; } + // we need to find all the user supplied producers and mark them as unremovable since there are no actual injection points + // for the ObjectMapper + private Set getUserSuppliedJsonbProducerBeans(IndexView index) { + Set result = new HashSet<>(); + for (AnnotationInstance annotation : index.getAnnotations(DotName.createSimple("javax.enterprise.inject.Produces"))) { + if (annotation.target().kind() != AnnotationTarget.Kind.METHOD) { + continue; + } + if (DotNames.JSONB.equals(annotation.target().asMethod().returnType().name())) { + result.add(annotation.target().asMethod().declaringClass().name().toString()); + } + } + return result; + } + private Set determineSerializationCandidates(IndexView index) { Set serializerCandidates = new HashSet<>(); for (DotName annotationType : ResteasyServerCommonProcessor.METHOD_ANNOTATIONS) { From a53c896f5033d687d4c26b621d7da323a23442a8 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Sat, 27 Jul 2019 01:55:49 +0300 Subject: [PATCH 18/20] Fix date format bug --- .../serializers/AbstractDatetimeSerializerGenerator.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractDatetimeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractDatetimeSerializerGenerator.java index d8c2ec9209e81..810e27a386df7 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractDatetimeSerializerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractDatetimeSerializerGenerator.java @@ -28,6 +28,9 @@ protected void generateNotNull(GenerateContext context) { locale = localeValue.asString(); } } + if (format.equals(JsonbDateFormat.DEFAULT_FORMAT) && context.getGlobalConfig().getDateFormat().isPresent()) { + format = context.getGlobalConfig().getDateFormat().get(); + } doGenerate(context, format, locale); } From e583a0eab337c801268e0b8a32e143097f3ee866 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Tue, 27 Aug 2019 13:21:13 +0300 Subject: [PATCH 19/20] Add native test CI configuration --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 00e3561160a8a..1e4ddf05978aa 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -269,6 +269,7 @@ stages: timeoutInMinutes: 25 modules: - resteasy-jackson + - resteasy-jsonb - vertx - vertx-http - virtual-http From d879ae90b7fa5318a7e0adc4fe1f8df466fa4e13 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 4 Sep 2019 18:56:08 +0300 Subject: [PATCH 20/20] Remove useless Class.forName --- .../jsonb/deployment/JsonbBeanProducerGenerator.java | 6 +----- .../jsonb/deployment/ResteasyJsonbClassGenerator.java | 5 +---- .../deployment/serializers/UnhandledTypeGenerator.java | 5 +---- 3 files changed, 3 insertions(+), 13 deletions(-) diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbBeanProducerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbBeanProducerGenerator.java index a187970afb257..48035abe46ed7 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbBeanProducerGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbBeanProducerGenerator.java @@ -128,10 +128,6 @@ void generateJsonbContextResolver(ClassOutput classOutput, Map t // build up the serializers array that will be passed to JsonbConfig createJsonb.writeArrayValue(serializersArray, createJsonb.load(i), serializer); - ResultHandle clazz = createJsonb.invokeStaticMethod( - MethodDescriptor.ofMethod(Class.class, "forName", Class.class, String.class), - createJsonb.load(entry.getKey())); - // add a ContainerSerializerProvider for the serializer ResultHandle serializerProvider = createJsonb.newInstance( MethodDescriptor.ofConstructor(SimpleContainerSerializerProvider.class, JsonbSerializer.class), @@ -139,7 +135,7 @@ void generateJsonbContextResolver(ClassOutput classOutput, Map t createJsonb.invokeVirtualMethod( MethodDescriptor.ofMethod(MappingContext.class, "addSerializerProvider", void.class, Class.class, ContainerSerializerProvider.class), - mappingContext, clazz, serializerProvider); + mappingContext, createJsonb.loadClass(entry.getKey()), serializerProvider); i++; } diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java index 14cc781393d4f..cfa2840efddc7 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java @@ -47,13 +47,10 @@ void generateJsonbContextResolver(ClassOutput classOutput) { ResultHandle arcContainer = instanceNull .invokeStaticMethod(MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class)); - ResultHandle jsonbClass = instanceNull.invokeStaticMethod( - MethodDescriptor.ofMethod(Class.class, "forName", Class.class, String.class), - instanceNull.load(Jsonb.class.getName())); ResultHandle instanceHandle = instanceNull.invokeInterfaceMethod( MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class, Annotation[].class), - arcContainer, jsonbClass, instanceNull.loadNull()); + arcContainer, instanceNull.loadClass(Jsonb.class), instanceNull.loadNull()); ResultHandle get = instanceNull.invokeInterfaceMethod( MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class), instanceHandle); diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/UnhandledTypeGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/UnhandledTypeGenerator.java index b796842ca7133..8a6e196285615 100644 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/UnhandledTypeGenerator.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/UnhandledTypeGenerator.java @@ -45,14 +45,11 @@ protected void generateNotNull(GenerateContext context) { ResultHandle serializationContext = context.getSerializationContext(); ResultHandle marshaller = bytecodeCreator.checkCast(serializationContext, Marshaller.class); - ResultHandle enclosingTypeClass = bytecodeCreator.invokeStaticMethod( - MethodDescriptor.ofMethod(Class.class, "forName", Class.class, String.class), - bytecodeCreator.load(enclosingType.name().toString())); ResultHandle propertyCachedSerializer = bytecodeCreator.invokeStaticMethod( MethodDescriptor.ofMethod(UnhandledTypeGeneratorUtil.class, "getSerializerForUnhandledType", JsonbSerializer.class, Marshaller.class, Class.class, Object.class, String.class), - marshaller, enclosingTypeClass, + marshaller, bytecodeCreator.loadClass(enclosingType.name().toString()), context.getCurrentItem(), bytecodeCreator.load(propertyName)); bytecodeCreator.invokeInterfaceMethod(