diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/GeneratedJsonSerializationBuildItem.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/GeneratedJsonSerializationBuildItem.java new file mode 100644 index 00000000000000..dd488c5d8a13a9 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/GeneratedJsonSerializationBuildItem.java @@ -0,0 +1,20 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.processor; + +import java.util.Map; + +import org.jboss.jandex.ClassInfo; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class GeneratedJsonSerializationBuildItem extends SimpleBuildItem { + + private final Map jsonClasses; + + public GeneratedJsonSerializationBuildItem(Map jsonClasses) { + this.jsonClasses = jsonClasses; + } + + public Map getJsonClasses() { + return jsonClasses; + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonSerializerFactory.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonSerializerFactory.java new file mode 100644 index 00000000000000..c50c7588e983a8 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonSerializerFactory.java @@ -0,0 +1,103 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.processor; + +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; + +import java.io.IOException; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.FieldInfo; + +import io.quarkus.deployment.GeneratedClassGizmoAdaptor; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.gizmo.ClassCreator; +import io.quarkus.gizmo.MethodCreator; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; + +public class JacksonSerializerFactory { + + private static final String SUPER_CLASS_NAME = "com.fasterxml.jackson.databind.ser.std.StdSerializer"; + private static final String JSON_GEN_CLASS_NAME = "com.fasterxml.jackson.core.JsonGenerator"; + + final BuildProducer generatedClassBuildItemBuildProducer; + + public JacksonSerializerFactory(BuildProducer generatedClassBuildItemBuildProducer) { + this.generatedClassBuildItemBuildProducer = generatedClassBuildItemBuildProducer; + } + + public String create(ClassInfo classInfo) { + String beanClassName = classInfo.name().toString(); + String generatedClassName = beanClassName + "$quarkusjacksonserializer"; + + try (ClassCreator classCreator = new ClassCreator( + new GeneratedClassGizmoAdaptor(generatedClassBuildItemBuildProducer, true), generatedClassName, null, + SUPER_CLASS_NAME)) { + + createConstructor(classCreator, beanClassName); + + createSerializeMethod(classInfo, classCreator, beanClassName); + } + + return generatedClassName; + } + + private static void createConstructor(ClassCreator classCreator, String beanClassName) { + MethodCreator constructor = classCreator.getConstructorCreator(new String[0]); + constructor.invokeSpecialMethod( + MethodDescriptor.ofConstructor(SUPER_CLASS_NAME, "java.lang.Class"), + constructor.getThis(), constructor.loadClass(beanClassName)); + constructor.returnVoid(); + } + + private void createSerializeMethod(ClassInfo classInfo, ClassCreator classCreator, String beanClassName) { + MethodCreator serialize = classCreator.getMethodCreator("serialize", "void", "java.lang.Object", JSON_GEN_CLASS_NAME, + "com.fasterxml.jackson.databind.SerializerProvider"); + serialize.setModifiers(ACC_PUBLIC); + serialize.addException(IOException.class); + + ResultHandle valueHandle = serialize.checkCast(serialize.getMethodParam(0), beanClassName); + ResultHandle jsonGenerator = serialize.getMethodParam(1); + + // jsonGenerator.writeStartObject(); + MethodDescriptor writeStartObject = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writeStartObject", "void"); + serialize.invokeVirtualMethod(writeStartObject, jsonGenerator); + + for (FieldInfo fieldInfo : classInfo.fields()) { + String typeName = fieldInfo.type().name().toString(); + switch (typeName) { + case "java.lang.String": + case "int": + case "long": + case "float": + case "double": + String writeMethodName = typeName.equals("java.lang.String") ? "writeStringField" : "writeNumberField"; + + MethodDescriptor readString = MethodDescriptor.ofMethod(beanClassName, + getterMethodName(classInfo, fieldInfo), typeName); + MethodDescriptor writeField = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, writeMethodName, "void", + "java.lang.String", typeName); + serialize.invokeVirtualMethod(writeField, jsonGenerator, serialize.load(fieldInfo.name()), + serialize.invokeVirtualMethod(readString, valueHandle)); + break; + } + } + + // jsonGenerator.writeEndObject(); + MethodDescriptor writeEndObject = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writeEndObject", "void"); + serialize.invokeVirtualMethod(writeEndObject, jsonGenerator); + + serialize.returnVoid(); + } + + private String getterMethodName(ClassInfo classInfo, FieldInfo fieldInfo) { + if (classInfo.method(fieldInfo.name()) != null) { + return fieldInfo.name(); + } + return "get" + ucFirst(fieldInfo.name()); + } + + public String ucFirst(String name) { + return name.substring(0, 1).toUpperCase() + name.substring(1); + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java index b589e330ee9035..8932da5869d1aa 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/ResteasyReactiveJacksonProcessor.java @@ -21,6 +21,7 @@ import jakarta.inject.Singleton; import jakarta.ws.rs.Priorities; +import jakarta.ws.rs.Produces; import jakarta.ws.rs.RuntimeType; import jakarta.ws.rs.core.Cookie; import jakarta.ws.rs.core.MediaType; @@ -57,6 +58,7 @@ import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; import io.quarkus.deployment.builditem.RuntimeConfigSetupCompleteBuildItem; import io.quarkus.deployment.builditem.ShutdownContextBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; @@ -236,6 +238,8 @@ void reflection(BuildProducer producer) { void handleJsonAnnotations(Optional resourceScanningResultBuildItem, CombinedIndexBuildItem index, List resourceMethodCustomSerializationBuildItems, + GeneratedJsonSerializationBuildItem generatedJsonSerializationBuildItems, + BuildProducer generatedClassBuildItemBuildProducer, BuildProducer reflectiveClassProducer, BuildProducer jacksonFeaturesProducer, ResteasyReactiveServerJacksonRecorder recorder, ShutdownContextBuildItem shutdown) { @@ -322,6 +326,13 @@ void handleJsonAnnotations(Optional resourceSca recorder.recordCustomSerialization(getMethodId(bi.getMethodInfo(), bi.getDeclaringClassInfo()), className); } + if (generatedJsonSerializationBuildItems != null) { + JacksonSerializerFactory factory = new JacksonSerializerFactory(generatedClassBuildItemBuildProducer); + for (ClassInfo classInfo : generatedJsonSerializationBuildItems.getJsonClasses().values()) { + recorder.recordGeneratedSerializer(factory.create(classInfo)); + } + } + if (!jacksonFeatures.isEmpty()) { for (JacksonFeatureBuildItem.Feature jacksonFeature : jacksonFeatures) { jacksonFeaturesProducer.produce(new JacksonFeatureBuildItem(jacksonFeature)); @@ -370,6 +381,39 @@ public void initializeRolesAllowedConfigExp(ResteasyReactiveServerJacksonRecorde } } + @BuildStep + public void handleEndpointParams(ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntries, + JaxRsResourceIndexBuildItem index, + BuildProducer producer) { + + IndexView indexView = index.getIndexView(); + + Map result = new HashMap<>(); + for (ResteasyReactiveResourceMethodEntriesBuildItem.Entry entry : resourceMethodEntries.getEntries()) { + MethodInfo methodInfo = entry.getMethodInfo(); + AnnotationInstance producesAnn = methodInfo.annotation(Produces.class); + if (producesAnn != null && producesAnn.value().toString().contains("json")) { + Type returnType = methodInfo.returnType(); + if (returnType.kind() == Type.Kind.VOID) { + continue; + } + Type effectiveReturnType = getEffectiveReturnType(returnType); + if (effectiveReturnType == null) { + continue; + } + + ClassInfo effectiveReturnClassInfo = indexView.getClassByName(effectiveReturnType.name()); + if (effectiveReturnClassInfo != null) { + result.put(effectiveReturnType.name().toString(), effectiveReturnClassInfo); + } + } + } + + if (!result.isEmpty()) { + producer.produce(new GeneratedJsonSerializationBuildItem(result)); + } + } + @BuildStep public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntries, JaxRsResourceIndexBuildItem index, @@ -411,25 +455,9 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r if (returnType.kind() == Type.Kind.VOID) { continue; } - Type effectiveReturnType = returnType; - if (effectiveReturnType.name().equals(ResteasyReactiveDotNames.REST_RESPONSE) || - effectiveReturnType.name().equals(ResteasyReactiveDotNames.UNI) || - effectiveReturnType.name().equals(ResteasyReactiveDotNames.COMPLETABLE_FUTURE) || - effectiveReturnType.name().equals(ResteasyReactiveDotNames.COMPLETION_STAGE) || - effectiveReturnType.name().equals(ResteasyReactiveDotNames.REST_MULTI) || - effectiveReturnType.name().equals(ResteasyReactiveDotNames.MULTI)) { - if (effectiveReturnType.kind() != Type.Kind.PARAMETERIZED_TYPE) { - continue; - } - - effectiveReturnType = returnType.asParameterizedType().arguments().get(0); - } - if (effectiveReturnType.name().equals(ResteasyReactiveDotNames.SET) || - effectiveReturnType.name().equals(ResteasyReactiveDotNames.COLLECTION) || - effectiveReturnType.name().equals(ResteasyReactiveDotNames.LIST)) { - effectiveReturnType = effectiveReturnType.asParameterizedType().arguments().get(0); - } else if (effectiveReturnType.name().equals(ResteasyReactiveDotNames.MAP)) { - effectiveReturnType = effectiveReturnType.asParameterizedType().arguments().get(1); + Type effectiveReturnType = getEffectiveReturnType(returnType); + if (effectiveReturnType == null) { + continue; } ClassInfo effectiveReturnClassInfo = indexView.getClassByName(effectiveReturnType.name()); @@ -461,6 +489,30 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r } } + private static Type getEffectiveReturnType(Type returnType) { + Type effectiveReturnType = returnType; + if (effectiveReturnType.name().equals(ResteasyReactiveDotNames.REST_RESPONSE) || + effectiveReturnType.name().equals(ResteasyReactiveDotNames.UNI) || + effectiveReturnType.name().equals(ResteasyReactiveDotNames.COMPLETABLE_FUTURE) || + effectiveReturnType.name().equals(ResteasyReactiveDotNames.COMPLETION_STAGE) || + effectiveReturnType.name().equals(ResteasyReactiveDotNames.REST_MULTI) || + effectiveReturnType.name().equals(ResteasyReactiveDotNames.MULTI)) { + if (effectiveReturnType.kind() != Type.Kind.PARAMETERIZED_TYPE) { + return null; + } + + effectiveReturnType = returnType.asParameterizedType().arguments().get(0); + } + if (effectiveReturnType.name().equals(ResteasyReactiveDotNames.SET) || + effectiveReturnType.name().equals(ResteasyReactiveDotNames.COLLECTION) || + effectiveReturnType.name().equals(ResteasyReactiveDotNames.LIST)) { + effectiveReturnType = effectiveReturnType.asParameterizedType().arguments().get(0); + } else if (effectiveReturnType.name().equals(ResteasyReactiveDotNames.MAP)) { + effectiveReturnType = effectiveReturnType.asParameterizedType().arguments().get(1); + } + return effectiveReturnType; + } + private static Map getTypesWithSecureField() { // if any of following types is detected as an endpoint return type or a field of endpoint return type, // we always need to apply security serialization as any type can be represented with them diff --git a/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java b/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java index a55ddadc4e3cef..33be678baad094 100644 --- a/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java +++ b/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/ResteasyReactiveServerJacksonRecorder.java @@ -2,7 +2,9 @@ import java.lang.reflect.Type; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.function.BiConsumer; import java.util.function.BiFunction; @@ -11,6 +13,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; import io.quarkus.arc.Arc; import io.quarkus.resteasy.reactive.jackson.runtime.security.RolesAllowedConfigExpStorage; @@ -25,6 +28,8 @@ public class ResteasyReactiveServerJacksonRecorder { private static final Map> customSerializationMap = new HashMap<>(); private static final Map> customDeserializationMap = new HashMap<>(); + private static final Set> generatedSerializer = new HashSet<>(); + /* STATIC INIT */ public RuntimeValue>> createConfigExpToAllowedRoles() { return new RuntimeValue<>(new ConcurrentHashMap<>()); @@ -76,6 +81,10 @@ public void recordCustomDeserialization(String target, String className) { customDeserializationMap.put(target, loadClass(className)); } + public void recordGeneratedSerializer(String className) { + generatedSerializer.add((Class) loadClass(className)); + } + public void configureShutdown(ShutdownContext shutdownContext) { shutdownContext.addShutdownTask(new Runnable() { @Override @@ -116,6 +125,10 @@ public static Class> cust return (Class>) customDeserializationMap.get(clazz.getName()); } + public static Set> getGeneratedSerializer() { + return generatedSerializer; + } + private Class loadClass(String className) { try { return Thread.currentThread().getContextClassLoader().loadClass(className); diff --git a/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/BasicServerJacksonMessageBodyWriter.java b/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/BasicServerJacksonMessageBodyWriter.java index 48bfd7cfe7ebab..92b4150846ae05 100644 --- a/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/BasicServerJacksonMessageBodyWriter.java +++ b/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/BasicServerJacksonMessageBodyWriter.java @@ -6,6 +6,7 @@ import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Type; import java.nio.charset.StandardCharsets; @@ -19,6 +20,10 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectWriter; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import io.quarkus.resteasy.reactive.jackson.runtime.ResteasyReactiveServerJacksonRecorder; public class BasicServerJacksonMessageBodyWriter extends ServerMessageBodyWriter.AllWriteableMessageBodyWriter { @@ -26,9 +31,26 @@ public class BasicServerJacksonMessageBodyWriter extends ServerMessageBodyWriter @Inject public BasicServerJacksonMessageBodyWriter(ObjectMapper mapper) { + registerGenerateSerializers(mapper); this.defaultWriter = createDefaultWriter(mapper); } + private static void registerGenerateSerializers(ObjectMapper mapper) { + if (ResteasyReactiveServerJacksonRecorder.getGeneratedSerializer().isEmpty()) { + return; + } + SimpleModule module = new SimpleModule(); + for (Class serClass : ResteasyReactiveServerJacksonRecorder.getGeneratedSerializer()) { + try { + StdSerializer serializer = serClass.getConstructor().newInstance(); + module.addSerializer(serializer.handledType(), serializer); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + mapper.registerModule(module); + } + @Override public void writeResponse(Object o, Type genericType, ServerRequestContext context) throws WebApplicationException, IOException {