Skip to content

Commit

Permalink
Allow to provide custom configuration for JAXB context
Browse files Browse the repository at this point in the history
==== 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<String, Object> 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.
====
  • Loading branch information
Sgitario authored and gastaldi committed Jun 11, 2022
1 parent 85913ab commit e812d24
Show file tree
Hide file tree
Showing 18 changed files with 703 additions and 28 deletions.
2 changes: 2 additions & 0 deletions docs/src/main/asciidoc/rest-json.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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<ObjectMapperCustomizer> customizers) {
ObjectMapper mapper = myObjectMapper(); // Custom `ObjectMapper`
Expand Down
87 changes: 87 additions & 0 deletions docs/src/main/asciidoc/resteasy-reactive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -1138,6 +1138,93 @@ Importing this module will allow HTTP message bodies to be read from XML
and serialised to XML, for <<resource-types,all the types not already registered with a more specific
serialisation>>.

==== 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<String, Object> 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]]
Expand Down
8 changes: 7 additions & 1 deletion extensions/jaxb/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-core-deployment</artifactId>
<artifactId>quarkus-arc-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
Expand All @@ -25,6 +25,12 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jaxb</artifactId>
</dependency>

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String> classes;

public JaxbClassesToBeBoundBuildItem(List<String> classes) {
this.classes = Objects.requireNonNull(classes);
}

public List<String> getClasses() {
return classes;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
Expand All @@ -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 {

Expand Down Expand Up @@ -174,8 +180,10 @@ void processAnnotationsAndIndexFiles(
BuildProducer<NativeImageResourceBuildItem> resource,
BuildProducer<NativeImageResourceBundleBuildItem> resourceBundle,
BuildProducer<RuntimeInitializedClassBuildItem> runtimeClasses,
ApplicationArchivesBuildItem applicationArchivesBuildItem) {
BuildProducer<JaxbClassesToBeBoundBuildItem> classesToBeBoundProducer,
ApplicationArchivesBuildItem applicationArchivesBuildItem) throws ClassNotFoundException {

List<String> classesToBeBound = new ArrayList<>();
IndexView index = combinedIndexBuildItem.getIndex();

// Register classes for reflection based on JAXB annotations
Expand All @@ -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;
}
}
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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
Expand All @@ -250,9 +264,9 @@ void ignoreWarnings(BuildProducer<ReflectiveHierarchyIgnoreWarningBuildItem> ign
@BuildStep
void registerClasses(
BuildProducer<NativeImageSystemPropertyBuildItem> nativeImageProps,
BuildProducer<ServiceProviderBuildItem> providerItem, final BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
final BuildProducer<NativeImageResourceBundleBuildItem> resourceBundle) {

BuildProducer<ServiceProviderBuildItem> providerItem,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
BuildProducer<NativeImageResourceBundleBuildItem> resourceBundle) {
addReflectiveClass(reflectiveClass, true, false, "com.sun.xml.bind.v2.ContextFactory");
addReflectiveClass(reflectiveClass, true, false, "com.sun.xml.internal.bind.v2.ContextFactory");

Expand All @@ -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<JaxbClassesToBeBoundBuildItem> classesToBeBoundBuildItems,
JaxbContextConfigRecorder jaxbContextConfig) {
for (JaxbClassesToBeBoundBuildItem classesToBeBoundBuildItem : classesToBeBoundBuildItems) {
jaxbContextConfig.addClassesToBeBound(classesToBeBoundBuildItem.getClasses());
}
}

@BuildStep
void registerProduces(BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
additionalBeans.produce(new AdditionalBeanBuildItem(JaxbContextProducer.class));
}

private void handleJaxbFile(Path p, BuildProducer<NativeImageResourceBuildItem> resource,
BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
List<String> classesToBeBound) {
try {
String path = p.toAbsolutePath().toString().substring(1);
String pkg = p.toAbsolutePath().getParent().toString().substring(1)
Expand All @@ -289,9 +318,10 @@ private void handleJaxbFile(Path p, BuildProducer<NativeImageResourceBuildItem>
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();
}
}
Expand All @@ -302,15 +332,16 @@ private void handleJaxbFile(Path p, BuildProducer<NativeImageResourceBuildItem>
}

private void iterateResources(ApplicationArchivesBuildItem applicationArchivesBuildItem, String path,
BuildProducer<NativeImageResourceBuildItem> resource, BuildProducer<ReflectiveClassBuildItem> reflectiveClass) {
BuildProducer<NativeImageResourceBuildItem> resource, BuildProducer<ReflectiveClassBuildItem> reflectiveClass,
List<String> classesToBeBound) {
for (ApplicationArchive archive : applicationArchivesBuildItem.getAllApplicationArchives()) {
archive.accept(tree -> {
var arch = tree.getPath(path);
if (arch != null && Files.isDirectory(arch)) {
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));
}
});
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
+ "<person>\n"
+ " <first>first</first>\n"
+ " <last>last</last>\n"
+ "</person>\n", sw.toString());
}

}
Loading

0 comments on commit e812d24

Please sign in to comment.