Skip to content

Commit

Permalink
Use SerializableString field names in generated Jackson serializers
Browse files Browse the repository at this point in the history
Take @JsonProperty into account in the reflection free Jackson serializers
  • Loading branch information
mariofusco committed Aug 20, 2024
1 parent 34ca0e0 commit af1d4f3
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 84 deletions.
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
package io.quarkus.resteasy.reactive.jackson.deployment.processor;

import static org.objectweb.asm.Opcodes.ACC_FINAL;
import static org.objectweb.asm.Opcodes.ACC_PUBLIC;
import static org.objectweb.asm.Opcodes.ACC_STATIC;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

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;
Expand All @@ -24,7 +27,10 @@
import org.jboss.jandex.ParameterizedType;
import org.jboss.jandex.Type;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.SerializableString;
import com.fasterxml.jackson.core.io.SerializedString;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.SerializerProvider;
Expand All @@ -37,6 +43,7 @@
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.gizmo.BytecodeCreator;
import io.quarkus.gizmo.ClassCreator;
import io.quarkus.gizmo.FieldCreator;
import io.quarkus.gizmo.FieldDescriptor;
import io.quarkus.gizmo.MethodCreator;
import io.quarkus.gizmo.MethodDescriptor;
Expand Down Expand Up @@ -109,11 +116,13 @@ public class JacksonSerializerFactory {

private static final String SUPER_CLASS_NAME = StdSerializer.class.getName();
private static final String JSON_GEN_CLASS_NAME = JsonGenerator.class.getName();
private static final String SER_STRINGS_CLASS_NAME = "SerializedStrings$quarkusjacksonserializer";

private final BuildProducer<GeneratedClassBuildItem> generatedClassBuildItemBuildProducer;
private final IndexView jandexIndex;

private final Set<String> generatedClassNames = new HashSet<>();
private final Map<String, Set<String>> generatedFields = new HashMap<>();
private final Deque<ClassInfo> toBeGenerated = new ArrayDeque<>();

public JacksonSerializerFactory(BuildProducer<GeneratedClassBuildItem> generatedClassBuildItemBuildProducer,
Expand All @@ -130,10 +139,39 @@ public Collection<String> create(Collection<ClassInfo> classInfos) {
create(toBeGenerated.removeFirst()).ifPresent(createdClasses::add);
}

createFieldNamesClass();

return createdClasses;
}

public Optional<String> create(ClassInfo classInfo) {
public void createFieldNamesClass() {
if (generatedFields.isEmpty()) {
return;
}

MethodDescriptor serStringCtor = MethodDescriptor.ofConstructor(SerializedString.class, String.class);

for (Map.Entry<String, Set<String>> fieldsInPkg : generatedFields.entrySet()) {
try (ClassCreator classCreator = new ClassCreator(
new GeneratedClassGizmoAdaptor(generatedClassBuildItemBuildProducer, true),
fieldsInPkg.getKey() + "." + SER_STRINGS_CLASS_NAME, null,
"java.lang.Object")) {

MethodCreator clinit = classCreator.getMethodCreator("<clinit>", void.class).setModifiers(ACC_STATIC);

for (String field : fieldsInPkg.getValue()) {
FieldCreator fieldCreator = classCreator.getFieldCreator(field, SerializedString.class.getName())
.setModifiers(ACC_STATIC | ACC_FINAL);
clinit.writeStaticField(fieldCreator.getFieldDescriptor(),
clinit.newInstance(serStringCtor, clinit.load(field)));
}

clinit.returnVoid();
}
}
}

private Optional<String> create(ClassInfo classInfo) {
String beanClassName = classInfo.name().toString();
if (vetoedClassName(beanClassName) || !generatedClassNames.add(beanClassName)) {
return Optional.empty();
Expand Down Expand Up @@ -161,9 +199,9 @@ private void createConstructor(ClassCreator classCreator, String beanClassName)

private boolean 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);
"com.fasterxml.jackson.databind.SerializerProvider")
.setModifiers(ACC_PUBLIC)
.addException(IOException.class);
boolean valid = serializeObject(classInfo, beanClassName, serialize);
serialize.returnVoid();
return valid;
Expand Down Expand Up @@ -206,16 +244,14 @@ private boolean serializeFields(ClassInfo classInfo, MethodCreator serialize, Re
if (Modifier.isStatic(fieldInfo.flags())) {
continue;
}
AnnotationTarget target = valueReader(classInfo, fieldInfo);
if (target != null) {
String fieldName = fieldInfo.name();
if (serializedFields.add(fieldName)) {
if (hasUnknownAnnotation(fieldInfo) || (fieldInfo != target && hasUnknownAnnotation(target))) {
FieldSpecs fieldSpecs = fieldSpecsFromField(classInfo, fieldInfo);
if (fieldSpecs != null) {
if (serializedFields.add(fieldSpecs.fieldName)) {
if (fieldSpecs.hasUnknownAnnotation()) {
return false;
}
ResultHandle arg = toValueReaderHandle(target, serialize, valueHandle);
writeField(fieldInfo.type(), fieldName, writeFieldBranch(serialize, fieldInfo, target), jsonGenerator,
serializerProvider, arg);
writeField(classInfo, fieldSpecs, writeFieldBranch(serialize, fieldSpecs), jsonGenerator,
serializerProvider, valueHandle);
}
}
}
Expand All @@ -228,34 +264,44 @@ private boolean serializeMethods(ClassInfo classInfo, MethodCreator serialize, R
if (Modifier.isStatic(methodInfo.flags())) {
continue;
}
String fieldName = fieldNameFromMethod(methodInfo);
if (fieldName != null && serializedFields.add(fieldName)) {
if (hasUnknownAnnotation(methodInfo)) {
FieldSpecs fieldSpecs = fieldSpecsFromMethod(methodInfo);
if (fieldSpecs != null && serializedFields.add(fieldSpecs.fieldName)) {
if (fieldSpecs.hasUnknownAnnotation()) {
return false;
}
ResultHandle arg = serialize.invokeVirtualMethod(MethodDescriptor.of(methodInfo), valueHandle);
writeField(methodInfo.returnType(), fieldName, serialize, jsonGenerator, serializerProvider, arg);
writeField(classInfo, fieldSpecs, serialize, jsonGenerator, serializerProvider, valueHandle);
}
}
return true;
}

private void writeField(Type fieldType, String fieldName, BytecodeCreator bytecode, ResultHandle jsonGenerator,
ResultHandle serializerProvider, ResultHandle fieldReader) {
String typeName = fieldType.name().toString();
private void writeField(ClassInfo classInfo, FieldSpecs fieldSpecs, BytecodeCreator bytecode, ResultHandle jsonGenerator,
ResultHandle serializerProvider, ResultHandle valueHandle) {
String pkgName = classInfo.name().packagePrefixName().toString();
generatedFields.computeIfAbsent(pkgName, pkg -> new HashSet<>()).add(fieldSpecs.jsonName);
MethodDescriptor writeFieldName = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writeFieldName", void.class,
SerializableString.class);
ResultHandle serStringHandle = bytecode.readStaticField(
FieldDescriptor.of(pkgName + "." + SER_STRINGS_CLASS_NAME, fieldSpecs.jsonName,
SerializedString.class.getName()));
bytecode.invokeVirtualMethod(writeFieldName, jsonGenerator, serStringHandle);

ResultHandle arg = fieldSpecs.toValueReaderHandle(bytecode, valueHandle);
String typeName = fieldSpecs.fieldType.name().toString();
String primitiveMethodName = writeMethodForPrimitiveFields(typeName);

if (primitiveMethodName != null) {
MethodDescriptor primitiveWriter = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, primitiveMethodName, "void",
"java.lang.String", typeName);
bytecode.invokeVirtualMethod(primitiveWriter, jsonGenerator, bytecode.load(fieldName), fieldReader);
typeName);
bytecode.invokeVirtualMethod(primitiveWriter, jsonGenerator, arg);
return;
}

registerTypeToBeGenerated(fieldType, typeName);
registerTypeToBeGenerated(fieldSpecs.fieldType, typeName);

MethodDescriptor writeMethod = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writePOJOField",
void.class, String.class, Object.class);
bytecode.invokeVirtualMethod(writeMethod, jsonGenerator, bytecode.load(fieldName), fieldReader);
MethodDescriptor writeMethod = MethodDescriptor.ofMethod(JSON_GEN_CLASS_NAME, "writePOJO",
void.class, Object.class);
bytecode.invokeVirtualMethod(writeMethod, jsonGenerator, arg);
}

private void registerTypeToBeGenerated(Type fieldType, String typeName) {
Expand Down Expand Up @@ -300,21 +346,17 @@ private void registerTypeToBeGenerated(String typeName) {

private String writeMethodForPrimitiveFields(String typeName) {
return switch (typeName) {
case "java.lang.String" -> "writeStringField";
case "java.lang.String" -> "writeString";
case "short", "java.lang.Short", "int", "java.lang.Integer", "long", "java.lang.Long", "float",
"java.lang.Float", "double", "java.lang.Double" ->
"writeNumberField";
case "boolean", "java.lang.Boolean" -> "writeBooleanField";
"writeNumber";
case "boolean", "java.lang.Boolean" -> "writeBoolean";
default -> null;
};
}

private boolean hasUnknownAnnotation(AnnotationTarget target) {
return target.annotations().stream().anyMatch(ann -> ann.name().toString().startsWith("com.fasterxml.jackson."));
}

private BytecodeCreator writeFieldBranch(MethodCreator serialize, FieldInfo fieldInfo, AnnotationTarget target) {
String[] rolesAllowed = rolesAllowed(fieldInfo, target);
private BytecodeCreator writeFieldBranch(MethodCreator serialize, FieldSpecs fieldSpecs) {
String[] rolesAllowed = fieldSpecs.rolesAllowed();
if (rolesAllowed != null) {
ResultHandle rolesArray = serialize.newArray(String.class, rolesAllowed.length);
for (int i = 0; i < rolesAllowed.length; ++i) {
Expand All @@ -329,18 +371,6 @@ private BytecodeCreator writeFieldBranch(MethodCreator serialize, FieldInfo fiel
return serialize;
}

private String[] rolesAllowed(FieldInfo fieldInfo, AnnotationTarget target) {
AnnotationInstance secureField = fieldInfo.annotation(SecureField.class);
if (secureField == null && target != fieldInfo) {
secureField = target.annotation(SecureField.class);
}
if (secureField != null) {
AnnotationValue rolesAllowed = secureField.value("rolesAllowed");
return rolesAllowed != null ? rolesAllowed.asStringArray() : null;
}
return null;
}

private Collection<FieldInfo> classFields(ClassInfo classInfo) {
Collection<FieldInfo> fields = new ArrayList<>();
classFields(classInfo, fields);
Expand Down Expand Up @@ -369,37 +399,6 @@ private void classMethods(ClassInfo classInfo, Collection<MethodInfo> methods) {
});
}

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

private AnnotationTarget valueReader(ClassInfo classInfo, FieldInfo fieldInfo) {
MethodInfo getterMethodInfo = getterMethodInfo(classInfo, fieldInfo);
if (getterMethodInfo != null) {
return getterMethodInfo;
}
if (Modifier.isPublic(fieldInfo.flags())) {
return fieldInfo;
}
return null;
}

private ResultHandle toValueReaderHandle(Object member, BytecodeCreator serialize, ResultHandle valueHandle) {
if (member instanceof MethodInfo m) {
return serialize.invokeVirtualMethod(MethodDescriptor.of(m), valueHandle);
}
if (member instanceof FieldInfo f) {
return serialize.readInstanceField(FieldDescriptor.of(f), valueHandle);
}
throw new UnsupportedOperationException("Unknown member type: " + member.getClass());
}

private <T> T onSuperClass(ClassInfo classInfo, Function<ClassInfo, T> f) {
Type superType = classInfo.superClassType();
if (superType != null && !vetoedClassName(superType.name().toString())) {
Expand Down Expand Up @@ -463,15 +462,108 @@ private MethodInfo findMethod(ClassInfo classInfo, String methodName, Type... pa
: onSuperClass(classInfo, superClassInfo -> findMethod(superClassInfo, methodName, parameters));
}

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

private boolean isBooleanType(String type) {
private static boolean isBooleanType(String type) {
return type.equals("boolean") || type.equals("java.lang.Boolean");
}

private boolean vetoedClassName(String className) {
private static boolean vetoedClassName(String className) {
return className.startsWith("java.") || className.startsWith("jakarta.") || className.startsWith("io.vertx.core.json.");
}

private FieldSpecs fieldSpecsFromField(ClassInfo classInfo, FieldInfo fieldInfo) {
MethodInfo getterMethodInfo = getterMethodInfo(classInfo, fieldInfo);
if (getterMethodInfo != null) {
return new FieldSpecs(fieldInfo, getterMethodInfo);
}
if (Modifier.isPublic(fieldInfo.flags())) {
return new FieldSpecs(fieldInfo);
}
return null;
}

private FieldSpecs fieldSpecsFromMethod(MethodInfo methodInfo) {
return isGetterMethod(methodInfo) ? new FieldSpecs(methodInfo) : null;
}

private static class FieldSpecs {

private final String fieldName;
private final String jsonName;
private final Type fieldType;
private final Map<String, AnnotationInstance> annotations = new HashMap<>();

private MethodInfo methodInfo;
private FieldInfo fieldInfo;

FieldSpecs(FieldInfo fieldInfo) {
this(fieldInfo, null);
}

FieldSpecs(MethodInfo methodInfo) {
this(null, methodInfo);
}

FieldSpecs(FieldInfo fieldInfo, MethodInfo methodInfo) {
if (fieldInfo != null) {
this.fieldInfo = fieldInfo;
fieldInfo.annotations().forEach(a -> annotations.put(a.name().toString(), a));
}
if (methodInfo != null) {
this.methodInfo = methodInfo;
methodInfo.annotations().forEach(a -> annotations.put(a.name().toString(), a));
}
this.fieldType = fieldType();
this.fieldName = fieldName();
this.jsonName = jsonName();
}

private Type fieldType() {
return fieldInfo != null ? fieldInfo.type() : methodInfo.returnType();
}

private String jsonName() {
AnnotationInstance jsonProperty = annotations.get(JsonProperty.class.getName());
if (jsonProperty != null) {
AnnotationValue value = jsonProperty.value();
if (value != null && !value.asString().isEmpty()) {
return value.asString();
}
}
return fieldName();
}

private String fieldName() {
return fieldInfo != null ? fieldInfo.name() : fieldNameFromMethod(methodInfo);
}

private String fieldNameFromMethod(MethodInfo methodInfo) {
String methodName = methodInfo.name();
return isBooleanType(methodInfo.returnType().toString())
? methodName.substring(2, 3).toLowerCase() + methodName.substring(3)
: methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
}

boolean hasUnknownAnnotation() {
return annotations.keySet().stream()
.anyMatch(ann -> ann.startsWith("com.fasterxml.jackson.") && !ann.equals(JsonProperty.class.getName()));
}

ResultHandle toValueReaderHandle(BytecodeCreator bytecode, ResultHandle valueHandle) {
return methodInfo != null ? bytecode.invokeVirtualMethod(MethodDescriptor.of(methodInfo), valueHandle)
: bytecode.readInstanceField(FieldDescriptor.of(fieldInfo), valueHandle);
}

private String[] rolesAllowed() {
AnnotationInstance secureField = annotations.get(SecureField.class.getName());
if (secureField != null) {
AnnotationValue rolesAllowed = secureField.value("rolesAllowed");
return rolesAllowed != null ? rolesAllowed.asStringArray() : null;
}
return null;
}
}
}
Loading

0 comments on commit af1d4f3

Please sign in to comment.