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 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..ee9255ea9d3e4 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/DotNames.java @@ -0,0 +1,63 @@ +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.Jsonb; +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 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()); + + 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()); + 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/JsonbBeanProducerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbBeanProducerGenerator.java new file mode 100644 index 0000000000000..48035abe46ed7 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbBeanProducerGenerator.java @@ -0,0 +1,156 @@ +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); + + // 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, createJsonb.loadClass(entry.getKey()), 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/JsonbConfig.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java new file mode 100644 index 0000000000000..53be33756cffe --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/JsonbConfig.java @@ -0,0 +1,72 @@ +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.Optional; +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))); + + /** + * 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 + */ + @ConfigItem + Optional locale; + + /** + * default date format to use + */ + @ConfigItem + Optional 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; + + // DESERIALIZER RELATED PROPERTIES + + /** + * encoding to use when de-serializing data + */ + @ConfigItem + Optional 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/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/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/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/ResteasyJsonbClassGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java new file mode 100644 index 0000000000000..cfa2840efddc7 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbClassGenerator.java @@ -0,0 +1,75 @@ +package io.quarkus.resteasy.jsonb.deployment; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; + +import javax.json.bind.Jsonb; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; + +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; +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; + +class ResteasyJsonbClassGenerator { + + static final String QUARKUS_CONTEXT_RESOLVER = "io.quarkus.jsonb.QuarkusJsonbContextResolver"; + + void generateJsonbContextResolver(ClassOutput classOutput) { + 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); + + FieldDescriptor instance = cc.getFieldCreator("INSTANCE", Jsonb.class) + .setModifiers(Modifier.STATIC | Modifier.PRIVATE) + .getFieldDescriptor(); + + 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(); + + ResultHandle arcContainer = instanceNull + .invokeStaticMethod(MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class)); + + ResultHandle instanceHandle = instanceNull.invokeInterfaceMethod( + MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class, + Annotation[].class), + arcContainer, instanceNull.loadClass(Jsonb.class), 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); + } + + 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 724cc4c022501..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 @@ -1,14 +1,255 @@ 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; +import javax.ws.rs.core.MediaType; +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; +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.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; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.gizmo.ClassOutput; +import io.quarkus.resteasy.common.deployment.ResteasyJaxrsProviderBuildItem; +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 { + private static final DotName JAX_RS_PRODUCES = DotName.createSimple("javax.ws.rs.Produces"); + @BuildStep(providesCapabilities = Capabilities.RESTEASY_JSON_EXTENSION) void build(BuildProducer feature) { feature.produce(new FeatureBuildItem(FeatureBuildItem.RESTEASY_JSONB)); } + + 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 generateClasses(CombinedIndexBuildItem combinedIndexBuildItem, + BuildProducer generatedClass, + BuildProducer jaxrsProvider, + BuildProducer typesWithoutReflection, + BuildProducer generatedBean, + BuildProducer unremovableBean) { + IndexView index = combinedIndexBuildItem.getIndex(); + + if (!jsonbConfig.enabled) { + return; + } + + // if the user has declared a custom ContextResolver for Jsonb, we don't generate anything + if (hasCustomContextResolverBeenSupplied(index)) { + return; + } + + ClassOutput classOutput = new ClassOutput() { + @Override + public void write(String name, byte[] data) { + generatedClass.produce(new GeneratedClassBuildItem(true, name, 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); + + Map typeToGeneratedSerializers = new HashMap<>(); + List typesThatDontNeedReflection = new ArrayList<>(); + for (ClassType type : serializerCandidates) { + SerializerClassGenerator.Result generationResult = serializerClassGenerator.generateSerializerForClassType(type, + typeSerializerGeneratorRegistry, + classOutput); + if (generationResult.isGenerated()) { + typeToGeneratedSerializers.put(generationResult.getClassActuallyUsed().toString(), + generationResult.getGeneratedClassName()); + if (!generationResult.isNeedsReflection()) { + typesThatDontNeedReflection.add(generationResult.getClassActuallyUsed().toString()); + } + } + } + + 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); + + for (String type : typesThatDontNeedReflection) { + typesWithoutReflection.produce(new ResteasyAdditionalReturnTypesWithoutReflectionBuildItem(type)); + } + } + + private boolean hasCustomContextResolverBeenSupplied(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; + } + + // 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) { + 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")) { + continue; + } + + if (returnType instanceof ClassType) { + serializerCandidates.add(returnType.asClassType()); + continue; + } + + // 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 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( + "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/SerializationClassInspector.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java new file mode 100644 index 0000000000000..70bf5072f4fbf --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializationClassInspector.java @@ -0,0 +1,362 @@ +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; +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) { + return inspect(classDotName, true); + } + + private Result inspect(DotName classDotName, boolean checkSubtypes) { + 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())) { + 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 or perhaps by generating serializers for all implementations? + return SerializationClassInspector.Result.notPossible(classInfo); + } else { + return inspect(allKnownImplementors.iterator().next().name()); + } + } + + 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 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); + } + + if (classInspectionResultMap.containsKey(classInfo.name())) { + return classInspectionResultMap.get(classInfo.name()); + } + + Result superClassResult = null; + if (!DotNames.OBJECT.equals(classInfo.superName())) { + superClassResult = inspect(classInfo.superName(), false); + if (!superClassResult.isPossible()) { + return SerializationClassInspector.Result.notPossible(classInfo); + } + classInspectionResultMap.put(classInfo.superName(), superClassResult); + } + + 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); + } + + List fields = PropertyUtil.getPublicFieldsWithoutGetters(classInfo, getters.keySet()); + if (removeTransientFields(fields)) { + return SerializationClassInspector.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; + } + + /** + * 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; + } + + // 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) { + List argumentTypes = type.asParameterizedType().arguments(); + for (Type argumentType : argumentTypes) { + if (containedInCandidates(argumentType, candidates, additionalTypesToCheck)) { + 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; + } + + /** + * 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); + } + } +} 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..f6d4aa3bdbb02 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/SerializerClassGenerator.java @@ -0,0 +1,129 @@ +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(); + } + + // 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) + .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(classDotName, generatedSerializerName) + : Result.reflectionNeeded(classDotName, generatedSerializerName); + } + + private GlobalSerializationConfig getGlobalConfig() { + return new GlobalSerializationConfig( + jsonbConfig.locale, jsonbConfig.dateFormat, jsonbConfig.serializeNullValues, jsonbConfig.propertyOrderStrategy); + } + + static class Result { + private final boolean generated; + 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, DotName classActuallyUsed, String generatedClassName, boolean needsReflection) { + this.generated = generated; + this.classActuallyUsed = classActuallyUsed; + this.generatedClassName = generatedClassName; + this.needsReflection = needsReflection; + } + + static Result notGenerated() { + return new Result(false, null, null, false); + } + + static Result noReflectionNeeded(DotName classActuallyUsed, String generatedClassName) { + return new Result(true, classActuallyUsed, generatedClassName, false); + } + + static Result reflectionNeeded(DotName classActuallyUsed, String generatedClassName) { + return new Result(true, classActuallyUsed, generatedClassName, true); + } + + boolean isGenerated() { + return generated; + } + + public DotName getClassActuallyUsed() { + return classActuallyUsed; + } + + String getGeneratedClassName() { + return generatedClassName; + } + + boolean isNeedsReflection() { + return needsReflection; + } + } +} 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..810e27a386df7 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractDatetimeSerializerGenerator.java @@ -0,0 +1,37 @@ +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(); + } + } + if (format.equals(JsonbDateFormat.DEFAULT_FORMAT) && context.getGlobalConfig().getDateFormat().isPresent()) { + format = context.getGlobalConfig().getDateFormat().get(); + } + + 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..6b61c77dd6b0c --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/AbstractTypeSerializerGenerator.java @@ -0,0 +1,37 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +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; + +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) + || (context.getType() instanceof PrimitiveType)) { + 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/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/CollectionTypeSerializerGenerator.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/CollectionTypeSerializerGenerator.java new file mode 100644 index 0000000000000..8768f1dd1e6a5 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/CollectionTypeSerializerGenerator.java @@ -0,0 +1,78 @@ +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 Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + if (!CollectionUtil.isCollection(type.name())) { + return Supported.UNSUPPORTED; + } + + Type genericType = CollectionUtil.getGenericType(type); + if (genericType == null) { + return Supported.UNSUPPORTED; + } + + TypeSerializerGenerator typeSerializerGenerator = registry.correspondingTypeSerializer(genericType); + return typeSerializerGenerator != null ? typeSerializerGenerator.supports(genericType, registry) + : Supported.UNSUPPORTED; + } + + @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..327f4661984f4 --- /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 Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.INTEGER.equals(type.name()) ? Supported.FULLY : Supported.UNSUPPORTED; + } + + @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..cc99831182aa5 --- /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 Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.LOCAL_DATE_TIME.equals(type.name()) ? Supported.FULLY : Supported.UNSUPPORTED; + } + + @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/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 new file mode 100644 index 0000000000000..355cca53223bc --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/ObjectTypeSerializerGenerator.java @@ -0,0 +1,455 @@ +package io.quarkus.resteasy.jsonb.deployment.serializers; + +import java.lang.reflect.Modifier; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Optional; +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.ClassInfo; +import org.jboss.jandex.ClassType; +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.FieldDescriptor; +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 Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + if (type instanceof ArrayType) { + return Supported.UNSUPPORTED; + } + + if (type.name().toString().startsWith("java")) { + return Supported.UNSUPPORTED; + } + + final SerializationClassInspector.Result inspectionsResult = registry.getInspector().inspect(type.name()); + if (!inspectionsResult.isPossible()) { + return Supported.UNSUPPORTED; + } + + boolean foundUnhandledType = false; + for (MethodInfo getter : inspectionsResult.getGetters().keySet()) { + if (registry.correspondingTypeSerializer(getter.returnType()) == null) { + if (canUseUnhandledTypeGenerator(getter.returnType())) { + foundUnhandledType = true; + } else { + return Supported.UNSUPPORTED; + } + } + } + + for (FieldInfo field : inspectionsResult.getVisibleFieldsWithoutGetters()) { + if (registry.correspondingTypeSerializer(field.type()) == null) { + if (canUseUnhandledTypeGenerator(field.type())) { + foundUnhandledType = true; + } else { + return Supported.UNSUPPORTED; + } + } + } + + return foundUnhandledType ? Supported.WITH_UNHANDLED : Supported.FULLY; + } + + private boolean canUseUnhandledTypeGenerator(Type type) { + // 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 + protected void generateNotNull(GenerateContext context) { + BytecodeCreator bytecodeCreator = context.getBytecodeCreator(); + ResultHandle jsonGenerator = context.getJsonGenerator(); + + 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); + } + + // 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 + // using the proper strategy + SortedMap> propertyNameToGenerator = PropertyOrderStrategy.REVERSE + .equalsIgnoreCase(context.getGlobalConfig().getPropertyOrderStrategy()) + ? new TreeMap<>(Collections.reverseOrder()) + : 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(); + String defaultKeyName = PropertyUtil.toFieldName(getterMethodInfo); + Type returnType = getterMethodInfo.returnType(); + TypeSerializerGenerator getterTypeSerializerGenerator = serializerRegistry.correspondingTypeSerializer(returnType); + if (getterTypeSerializerGenerator == null) { + if (canUseUnhandledTypeGenerator(returnType)) { + getterTypeSerializerGenerator = new UnhandledTypeGenerator(context.getType(), defaultKeyName); + } else { + throw new IllegalStateException("Could not generate serializer for getter " + getterMethodInfo.name() + + " of type " + classDotNate); + } + } + + 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))); + } + + // 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() + .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); + } + } + } + + 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)); + } + } + } + + 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; + + 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(); + 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, + returnType, getter, true, effectivePropertyAnnotations)); + } + } + } + + 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)); + } + } + } + +} 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/SerializerGeneratorUtil.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/SerializerGeneratorUtil.java new file mode 100644 index 0000000000000..60cc83358ad26 --- /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.JsonbSupportClassGenerator; + +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(JsonbSupportClassGenerator.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..1138e471e532b --- /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 Supported supports(Type type, TypeSerializerGeneratorRegistry registry) { + return DotNames.STRING.equals(type.name()) ? Supported.FULLY : Supported.UNSUPPORTED; + } + + @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..023d511d6bb6a --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGenerator.java @@ -0,0 +1,114 @@ +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 { + + Supported supports(Type type, TypeSerializerGeneratorRegistry registry); + + enum Supported { + FULLY, + WITH_UNHANDLED, + UNSUPPORTED + } + + void generate(GenerateContext context); + + 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; + + // 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 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; + this.nullChecked = nullChecked; + this.effectivePropertyAnnotations = effectivePropertyAnnotations; + } + + Type getType() { + return type; + } + + BytecodeCreator getBytecodeCreator() { + return bytecodeCreator; + } + + ResultHandle getJsonGenerator() { + return jsonGenerator; + } + + ResultHandle getSerializationContext() { + return serializationContext; + } + + ResultHandle getCurrentItem() { + return currentItem; + } + + TypeSerializerGeneratorRegistry getRegistry() { + return registry; + } + + GlobalSerializationConfig getGlobalConfig() { + return globalConfig; + } + + boolean isNullChecked() { + return nullChecked; + } + + 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, + registry, + globalConfig, + newNullChecked, effectivePropertyAnnotations); + } + + GenerateContext changeItem(BytecodeCreator newBytecodeCreator, Type newType, + ResultHandle newCurrentItem, boolean newNullChecked, + Map newEffectivePropertyAnnotations) { + return new GenerateContext(newType, newBytecodeCreator, jsonGenerator, serializationContext, newCurrentItem, + registry, + globalConfig, newNullChecked, newEffectivePropertyAnnotations); + } + + GenerateContext changeByteCodeCreator(BytecodeCreator newBytecodeCreator) { + 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 new file mode 100644 index 0000000000000..4bfe381a39a89 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/TypeSerializerGeneratorRegistry.java @@ -0,0 +1,51 @@ +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 PrimitiveIntTypeSerializerGenerator(), new PrimitiveLongTypeSerializerGenerator(), + new PrimitiveBooleanTypeSerializerGenerator(), + new BooleanTypeSerializerGenerator(), new IntegerTypeSerializerGenerator(), new LongTypeSerializerGenerator(), + new BigDecimalTypeSerializerGenerator(), + new LocalDateTimeSerializerGenerator(), + objectSerializer, + new ObjectArrayTypeSerializerGenerator(), new CollectionTypeSerializerGenerator(), + new MapTypeSerializerGenerator(), new OptionalTypeSerializerGenerator()); + + 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) != TypeSerializerGenerator.Supported.UNSUPPORTED) { + 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/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..8a6e196285615 --- /dev/null +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/serializers/UnhandledTypeGenerator.java @@ -0,0 +1,61 @@ +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 propertyCachedSerializer = bytecodeCreator.invokeStaticMethod( + MethodDescriptor.ofMethod(UnhandledTypeGeneratorUtil.class, "getSerializerForUnhandledType", + JsonbSerializer.class, Marshaller.class, Class.class, Object.class, String.class), + marshaller, bytecodeCreator.loadClass(enclosingType.name().toString()), + 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/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); + } +} 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; + } +} 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; + } +} 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 4eedd22e9367a..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 @@ -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, @@ -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++) { @@ -722,7 +732,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); 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()); + } +}