Skip to content

Commit 5e39264

Browse files
committed
Validate the default JAXBContext at build time only if it is really used
in the application; do not validate if user provides his own JAXBContext bean or if there is no JAXBContext injection point #31646
1 parent 885291b commit 5e39264

File tree

11 files changed

+407
-60
lines changed

11 files changed

+407
-60
lines changed

extensions/jaxb/deployment/pom.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,12 @@
2828

2929
<dependency>
3030
<groupId>io.quarkus</groupId>
31-
<artifactId>quarkus-junit5</artifactId>
31+
<artifactId>quarkus-junit5-internal</artifactId>
32+
<scope>test</scope>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.assertj</groupId>
36+
<artifactId>assertj-core</artifactId>
3237
<scope>test</scope>
3338
</dependency>
3439
</dependencies>
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package io.quarkus.jaxb.deployment;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collection;
5+
import java.util.LinkedHashSet;
6+
import java.util.List;
7+
import java.util.Set;
8+
import java.util.stream.Collectors;
9+
10+
import io.quarkus.builder.item.SimpleBuildItem;
11+
import io.quarkus.jaxb.deployment.utils.JaxbType;
12+
13+
/**
14+
* List of classes to be bound in the JAXB context. Aggregates all classes passed via
15+
* {@link JaxbClassesToBeBoundBuildItem}. All class names excluded via {@code quarkus.jaxb.exclude-classes} are not
16+
* present in this list.
17+
*/
18+
public final class FilteredJaxbClassesToBeBoundBuildItem extends SimpleBuildItem {
19+
20+
private final List<Class<?>> classes;
21+
22+
public static Builder builder() {
23+
return new Builder();
24+
}
25+
26+
private FilteredJaxbClassesToBeBoundBuildItem(List<Class<?>> classes) {
27+
this.classes = classes;
28+
}
29+
30+
public List<Class<?>> getClasses() {
31+
return new ArrayList<>(classes);
32+
}
33+
34+
public static class Builder {
35+
private final Set<String> classNames = new LinkedHashSet<>();
36+
private final Set<String> classNameExcludes = new LinkedHashSet<>();
37+
38+
public Builder classNameExcludes(Collection<String> classNameExcludes) {
39+
for (String className : classNameExcludes) {
40+
this.classNameExcludes.add(className);
41+
}
42+
return this;
43+
}
44+
45+
public Builder classNames(Collection<String> classNames) {
46+
for (String className : classNames) {
47+
this.classNames.add(className);
48+
}
49+
return this;
50+
}
51+
52+
public FilteredJaxbClassesToBeBoundBuildItem build() {
53+
final List<Class<?>> classes = classNames.stream()
54+
.filter(className -> !this.classNameExcludes.contains(className))
55+
.map(FilteredJaxbClassesToBeBoundBuildItem::getClassByName)
56+
.filter(JaxbType::isValidType)
57+
.collect(Collectors.toList());
58+
59+
return new FilteredJaxbClassesToBeBoundBuildItem(classes);
60+
}
61+
}
62+
63+
private static Class<?> getClassByName(String name) {
64+
try {
65+
return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
66+
} catch (ClassNotFoundException e) {
67+
throw new RuntimeException(e);
68+
}
69+
}
70+
}

extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbClassesToBeBoundBuildItem.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,24 @@
11
package io.quarkus.jaxb.deployment;
22

3+
import java.util.ArrayList;
4+
import java.util.Collections;
35
import java.util.List;
46
import java.util.Objects;
57

68
import io.quarkus.builder.item.MultiBuildItem;
79

810
/**
9-
* List of classes to be bound in the JAXB context.
11+
* List of class names to be bound in the JAXB context. Note that some of the class names can be removed via
12+
* {@code quarkus.jaxb.exclude-classes}.
13+
*
14+
* @see FilteredJaxbClassesToBeBoundBuildItem
1015
*/
1116
public final class JaxbClassesToBeBoundBuildItem extends MultiBuildItem {
1217

1318
private final List<String> classes;
1419

1520
public JaxbClassesToBeBoundBuildItem(List<String> classes) {
16-
this.classes = Objects.requireNonNull(classes);
21+
this.classes = Objects.requireNonNull(Collections.unmodifiableList(new ArrayList<>(classes)));
1722
}
1823

1924
public List<String> getClasses() {

extensions/jaxb/deployment/src/main/java/io/quarkus/jaxb/deployment/JaxbProcessor.java

Lines changed: 53 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,16 @@
11
package io.quarkus.jaxb.deployment;
22

3-
import static io.quarkus.jaxb.deployment.utils.JaxbType.isValidType;
4-
53
import java.io.IOError;
64
import java.io.IOException;
75
import java.lang.annotation.Annotation;
86
import java.nio.file.Files;
97
import java.nio.file.Path;
108
import java.util.ArrayList;
11-
import java.util.Collection;
12-
import java.util.HashSet;
139
import java.util.List;
1410
import java.util.Set;
1511
import java.util.stream.Stream;
1612

13+
import jakarta.enterprise.inject.spi.DeploymentException;
1714
import jakarta.xml.bind.JAXBContext;
1815
import jakarta.xml.bind.JAXBException;
1916
import jakarta.xml.bind.annotation.XmlAccessOrder;
@@ -58,6 +55,9 @@
5855
import org.jboss.logging.Logger;
5956

6057
import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
58+
import io.quarkus.arc.deployment.SynthesisFinishedBuildItem;
59+
import io.quarkus.arc.processor.BeanInfo;
60+
import io.quarkus.arc.processor.BeanResolver;
6161
import io.quarkus.deployment.ApplicationArchive;
6262
import io.quarkus.deployment.annotations.BuildProducer;
6363
import io.quarkus.deployment.annotations.BuildStep;
@@ -297,29 +297,62 @@ void registerClasses(
297297
}
298298

299299
@BuildStep
300-
@Record(ExecutionTime.STATIC_INIT)
301-
void setupJaxbContextConfig(JaxbConfig config,
302-
List<JaxbClassesToBeBoundBuildItem> classesToBeBoundBuildItems,
303-
JaxbContextConfigRecorder jaxbContextConfig) {
304-
Set<String> classNamesToBeBound = new HashSet<>();
305-
for (JaxbClassesToBeBoundBuildItem classesToBeBoundBuildItem : classesToBeBoundBuildItems) {
306-
classNamesToBeBound.addAll(classesToBeBoundBuildItem.getClasses());
307-
}
300+
FilteredJaxbClassesToBeBoundBuildItem filterBoundClasses(
301+
JaxbConfig config,
302+
List<JaxbClassesToBeBoundBuildItem> classesToBeBoundBuildItems) {
303+
304+
FilteredJaxbClassesToBeBoundBuildItem.Builder builder = FilteredJaxbClassesToBeBoundBuildItem.builder();
305+
classesToBeBoundBuildItems.stream()
306+
.map(JaxbClassesToBeBoundBuildItem::getClasses)
307+
.forEach(builder::classNames);
308308

309309
// remove classes that have been excluded by users
310310
if (config.excludeClasses.isPresent()) {
311-
classNamesToBeBound.removeAll(config.excludeClasses.get());
311+
builder.classNameExcludes(config.excludeClasses.get());
312312
}
313+
return builder.build();
314+
}
315+
316+
@BuildStep
317+
@Record(ExecutionTime.STATIC_INIT)
318+
void setupJaxbContextConfig(
319+
FilteredJaxbClassesToBeBoundBuildItem filteredClassesToBeBound,
320+
JaxbContextConfigRecorder jaxbContextConfig) {
321+
jaxbContextConfig.addClassesToBeBound(filteredClassesToBeBound.getClasses());
322+
}
323+
324+
@BuildStep
325+
@Record(ExecutionTime.STATIC_INIT)
326+
void validateDefaultJaxbContext(
327+
JaxbConfig config,
328+
FilteredJaxbClassesToBeBoundBuildItem filteredClassesToBeBound,
329+
SynthesisFinishedBuildItem beanContainerState,
330+
JaxbContextConfigRecorder jaxbContextConfig /* Force the build time container to invoke this method */) {
313331

314-
// parse class names to class
315-
Set<Class<?>> classes = getAllClassesFromClassNames(classNamesToBeBound);
316332
if (config.validateJaxbContext) {
317-
// validate the context to fail at build time if it's not valid
318-
validateContext(classes);
333+
final BeanResolver beanResolver = beanContainerState.getBeanResolver();
334+
final Set<BeanInfo> beans = beanResolver
335+
.resolveBeans(Type.create(DotName.createSimple(JAXBContext.class), org.jboss.jandex.Type.Kind.CLASS));
336+
if (!beans.isEmpty()) {
337+
final BeanInfo bean = beanResolver.resolveAmbiguity(beans);
338+
if (bean.isDefaultBean()) {
339+
/*
340+
* Validate the default JAXB context at build time and fail early.
341+
* Do this only if the user application actually requires the default JAXBContext bean
342+
*/
343+
try {
344+
JAXBContext.newInstance(filteredClassesToBeBound.getClasses().toArray(new Class[0]));
345+
} catch (JAXBException e) {
346+
/*
347+
* Producing a ValidationErrorBuildItem would perhaps be more natural here,
348+
* but doing so causes a cycle between this and reactive JAXB extension
349+
* Throwing from here works well too
350+
*/
351+
throw new DeploymentException("Failed to create or validate the default JAXBContext", e);
352+
}
353+
}
354+
}
319355
}
320-
321-
// register the classes to be used at runtime
322-
jaxbContextConfig.addClassesToBeBound(classes);
323356
}
324357

325358
@BuildStep
@@ -388,31 +421,4 @@ private void addResourceBundle(BuildProducer<NativeImageResourceBundleBuildItem>
388421
resourceBundle.produce(new NativeImageResourceBundleBuildItem(bundle));
389422
}
390423

391-
private void validateContext(Set<Class<?>> classes) {
392-
try {
393-
JAXBContext.newInstance(classes.toArray(new Class[0]));
394-
} catch (JAXBException e) {
395-
throw new IllegalStateException("Failed to configure JAXB context", e);
396-
}
397-
}
398-
399-
private Set<Class<?>> getAllClassesFromClassNames(Collection<String> classNames) {
400-
Set<Class<?>> classes = new HashSet<>();
401-
for (String className : classNames) {
402-
Class<?> clazz = getClassByName(className);
403-
if (isValidType(clazz)) {
404-
classes.add(clazz);
405-
}
406-
}
407-
408-
return classes;
409-
}
410-
411-
private Class<?> getClassByName(String name) {
412-
try {
413-
return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
414-
} catch (ClassNotFoundException e) {
415-
throw new RuntimeException(e);
416-
}
417-
}
418424
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package io.quarkus.jaxb.deployment;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.List;
6+
7+
import jakarta.enterprise.context.control.ActivateRequestContext;
8+
import jakarta.enterprise.inject.spi.DeploymentException;
9+
import jakarta.inject.Inject;
10+
import jakarta.xml.bind.JAXBContext;
11+
import jakarta.xml.bind.Marshaller;
12+
13+
import org.assertj.core.api.Assertions;
14+
import org.glassfish.jaxb.core.v2.runtime.IllegalAnnotationException;
15+
import org.glassfish.jaxb.runtime.v2.runtime.IllegalAnnotationsException;
16+
import org.junit.jupiter.api.Test;
17+
import org.junit.jupiter.api.extension.RegisterExtension;
18+
19+
import io.quarkus.test.QuarkusUnitTest;
20+
21+
/**
22+
* Make sure that the validation of the default JAXB context fails if there conflicting model classes and there is only
23+
* a {@link Marshaller} injection point (which actually requires a {@link JAXBContext} bean to be available too).
24+
*/
25+
public class ConflictingModelClassesMarshalerOnlyTest {
26+
27+
@RegisterExtension
28+
static final QuarkusUnitTest config = new QuarkusUnitTest()
29+
.withApplicationRoot((jar) -> jar
30+
.addClasses(
31+
io.quarkus.jaxb.deployment.one.Model.class,
32+
io.quarkus.jaxb.deployment.two.Model.class))
33+
.assertException(e -> {
34+
assertThat(e).isInstanceOf(DeploymentException.class);
35+
assertThat(e.getMessage()).isEqualTo("Failed to create or validate the default JAXBContext");
36+
Throwable cause = e.getCause();
37+
assertThat(cause).isInstanceOf(IllegalAnnotationsException.class);
38+
assertThat(cause.getMessage()).isEqualTo("1 counts of IllegalAnnotationExceptions");
39+
List<IllegalAnnotationException> errors = ((IllegalAnnotationsException) cause).getErrors();
40+
assertThat(errors.size()).isEqualTo(1);
41+
assertThat(errors.get(0).getMessage()).contains("Two classes have the same XML type name \"model\"");
42+
43+
});
44+
45+
@Inject
46+
Marshaller marshaller;
47+
48+
@Test
49+
@ActivateRequestContext
50+
public void shouldFail() {
51+
Assertions.fail("The application should fail at boot");
52+
}
53+
54+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.quarkus.jaxb.deployment;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
5+
import java.util.List;
6+
7+
import jakarta.enterprise.context.control.ActivateRequestContext;
8+
import jakarta.enterprise.inject.spi.DeploymentException;
9+
import jakarta.inject.Inject;
10+
import jakarta.xml.bind.JAXBContext;
11+
12+
import org.assertj.core.api.Assertions;
13+
import org.glassfish.jaxb.core.v2.runtime.IllegalAnnotationException;
14+
import org.glassfish.jaxb.runtime.v2.runtime.IllegalAnnotationsException;
15+
import org.junit.jupiter.api.Test;
16+
import org.junit.jupiter.api.extension.RegisterExtension;
17+
18+
import io.quarkus.test.QuarkusUnitTest;
19+
20+
/**
21+
* Make sure that the validation of the default JAXB context fails if there conflicting model classes and there actually
22+
* is a {@link JAXBContext} injection point.
23+
*/
24+
public class ConflictingModelClassesTest {
25+
26+
@RegisterExtension
27+
static final QuarkusUnitTest config = new QuarkusUnitTest()
28+
.withApplicationRoot((jar) -> jar
29+
.addClasses(
30+
io.quarkus.jaxb.deployment.one.Model.class,
31+
io.quarkus.jaxb.deployment.two.Model.class))
32+
.assertException(e -> {
33+
assertThat(e).isInstanceOf(DeploymentException.class);
34+
assertThat(e.getMessage()).isEqualTo("Failed to create or validate the default JAXBContext");
35+
Throwable cause = e.getCause();
36+
assertThat(cause).isInstanceOf(IllegalAnnotationsException.class);
37+
assertThat(cause.getMessage()).isEqualTo("1 counts of IllegalAnnotationExceptions");
38+
List<IllegalAnnotationException> errors = ((IllegalAnnotationsException) cause).getErrors();
39+
assertThat(errors.size()).isEqualTo(1);
40+
assertThat(errors.get(0).getMessage()).contains("Two classes have the same XML type name \"model\"");
41+
42+
});
43+
44+
@Inject
45+
JAXBContext jaxbContext;
46+
47+
@Test
48+
@ActivateRequestContext
49+
public void shouldFail() {
50+
Assertions.fail("The application should fail at boot");
51+
}
52+
53+
}

0 commit comments

Comments
 (0)