From eb071c352f8db4f846aaff7627773fdac081320d Mon Sep 17 00:00:00 2001 From: Jose Date: Thu, 9 Jun 2022 06:48:04 +0200 Subject: [PATCH] Allow to provide custom configuration for JAXB context ==== Advanced JAXB-specific features When using the `quarkus-resteasy-reactive-jaxb` extension there are some advanced features that RESTEasy Reactive supports. ===== Inject JAXB components The JAXB resteasy reactive extension will serialize and unserialize requests and responses transparently for users. However, if you need finer usage of the JAXB components, you can inject either the JAXBContext, Marshaller, or Unmarshaller components into your beans: [source,java] ---- @ApplicationScoped public class MyService { @Inject JAXBContext jaxbContext; @Inject Marshaller marshaller; @Inject Unmarshaller unmarshaller; // ... } ---- [NOTE] ==== Quarkus will automatically register all the classes annotated with `@XmlRootElement` to the JAXB context. ==== ===== Customize the JAXB configuration To customize the JAXB configuration for either the JAXB context, and/or the Marshaller/Unmarshaller components, the suggested approach is to define a CDI bean of type `io.quarkus.jaxb.runtime.JaxbContextCustomizer`. An example where a custom module needs to be registered would look like so: [source,java] ---- @Singleton public class RegisterCustomModuleCustomizer implements JaxbContextCustomizer { // For JAXB context configuration @Override public void customizeContextProperties(Map properties) { } // For Marshaller configuration @Override public void customizeMarshaller(Marshaller marshaller) throws PropertyException { marshaller.setProperty("jaxb.formatted.output", Boolean.TRUE); } // For Unmarshaller configuration @Override public void customizeUnmarshaller(Unmarshaller unmarshaller) throws PropertyException { // ... } } ---- [NOTE] ==== It's not necessary to implement the three methods, but only the want you need. ==== Alternatively, you can provide your own `JAXBContext` bean by doing: [source,java] ---- public class CustomJaxbContext { // Replaces the CDI producer for JAXBContext built into Quarkus @Singleton JAXBContext jaxbContext() { // ... } } ---- [IMPORTANT] ==== By replacing the JAXB context, you need to bound the classes to be serialized/unserialized and also configure the JAXB context accordingly. ==== --- docs/src/main/asciidoc/rest-json.adoc | 2 + docs/src/main/asciidoc/resteasy-reactive.adoc | 87 ++++++++++ extensions/jaxb/deployment/pom.xml | 8 +- .../JaxbClassesToBeBoundBuildItem.java | 22 +++ .../jaxb/deployment/JaxbProcessor.java | 57 +++++-- .../CustomJaxbContextCustomizer.java | 15 ++ .../deployment/InjectJaxbContextTest.java | 53 ++++++ .../io/quarkus/jaxb/deployment/Person.java | 38 +++++ extensions/jaxb/runtime/pom.xml | 2 +- .../runtime/JaxbContextConfigRecorder.java | 20 +++ .../jaxb/runtime/JaxbContextCustomizer.java | 45 +++++ .../jaxb/runtime/JaxbContextProducer.java | 93 +++++++++++ .../deployment/ResteasyJaxbProcessor.java | 13 ++ .../serialisers/JaxbMessageBodyReader.java | 20 ++- .../serialisers/JaxbMessageBodyWriter.java | 31 +++- .../ResteasyReactiveJaxbProcessor.java | 157 ++++++++++++++++++ ...partOutputTest.java => MultipartTest.java} | 67 +++++++- .../jaxb/deployment/test/SimpleXmlTest.java | 1 + 18 files changed, 703 insertions(+), 28 deletions(-) create mode 100644 extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbClassesToBeBoundBuildItem.java create mode 100644 extensions/jaxb/deployment/src/test/java/io/quarkus/jaxb/deployment/CustomJaxbContextCustomizer.java create mode 100644 extensions/jaxb/deployment/src/test/java/io/quarkus/jaxb/deployment/InjectJaxbContextTest.java create mode 100644 extensions/jaxb/deployment/src/test/java/io/quarkus/jaxb/deployment/Person.java create mode 100644 extensions/jaxb/runtime/src/main/java/io/quarkus/jaxb/runtime/JaxbContextConfigRecorder.java create mode 100644 extensions/jaxb/runtime/src/main/java/io/quarkus/jaxb/runtime/JaxbContextCustomizer.java create mode 100644 extensions/jaxb/runtime/src/main/java/io/quarkus/jaxb/runtime/JaxbContextProducer.java rename extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/{MultipartOutputTest.java => MultipartTest.java} (65%) diff --git a/docs/src/main/asciidoc/rest-json.adoc b/docs/src/main/asciidoc/rest-json.adoc index be11639411711..1ddab42950c27 100644 --- a/docs/src/main/asciidoc/rest-json.adoc +++ b/docs/src/main/asciidoc/rest-json.adoc @@ -233,12 +233,14 @@ import com.fasterxml.jackson.databind.ObjectMapper; import io.quarkus.jackson.ObjectMapperCustomizer; import javax.enterprise.inject.Instance; +import javax.enterprise.inject.Produces; import javax.inject.Singleton; public class CustomObjectMapper { // Replaces the CDI producer for ObjectMapper built into Quarkus @Singleton + @Produces ObjectMapper objectMapper(Instance customizers) { ObjectMapper mapper = myObjectMapper(); // Custom `ObjectMapper` diff --git a/docs/src/main/asciidoc/resteasy-reactive.adoc b/docs/src/main/asciidoc/resteasy-reactive.adoc index b079a71f9db03..fa5e7f208719b 100644 --- a/docs/src/main/asciidoc/resteasy-reactive.adoc +++ b/docs/src/main/asciidoc/resteasy-reactive.adoc @@ -1138,6 +1138,93 @@ Importing this module will allow HTTP message bodies to be read from XML and serialised to XML, for <>. +==== Advanced JAXB-specific features + +When using the `quarkus-resteasy-reactive-jaxb` extension there are some advanced features that RESTEasy Reactive supports. + +===== Inject JAXB components + +The JAXB resteasy reactive extension will serialize and unserialize requests and responses transparently for users. However, if you need finer grain control over JAXB components, you can inject either the JAXBContext, Marshaller, or Unmarshaller components into your beans: + +[source,java] +---- +@ApplicationScoped +public class MyService { + + @Inject + JAXBContext jaxbContext; + + @Inject + Marshaller marshaller; + + @Inject + Unmarshaller unmarshaller; + + // ... +} +---- + +[NOTE] +==== +Quarkus will automatically find all the classes annotated with `@XmlRootElement` and then bound them to the JAXB context. +==== + +===== Customize the JAXB configuration + +To customize the JAXB configuration for either the JAXB context, and/or the Marshaller/Unmarshaller components, the suggested approach is to define a CDI bean of type `io.quarkus.jaxb.runtime.JaxbContextCustomizer`. + +An example where a custom module needs to be registered would look like so: + +[source,java] +---- +@Singleton +public class RegisterCustomModuleCustomizer implements JaxbContextCustomizer { + + // For JAXB context configuration + @Override + public void customizeContextProperties(Map properties) { + + } + + // For Marshaller configuration + @Override + public void customizeMarshaller(Marshaller marshaller) throws PropertyException { + marshaller.setProperty("jaxb.formatted.output", Boolean.TRUE); + } + + // For Unmarshaller configuration + @Override + public void customizeUnmarshaller(Unmarshaller unmarshaller) throws PropertyException { + // ... + } +} +---- + +[NOTE] +==== +It's not necessary to implement all three methods, but only the want you need. +==== + +Alternatively, you can provide your own `JAXBContext` bean by doing: + +[source,java] +---- +public class CustomJaxbContext { + + // Replaces the CDI producer for JAXBContext built into Quarkus + @Singleton + @Produces + JAXBContext jaxbContext() { + // ... + } +} +---- + +[IMPORTANT] +==== +Note that if you provide your custom JAXB context instance, you will need to register the classes you want to use for the XML serialization. This means that Quarkus will not update your custom JAXB context instance with the auto-discovered classes. +==== + === Web Links support [[links]] diff --git a/extensions/jaxb/deployment/pom.xml b/extensions/jaxb/deployment/pom.xml index 3bfb4cd06e756..30e68d12c9c46 100644 --- a/extensions/jaxb/deployment/pom.xml +++ b/extensions/jaxb/deployment/pom.xml @@ -15,7 +15,7 @@ io.quarkus - quarkus-core-deployment + quarkus-arc-deployment io.quarkus @@ -25,6 +25,12 @@ io.quarkus quarkus-jaxb + + + io.quarkus + quarkus-junit5 + test + diff --git a/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbClassesToBeBoundBuildItem.java b/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbClassesToBeBoundBuildItem.java new file mode 100644 index 0000000000000..199842f0b7586 --- /dev/null +++ b/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbClassesToBeBoundBuildItem.java @@ -0,0 +1,22 @@ +package io.quarkus.jaxb.deployment; + +import java.util.List; +import java.util.Objects; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * List of classes to be bound in the JAXB context. + */ +public final class JaxbClassesToBeBoundBuildItem extends MultiBuildItem { + + private final List classes; + + public JaxbClassesToBeBoundBuildItem(List classes) { + this.classes = Objects.requireNonNull(classes); + } + + public List getClasses() { + return classes; + } +} diff --git a/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java b/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java index 3fc14ef85dbe5..fbc1fbbc04ad4 100644 --- a/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java +++ b/extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java @@ -5,6 +5,7 @@ import java.lang.annotation.Annotation; import java.nio.file.Files; import java.nio.file.Path; +import java.util.ArrayList; import java.util.List; import java.util.stream.Stream; @@ -49,9 +50,12 @@ import org.jboss.jandex.IndexView; import org.jboss.jandex.Type; +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; import io.quarkus.deployment.ApplicationArchive; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.builditem.ApplicationArchivesBuildItem; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.nativeimage.NativeImageProxyDefinitionBuildItem; @@ -62,6 +66,8 @@ import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyIgnoreWarningBuildItem; import io.quarkus.deployment.builditem.nativeimage.RuntimeInitializedClassBuildItem; import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem; +import io.quarkus.jaxb.runtime.JaxbContextConfigRecorder; +import io.quarkus.jaxb.runtime.JaxbContextProducer; class JaxbProcessor { @@ -174,8 +180,10 @@ void processAnnotationsAndIndexFiles( BuildProducer resource, BuildProducer resourceBundle, BuildProducer runtimeClasses, - ApplicationArchivesBuildItem applicationArchivesBuildItem) { + BuildProducer classesToBeBoundProducer, + ApplicationArchivesBuildItem applicationArchivesBuildItem) throws ClassNotFoundException { + List classesToBeBound = new ArrayList<>(); IndexView index = combinedIndexBuildItem.getIndex(); // Register classes for reflection based on JAXB annotations @@ -185,8 +193,9 @@ void processAnnotationsAndIndexFiles( for (AnnotationInstance jaxbRootAnnotationInstance : index .getAnnotations(jaxbRootAnnotation)) { if (jaxbRootAnnotationInstance.target().kind() == Kind.CLASS) { - addReflectiveClass(reflectiveClass, true, true, - jaxbRootAnnotationInstance.target().asClass().name().toString()); + String className = jaxbRootAnnotationInstance.target().asClass().name().toString(); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, className)); + classesToBeBound.add(className); jaxbRootAnnotationsDetected = true; } } @@ -199,8 +208,11 @@ void processAnnotationsAndIndexFiles( // Register package-infos for reflection for (AnnotationInstance xmlSchemaInstance : index.getAnnotations(XML_SCHEMA)) { if (xmlSchemaInstance.target().kind() == Kind.CLASS) { - reflectiveClass.produce( - new ReflectiveClassBuildItem(false, false, xmlSchemaInstance.target().asClass().name().toString())); + String className = xmlSchemaInstance.target().asClass().name().toString(); + + reflectiveClass.produce(new ReflectiveClassBuildItem(false, false, className)); + + classesToBeBound.add(className); } } @@ -236,8 +248,10 @@ void processAnnotationsAndIndexFiles( } for (JaxbFileRootBuildItem i : fileRoots) { - iterateResources(applicationArchivesBuildItem, i.getFileRoot(), resource, reflectiveClass); + iterateResources(applicationArchivesBuildItem, i.getFileRoot(), resource, reflectiveClass, classesToBeBound); } + + classesToBeBoundProducer.produce(new JaxbClassesToBeBoundBuildItem(classesToBeBound)); } @BuildStep @@ -250,9 +264,9 @@ void ignoreWarnings(BuildProducer ign @BuildStep void registerClasses( BuildProducer nativeImageProps, - BuildProducer providerItem, final BuildProducer reflectiveClass, - final BuildProducer resourceBundle) { - + BuildProducer providerItem, + BuildProducer reflectiveClass, + BuildProducer resourceBundle) { addReflectiveClass(reflectiveClass, true, false, "com.sun.xml.bind.v2.ContextFactory"); addReflectiveClass(reflectiveClass, true, false, "com.sun.xml.internal.bind.v2.ContextFactory"); @@ -275,8 +289,23 @@ void registerClasses( .produce(new ServiceProviderBuildItem(JAXBContext.class.getName(), "com.sun.xml.bind.v2.ContextFactory")); } + @BuildStep + @Record(ExecutionTime.STATIC_INIT) + void setupJaxbContextConfig(List classesToBeBoundBuildItems, + JaxbContextConfigRecorder jaxbContextConfig) { + for (JaxbClassesToBeBoundBuildItem classesToBeBoundBuildItem : classesToBeBoundBuildItems) { + jaxbContextConfig.addClassesToBeBound(classesToBeBoundBuildItem.getClasses()); + } + } + + @BuildStep + void registerProduces(BuildProducer additionalBeans) { + additionalBeans.produce(new AdditionalBeanBuildItem(JaxbContextProducer.class)); + } + private void handleJaxbFile(Path p, BuildProducer resource, - BuildProducer reflectiveClass) { + BuildProducer reflectiveClass, + List classesToBeBound) { try { String path = p.toAbsolutePath().toString().substring(1); String pkg = p.toAbsolutePath().getParent().toString().substring(1) @@ -289,9 +318,10 @@ private void handleJaxbFile(Path p, BuildProducer if (!line.isEmpty() && !line.startsWith("#")) { String clazz = pkg + line; Class cl = Class.forName(clazz, false, Thread.currentThread().getContextClassLoader()); + classesToBeBound.add(clazz); while (cl != Object.class) { - addReflectiveClass(reflectiveClass, true, true, cl.getName()); + reflectiveClass.produce(new ReflectiveClassBuildItem(true, true, cl)); cl = cl.getSuperclass(); } } @@ -302,7 +332,8 @@ private void handleJaxbFile(Path p, BuildProducer } private void iterateResources(ApplicationArchivesBuildItem applicationArchivesBuildItem, String path, - BuildProducer resource, BuildProducer reflectiveClass) { + BuildProducer resource, BuildProducer reflectiveClass, + List classesToBeBound) { for (ApplicationArchive archive : applicationArchivesBuildItem.getAllApplicationArchives()) { archive.accept(tree -> { var arch = tree.getPath(path); @@ -310,7 +341,7 @@ private void iterateResources(ApplicationArchivesBuildItem applicationArchivesBu JaxbProcessor.safeWalk(arch) .filter(Files::isRegularFile) .filter(p -> p.getFileName().toString().equals("jaxb.index")) - .forEach(p1 -> handleJaxbFile(p1, resource, reflectiveClass)); + .forEach(p1 -> handleJaxbFile(p1, resource, reflectiveClass, classesToBeBound)); } }); } diff --git a/extensions/jaxb/deployment/src/test/java/io/quarkus/jaxb/deployment/CustomJaxbContextCustomizer.java b/extensions/jaxb/deployment/src/test/java/io/quarkus/jaxb/deployment/CustomJaxbContextCustomizer.java new file mode 100644 index 0000000000000..dd9213cadc501 --- /dev/null +++ b/extensions/jaxb/deployment/src/test/java/io/quarkus/jaxb/deployment/CustomJaxbContextCustomizer.java @@ -0,0 +1,15 @@ +package io.quarkus.jaxb.deployment; + +import javax.inject.Singleton; +import javax.xml.bind.Marshaller; +import javax.xml.bind.PropertyException; + +import io.quarkus.jaxb.runtime.JaxbContextCustomizer; + +@Singleton +public class CustomJaxbContextCustomizer implements JaxbContextCustomizer { + @Override + public void customizeMarshaller(Marshaller marshaller) throws PropertyException { + marshaller.setProperty("jaxb.formatted.output", Boolean.TRUE); + } +} diff --git a/extensions/jaxb/deployment/src/test/java/io/quarkus/jaxb/deployment/InjectJaxbContextTest.java b/extensions/jaxb/deployment/src/test/java/io/quarkus/jaxb/deployment/InjectJaxbContextTest.java new file mode 100644 index 0000000000000..308ffe5a4d2fa --- /dev/null +++ b/extensions/jaxb/deployment/src/test/java/io/quarkus/jaxb/deployment/InjectJaxbContextTest.java @@ -0,0 +1,53 @@ +package io.quarkus.jaxb.deployment; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.StringWriter; + +import javax.inject.Inject; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; + +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +public class InjectJaxbContextTest { + + @Inject + JAXBContext jaxbContext; + + @Inject + Marshaller marshaller; + + @Inject + Unmarshaller unmarshaller; + + @Test + public void shouldInjectJaxbBeans() { + assertNotNull(jaxbContext); + assertNotNull(marshaller); + assertNotNull(unmarshaller); + } + + @Test + public void shouldPersonBeInTheJaxbContext() throws JAXBException { + Person person = new Person(); + person.setFirst("first"); + person.setLast("last"); + + StringWriter sw = new StringWriter(); + marshaller.marshal(person, sw); + + assertEquals("\n" + + "\n" + + " first\n" + + " last\n" + + "\n", sw.toString()); + } + +} diff --git a/extensions/jaxb/deployment/src/test/java/io/quarkus/jaxb/deployment/Person.java b/extensions/jaxb/deployment/src/test/java/io/quarkus/jaxb/deployment/Person.java new file mode 100644 index 0000000000000..c038dce2d5348 --- /dev/null +++ b/extensions/jaxb/deployment/src/test/java/io/quarkus/jaxb/deployment/Person.java @@ -0,0 +1,38 @@ +package io.quarkus.jaxb.deployment; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlRootElement; + +@XmlRootElement +@XmlAccessorType(XmlAccessType.FIELD) +public class Person { + + private String first; + private String last; + + public Person() { + + } + + public Person(String first, String last) { + this.first = first; + this.last = last; + } + + public String getFirst() { + return first; + } + + public void setFirst(String first) { + this.first = first; + } + + public String getLast() { + return last; + } + + public void setLast(String last) { + this.last = last; + } +} diff --git a/extensions/jaxb/runtime/pom.xml b/extensions/jaxb/runtime/pom.xml index db9f610c4903e..4ed45fc383ffb 100644 --- a/extensions/jaxb/runtime/pom.xml +++ b/extensions/jaxb/runtime/pom.xml @@ -20,7 +20,7 @@ io.quarkus - quarkus-core + quarkus-arc io.quarkus diff --git a/extensions/jaxb/runtime/src/main/java/io/quarkus/jaxb/runtime/JaxbContextConfigRecorder.java b/extensions/jaxb/runtime/src/main/java/io/quarkus/jaxb/runtime/JaxbContextConfigRecorder.java new file mode 100644 index 0000000000000..b347a87ed5f82 --- /dev/null +++ b/extensions/jaxb/runtime/src/main/java/io/quarkus/jaxb/runtime/JaxbContextConfigRecorder.java @@ -0,0 +1,20 @@ +package io.quarkus.jaxb.runtime; + +import java.util.Collection; +import java.util.HashSet; +import java.util.Set; + +import io.quarkus.runtime.annotations.Recorder; + +@Recorder +public class JaxbContextConfigRecorder { + private volatile static Set classesToBeBound = new HashSet<>(); + + public void addClassesToBeBound(Collection additionalClassesToBeBound) { + this.classesToBeBound.addAll(additionalClassesToBeBound); + } + + public static String[] getClassesToBeBound() { + return classesToBeBound.toArray(new String[0]); + } +} diff --git a/extensions/jaxb/runtime/src/main/java/io/quarkus/jaxb/runtime/JaxbContextCustomizer.java b/extensions/jaxb/runtime/src/main/java/io/quarkus/jaxb/runtime/JaxbContextCustomizer.java new file mode 100644 index 0000000000000..fd354b24261da --- /dev/null +++ b/extensions/jaxb/runtime/src/main/java/io/quarkus/jaxb/runtime/JaxbContextCustomizer.java @@ -0,0 +1,45 @@ +package io.quarkus.jaxb.runtime; + +import java.util.Map; + +import javax.xml.bind.JAXBContext; +import javax.xml.bind.Marshaller; +import javax.xml.bind.PropertyException; +import javax.xml.bind.Unmarshaller; + +/** + * Meant to be implemented by a CDI bean that provides arbitrary customization for the default {@link JAXBContext}. + *

+ * All implementations (that are registered as CDI beans) are taken into account when producing the default + * {@link JAXBContext}. + *

+ * See also {@link JaxbContextProducer#jaxbContext}. + */ +public interface JaxbContextCustomizer extends Comparable { + + int DEFAULT_PRIORITY = 0; + + default void customizeContextProperties(Map properties) { + + } + + default void customizeMarshaller(Marshaller marshaller) throws PropertyException { + + } + + default void customizeUnmarshaller(Unmarshaller unmarshaller) throws PropertyException { + + } + + /** + * Defines the priority that the customizers are applied. + * A lower integer value means that the customizer will be applied after a customizer with a higher priority + */ + default int priority() { + return DEFAULT_PRIORITY; + } + + default int compareTo(JaxbContextCustomizer o) { + return Integer.compare(o.priority(), priority()); + } +} diff --git a/extensions/jaxb/runtime/src/main/java/io/quarkus/jaxb/runtime/JaxbContextProducer.java b/extensions/jaxb/runtime/src/main/java/io/quarkus/jaxb/runtime/JaxbContextProducer.java new file mode 100644 index 0000000000000..3da8e6612202e --- /dev/null +++ b/extensions/jaxb/runtime/src/main/java/io/quarkus/jaxb/runtime/JaxbContextProducer.java @@ -0,0 +1,93 @@ +package io.quarkus.jaxb.runtime; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Instance; +import javax.enterprise.inject.Produces; +import javax.inject.Singleton; +import javax.xml.bind.JAXBContext; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.Unmarshaller; + +import io.quarkus.arc.DefaultBean; + +@ApplicationScoped +public class JaxbContextProducer { + @DefaultBean + @Singleton + @Produces + public JAXBContext jaxbContext(Instance customizers) { + try { + Map properties = new HashMap<>(); + List sortedCustomizers = sortCustomizersInDescendingPriorityOrder(customizers); + for (JaxbContextCustomizer customizer : sortedCustomizers) { + customizer.customizeContextProperties(properties); + } + + String[] classNamesToBeBounded = JaxbContextConfigRecorder.getClassesToBeBound(); + List> classes = new ArrayList<>(); + for (int i = 0; i < classNamesToBeBounded.length; i++) { + Class clazz = getClassByName(classNamesToBeBounded[i]); + if (!clazz.isPrimitive()) { + classes.add(clazz); + } + } + return JAXBContext.newInstance(classes.toArray(new Class[0]), properties); + } catch (JAXBException | ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + + @DefaultBean + @Singleton + @Produces + public Marshaller marshaller(JAXBContext jaxbContext, Instance customizers) { + try { + Marshaller marshaller = jaxbContext.createMarshaller(); + List sortedCustomizers = sortCustomizersInDescendingPriorityOrder(customizers); + for (JaxbContextCustomizer customizer : sortedCustomizers) { + customizer.customizeMarshaller(marshaller); + } + + return marshaller; + } catch (JAXBException e) { + throw new RuntimeException(e); + } + } + + @DefaultBean + @Singleton + @Produces + public Unmarshaller unmarshaller(JAXBContext jaxbContext, Instance customizers) { + try { + Unmarshaller unmarshaller = jaxbContext.createUnmarshaller(); + List sortedCustomizers = sortCustomizersInDescendingPriorityOrder(customizers); + for (JaxbContextCustomizer customizer : sortedCustomizers) { + customizer.customizeUnmarshaller(unmarshaller); + } + + return unmarshaller; + } catch (JAXBException e) { + throw new RuntimeException(e); + } + } + + private List sortCustomizersInDescendingPriorityOrder(Instance customizers) { + List sortedCustomizers = new ArrayList<>(); + for (JaxbContextCustomizer customizer : customizers) { + sortedCustomizers.add(customizer); + } + Collections.sort(sortedCustomizers); + return sortedCustomizers; + } + + private Class getClassByName(String name) throws ClassNotFoundException { + return Class.forName(name, false, Thread.currentThread().getContextClassLoader()); + } +} diff --git a/extensions/resteasy-classic/resteasy-jaxb/deployment/src/main/java/io/quarkus/resteasy/jaxb/deployment/ResteasyJaxbProcessor.java b/extensions/resteasy-classic/resteasy-jaxb/deployment/src/main/java/io/quarkus/resteasy/jaxb/deployment/ResteasyJaxbProcessor.java index 36c0b87eb143c..8b3d4f1f40e25 100644 --- a/extensions/resteasy-classic/resteasy-jaxb/deployment/src/main/java/io/quarkus/resteasy/jaxb/deployment/ResteasyJaxbProcessor.java +++ b/extensions/resteasy-classic/resteasy-jaxb/deployment/src/main/java/io/quarkus/resteasy/jaxb/deployment/ResteasyJaxbProcessor.java @@ -2,6 +2,7 @@ import java.lang.annotation.Annotation; import java.util.Arrays; +import java.util.Collections; import java.util.List; import org.jboss.jandex.DotName; @@ -10,12 +11,15 @@ import org.jboss.resteasy.annotations.providers.jaxb.WrappedMap; import org.jboss.resteasy.api.validation.ConstraintType; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem; +import io.quarkus.jaxb.deployment.JaxbClassesToBeBoundBuildItem; public class ResteasyJaxbProcessor { @@ -43,6 +47,15 @@ void addReflectiveClasses(BuildProducer reflectiveClas } } + @BuildStep + void setupJaxbContextConfigForValidator(Capabilities capabilities, + BuildProducer classesToBeBoundProducer) { + if (capabilities.isPresent(Capability.HIBERNATE_VALIDATOR)) { + classesToBeBoundProducer.produce(new JaxbClassesToBeBoundBuildItem( + Collections.singletonList(org.jboss.resteasy.api.validation.ViolationReport.class.getName()))); + } + } + @BuildStep void build(BuildProducer feature) { feature.produce(new FeatureBuildItem(Feature.RESTEASY_JAXB)); diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb-common/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/common/runtime/serialisers/JaxbMessageBodyReader.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb-common/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/common/runtime/serialisers/JaxbMessageBodyReader.java index f78d995fb4602..f98dda9bc0071 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb-common/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/common/runtime/serialisers/JaxbMessageBodyReader.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb-common/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/common/runtime/serialisers/JaxbMessageBodyReader.java @@ -5,10 +5,14 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Type; +import javax.inject.Inject; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; -import javax.xml.bind.JAXB; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Unmarshaller; +import javax.xml.transform.stream.StreamSource; import org.jboss.resteasy.reactive.common.util.StreamUtil; import org.jboss.resteasy.reactive.server.spi.ResteasyReactiveResourceInfo; @@ -17,6 +21,9 @@ public class JaxbMessageBodyReader implements ServerMessageBodyReader { + @Inject + Unmarshaller unmarshaller; + @Override public Object readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, InputStream entityStream) throws WebApplicationException, IOException { @@ -52,12 +59,21 @@ protected boolean isReadable(MediaType mediaType, Class type) { || (mediaType.isWildcardSubtype() && (mediaType.isWildcardType() || isCorrectMediaType)); } + protected Object unmarshal(InputStream entityStream, Class type) { + try { + JAXBElement item = unmarshaller.unmarshal(new StreamSource(entityStream), type); + return item.getValue(); + } catch (JAXBException e) { + throw new RuntimeException(e); + } + } + private Object doReadFrom(Class type, Type genericType, InputStream entityStream) throws IOException { if (isInputStreamEmpty(entityStream)) { return null; } - return JAXB.unmarshal(entityStream, type); + return unmarshal(entityStream, type); } private boolean isInputStreamEmpty(InputStream entityStream) throws IOException { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb-common/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/common/runtime/serialisers/JaxbMessageBodyWriter.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb-common/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/common/runtime/serialisers/JaxbMessageBodyWriter.java index cae0eca61dc5f..41132dcbf0611 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb-common/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/common/runtime/serialisers/JaxbMessageBodyWriter.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb-common/runtime/src/main/java/io/quarkus/resteasy/reactive/jaxb/common/runtime/serialisers/JaxbMessageBodyWriter.java @@ -1,16 +1,22 @@ package io.quarkus.resteasy.reactive.jaxb.common.runtime.serialisers; +import java.beans.Introspector; import java.io.IOException; import java.io.OutputStream; import java.lang.annotation.Annotation; import java.lang.reflect.Type; import java.util.Map; +import javax.inject.Inject; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.MultivaluedMap; -import javax.xml.bind.JAXB; +import javax.xml.bind.JAXBElement; +import javax.xml.bind.JAXBException; +import javax.xml.bind.Marshaller; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.namespace.QName; import org.jboss.resteasy.reactive.server.spi.ServerMessageBodyWriter; import org.jboss.resteasy.reactive.server.spi.ServerRequestContext; @@ -19,11 +25,14 @@ public class JaxbMessageBodyWriter extends ServerMessageBodyWriter.AllWriteableMessageBodyWriter { + @Inject + Marshaller marshaller; + @Override public void writeTo(Object o, Class type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) throws WebApplicationException { setContentTypeIfNecessary(httpHeaders); - JAXB.marshal(o, entityStream); + marshal(o, entityStream); } @Override @@ -31,11 +40,27 @@ public void writeResponse(Object o, Type genericType, ServerRequestContext conte throws WebApplicationException, IOException { setContentTypeIfNecessary(context); OutputStream stream = context.getOrCreateOutputStream(); - JAXB.marshal(o, stream); + marshal(o, stream); // we don't use try-with-resources because that results in writing to the http output without the exception mapping coming into play stream.close(); } + protected void marshal(Object o, OutputStream outputStream) { + try { + Object jaxbObject = o; + Class clazz = o.getClass(); + XmlRootElement jaxbElement = clazz.getAnnotation(XmlRootElement.class); + if (jaxbElement == null) { + jaxbObject = new JAXBElement(new QName(Introspector.decapitalize(clazz.getSimpleName())), clazz, o); + } + + marshaller.marshal(jaxbObject, outputStream); + + } catch (JAXBException e) { + throw new RuntimeException(e); + } + } + private void setContentTypeIfNecessary(MultivaluedMap httpHeaders) { Object contentType = httpHeaders.getFirst(HttpHeaders.CONTENT_TYPE); if (isNotXml(contentType)) { diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/main/java/io/quarkus/resteasy/reactive/jaxb/deployment/ResteasyReactiveJaxbProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/main/java/io/quarkus/resteasy/reactive/jaxb/deployment/ResteasyReactiveJaxbProcessor.java index 794feb434126e..2cffe94d9cd3f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/main/java/io/quarkus/resteasy/reactive/jaxb/deployment/ResteasyReactiveJaxbProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/main/java/io/quarkus/resteasy/reactive/jaxb/deployment/ResteasyReactiveJaxbProcessor.java @@ -1,9 +1,34 @@ package io.quarkus.resteasy.reactive.jaxb.deployment; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +import javax.ws.rs.core.MediaType; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.Type; +import org.jboss.resteasy.reactive.common.model.ResourceMethod; +import org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames; + +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.Capability; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.jaxb.deployment.JaxbClassesToBeBoundBuildItem; +import io.quarkus.resteasy.reactive.common.deployment.JaxRsResourceIndexBuildItem; +import io.quarkus.resteasy.reactive.server.deployment.ResteasyReactiveResourceMethodEntriesBuildItem; public class ResteasyReactiveJaxbProcessor { @@ -11,4 +36,136 @@ public class ResteasyReactiveJaxbProcessor { void feature(BuildProducer feature) { feature.produce(new FeatureBuildItem(Feature.RESTEASY_REACTIVE_JAXB)); } + + @BuildStep + void registerClassesToBeBound(ResteasyReactiveResourceMethodEntriesBuildItem resourceMethodEntries, + JaxRsResourceIndexBuildItem index, + BuildProducer classesToBeBoundBuildItemProducer) { + Set classesInfo = new HashSet<>(); + + IndexView indexView = index.getIndexView(); + for (ResteasyReactiveResourceMethodEntriesBuildItem.Entry entry : resourceMethodEntries.getEntries()) { + ResourceMethod resourceInfo = entry.getResourceMethod(); + MethodInfo methodInfo = entry.getMethodInfo(); + ClassInfo effectiveReturnType = getEffectiveClassInfo(methodInfo.returnType(), indexView); + + if (effectiveReturnType != null) { + // When using "application/xml", the return type needs to be registered + if (producesXml(resourceInfo)) { + classesInfo.add(effectiveReturnType); + } + + // When using "multipart/form-data", the parts that use "application/xml" need to be registered + if (producesMultipart(resourceInfo)) { + classesInfo.addAll(getEffectivePartsUsingXml(effectiveReturnType, indexView)); + } + } + + // If consumes "application/xml" or "multipart/form-data", we register all the classes of the parameters + if (consumesXml(resourceInfo) || consumesMultipart(resourceInfo)) { + for (Type parameter : methodInfo.parameters()) { + ClassInfo effectiveParameter = getEffectiveClassInfo(parameter, indexView); + if (effectiveParameter != null) { + classesInfo.add(effectiveParameter); + } + } + } + } + + classesToBeBoundBuildItemProducer.produce(new JaxbClassesToBeBoundBuildItem(toClasses(classesInfo))); + } + + @BuildStep + void setupJaxbContextConfigForValidator(Capabilities capabilities, + BuildProducer classesToBeBoundProducer) { + if (capabilities.isPresent(Capability.HIBERNATE_VALIDATOR)) { + classesToBeBoundProducer.produce(new JaxbClassesToBeBoundBuildItem( + Collections.singletonList("io.quarkus.hibernate.validator.runtime.jaxrs.ViolationReport"))); + } + } + + private List getEffectivePartsUsingXml(ClassInfo returnType, IndexView indexView) { + List classInfos = new ArrayList<>(); + for (FieldInfo field : returnType.fields()) { + AnnotationInstance partTypeInstance = field.annotation(ResteasyReactiveDotNames.PART_TYPE_NAME); + if (partTypeInstance != null) { + AnnotationValue partTypeValue = partTypeInstance.value(); + if (partTypeValue != null && MediaType.APPLICATION_XML.equals(partTypeValue.asString())) { + classInfos.add(getEffectiveClassInfo(field.type(), indexView)); + } + } + } + + return classInfos; + } + + private ClassInfo getEffectiveClassInfo(Type type, IndexView indexView) { + if (type.kind() == Type.Kind.VOID || type.kind() == Type.Kind.PRIMITIVE) { + return null; + } + + Type effectiveType = type; + if (effectiveType.name().equals(ResteasyReactiveDotNames.REST_RESPONSE) || + effectiveType.name().equals(ResteasyReactiveDotNames.UNI) || + effectiveType.name().equals(ResteasyReactiveDotNames.COMPLETABLE_FUTURE) || + effectiveType.name().equals(ResteasyReactiveDotNames.COMPLETION_STAGE) || + effectiveType.name().equals(ResteasyReactiveDotNames.MULTI)) { + if (effectiveType.kind() != Type.Kind.PARAMETERIZED_TYPE) { + return null; + } + + effectiveType = type.asParameterizedType().arguments().get(0); + } + if (effectiveType.name().equals(ResteasyReactiveDotNames.SET) || + effectiveType.name().equals(ResteasyReactiveDotNames.COLLECTION) || + effectiveType.name().equals(ResteasyReactiveDotNames.LIST)) { + effectiveType = effectiveType.asParameterizedType().arguments().get(0); + } else if (effectiveType.name().equals(ResteasyReactiveDotNames.MAP)) { + effectiveType = effectiveType.asParameterizedType().arguments().get(1); + } + + ClassInfo effectiveReturnClassInfo = indexView.getClassByName(effectiveType.name()); + if ((effectiveReturnClassInfo == null) || effectiveReturnClassInfo.name().equals(ResteasyReactiveDotNames.OBJECT)) { + return null; + } + + return effectiveReturnClassInfo; + } + + private boolean consumesXml(ResourceMethod resourceInfo) { + return containsMediaType(resourceInfo.getConsumes(), MediaType.APPLICATION_XML); + } + + private boolean consumesMultipart(ResourceMethod resourceInfo) { + return containsMediaType(resourceInfo.getConsumes(), MediaType.MULTIPART_FORM_DATA); + } + + private boolean producesXml(ResourceMethod resourceInfo) { + return containsMediaType(resourceInfo.getProduces(), MediaType.APPLICATION_XML); + } + + private boolean producesMultipart(ResourceMethod resourceInfo) { + return containsMediaType(resourceInfo.getProduces(), MediaType.MULTIPART_FORM_DATA); + } + + private boolean containsMediaType(String[] types, String mediaType) { + if (types != null) { + for (String type : types) { + if (type.toLowerCase(Locale.ROOT).contains(mediaType)) { + return true; + } + } + } + + return false; + } + + private List toClasses(Collection classesInfo) { + List classes = new ArrayList<>(); + for (ClassInfo classInfo : classesInfo) { + classes.add(classInfo.toString()); + } + + return classes; + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartOutputTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartTest.java similarity index 65% rename from extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartOutputTest.java rename to extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartTest.java index d2c6ce7ec69f4..9fe9a051645de 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartOutputTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/MultipartTest.java @@ -2,11 +2,14 @@ import static org.assertj.core.api.Assertions.assertThat; +import javax.ws.rs.Consumes; import javax.ws.rs.GET; +import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; +import org.jboss.resteasy.reactive.MultipartForm; import org.jboss.resteasy.reactive.PartType; import org.jboss.resteasy.reactive.RestForm; import org.jboss.shrinkwrap.api.ShrinkWrap; @@ -18,17 +21,21 @@ import io.restassured.RestAssured; import io.restassured.http.ContentType; -public class MultipartOutputTest { +public class MultipartTest { private static final String EXPECTED_CONTENT_DISPOSITION_PART = "Content-Disposition: form-data; name=\"%s\""; private static final String EXPECTED_CONTENT_TYPE_PART = "Content-Type: %s"; private static final String EXPECTED_RESPONSE_NAME = "a name"; private static final String EXPECTED_RESPONSE_PERSON_NAME = "Michal"; private static final int EXPECTED_RESPONSE_PERSON_AGE = 23; - private static final String EXPECTED_RESPONSE_PERSON = "\n" - + "\n" - + " " + EXPECTED_RESPONSE_PERSON_AGE + "\n" - + " " + EXPECTED_RESPONSE_PERSON_NAME + "\n" + private static final String EXPECTED_RESPONSE_PERSON = "" + + "" + + "" + EXPECTED_RESPONSE_PERSON_AGE + "" + + "" + EXPECTED_RESPONSE_PERSON_NAME + "" + ""; + private static final String SCHOOL = "" + + "" + + "Divino Pastor" + + ""; @RegisterExtension static QuarkusUnitTest test = new QuarkusUnitTest() @@ -36,7 +43,7 @@ public class MultipartOutputTest { .addClasses(MultipartOutputResource.class, MultipartOutputResponse.class, Person.class)); @Test - public void testSimple() { + public void testOutput() { String response = RestAssured.get("/multipart/output") .then() .contentType(ContentType.MULTIPART) @@ -47,6 +54,20 @@ public void testSimple() { assertContains(response, "person", MediaType.APPLICATION_XML, EXPECTED_RESPONSE_PERSON); } + @Test + public void testInput() { + String response = RestAssured + .given() + .multiPart("name", "John") + .multiPart("school", SCHOOL, MediaType.APPLICATION_XML) + .post("/multipart/input") + .then() + .statusCode(200) + .extract().asString(); + + assertThat(response).isEqualTo("John-Divino Pastor"); + } + private void assertContains(String response, String name, String contentType, Object value) { String[] lines = response.split("--"); assertThat(lines).anyMatch(line -> line.contains(String.format(EXPECTED_CONTENT_DISPOSITION_PART, name)) @@ -54,12 +75,13 @@ private void assertContains(String response, String name, String contentType, Ob && line.contains(value.toString())); } - @Path("/multipart/output") + @Path("/multipart") private static class MultipartOutputResource { @GET + @Path("/output") @Produces(MediaType.MULTIPART_FORM_DATA) - public MultipartOutputResponse simple() { + public MultipartOutputResponse output() { MultipartOutputResponse response = new MultipartOutputResponse(); response.name = EXPECTED_RESPONSE_NAME; response.person = new Person(); @@ -68,6 +90,13 @@ public MultipartOutputResponse simple() { return response; } + @POST + @Path("/input") + @Consumes(MediaType.MULTIPART_FORM_DATA) + public String input(@MultipartForm MultipartInput input) { + return input.name + "-" + input.school.name; + } + } private static class MultipartOutputResponse { @@ -80,6 +109,16 @@ private static class MultipartOutputResponse { Person person; } + public static class MultipartInput { + + @RestForm + String name; + + @RestForm + @PartType(MediaType.APPLICATION_XML) + School school; + } + private static class Person { private String name; private Integer age; @@ -100,4 +139,16 @@ public void setAge(Integer age) { this.age = age; } } + + private static class School { + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/SimpleXmlTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/SimpleXmlTest.java index bca32019b44e9..675b19200347f 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/SimpleXmlTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jaxb/deployment/src/test/java/io/quarkus/resteasy/reactive/jaxb/deployment/test/SimpleXmlTest.java @@ -211,6 +211,7 @@ public void setMessage(String message) { public static class SimpleXmlResource { @GET + @Produces(MediaType.APPLICATION_XML) @Path("/person") public Person getPerson() { Person person = new Person();