Skip to content

Commit

Permalink
Generate Jackson deserializers
Browse files Browse the repository at this point in the history
move generated serializers registration in its own ObjectMapperCustomizer

add test
  • Loading branch information
mariofusco committed Sep 2, 2024
1 parent dcd9928 commit d2f8463
Show file tree
Hide file tree
Showing 11 changed files with 986 additions and 346 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,373 @@
package io.quarkus.resteasy.reactive.jackson.deployment.processor;

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.AnnotationValue;
import org.jboss.jandex.ArrayType;
import org.jboss.jandex.ClassInfo;
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;
import org.jboss.jandex.TypeVariable;

import com.fasterxml.jackson.annotation.JsonProperty;

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;
import io.quarkus.resteasy.reactive.jackson.SecureField;

public abstract class JacksonCodeGenerator {
protected final BuildProducer<GeneratedClassBuildItem> generatedClassBuildItemBuildProducer;
protected final IndexView jandexIndex;

protected final Set<String> generatedClassNames = new HashSet<>();
protected final Deque<ClassInfo> toBeGenerated = new ArrayDeque<>();

public JacksonCodeGenerator(BuildProducer<GeneratedClassBuildItem> generatedClassBuildItemBuildProducer,
IndexView jandexIndex) {
this.generatedClassBuildItemBuildProducer = generatedClassBuildItemBuildProducer;
this.jandexIndex = jandexIndex;
}

protected abstract String getSuperClassName();

protected String[] getInterfacesNames(ClassInfo classInfo) {
return new String[0];
}

protected abstract String getClassSuffix();

public Collection<String> create(Collection<ClassInfo> classInfos) {
Set<String> createdClasses = new HashSet<>();
toBeGenerated.addAll(classInfos);

while (!toBeGenerated.isEmpty()) {
create(toBeGenerated.removeFirst()).ifPresent(createdClasses::add);
}

return createdClasses;
}

private Optional<String> create(ClassInfo classInfo) {
String beanClassName = classInfo.name().toString();
if (vetoedClassName(beanClassName) || !generatedClassNames.add(beanClassName)) {
return Optional.empty();
}

String generatedClassName = beanClassName + getClassSuffix();

try (ClassCreator classCreator = new ClassCreator(
new GeneratedClassGizmoAdaptor(generatedClassBuildItemBuildProducer, true), generatedClassName, null,
getSuperClassName(), getInterfacesNames(classInfo))) {

createConstructor(classCreator, beanClassName);
boolean valid = createSerializationMethod(classInfo, classCreator, beanClassName);
return valid ? Optional.of(generatedClassName) : Optional.empty();
}
}

private void createConstructor(ClassCreator classCreator, String beanClassName) {
MethodCreator constructor = classCreator.getConstructorCreator(new String[0]);
constructor.invokeSpecialMethod(
MethodDescriptor.ofConstructor(getSuperClassName(), "java.lang.Class"),
constructor.getThis(), constructor.loadClass(beanClassName));
constructor.returnVoid();
}

protected abstract boolean createSerializationMethod(ClassInfo classInfo, ClassCreator classCreator, String beanClassName);

protected Collection<FieldInfo> classFields(ClassInfo classInfo) {
Collection<FieldInfo> fields = new ArrayList<>();
classFields(classInfo, fields);
return fields;
}

protected void classFields(ClassInfo classInfo, Collection<FieldInfo> fields) {
fields.addAll(classInfo.fields());
onSuperClass(classInfo, superClassInfo -> {
classFields(superClassInfo, fields);
return null;
});
}

protected <T> T onSuperClass(ClassInfo classInfo, Function<ClassInfo, T> f) {
Type superType = classInfo.superClassType();
if (superType != null && !vetoedClassName(superType.name().toString())) {
ClassInfo superClassInfo = jandexIndex.getClassByName(superType.name());
if (superClassInfo != null) {
return f.apply(superClassInfo);
}
}
return null;
}

protected Collection<MethodInfo> classMethods(ClassInfo classInfo) {
Collection<MethodInfo> methods = new ArrayList<>();
classMethods(classInfo, methods);
return methods;
}

private void classMethods(ClassInfo classInfo, Collection<MethodInfo> methods) {
methods.addAll(classInfo.methods());
onSuperClass(classInfo, superClassInfo -> {
classMethods(superClassInfo, methods);
return null;
});
}

protected MethodInfo findMethod(ClassInfo classInfo, String methodName, Type... parameters) {
MethodInfo method = classInfo.method(methodName, parameters);
return method != null ? method
: onSuperClass(classInfo, superClassInfo -> findMethod(superClassInfo, methodName, parameters));
}

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

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

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

protected enum FieldKind {
OBJECT(false),
ARRAY(false),
LIST(true),
SET(true),
MAP(true),
TYPE_VARIABLE(true);

private boolean generic;

FieldKind(boolean generic) {
this.generic = generic;
}

public boolean isGeneric() {
return generic;
}
}

protected FieldKind registerTypeToBeGenerated(Type fieldType, String typeName) {
if (fieldType instanceof TypeVariable) {
return FieldKind.TYPE_VARIABLE;
}
if (fieldType instanceof ArrayType aType) {
registerTypeToBeGenerated(aType.constituent());
return FieldKind.ARRAY;
}
if (fieldType instanceof ParameterizedType pType) {
if (pType.arguments().size() == 1) {
if (typeName.equals("java.util.List") || typeName.equals("java.util.Collection")
|| typeName.equals("java.lang.Iterable")) {
registerTypeToBeGenerated(pType.arguments().get(0));
return FieldKind.LIST;
}
if (typeName.equals("java.util.Set")) {
registerTypeToBeGenerated(pType.arguments().get(0));
return FieldKind.SET;
}
}
if (pType.arguments().size() == 2 && typeName.equals("java.util.Map")) {
registerTypeToBeGenerated(pType.arguments().get(1));
registerTypeToBeGenerated(pType.arguments().get(1));
return FieldKind.MAP;
}
}
registerTypeToBeGenerated(typeName);
return FieldKind.OBJECT;
}

private void registerTypeToBeGenerated(Type type) {
registerTypeToBeGenerated(type.name().toString());
}

private void registerTypeToBeGenerated(String typeName) {
if (!vetoedClassName(typeName)) {
ClassInfo classInfo = jandexIndex.getClassByName(typeName);
if (classInfo != null && shouldGenerateCodeFor(classInfo)) {
toBeGenerated.add(classInfo);
}
}
}

protected boolean shouldGenerateCodeFor(ClassInfo classInfo) {
return !classInfo.isEnum();
}

private MethodInfo getterMethodInfo(ClassInfo classInfo, FieldInfo fieldInfo) {
MethodInfo namedAccessor = findMethod(classInfo, fieldInfo.name());
if (namedAccessor != null) {
return namedAccessor;
}
String methodName = (isBooleanType(fieldInfo.type().name().toString()) ? "is" : "get") + ucFirst(fieldInfo.name());
return findMethod(classInfo, methodName);
}

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

protected static class FieldSpecs {

final String fieldName;
final String jsonName;
final Type fieldType;

private final Map<String, AnnotationInstance> annotations = new HashMap<>();

MethodInfo methodInfo;
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();
}

public boolean isPublicField() {
return fieldInfo != null && Modifier.isPublic(fieldInfo.flags());
}

private Type fieldType() {
if (fieldInfo != null) {
return fieldInfo.type();
}
if (methodInfo.name().startsWith("set")) {
return methodInfo.parameterType(0);
}
return 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();
if (methodName.startsWith("is")) {
return methodName.substring(2, 3).toLowerCase() + methodName.substring(3);
}
if (methodName.startsWith("get") || methodName.startsWith("set")) {
return methodName.substring(3, 4).toLowerCase() + methodName.substring(4);
}
return methodName;
}

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

ResultHandle toValueWriterHandle(BytecodeCreator bytecode, ResultHandle valueHandle) {
return switch (fieldType.name().toString()) {
case "char", "java.lang.Character" -> bytecode.invokeVirtualMethod(
MethodDescriptor.ofMethod(String.class, "charAt", char.class, int.class), valueHandle,
bytecode.load(0));
default -> valueHandle;
};
}

ResultHandle toValueReaderHandle(BytecodeCreator bytecode, ResultHandle valueHandle) {
ResultHandle handle = accessorHandle(bytecode, valueHandle);

return switch (fieldType.name().toString()) {
case "char", "java.lang.Character" -> bytecode.invokeStaticMethod(
MethodDescriptor.ofMethod(Character.class, "toString", String.class, char.class), handle);
default -> handle;
};
}

private ResultHandle accessorHandle(BytecodeCreator bytecode, ResultHandle valueHandle) {
if (methodInfo != null) {
if (methodInfo.declaringClass().isInterface()) {
return bytecode.invokeInterfaceMethod(MethodDescriptor.of(methodInfo), valueHandle);
}
return bytecode.invokeVirtualMethod(MethodDescriptor.of(methodInfo), valueHandle);
}
return bytecode.readInstanceField(FieldDescriptor.of(fieldInfo), valueHandle);
}

String writtenType() {
return switch (fieldType.name().toString()) {
case "char", "java.lang.Character" -> "java.lang.String";
case "java.lang.Integer" -> "int";
case "java.lang.Short" -> "short";
case "java.lang.Long" -> "long";
case "java.lang.Double" -> "double";
case "java.lang.Float" -> "float";
default -> fieldType.name().toString();
};
}

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 d2f8463

Please sign in to comment.