Skip to content

Commit

Permalink
Validate the default JAXBContext at build time only if it is really u…
Browse files Browse the repository at this point in the history
…sed in the application; do not validate if user provides his own JAXBContext bean or if there is no JAXBContext injection point #4192
  • Loading branch information
ppalaga committed Mar 7, 2023
1 parent 58d9999 commit ba6808f
Show file tree
Hide file tree
Showing 10 changed files with 345 additions and 60 deletions.
7 changes: 6 additions & 1 deletion extensions/jaxb/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,12 @@

<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<artifactId>quarkus-junit5-internal</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package io.quarkus.jaxb.deployment;

import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;

import io.quarkus.builder.item.SimpleBuildItem;
import io.quarkus.jaxb.deployment.utils.JaxbType;

/**
* List of classes to be bound in the JAXB context. Aggregates all classes passed via
* {@link JaxbClassesToBeBoundBuildItem}. All class names excluded via {@code quarkus.jaxb.exclude-classes} are not
* present in this list.
*/
public final class FilteredJaxbClassesToBeBoundBuildItem extends SimpleBuildItem {

private final List<Class<?>> classes;

public static Builder builder() {
return new Builder();
}

private FilteredJaxbClassesToBeBoundBuildItem(List<Class<?>> classes) {
this.classes = classes;
}

public List<Class<?>> getClasses() {
return new ArrayList<>(classes);
}

public static class Builder {
private final Set<String> classNames = new LinkedHashSet<>();
private final Set<String> classNameExcludes = new LinkedHashSet<>();

public Builder classNameExcludes(Collection<String> classNameExcludes) {
for (String className : classNameExcludes) {
this.classNameExcludes.add(className);
}
return this;
}

public Builder classNames(Collection<String> classNames) {
for (String className : classNames) {
this.classNames.add(className);
}
return this;
}

public FilteredJaxbClassesToBeBoundBuildItem build() {
final List<Class<?>> classes = classNames.stream()
.filter(className -> !this.classNameExcludes.contains(className))
.map(FilteredJaxbClassesToBeBoundBuildItem::getClassByName)
.filter(JaxbType::isValidType)
.collect(Collectors.toList());

return new FilteredJaxbClassesToBeBoundBuildItem(classes);
}
}

private static Class<?> getClassByName(String name) {
try {
return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
package io.quarkus.jaxb.deployment;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import io.quarkus.builder.item.MultiBuildItem;

/**
* List of classes to be bound in the JAXB context.
* List of class names to be bound in the JAXB context. Note that some of the class names can be removed via
* {@code quarkus.jaxb.exclude-classes}.
*
* @see FilteredJaxbClassesToBeBoundBuildItem
*/
public final class JaxbClassesToBeBoundBuildItem extends MultiBuildItem {

private final List<String> classes;

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

public List<String> getClasses() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,11 @@
package io.quarkus.jaxb.deployment;

import static io.quarkus.jaxb.deployment.utils.JaxbType.isValidType;

import java.io.IOError;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
Expand Down Expand Up @@ -58,6 +54,10 @@
import org.jboss.logging.Logger;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.SynthesisFinishedBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.arc.processor.BeanResolver;
import io.quarkus.deployment.ApplicationArchive;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
Expand Down Expand Up @@ -297,29 +297,55 @@ void registerClasses(
}

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void setupJaxbContextConfig(JaxbConfig config,
List<JaxbClassesToBeBoundBuildItem> classesToBeBoundBuildItems,
JaxbContextConfigRecorder jaxbContextConfig) {
Set<String> classNamesToBeBound = new HashSet<>();
for (JaxbClassesToBeBoundBuildItem classesToBeBoundBuildItem : classesToBeBoundBuildItems) {
classNamesToBeBound.addAll(classesToBeBoundBuildItem.getClasses());
}
FilteredJaxbClassesToBeBoundBuildItem filterBoundClasses(
JaxbConfig config,
List<JaxbClassesToBeBoundBuildItem> classesToBeBoundBuildItems) {

FilteredJaxbClassesToBeBoundBuildItem.Builder builder = FilteredJaxbClassesToBeBoundBuildItem.builder();
classesToBeBoundBuildItems.stream()
.map(JaxbClassesToBeBoundBuildItem::getClasses)
.forEach(builder::classNames);

// remove classes that have been excluded by users
if (config.excludeClasses.isPresent()) {
classNamesToBeBound.removeAll(config.excludeClasses.get());
builder.classNameExcludes(config.excludeClasses.get());
}
return builder.build();
}

@BuildStep
@Record(ExecutionTime.STATIC_INIT)
void setupJaxbContextConfig(
FilteredJaxbClassesToBeBoundBuildItem filteredClassesToBeBound,
JaxbContextConfigRecorder jaxbContextConfig) {
jaxbContextConfig.addClassesToBeBound(filteredClassesToBeBound.getClasses());
}

// parse class names to class
Set<Class<?>> classes = getAllClassesFromClassNames(classNamesToBeBound);
if (config.validateJaxbContext) {
// validate the context to fail at build time if it's not valid
validateContext(classes);
@BuildStep
void validateDefaultJaxbContext(
JaxbConfig config,
FilteredJaxbClassesToBeBoundBuildItem filteredClassesToBeBound,
SynthesisFinishedBuildItem beanContainerState,
BuildProducer<ValidationPhaseBuildItem.ValidationErrorBuildItem> errors) {

final BeanResolver beanResolver = beanContainerState.getBeanResolver();
final Set<BeanInfo> beans = beanResolver
.resolveBeans(Type.create(DotName.createSimple(JAXBContext.class), org.jboss.jandex.Type.Kind.CLASS));
if (config.validateJaxbContext && !beans.isEmpty()) {
final BeanInfo bean = beanResolver.resolveAmbiguity(beans);
if (bean.isDefaultBean()) {
/*
* Validate the default JAXB context at build time and fail early.
* Do this only if the user application actually requires the default JAXBContext bean
*/
try {
JAXBContext.newInstance(filteredClassesToBeBound.getClasses().toArray(new Class[0]));
} catch (JAXBException e) {
throw new IllegalStateException("Failed to configure JAXB context", e);
}
}
}

// register the classes to be used at runtime
jaxbContextConfig.addClassesToBeBound(classes);
}

@BuildStep
Expand Down Expand Up @@ -388,31 +414,4 @@ private void addResourceBundle(BuildProducer<NativeImageResourceBundleBuildItem>
resourceBundle.produce(new NativeImageResourceBundleBuildItem(bundle));
}

private void validateContext(Set<Class<?>> classes) {
try {
JAXBContext.newInstance(classes.toArray(new Class[0]));
} catch (JAXBException e) {
throw new IllegalStateException("Failed to configure JAXB context", e);
}
}

private Set<Class<?>> getAllClassesFromClassNames(Collection<String> classNames) {
Set<Class<?>> classes = new HashSet<>();
for (String className : classNames) {
Class<?> clazz = getClassByName(className);
if (isValidType(clazz)) {
classes.add(clazz);
}
}

return classes;
}

private Class<?> getClassByName(String name) {
try {
return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.quarkus.jaxb.deployment;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;

import jakarta.enterprise.context.control.ActivateRequestContext;
import jakarta.inject.Inject;
import jakarta.xml.bind.JAXBContext;

import org.assertj.core.api.Assertions;
import org.glassfish.jaxb.core.v2.runtime.IllegalAnnotationException;
import org.glassfish.jaxb.runtime.v2.runtime.IllegalAnnotationsException;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

/**
* Make sure that the validation of the default JAXB context fails if there conflicting model classes and there actually
* is a JAXBContext injection point.
*/
public class ConflictingModelClassesTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(
io.quarkus.jaxb.deployment.one.Model.class,
io.quarkus.jaxb.deployment.two.Model.class))
.assertException(e -> {
assertThat(e).isInstanceOf(IllegalStateException.class);
assertThat(e.getMessage()).isEqualTo("Failed to configure JAXB context");
Throwable cause = e.getCause();
assertThat(cause).isInstanceOf(IllegalAnnotationsException.class);
assertThat(cause.getMessage()).isEqualTo("1 counts of IllegalAnnotationExceptions");
List<IllegalAnnotationException> errors = ((IllegalAnnotationsException) cause).getErrors();
assertThat(errors.size()).isEqualTo(1);
assertThat(errors.get(0).getMessage()).contains("Two classes have the same XML type name \"model\"");

});

@Inject
JAXBContext jaxbContext;

@Test
@ActivateRequestContext
public void shouldFail() {
Assertions.fail("The application should fail at boot");
}

}
Original file line number Diff line number Diff line change
@@ -1,23 +1,39 @@
package io.quarkus.jaxb.deployment;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.assertj.core.api.Assertions.assertThat;

import java.io.StringWriter;

import jakarta.enterprise.context.control.ActivateRequestContext;
import jakarta.inject.Inject;
import jakarta.xml.bind.JAXBContext;
import jakarta.xml.bind.JAXBException;
import jakarta.xml.bind.Marshaller;
import jakarta.xml.bind.Unmarshaller;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.QuarkusUnitTest;

@QuarkusTest
/**
* Make sure that the default JAXBContext passes the validation thanks to
* {@code quarkus.jaxb.exclude-classes=io.quarkus.jaxb.deployment.two.Model} even though there are conflicting classes
* in the application.
*/
public class InjectJaxbContextTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(
io.quarkus.jaxb.deployment.one.Model.class,
io.quarkus.jaxb.deployment.two.Model.class,
Person.class,
CustomJaxbContextCustomizer.class)
.addPackage("io.quarkus.jaxb.deployment.info"))
.overrideConfigKey("quarkus.jaxb.exclude-classes", "io.quarkus.jaxb.deployment.two.Model");

@Inject
JAXBContext jaxbContext;

Expand All @@ -28,13 +44,23 @@ public class InjectJaxbContextTest {
Unmarshaller unmarshaller;

@Test
@ActivateRequestContext
public void shouldInjectJaxbBeans() {
assertNotNull(jaxbContext);
assertNotNull(marshaller);
assertNotNull(unmarshaller);
assertThat(jaxbContext).isNotNull();
assertThat(marshaller).isNotNull();
assertThat(unmarshaller).isNotNull();
}

@Test
@ActivateRequestContext
public void packageInfoLoaded() {
/* make sure the package-info.class is present in the test archive */
assertThat(io.quarkus.jaxb.deployment.info.Foo.class.getPackage()
.getAnnotation(jakarta.xml.bind.annotation.XmlSchema.class)).isNotNull();
}

@Test
@ActivateRequestContext
public void shouldPersonBeInTheJaxbContext() throws JAXBException {
Person person = new Person();
person.setFirst("first");
Expand All @@ -43,11 +69,11 @@ public void shouldPersonBeInTheJaxbContext() throws JAXBException {
StringWriter sw = new StringWriter();
marshaller.marshal(person, sw);

assertEquals("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
assertThat(sw.toString()).isEqualTo("<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>\n"
+ "<person>\n"
+ " <first>first</first>\n"
+ " <last>last</last>\n"
+ "</person>\n", sw.toString());
+ "</person>\n");
}

}
Loading

0 comments on commit ba6808f

Please sign in to comment.