Skip to content

Commit

Permalink
generate jackson serializers
Browse files Browse the repository at this point in the history
  • Loading branch information
mariofusco committed Jun 7, 2024
1 parent 48fbb51 commit 73962b5
Show file tree
Hide file tree
Showing 5 changed files with 229 additions and 19 deletions.
Original file line number Diff line number Diff line change
@@ -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<String, ClassInfo> jsonClasses;

public GeneratedJsonSerializationBuildItem(Map<String, ClassInfo> jsonClasses) {
this.jsonClasses = jsonClasses;
}

public Map<String, ClassInfo> getJsonClasses() {
return jsonClasses;
}
}
Original file line number Diff line number Diff line change
@@ -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<GeneratedClassBuildItem> generatedClassBuildItemBuildProducer;

public JacksonSerializerFactory(BuildProducer<GeneratedClassBuildItem> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -236,6 +238,8 @@ void reflection(BuildProducer<ReflectiveClassBuildItem> producer) {
void handleJsonAnnotations(Optional<ResourceScanningResultBuildItem> resourceScanningResultBuildItem,
CombinedIndexBuildItem index,
List<ResourceMethodCustomSerializationBuildItem> resourceMethodCustomSerializationBuildItems,
GeneratedJsonSerializationBuildItem generatedJsonSerializationBuildItems,
BuildProducer<GeneratedClassBuildItem> generatedClassBuildItemBuildProducer,
BuildProducer<ReflectiveClassBuildItem> reflectiveClassProducer,
BuildProducer<JacksonFeatureBuildItem> jacksonFeaturesProducer,
ResteasyReactiveServerJacksonRecorder recorder, ShutdownContextBuildItem shutdown) {
Expand Down Expand Up @@ -322,6 +326,13 @@ void handleJsonAnnotations(Optional<ResourceScanningResultBuildItem> 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));
Expand Down Expand Up @@ -370,6 +381,39 @@ public void initializeRolesAllowedConfigExp(ResteasyReactiveServerJacksonRecorde
}
}

@BuildStep
public void handleEndpointParams(ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntries,
JaxRsResourceIndexBuildItem index,
BuildProducer<GeneratedJsonSerializationBuildItem> producer) {

IndexView indexView = index.getIndexView();

Map<String, ClassInfo> 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,
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -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<String, Boolean> 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -25,6 +28,8 @@ public class ResteasyReactiveServerJacksonRecorder {
private static final Map<String, Class<?>> customSerializationMap = new HashMap<>();
private static final Map<String, Class<?>> customDeserializationMap = new HashMap<>();

private static final Set<Class<? extends StdSerializer>> generatedSerializer = new HashSet<>();

/* STATIC INIT */
public RuntimeValue<Map<String, Supplier<String[]>>> createConfigExpToAllowedRoles() {
return new RuntimeValue<>(new ConcurrentHashMap<>());
Expand Down Expand Up @@ -76,6 +81,10 @@ public void recordCustomDeserialization(String target, String className) {
customDeserializationMap.put(target, loadClass(className));
}

public void recordGeneratedSerializer(String className) {
generatedSerializer.add((Class<? extends StdSerializer>) loadClass(className));
}

public void configureShutdown(ShutdownContext shutdownContext) {
shutdownContext.addShutdownTask(new Runnable() {
@Override
Expand Down Expand Up @@ -116,6 +125,10 @@ public static Class<? extends BiFunction<ObjectMapper, Type, ObjectReader>> cust
return (Class<? extends BiFunction<ObjectMapper, Type, ObjectReader>>) customDeserializationMap.get(clazz.getName());
}

public static Set<Class<? extends StdSerializer>> getGeneratedSerializer() {
return generatedSerializer;
}

private Class<?> loadClass(String className) {
try {
return Thread.currentThread().getContextClassLoader().loadClass(className);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -19,16 +20,37 @@

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 {

private final ObjectWriter defaultWriter;

@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<? extends StdSerializer> 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 {
Expand Down

0 comments on commit 73962b5

Please sign in to comment.