diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java new file mode 100644 index 00000000000000..fc49caf53da994 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonCodeGenerator.java @@ -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 generatedClassBuildItemBuildProducer; + protected final IndexView jandexIndex; + + protected final Set generatedClassNames = new HashSet<>(); + protected final Deque toBeGenerated = new ArrayDeque<>(); + + public JacksonCodeGenerator(BuildProducer 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 create(Collection classInfos) { + Set createdClasses = new HashSet<>(); + toBeGenerated.addAll(classInfos); + + while (!toBeGenerated.isEmpty()) { + create(toBeGenerated.removeFirst()).ifPresent(createdClasses::add); + } + + return createdClasses; + } + + private Optional 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 classFields(ClassInfo classInfo) { + Collection fields = new ArrayList<>(); + classFields(classInfo, fields); + return fields; + } + + protected void classFields(ClassInfo classInfo, Collection fields) { + fields.addAll(classInfo.fields()); + onSuperClass(classInfo, superClassInfo -> { + classFields(superClassInfo, fields); + return null; + }); + } + + protected T onSuperClass(ClassInfo classInfo, Function 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 classMethods(ClassInfo classInfo) { + Collection methods = new ArrayList<>(); + classMethods(classInfo, methods); + return methods; + } + + private void classMethods(ClassInfo classInfo, Collection 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 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; + } + } +} diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java new file mode 100644 index 00000000000000..73d843f80d00ac --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/main/java/io/quarkus/resteasy/reactive/jackson/deployment/processor/JacksonDeserializerFactory.java @@ -0,0 +1,442 @@ +package io.quarkus.resteasy.reactive.jackson.deployment.processor; + +import static io.quarkus.gizmo.MethodDescriptor.ofMethod; +import static org.objectweb.asm.Opcodes.ACC_PUBLIC; + +import java.io.IOException; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.jboss.jandex.*; + +import com.fasterxml.jackson.core.JacksonException; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.core.TreeNode; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.deser.ContextualDeserializer; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.type.CollectionType; +import com.fasterxml.jackson.databind.type.MapType; +import com.fasterxml.jackson.databind.type.TypeFactory; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.builditem.GeneratedClassBuildItem; +import io.quarkus.gizmo.BranchResult; +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.gizmo.Switch; +import io.quarkus.resteasy.reactive.jackson.runtime.mappers.JacksonMapperUtil; + +/** + * Generates an implementation of the Jackson's {@code StdDeserializer} for each class that needs to be deserialized from json. + * In this way the deserialization process can be performed through the ad-hoc generate deserializer and then without + * any use of reflection. For instance for a pojo like this + * + *
{@code
+ * public class Person {
+ *     private String firstName;
+ *
+ *     @JsonProperty("familyName")
+ *     private String lastName;
+ *
+ *     private int age;
+ *
+ *     @SecureField(rolesAllowed = "admin")
+ *     private Address address;
+ *
+ *     public Person() {
+ *     }
+ *
+ *     public Person(String firstName, String lastName, int age, Address address) {
+ *         this.firstName = firstName;
+ *         this.lastName = lastName;
+ *         this.age = age;
+ *         this.address = address;
+ *     }
+ *
+ *     // getters and setters omitted
+ * }
+ * }
+ * + * it generates the following {@code StdDeserializer} implementation + * + *
{@code
+ * public class Person$quarkusjacksondeserializer extends StdDeserializer {
+ *     public Person$quarkusjacksondeserializer() {
+ *         super(Person.class);
+ *     }
+ *
+ *     public Object deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JacksonException {
+ *         Person person = new Person();
+ *         Iterator iterator = ((JsonNode) jsonParser.getCodec().readTree(jsonParser)).fields();
+ *
+ *         while (iterator.hasNext()) {
+ *             Map.Entry entry = (Map.iterator) var3.next();
+ *             String field = (String) entry.getKey();
+ *             JsonNode jsonNode = (JsonNode) entry.getValue();
+ *             switch (field) {
+ *                 case "firstName":
+ *                     person.setFirstName(jsonNode.asText());
+ *                     break;
+ *                 case "familyName":
+ *                     person.setLastName(jsonNode.asText());
+ *                     break;
+ *                 case "age":
+ *                     person.setAge(jsonNode.asInt());
+ *                     break;
+ *                 case "address":
+ *                     person.setAddress(context.readTreeAsValue(jsonNode, Address.class));
+ *                     break;
+ *             }
+ *         }
+ *
+ *         return person;
+ *     }
+ * }
+ * }
+ * + * Note that in this case also the {@code Address} class has to be deserialized in the same way, and then this factory triggers + * the generation of a second StdDeserializer also for it. More in general if during the generation of a deserializer for a + * given class it discovers a non-primitive field of another type for which a deserializer hasn't been generated yet, this + * factory enqueues a code generation also for that type. The same is valid for both arrays of that type, like + * {@code Address[]}, and collections, like {@code List<Address>}. + * + * Also note that this works only if the Java class to be deserialized has an empty constructor, while the generation of + * this deserializer is skipped in all other cases. In particular this cannot work with records. + * + * If the class to be deserialized has one or more generics parameter, the generated deserializer also implements the + * {@code ContextualDeserializer} interface. For instance for a class like the following + * + *
{@code
+ * public class DataItem {
+ *
+ *     private T content;
+ *
+ *     public T getContent() {
+ *         return content;
+ *     }
+ *
+ *     public void setContent(T content) {
+ *         this.content = content;
+ *     }
+ * }
+ * }
+ * + * the corresponding generated deserializer will be + * + *
{@code
+ * public class DataItem$quarkusjacksondeserializer extends StdDeserializer implements ContextualDeserializer {
+ *     private JavaType[] valueTypesmvn clean install;
+ *
+ *     public DataItem$quarkusjacksondeserializer() {
+ *         super(DataItem.class);
+ *     }
+ *
+ *     public Object deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JacksonException {
+ *         DataItem dataItem = new DataItem();
+ *         Iterator iterator = ((JsonNode) jsonParser.getCodec().readTree(jsonParser)).fields();
+ *
+ *         while (iterator.hasNext()) {
+ *             Map.Entry entry = (Map.iterator) var3.next();
+ *             String field = (String) entry.getKey();
+ *             JsonNode jsonNode = (JsonNode) entry.getValue();
+ *             switch (field) {
+ *                 case "content":
+ *                     dataItem.setContent(context.readTreeAsValue(jsonNode, this.valueTypes[0]));
+ *                     break;
+ *             }
+ *         }
+ *
+ *         return dataItem;
+ *     }
+ *
+ *     public JsonDeserializer createContextual(DeserializationContext context, BeanProperty beanProperty) {
+ *         JavaType[] valueTypes = JacksonMapperUtil.getGenericsJavaTypes(context, beanProperty);
+ *         DataItem$quarkusjacksondeserializer deserializer = new DataItem$quarkusjacksondeserializer();
+ *         deserializer.valueTypes = valueTypes;
+ *         return (JsonDeserializer) deserializer;
+ *     }
+ * }
+ * }
+ */ +public class JacksonDeserializerFactory extends JacksonCodeGenerator { + + public JacksonDeserializerFactory(BuildProducer generatedClassBuildItemBuildProducer, + IndexView jandexIndex) { + super(generatedClassBuildItemBuildProducer, jandexIndex); + } + + @Override + protected String getSuperClassName() { + return StdDeserializer.class.getName(); + } + + @Override + protected String getClassSuffix() { + return "$quarkusjacksondeserializer"; + } + + protected String[] getInterfacesNames(ClassInfo classInfo) { + return classInfo.typeParameters().isEmpty() ? new String[0] : new String[] { ContextualDeserializer.class.getName() }; + } + + @Override + protected boolean createSerializationMethod(ClassInfo classInfo, ClassCreator classCreator, String beanClassName) { + if (!classInfo.hasNoArgsConstructor()) { + return false; + } + + MethodCreator deserialize = classCreator + .getMethodCreator("deserialize", Object.class, JsonParser.class, DeserializationContext.class) + .setModifiers(ACC_PUBLIC) + .addException(IOException.class) + .addException(JacksonException.class); + + ResultHandle deserializedHandle = deserialize + .newInstance(MethodDescriptor.ofConstructor(classInfo.name().toString())); + + boolean valid = deserializeObject(classInfo, deserializedHandle, classCreator, deserialize); + deserialize.returnValue(deserializedHandle); + return valid; + } + + private static ResultHandle getJsonNode(MethodCreator deserialize) { + ResultHandle jsonParser = deserialize.getMethodParam(0); + ResultHandle objectCodec = deserialize + .invokeVirtualMethod(ofMethod(JsonParser.class, "getCodec", ObjectCodec.class), jsonParser); + ResultHandle treeNode = deserialize.invokeVirtualMethod( + ofMethod(ObjectCodec.class, "readTree", TreeNode.class, JsonParser.class), objectCodec, + jsonParser); + return deserialize.checkCast(treeNode, JsonNode.class); + } + + private boolean deserializeObject(ClassInfo classInfo, ResultHandle objHandle, ClassCreator classCreator, + MethodCreator deserialize) { + ResultHandle jsonNode = getJsonNode(deserialize); + + ResultHandle fieldsIterator = deserialize + .invokeVirtualMethod(ofMethod(JsonNode.class, "fields", Iterator.class), jsonNode); + BytecodeCreator loopCreator = deserialize.whileLoop(c -> iteratorHasNext(c, fieldsIterator)).block(); + ResultHandle nextField = loopCreator + .invokeInterfaceMethod(ofMethod(Iterator.class, "next", Object.class), fieldsIterator); + ResultHandle mapEntry = loopCreator.checkCast(nextField, Map.Entry.class); + ResultHandle fieldName = loopCreator + .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getKey", Object.class), mapEntry); + ResultHandle fieldValue = loopCreator.checkCast(loopCreator + .invokeInterfaceMethod(ofMethod(Map.Entry.class, "getValue", Object.class), mapEntry), JsonNode.class); + Switch.StringSwitch strSwitch = loopCreator.stringSwitch(fieldName); + + Set deserializedFields = new HashSet<>(); + ResultHandle deserializationContext = deserialize.getMethodParam(1); + return deserializeFields(classCreator, classInfo, deserializationContext, objHandle, fieldValue, deserializedFields, + strSwitch, parseTypeParameters(classInfo, classCreator)); + } + + private BranchResult iteratorHasNext(BytecodeCreator creator, ResultHandle iterator) { + return creator.ifTrue(creator.invokeInterfaceMethod(ofMethod(Iterator.class, "hasNext", boolean.class), iterator)); + } + + private Map parseTypeParameters(ClassInfo classInfo, ClassCreator classCreator) { + List typeParameters = classInfo.typeParameters(); + if (typeParameters.isEmpty()) { + return null; + } + + createContextualMethod(classCreator); + + Map typeParametersIndex = new HashMap<>(); + int index = 0; + for (TypeVariable typeParameter : typeParameters) { + typeParametersIndex.put(typeParameter.toString(), index++); + } + return typeParametersIndex; + } + + private static void createContextualMethod(ClassCreator classCreator) { + FieldDescriptor valueTypesField = FieldDescriptor.of(classCreator.getClassName(), "valueTypes", JavaType[].class); + classCreator.getFieldCreator(valueTypesField); + + MethodCreator createContextual = classCreator + .getMethodCreator("createContextual", JsonDeserializer.class, DeserializationContext.class, BeanProperty.class) + .setModifiers(ACC_PUBLIC); + + ResultHandle deserializationContext = createContextual.getMethodParam(0); + ResultHandle beanProperty = createContextual.getMethodParam(1); + MethodDescriptor getGenericsJavaTypes = ofMethod(JacksonMapperUtil.class, "getGenericsJavaTypes", + JavaType[].class, DeserializationContext.class, BeanProperty.class); + ResultHandle valueTypes = createContextual.invokeStaticMethod(getGenericsJavaTypes, deserializationContext, + beanProperty); + + ResultHandle deserializer = createContextual.newInstance(MethodDescriptor.ofConstructor(classCreator.getClassName())); + createContextual.writeInstanceField(valueTypesField, deserializer, valueTypes); + createContextual.returnValue(deserializer); + } + + private boolean deserializeFields(ClassCreator classCreator, ClassInfo classInfo, ResultHandle deserializationContext, + ResultHandle objHandle, ResultHandle fieldValue, Set deserializedFields, Switch.StringSwitch strSwitch, + Map typeParametersIndex) { + + AtomicBoolean valid = new AtomicBoolean(true); + + for (FieldInfo fieldInfo : classFields(classInfo)) { + if (!deserializeFieldSpecs(classCreator, classInfo, deserializationContext, objHandle, fieldValue, + deserializedFields, strSwitch, typeParametersIndex, fieldSpecsFromField(classInfo, fieldInfo), valid)) + return false; + } + + for (MethodInfo methodInfo : classMethods(classInfo)) { + if (!deserializeFieldSpecs(classCreator, classInfo, deserializationContext, objHandle, fieldValue, + deserializedFields, strSwitch, typeParametersIndex, fieldSpecsFromMethod(methodInfo), valid)) + return false; + } + + return valid.get(); + } + + private boolean deserializeFieldSpecs(ClassCreator classCreator, ClassInfo classInfo, ResultHandle deserializationContext, + ResultHandle objHandle, ResultHandle fieldValue, Set deserializedFields, Switch.StringSwitch strSwitch, + Map typeParametersIndex, FieldSpecs fieldSpecs, AtomicBoolean valid) { + if (fieldSpecs != null && deserializedFields.add(fieldSpecs.fieldName)) { + if (fieldSpecs.hasUnknownAnnotation()) { + return false; + } + strSwitch.caseOf(fieldSpecs.jsonName, + bytecode -> valid.compareAndSet(true, deserializeField(classCreator, classInfo, bytecode, objHandle, + fieldValue, typeParametersIndex, fieldSpecs, deserializationContext))); + } + return true; + } + + private boolean deserializeField(ClassCreator classCreator, ClassInfo classInfo, BytecodeCreator bytecode, + ResultHandle objHandle, ResultHandle fieldValue, Map typeParametersIndex, FieldSpecs fieldSpecs, + ResultHandle deserializationContext) { + ResultHandle valueHandle = readValueFromJson(classCreator, bytecode, deserializationContext, fieldSpecs, + typeParametersIndex, fieldValue); + if (valueHandle == null) { + return false; + } + writeValueToObject(classInfo, objHandle, fieldSpecs, bytecode, fieldSpecs.toValueWriterHandle(bytecode, valueHandle)); + return true; + } + + private FieldSpecs fieldSpecsFromMethod(MethodInfo methodInfo) { + return isSetterMethod(methodInfo) ? new FieldSpecs(methodInfo) : null; + } + + private boolean isSetterMethod(MethodInfo methodInfo) { + return Modifier.isPublic(methodInfo.flags()) && !Modifier.isStatic(methodInfo.flags()) + && methodInfo.returnType() instanceof VoidType && methodInfo.parametersCount() == 1 + && methodInfo.name().startsWith("set"); + } + + private ResultHandle readValueFromJson(ClassCreator classCreator, BytecodeCreator bytecode, + ResultHandle deserializationContext, FieldSpecs fieldSpecs, Map typeParametersIndex, + ResultHandle valueNode) { + Type fieldType = fieldSpecs.fieldType; + String fieldTypeName = fieldType.name().toString(); + MethodDescriptor readMethod = readMethodForPrimitiveFields(fieldTypeName); + if (readMethod != null) { + return bytecode.invokeVirtualMethod(readMethod, valueNode); + } + + FieldKind fieldKind = registerTypeToBeGenerated(fieldType, fieldTypeName); + ResultHandle typeHandle = switch (fieldKind) { + case TYPE_VARIABLE -> { + Integer parameterIndex = typeParametersIndex.get(fieldType.toString()); + if (parameterIndex == null) { + yield null; + } + FieldDescriptor valueTypesField = FieldDescriptor.of(classCreator.getClassName(), "valueTypes", + JavaType[].class); + ResultHandle valueTypes = bytecode.readInstanceField(valueTypesField, bytecode.getThis()); + yield bytecode.readArrayValue(valueTypes, parameterIndex); + } + case LIST, SET -> { + Type listType = ((ParameterizedType) fieldType).arguments().get(0); + MethodDescriptor getTypeFactory = ofMethod(DeserializationContext.class, "getTypeFactory", + TypeFactory.class); + ResultHandle typeFactory = bytecode.invokeVirtualMethod(getTypeFactory, deserializationContext); + MethodDescriptor constructCollectionType = ofMethod(TypeFactory.class, + "constructCollectionType", CollectionType.class, Class.class, Class.class); + yield bytecode.invokeVirtualMethod(constructCollectionType, typeFactory, + bytecode.loadClass(fieldKind == FieldKind.SET ? HashSet.class : ArrayList.class), + bytecode.loadClass(listType.name().toString())); + } + case MAP -> { + Type keyType = ((ParameterizedType) fieldType).arguments().get(0); + Type valueType = ((ParameterizedType) fieldType).arguments().get(1); + MethodDescriptor getTypeFactory = ofMethod(DeserializationContext.class, "getTypeFactory", + TypeFactory.class); + ResultHandle typeFactory = bytecode.invokeVirtualMethod(getTypeFactory, deserializationContext); + MethodDescriptor constructMapType = ofMethod(TypeFactory.class, "constructMapType", + MapType.class, Class.class, Class.class, Class.class); + yield bytecode.invokeVirtualMethod(constructMapType, typeFactory, bytecode.loadClass(HashMap.class), + bytecode.loadClass(keyType.name().toString()), bytecode.loadClass(valueType.name().toString())); + } + default -> bytecode.loadClass(fieldTypeName); + }; + + if (typeHandle == null) { + return null; + } + + MethodDescriptor readTreeAsValue = ofMethod(DeserializationContext.class, "readTreeAsValue", + Object.class, JsonNode.class, fieldKind.isGeneric() ? JavaType.class : Class.class); + return bytecode.invokeVirtualMethod(readTreeAsValue, deserializationContext, valueNode, typeHandle); + } + + private void writeValueToObject(ClassInfo classInfo, ResultHandle objHandle, FieldSpecs fieldSpecs, + BytecodeCreator bytecode, ResultHandle valueHandle) { + if (fieldSpecs.isPublicField()) { + bytecode.writeInstanceField(fieldSpecs.fieldInfo, objHandle, valueHandle); + } else { + MethodInfo setterMethod = setterMethodInfo(classInfo, fieldSpecs); + if (setterMethod != null) { + if (setterMethod.declaringClass().isInterface()) { + bytecode.invokeInterfaceMethod(setterMethod, objHandle, valueHandle); + } else { + bytecode.invokeVirtualMethod(setterMethod, objHandle, valueHandle); + } + } + } + } + + private MethodInfo setterMethodInfo(ClassInfo classInfo, FieldSpecs fieldSpecs) { + String methodName = "set" + ucFirst(fieldSpecs.fieldName); + return findMethod(classInfo, methodName, fieldSpecs.fieldType); + } + + private MethodDescriptor readMethodForPrimitiveFields(String typeName) { + return switch (typeName) { + case "java.lang.String", "char", "java.lang.Character" -> ofMethod(JsonNode.class, "asText", String.class); + case "short", "java.lang.Short", "int", "java.lang.Integer" -> + ofMethod(JsonNode.class, "asInt", int.class); + case "long", "java.lang.Long" -> ofMethod(JsonNode.class, "asLong", long.class); + case "float", "java.lang.Float", "double", "java.lang.Double" -> + ofMethod(JsonNode.class, "asDouble", double.class); + case "boolean", "java.lang.Boolean" -> ofMethod(JsonNode.class, "asBoolean", boolean.class); + default -> null; + }; + } + + @Override + protected boolean shouldGenerateCodeFor(ClassInfo classInfo) { + return super.shouldGenerateCodeFor(classInfo) && classInfo.hasNoArgsConstructor(); + } +} 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 index 691d102c35ddfe..15e809ab95e078 100644 --- 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 @@ -6,28 +6,17 @@ 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.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 com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.SerializableString; import com.fasterxml.jackson.core.io.SerializedString; @@ -48,7 +37,6 @@ import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; -import io.quarkus.resteasy.reactive.jackson.SecureField; import io.quarkus.resteasy.reactive.jackson.runtime.mappers.JacksonMapperUtil; /** @@ -131,39 +119,28 @@ * factory enqueues a code generation also for that type. The same is valid for both arrays of that type, like * {@code Address[]}, and collections, like {@code List<Address>}. */ -public class JacksonSerializerFactory { +public class JacksonSerializerFactory extends JacksonCodeGenerator { + private static final String CLASS_NAME_SUFFIX = "$quarkusjacksonserializer"; 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 generatedClassBuildItemBuildProducer; - private final IndexView jandexIndex; - - private final Set generatedClassNames = new HashSet<>(); private final Map> generatedFields = new HashMap<>(); - private final Deque toBeGenerated = new ArrayDeque<>(); public JacksonSerializerFactory(BuildProducer generatedClassBuildItemBuildProducer, IndexView jandexIndex) { - this.generatedClassBuildItemBuildProducer = generatedClassBuildItemBuildProducer; - this.jandexIndex = jandexIndex; + super(generatedClassBuildItemBuildProducer, jandexIndex); } + @Override public Collection create(Collection classInfos) { - Set createdClasses = new HashSet<>(); - toBeGenerated.addAll(classInfos); - - while (!toBeGenerated.isEmpty()) { - create(toBeGenerated.removeFirst()).ifPresent(createdClasses::add); - } - + Collection createdClasses = super.create(classInfos); createFieldNamesClass(); - return createdClasses; } - public void createFieldNamesClass() { + private void createFieldNamesClass() { if (generatedFields.isEmpty()) { return; } @@ -190,33 +167,18 @@ public void createFieldNamesClass() { } } - private Optional create(ClassInfo classInfo) { - String beanClassName = classInfo.name().toString(); - if (vetoedClassName(beanClassName) || !generatedClassNames.add(beanClassName)) { - return Optional.empty(); - } - - String generatedClassName = beanClassName + "$quarkusjacksonserializer"; - - try (ClassCreator classCreator = new ClassCreator( - new GeneratedClassGizmoAdaptor(generatedClassBuildItemBuildProducer, true), generatedClassName, null, - SUPER_CLASS_NAME)) { - - createConstructor(classCreator, beanClassName); - boolean valid = createSerializeMethod(classInfo, classCreator, beanClassName); - return valid ? Optional.of(generatedClassName) : Optional.empty(); - } + @Override + protected String getSuperClassName() { + return SUPER_CLASS_NAME; } - private 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(); + @Override + protected String getClassSuffix() { + return CLASS_NAME_SUFFIX; } - private boolean createSerializeMethod(ClassInfo classInfo, ClassCreator classCreator, String beanClassName) { + @Override + protected boolean createSerializationMethod(ClassInfo classInfo, ClassCreator classCreator, String beanClassName) { MethodCreator serialize = classCreator.getMethodCreator("serialize", "void", "java.lang.Object", JSON_GEN_CLASS_NAME, "com.fasterxml.jackson.databind.SerializerProvider") .setModifiers(ACC_PUBLIC) @@ -263,33 +225,25 @@ private boolean serializeObjectData(ClassInfo classInfo, ClassCreator classCreat } private boolean serializeFields(ClassInfo classInfo, ClassCreator classCreator, MethodCreator serialize, - ResultHandle valueHandle, - ResultHandle jsonGenerator, ResultHandle serializerProvider, Set serializedFields) { + ResultHandle valueHandle, ResultHandle jsonGenerator, ResultHandle serializerProvider, + Set serializedFields) { for (FieldInfo fieldInfo : classFields(classInfo)) { - if (Modifier.isStatic(fieldInfo.flags())) { - continue; - } FieldSpecs fieldSpecs = fieldSpecsFromField(classInfo, fieldInfo); - if (fieldSpecs != null) { - if (serializedFields.add(fieldSpecs.fieldName)) { - if (fieldSpecs.hasUnknownAnnotation()) { - return false; - } - writeField(classInfo, fieldSpecs, writeFieldBranch(classCreator, serialize, fieldSpecs), jsonGenerator, - serializerProvider, valueHandle); + if (fieldSpecs != null && serializedFields.add(fieldSpecs.fieldName)) { + if (fieldSpecs.hasUnknownAnnotation()) { + return false; } + writeField(classInfo, fieldSpecs, writeFieldBranch(classCreator, serialize, fieldSpecs), jsonGenerator, + serializerProvider, valueHandle); } } return true; } private boolean serializeMethods(ClassInfo classInfo, ClassCreator classCreator, MethodCreator serialize, - ResultHandle valueHandle, - ResultHandle jsonGenerator, ResultHandle serializerProvider, Set serializedFields) { + ResultHandle valueHandle, ResultHandle jsonGenerator, ResultHandle serializerProvider, + Set serializedFields) { for (MethodInfo methodInfo : classMethods(classInfo)) { - if (Modifier.isStatic(methodInfo.flags())) { - continue; - } FieldSpecs fieldSpecs = fieldSpecsFromMethod(methodInfo); if (fieldSpecs != null && serializedFields.add(fieldSpecs.fieldName)) { if (fieldSpecs.hasUnknownAnnotation()) { @@ -301,6 +255,17 @@ private boolean serializeMethods(ClassInfo classInfo, ClassCreator classCreator, return true; } + private FieldSpecs fieldSpecsFromMethod(MethodInfo methodInfo) { + return !Modifier.isStatic(methodInfo.flags()) && isGetterMethod(methodInfo) ? new FieldSpecs(methodInfo) : null; + } + + private boolean isGetterMethod(MethodInfo methodInfo) { + String methodName = methodInfo.name(); + return Modifier.isPublic(methodInfo.flags()) && !Modifier.isStatic(methodInfo.flags()) + && methodInfo.parametersCount() == 0 + && (methodName.startsWith("get") || methodName.startsWith("is")); + } + private void writeField(ClassInfo classInfo, FieldSpecs fieldSpecs, BytecodeCreator bytecode, ResultHandle jsonGenerator, ResultHandle serializerProvider, ResultHandle valueHandle) { String pkgName = classInfo.name().packagePrefixName().toString(); @@ -330,46 +295,6 @@ private void writeField(ClassInfo classInfo, FieldSpecs fieldSpecs, BytecodeCrea bytecode.invokeVirtualMethod(writeMethod, jsonGenerator, arg); } - private void registerTypeToBeGenerated(Type fieldType, String typeName) { - if (!isCollectionType(fieldType, typeName)) { - registerTypeToBeGenerated(typeName); - } - } - - private boolean isCollectionType(Type fieldType, String typeName) { - if (fieldType instanceof ArrayType aType) { - registerTypeToBeGenerated(aType.constituent()); - return true; - } - if (fieldType instanceof ParameterizedType pType) { - if (pType.arguments().size() == 1 && (typeName.equals("java.util.List") || - typeName.equals("java.util.Collection") || typeName.equals("java.util.Set") || - typeName.equals("java.lang.Iterable"))) { - registerTypeToBeGenerated(pType.arguments().get(0)); - return true; - } - if (pType.arguments().size() == 2 && typeName.equals("java.util.Map")) { - registerTypeToBeGenerated(pType.arguments().get(1)); - registerTypeToBeGenerated(pType.arguments().get(1)); - return true; - } - } - return false; - } - - 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 && !classInfo.isEnum()) { - toBeGenerated.add(classInfo); - } - } - } - private String writeMethodForPrimitiveFields(String typeName) { return switch (typeName) { case "java.lang.String", "char", "java.lang.Character" -> "writeString"; @@ -408,52 +333,6 @@ private BytecodeCreator writeFieldBranch(ClassCreator classCreator, MethodCreato return serialize; } - private Collection classFields(ClassInfo classInfo) { - Collection fields = new ArrayList<>(); - classFields(classInfo, fields); - return fields; - } - - private void classFields(ClassInfo classInfo, Collection fields) { - fields.addAll(classInfo.fields()); - onSuperClass(classInfo, superClassInfo -> { - classFields(superClassInfo, fields); - return null; - }); - } - - private Collection classMethods(ClassInfo classInfo) { - Collection methods = new ArrayList<>(); - classMethods(classInfo, methods); - return methods; - } - - private void classMethods(ClassInfo classInfo, Collection methods) { - methods.addAll(classInfo.methods()); - onSuperClass(classInfo, superClassInfo -> { - classMethods(superClassInfo, methods); - return null; - }); - } - - private T onSuperClass(ClassInfo classInfo, Function 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; - } - - private boolean isGetterMethod(MethodInfo methodInfo) { - String methodName = methodInfo.name(); - return Modifier.isPublic(methodInfo.flags()) && !Modifier.isStatic(methodInfo.flags()) - && methodInfo.parametersCount() == 0 - && (methodName.startsWith("get") || methodName.startsWith("is")); - } - private void throwExceptionForEmptyBean(String beanClassName, MethodCreator serialize, ResultHandle jsonGenerator) { String serializationFeatureClassName = SerializationFeature.class.getName(); @@ -483,153 +362,4 @@ private void throwExceptionForEmptyBean(String beanClassName, MethodCreator seri isFailEnabledBranch.load(errorMsg), javaType); isFailEnabledBranch.throwException(invalidException); } - - 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); - } - - private 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)); - } - - private static String ucFirst(String name) { - return name.substring(0, 1).toUpperCase() + name.substring(1); - } - - private static boolean isBooleanType(String type) { - return type.equals("boolean") || type.equals("java.lang.Boolean"); - } - - 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 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) { - ResultHandle handle = accessorHandle(bytecode, valueHandle); - - handle = switch (fieldType.name().toString()) { - case "char", "java.lang.Character" -> bytecode.invokeStaticMethod( - MethodDescriptor.ofMethod(Character.class, "toString", String.class, char.class), handle); - default -> handle; - }; - - return 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(); - }; - } - - 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; - } - } } 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 386200105bd095..9fa08c45b54d25 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 @@ -20,6 +20,7 @@ import java.util.function.Supplier; import jakarta.inject.Singleton; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Priorities; import jakarta.ws.rs.RuntimeType; import jakarta.ws.rs.core.Cookie; @@ -75,10 +76,7 @@ import io.quarkus.resteasy.reactive.jackson.runtime.mappers.NativeInvalidDefinitionExceptionMapper; import io.quarkus.resteasy.reactive.jackson.runtime.security.RolesAllowedConfigExpStorage; import io.quarkus.resteasy.reactive.jackson.runtime.security.SecurityCustomSerialization; -import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.BasicServerJacksonMessageBodyWriter; -import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.FullyFeaturedServerJacksonMessageBodyReader; -import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.FullyFeaturedServerJacksonMessageBodyWriter; -import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.ServerJacksonMessageBodyReader; +import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.*; import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.vertx.VertxJsonArrayMessageBodyReader; import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.vertx.VertxJsonArrayMessageBodyWriter; import io.quarkus.resteasy.reactive.jackson.runtime.serialisers.vertx.VertxJsonObjectMessageBodyReader; @@ -384,21 +382,44 @@ public void handleEndpointParams(ResteasyReactiveResourceMethodEntriesBuildItem IndexView indexView = jaxRsIndex.getIndexView(); - Map jsonClasses = new HashMap<>(); + Map serializedClasses = new HashMap<>(); + Map deserializedClasses = new HashMap<>(); + for (ResteasyReactiveResourceMethodEntriesBuildItem.Entry entry : resourceMethodEntries.getEntries()) { MethodInfo methodInfo = entry.getMethodInfo(); - ClassInfo effectiveReturnClassInfo = getEffectiveReturnClassInfo(methodInfo, indexView); + ClassInfo effectiveReturnClassInfo = getEffectiveClassInfo(methodInfo.returnType(), indexView); if (effectiveReturnClassInfo != null) { - jsonClasses.put(effectiveReturnClassInfo.name().toString(), effectiveReturnClassInfo); + serializedClasses.put(effectiveReturnClassInfo.name().toString(), effectiveReturnClassInfo); + } + + if (methodInfo.hasAnnotation(POST.class)) { + for (Type paramType : methodInfo.parameterTypes()) { + ClassInfo effectiveParamClassInfo = getEffectiveClassInfo(paramType, indexView); + if (effectiveParamClassInfo != null) { + deserializedClasses.put(effectiveParamClassInfo.name().toString(), effectiveParamClassInfo); + } + } } } - if (!jsonClasses.isEmpty()) { + if (!serializedClasses.isEmpty()) { JacksonSerializerFactory factory = new JacksonSerializerFactory(generatedClassBuildItemBuildProducer, index.getComputingIndex()); - factory.create(jsonClasses.values()) + factory.create(serializedClasses.values()) .forEach(recorder::recordGeneratedSerializer); } + + if (!deserializedClasses.isEmpty()) { + JacksonDeserializerFactory factory = new JacksonDeserializerFactory(generatedClassBuildItemBuildProducer, + index.getComputingIndex()); + factory.create(deserializedClasses.values()) + .forEach(recorder::recordGeneratedDeserializer); + } + } + + @BuildStep(onlyIf = JacksonOptimizationConfig.IsReflectionFreeSerializersEnabled.class) + void unremovable(BuildProducer additionalProducer) { + additionalProducer.produce(AdditionalBeanBuildItem.unremovableOf(GeneratedSerializersRegister.class)); } @BuildStep @@ -438,7 +459,7 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r continue; } - ClassInfo effectiveReturnClassInfo = getEffectiveReturnClassInfo(methodInfo, indexView); + ClassInfo effectiveReturnClassInfo = getEffectiveClassInfo(methodInfo.returnType(), indexView); if (effectiveReturnClassInfo == null) { continue; } @@ -467,17 +488,16 @@ public void handleFieldSecurity(ResteasyReactiveResourceMethodEntriesBuildItem r } } - private static ClassInfo getEffectiveReturnClassInfo(MethodInfo methodInfo, IndexView indexView) { - Type returnType = methodInfo.returnType(); - if (returnType.kind() == Type.Kind.VOID) { + private static ClassInfo getEffectiveClassInfo(Type type, IndexView indexView) { + if (type.kind() == Type.Kind.VOID) { return null; } - Type effectiveReturnType = getEffectiveReturnType(returnType); + Type effectiveReturnType = getEffectiveType(type); return effectiveReturnType == null ? null : indexView.getClassByName(effectiveReturnType.name()); } - private static Type getEffectiveReturnType(Type returnType) { - Type effectiveReturnType = returnType; + private static Type getEffectiveType(Type type) { + Type effectiveReturnType = type; if (effectiveReturnType.name().equals(ResteasyReactiveDotNames.REST_RESPONSE) || effectiveReturnType.name().equals(ResteasyReactiveDotNames.UNI) || effectiveReturnType.name().equals(ResteasyReactiveDotNames.COMPLETABLE_FUTURE) || @@ -488,7 +508,7 @@ private static Type getEffectiveReturnType(Type returnType) { return null; } - effectiveReturnType = returnType.asParameterizedType().arguments().get(0); + effectiveReturnType = type.asParameterizedType().arguments().get(0); } if (effectiveReturnType.name().equals(ResteasyReactiveDotNames.SET) || effectiveReturnType.name().equals(ResteasyReactiveDotNames.COLLECTION) || diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java index 6a485841647601..0595f346dc4f73 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonResource.java @@ -109,6 +109,13 @@ public Dog getDog() { return createDog(); } + @POST + @Path("/dog-echo") + @Consumes(MediaType.APPLICATION_JSON) + public Dog echoDog(Dog dog) { + return dog; + } + @EnableSecureSerialization @GET @Path("/abstract-cat") diff --git a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java index 3db48396587fa4..93717d4c53028e 100644 --- a/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java +++ b/extensions/resteasy-reactive/rest-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/SimpleJsonTest.java @@ -656,4 +656,21 @@ private static void testSecuredFieldOnAbstractClass(String catPath, String dogPa .body("veterinarian.name", Matchers.is("Dolittle")) .body("veterinarian.title", Matchers.is("VMD")); } + + @Test + public void testEcho() { + RestAssured + .with() + .body("{\"publicName\":\"Leo\",\"veterinarian\":{\"name\":\"Dolittle\"},\"age\":5}") + .contentType("application/json; charset=utf-8") + .post("/simple/dog-echo") + .then() + .statusCode(200) + .contentType("application/json") + .body("publicName", Matchers.is("Leo")) + .body("privateName", Matchers.nullValue()) + .body("age", Matchers.is(5)) + .body("veterinarian.name", Matchers.is("Dolittle")) + .body("veterinarian.title", Matchers.nullValue()); + } } 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 3da2ef88286c51..f2a57067cf68b7 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 @@ -13,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.deser.std.StdDeserializer; import com.fasterxml.jackson.databind.ser.std.StdSerializer; import io.quarkus.arc.Arc; @@ -29,6 +30,7 @@ public class ResteasyReactiveServerJacksonRecorder { private static final Map> customDeserializationMap = new HashMap<>(); private static final Set> generatedSerializers = new HashSet<>(); + private static final Set> generatedDeserializers = new HashSet<>(); /* STATIC INIT */ public RuntimeValue>> createConfigExpToAllowedRoles() { @@ -85,6 +87,10 @@ public void recordGeneratedSerializer(String className) { generatedSerializers.add((Class) loadClass(className)); } + public void recordGeneratedDeserializer(String className) { + generatedDeserializers.add((Class) loadClass(className)); + } + public void configureShutdown(ShutdownContext shutdownContext) { shutdownContext.addShutdownTask(new Runnable() { @Override @@ -129,6 +135,10 @@ public static Set> getGeneratedSerializers() { return generatedSerializers; } + public static Set> getGeneratedDeserializers() { + return generatedDeserializers; + } + 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/mappers/JacksonMapperUtil.java b/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/mappers/JacksonMapperUtil.java index 82ef4047409aed..2062375603a09a 100644 --- a/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/mappers/JacksonMapperUtil.java +++ b/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/mappers/JacksonMapperUtil.java @@ -1,5 +1,9 @@ package io.quarkus.resteasy.reactive.jackson.runtime.mappers; +import com.fasterxml.jackson.databind.BeanProperty; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; + import io.quarkus.arc.Arc; import io.quarkus.arc.ArcContainer; import io.quarkus.arc.InstanceHandle; @@ -61,4 +65,13 @@ private static RolesAllowedConfigExpStorage createRolesAllowedConfigExpStorage() return rolesAllowedConfigExpStorage.isAvailable() ? rolesAllowedConfigExpStorage.get() : null; } } + + public static JavaType[] getGenericsJavaTypes(DeserializationContext context, BeanProperty property) { + JavaType wrapperType = property != null ? property.getType() : context.getContextualType(); + JavaType[] valueTypes = new JavaType[wrapperType.containedTypeCount()]; + for (int i = 0; i < valueTypes.length; i++) { + valueTypes[i] = wrapperType.containedType(0); + } + return valueTypes; + } } 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 cc6032422b1fce..48bfd7cfe7ebab 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,7 +6,6 @@ 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; @@ -20,10 +19,6 @@ 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 { @@ -31,28 +26,9 @@ public class BasicServerJacksonMessageBodyWriter extends ServerMessageBodyWriter @Inject public BasicServerJacksonMessageBodyWriter(ObjectMapper mapper) { - mapper.registerModule(MappingModuleHolder.mappingModule); this.defaultWriter = createDefaultWriter(mapper); } - static class MappingModuleHolder { - static final SimpleModule mappingModule = createMappingModule(); - - private static SimpleModule createMappingModule() { - SimpleModule module = new SimpleModule(); - for (Class serClass : ResteasyReactiveServerJacksonRecorder.getGeneratedSerializers()) { - try { - StdSerializer serializer = serClass.getConstructor().newInstance(); - module.addSerializer(serializer.handledType(), serializer); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException - | NoSuchMethodException e) { - throw new RuntimeException(e); - } - } - return module; - } - } - @Override public void writeResponse(Object o, Type genericType, ServerRequestContext context) throws WebApplicationException, IOException { diff --git a/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java b/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java index 3b2e59cb77af6a..df6601849601f0 100644 --- a/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java +++ b/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/FullyFeaturedServerJacksonMessageBodyWriter.java @@ -56,7 +56,6 @@ public void writeResponse(Object o, Type genericType, ServerRequestContext conte stream.write(((String) o).getBytes(StandardCharsets.UTF_8)); } else { ObjectMapper effectiveMapper = getEffectiveMapper(o, context); - effectiveMapper.registerModule(BasicServerJacksonMessageBodyWriter.MappingModuleHolder.mappingModule); ObjectWriter effectiveWriter = getEffectiveWriter(effectiveMapper); ResteasyReactiveResourceInfo resourceInfo = context.getResteasyReactiveResourceInfo(); if (resourceInfo != null) { diff --git a/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/GeneratedSerializersRegister.java b/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/GeneratedSerializersRegister.java new file mode 100644 index 00000000000000..24aea97433a8f2 --- /dev/null +++ b/extensions/resteasy-reactive/rest-jackson/runtime/src/main/java/io/quarkus/resteasy/reactive/jackson/runtime/serialisers/GeneratedSerializersRegister.java @@ -0,0 +1,53 @@ +package io.quarkus.resteasy.reactive.jackson.runtime.serialisers; + +import java.lang.reflect.InvocationTargetException; + +import jakarta.inject.Singleton; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.deser.std.StdDeserializer; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.StdSerializer; + +import io.quarkus.jackson.ObjectMapperCustomizer; +import io.quarkus.resteasy.reactive.jackson.runtime.ResteasyReactiveServerJacksonRecorder; + +@Singleton +public class GeneratedSerializersRegister implements ObjectMapperCustomizer { + + @Override + public void customize(ObjectMapper objectMapper) { + objectMapper.registerModule(MappingModuleHolder.mappingModule); + } + + static class MappingModuleHolder { + static final SimpleModule mappingModule = createMappingModule(); + + private static SimpleModule createMappingModule() { + SimpleModule module = new SimpleModule(); + + for (Class serClass : ResteasyReactiveServerJacksonRecorder.getGeneratedSerializers()) { + try { + StdSerializer serializer = serClass.getConstructor().newInstance(); + module.addSerializer(serializer.handledType(), serializer); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException + | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + for (Class deserClass : ResteasyReactiveServerJacksonRecorder + .getGeneratedDeserializers()) { + try { + StdDeserializer deserializer = deserClass.getConstructor().newInstance(); + module.addDeserializer(deserializer.handledType(), deserializer); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException + | NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + return module; + } + } +}