Skip to content

Commit

Permalink
fix object mapper generation for all primitive types
Browse files Browse the repository at this point in the history
  • Loading branch information
mariofusco committed Jul 18, 2024
1 parent 6a3d500 commit 524d373
Show file tree
Hide file tree
Showing 2 changed files with 153 additions and 28 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,47 @@
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;

import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException;
import com.fasterxml.jackson.databind.ser.std.StdSerializer;
import com.fasterxml.jackson.databind.type.SimpleType;

import io.quarkus.deployment.GeneratedClassGizmoAdaptor;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.gizmo.BytecodeCreator;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.FieldDescriptor;
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";
private static final String SUPER_CLASS_NAME = StdSerializer.class.getName();
private static final String JSON_GEN_CLASS_NAME = JsonGenerator.class.getName();

final BuildProducer<GeneratedClassBuildItem> generatedClassBuildItemBuildProducer;

public JacksonSerializerFactory(BuildProducer<GeneratedClassBuildItem> generatedClassBuildItemBuildProducer) {
this.generatedClassBuildItemBuildProducer = generatedClassBuildItemBuildProducer;
}

public String create(ClassInfo classInfo) {
public Optional<String> create(ClassInfo classInfo) {
//if (true) return Optional.empty();
String beanClassName = classInfo.name().toString();
String generatedClassName = beanClassName + "$quarkusjacksonserializer";

Expand All @@ -39,7 +56,7 @@ public String create(ClassInfo classInfo) {
createSerializeMethod(classInfo, classCreator, beanClassName);
}

return generatedClassName;
return Optional.of(generatedClassName);
}

private static void createConstructor(ClassCreator classCreator, String beanClassName) {
Expand All @@ -55,49 +72,157 @@ private void createSerializeMethod(ClassInfo classInfo, ClassCreator classCreato
"com.fasterxml.jackson.databind.SerializerProvider");
serialize.setModifiers(ACC_PUBLIC);
serialize.addException(IOException.class);
serializeObject(classInfo, beanClassName, serialize);
serialize.returnVoid();
}

private void serializeObject(ClassInfo classInfo, String beanClassName, MethodCreator serialize) {
Set<String> serializedFields = new HashSet<>();
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;
}
}
serializeFields(classInfo, serialize, valueHandle, jsonGenerator, serializedFields);
serializeMethods(classInfo, serialize, valueHandle, jsonGenerator, serializedFields);

// jsonGenerator.writeEndObject();
MethodDescriptor writeEndObject = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writeEndObject", "void");
serialize.invokeVirtualMethod(writeEndObject, jsonGenerator);

serialize.returnVoid();
if (serializedFields.isEmpty()) {
throwExceptionForEmptyBean(beanClassName, serialize, valueHandle, jsonGenerator);
}
}

private void serializeFields(ClassInfo classInfo, MethodCreator serialize, ResultHandle valueHandle,
ResultHandle jsonGenerator, Set<String> serializedFields) {
for (FieldInfo fieldInfo : classInfo.fields()) {
valueReaderHandle(classInfo, fieldInfo, serialize, valueHandle).ifPresent(arg -> {
String fieldName = fieldInfo.name();
serialize.invokeVirtualMethod(writeField(fieldInfo.type()), jsonGenerator, serialize.load(fieldName), arg);
serializedFields.add(fieldName);
});
}
}

private void serializeMethods(ClassInfo classInfo, MethodCreator serialize, ResultHandle valueHandle,
ResultHandle jsonGenerator, Set<String> serializedFields) {
for (MethodInfo methodInfo : classInfo.methods()) {
fieldNameFromMethod(methodInfo).ifPresent(fieldName -> {
if (serializedFields.add(fieldName)) {
ResultHandle arg = serialize.invokeVirtualMethod(MethodDescriptor.of(methodInfo), valueHandle);
serialize.invokeVirtualMethod(writeField(methodInfo.returnType()), jsonGenerator, serialize.load(fieldName),
arg);
}
});
}
}

private Optional<String> fieldNameFromMethod(MethodInfo methodInfo) {
if (isGetterMethod(methodInfo)) {
String methodName = methodInfo.name();
String fieldName = isBooleanType(methodInfo.returnType().toString())
? methodName.substring(2, 3).toLowerCase() + methodName.substring(3)
: methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
return Optional.of(fieldName);
}
return Optional.empty();
}

private Optional<ResultHandle> valueReaderHandle(ClassInfo classInfo, FieldInfo fieldInfo, MethodCreator serialize,
ResultHandle valueHandle) {
String getterMethodName = getterMethodName(classInfo, fieldInfo);
MethodInfo getterMethodInfo = classInfo.method(getterMethodName);
if (getterMethodInfo != null && isGetterMethod(getterMethodInfo)) {
return Optional.of(serialize.invokeVirtualMethod(MethodDescriptor.of(getterMethodInfo), valueHandle));
}
if (Modifier.isPublic(fieldInfo.flags())) {
return Optional.of(serialize.readInstanceField(FieldDescriptor.of(fieldInfo), valueHandle));
}
return Optional.empty();
}

private static boolean isGetterMethod(MethodInfo methodInfo) {
String methodName = methodInfo.name();
return Modifier.isPublic(methodInfo.flags()) && methodInfo.parametersCount() == 0
&& (methodName.startsWith("get") || methodName.startsWith("is"));
}

private MethodDescriptor writeField(Type fieldInfo) {
String typeName = fieldInfo.name().toString();
return MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, writeMethodName(typeName), "void", "java.lang.String", typeName);
}

private static void throwExceptionForEmptyBean(String beanClassName, MethodCreator serialize, ResultHandle valueHandle,
ResultHandle jsonGenerator) {
String serializationFeatureClassName = SerializationFeature.class.getName();

ResultHandle serializerProvider = serialize.getMethodParam(2);
MethodDescriptor isEnabled = MethodDescriptor.ofMethod(SerializerProvider.class.getName(), "isEnabled", "boolean",
serializationFeatureClassName);

// if (serializerProvider.isEnabled(SerializationFeature.FAIL_ON_EMPTY_BEANS))
FieldDescriptor failField = FieldDescriptor.of(serializationFeatureClassName, "FAIL_ON_EMPTY_BEANS",
serializationFeatureClassName);
ResultHandle failOnEmptyBeans = serialize.readStaticField(failField);
ResultHandle isFailEnabled = serialize.invokeVirtualMethod(isEnabled, serializerProvider, failOnEmptyBeans);
BytecodeCreator isFailEnabledBranch = serialize.ifTrue(isFailEnabled).trueBranch();

// JavaType type = SimpleType.constructUnsafe(Class<?> cls)
ResultHandle javaType = isFailEnabledBranch.invokeStaticMethod(
MethodDescriptor.ofMethod(SimpleType.class, "constructUnsafe", SimpleType.class, Class.class),
isFailEnabledBranch.loadClass(beanClassName));

// throw InvalidDefinitionException.from(JsonGenerator g, String msg, JavaType type)
MethodDescriptor exceptionConstructor = MethodDescriptor.ofMethod(InvalidDefinitionException.class, "from",
InvalidDefinitionException.class, JsonGenerator.class, String.class, JavaType.class);
String errorMsg = String.format(
"No serializer found for class %s and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS)",
beanClassName);
ResultHandle invalidException = isFailEnabledBranch.invokeStaticMethod(exceptionConstructor, jsonGenerator,
isFailEnabledBranch.load(errorMsg), javaType);
isFailEnabledBranch.throwException(invalidException);
}

private String writeMethodName(String typeName) {
switch (typeName) {
case "java.lang.String":
return "writeStringField";
case "short":
case "java.lang.Short":
case "int":
case "java.lang.Integer":
case "long":
case "java.lang.long":
case "float":
case "java.lang.Float":
case "double":
case "java.lang.Double":
return "writeNumberField";
case "boolean":
case "java.lang.Boolean":
return "writeBooleanField";
default:
throw new UnsupportedOperationException("Unknown type: " + typeName);
}
}

private String getterMethodName(ClassInfo classInfo, FieldInfo fieldInfo) {
if (classInfo.method(fieldInfo.name()) != null) {
return fieldInfo.name();
}
return "get" + ucFirst(fieldInfo.name());
String typeName = fieldInfo.name().toString();
return (isBooleanType(typeName) ? "is" : "get") + ucFirst(fieldInfo.name());
}

public String ucFirst(String name) {
private String ucFirst(String name) {
return name.substring(0, 1).toUpperCase() + name.substring(1);
}

private boolean isBooleanType(String type) {
return type.equals("boolean") || type.equals("java.lang.Boolean");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -326,10 +326,10 @@ void handleJsonAnnotations(Optional<ResourceScanningResultBuildItem> resourceSca
recorder.recordCustomSerialization(getMethodId(bi.getMethodInfo(), bi.getDeclaringClassInfo()), className);
}

if (generatedJsonSerializationBuildItems != null) {
if (generatedJsonSerializationBuildItems != null && !generatedJsonSerializationBuildItems.getJsonClasses().isEmpty()) {
JacksonSerializerFactory factory = new JacksonSerializerFactory(generatedClassBuildItemBuildProducer);
for (ClassInfo classInfo : generatedJsonSerializationBuildItems.getJsonClasses().values()) {
recorder.recordGeneratedSerializer(factory.create(classInfo));
factory.create(classInfo).ifPresent(recorder::recordGeneratedSerializer);
}
}

Expand Down

0 comments on commit 524d373

Please sign in to comment.