Skip to content

Commit

Permalink
Merge pull request #14207 from essobedo/3262/application-support-rest…
Browse files Browse the repository at this point in the history
…easy

Add partial support of JAX-RS Application in resteasy extension
  • Loading branch information
geoand authored Feb 19, 2021
2 parents fdb24a4 + 901d53f commit 93afc60
Show file tree
Hide file tree
Showing 7 changed files with 582 additions and 8 deletions.
6 changes: 6 additions & 0 deletions docs/src/main/asciidoc/rest-json.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,12 @@ If multiple JAX-RS `Application` classes are defined, the build will fail with t

If multiple JAX-RS applications are defined, the property `quarkus.resteasy.ignoreApplicationClasses=true` can be used to ignore all explicit `Application` classes. This makes all resource-classes available via the application-path as defined by `quarkus.resteasy.path` (default: `/`).

=== Support limitations of JAX-RS application

The RESTEasy extension doesn't support the method `getProperties()` of the class `javax.ws.rs.core.Application`. Moreover, it only relies on the methods `getClasses()` and `getSingletons()` to filter out the annotated resource, provider and feature classes.
It doesn't filter out the built-in resource, provider and feature classes and also the resource, provider and feature classes registered by the other extensions.
Finally the objects returned by the method `getSingletons()` are ignored, only the classes are took into account to filter out the resource, provider and feature classes, in other words the method `getSingletons()` is actually managed the same way as `getClasses()`.

=== Lifecycle of Resources

In Quarkus all JAX-RS resources are treated as CDI beans.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@ public final class JaxrsProvidersToRegisterBuildItem extends SimpleBuildItem {

private final Set<String> providers;
private final Set<String> contributedProviders;
private final Set<String> annotatedProviders;
private final boolean useBuiltIn;

public JaxrsProvidersToRegisterBuildItem(Set<String> providers, Set<String> contributedProviders, boolean useBuiltIn) {
public JaxrsProvidersToRegisterBuildItem(Set<String> providers, Set<String> contributedProviders,
Set<String> annotatedProviders, boolean useBuiltIn) {
this.providers = providers;
this.contributedProviders = contributedProviders;
this.annotatedProviders = annotatedProviders;
this.useBuiltIn = useBuiltIn;
}

Expand All @@ -24,6 +27,10 @@ public Set<String> getContributedProviders() {
return this.contributedProviders;
}

public Set<String> getAnnotatedProviders() {
return annotatedProviders;
}

public boolean useBuiltIn() {
return useBuiltIn;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,14 +164,15 @@ JaxrsProvidersToRegisterBuildItem setupProviders(BuildProducer<ReflectiveClassBu
contributedProviders.add(contributedProviderBuildItem.getName());
}

Set<String> annotatedProviders = new HashSet<>();
for (AnnotationInstance i : indexBuildItem.getIndex().getAnnotations(ResteasyDotNames.PROVIDER)) {
if (i.target().kind() == AnnotationTarget.Kind.CLASS) {
contributedProviders.add(i.target().asClass().name().toString());
annotatedProviders.add(i.target().asClass().name().toString());
}
checkProperConfigAccessInProvider(i);
checkProperConstructorInProvider(i);
}

contributedProviders.addAll(annotatedProviders);
Set<String> availableProviders = new HashSet<>(ServiceUtil.classNamesNamedIn(getClass().getClassLoader(),
"META-INF/services/" + Providers.class.getName()));
// this one is added manually in RESTEasy's ResteasyDeploymentImpl
Expand Down Expand Up @@ -238,7 +239,8 @@ JaxrsProvidersToRegisterBuildItem setupProviders(BuildProducer<ReflectiveClassBu
"org.jboss.resteasy.plugins.providers.jsonb.AbstractJsonBindingProvider"));
}

return new JaxrsProvidersToRegisterBuildItem(providersToRegister, contributedProviders, useBuiltinProviders);
return new JaxrsProvidersToRegisterBuildItem(
providersToRegister, contributedProviders, annotatedProviders, useBuiltinProviders);
}

private String mutinySupportNeeded(CombinedIndexBuildItem indexBuildItem) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

public final class ResteasyDotNames {

public static final DotName APPLICATION = DotName.createSimple("javax.ws.rs.core.Application");
public static final DotName CONSUMES = DotName.createSimple("javax.ws.rs.Consumes");
public static final DotName PRODUCES = DotName.createSimple("javax.ws.rs.Produces");
public static final DotName PROVIDER = DotName.createSimple("javax.ws.rs.ext.Provider");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
package io.quarkus.resteasy.reactive.server.test.simple;

import static io.restassured.RestAssured.when;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;

import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.container.*;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Feature;
import javax.ws.rs.core.FeatureContext;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.Provider;

import org.hamcrest.Matchers;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.jboss.shrinkwrap.api.spec.JavaArchive;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;

/**
* The integration test allowing to ensure that we can rely on {@link Application#getClasses()} to specify explicitly
* the classes to use for the application.
*/
class ApplicationTest {

@RegisterExtension
static QuarkusUnitTest runner = new QuarkusUnitTest()
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
.addClasses(
ResourceTest1.class, ResourceTest2.class, ResponseFilter1.class, ResponseFilter2.class,
ResponseFilter3.class, ResponseFilter4.class, ResponseFilter5.class, ResponseFilter6.class,
Feature1.class, Feature2.class, DynamicFeature1.class, DynamicFeature2.class,
ExceptionMapper1.class, ExceptionMapper2.class, AppTest.class));

@DisplayName("Should access to ok of resource 1 and provide a response with the expected headers")
@Test
void should_call_ok_of_resource_1() {
when()
.get("/rt-1/ok")
.then()
.header("X-RF-1", notNullValue())
.header("X-RF-2", nullValue())
.header("X-RF-3", notNullValue())
.header("X-RF-4", nullValue())
.header("X-RF-5", notNullValue())
.header("X-RF-6", nullValue())
.body(Matchers.is("ok1"));
}

@DisplayName("Should access to ko of resource 1 and call the expected exception mapper")
@Test
void should_call_ko_of_resource_1() {
when()
.get("/rt-1/ko")
.then()
.statusCode(Response.Status.SERVICE_UNAVAILABLE.getStatusCode());
}

@DisplayName("Should access to ok of resource 1 and provide a response with the expected headers")
@Test
void should_not_call_ok_of_resource_2() {
when()
.get("/rt-2/ok")
.then()
.statusCode(Response.Status.SERVICE_UNAVAILABLE.getStatusCode());
}

@Path("rt-1")
public static class ResourceTest1 {

@GET
@Path("ok")
public String ok() {
return "ok1";
}

@GET
@Path("ko")
public String ko() {
throw new UnsupportedOperationException();
}
}

@Path("rt-2")
public static class ResourceTest2 {

@GET
@Path("ok")
public String ok() {
return "ok2";
}
}

@Provider
public static class ResponseFilter1 implements ContainerResponseFilter {

@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
responseContext.getHeaders().add("X-RF-1", "Value");
}
}

@Provider
public static class ResponseFilter2 implements ContainerResponseFilter {

@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
responseContext.getHeaders().add("X-RF-2", "Value");
}
}

@Provider
public static class ResponseFilter3 implements ContainerResponseFilter {

@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
responseContext.getHeaders().add("X-RF-3", "Value");
}
}

@Provider
public static class ResponseFilter4 implements ContainerResponseFilter {

@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
responseContext.getHeaders().add("X-RF-4", "Value");
}
}

@Provider
public static class ResponseFilter5 implements ContainerResponseFilter {

@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
responseContext.getHeaders().add("X-RF-5", "Value");
}
}

@Provider
public static class ResponseFilter6 implements ContainerResponseFilter {

@Override
public void filter(ContainerRequestContext requestContext, ContainerResponseContext responseContext)
throws IOException {
responseContext.getHeaders().add("X-RF-6", "Value");
}
}

@Provider
public static class Feature1 implements Feature {

@Override
public boolean configure(FeatureContext context) {
context.register(ResponseFilter3.class);
return true;
}
}

@Provider
public static class Feature2 implements Feature {

@Override
public boolean configure(FeatureContext context) {
context.register(ResponseFilter4.class);
return true;
}
}

@Provider
public static class ExceptionMapper1 implements ExceptionMapper<RuntimeException> {

@Override
public Response toResponse(RuntimeException exception) {
return Response.status(Response.Status.SERVICE_UNAVAILABLE.getStatusCode()).build();
}
}

@Provider
public static class ExceptionMapper2 implements ExceptionMapper<UnsupportedOperationException> {

@Override
public Response toResponse(UnsupportedOperationException exception) {
return Response.status(Response.Status.NOT_IMPLEMENTED.getStatusCode()).build();
}
}

@Provider
public static class DynamicFeature1 implements DynamicFeature {

@Override
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
context.register(ResponseFilter5.class);
}
}

@Provider
public static class DynamicFeature2 implements DynamicFeature {

@Override
public void configure(ResourceInfo resourceInfo, FeatureContext context) {
context.register(ResponseFilter6.class);
}
}

public static class AppTest extends Application {

@Override
public Set<Class<?>> getClasses() {
return new HashSet<>(
Arrays.asList(
ResourceTest1.class, Feature1.class, ExceptionMapper1.class));
}

@Override
public Set<Object> getSingletons() {
return new HashSet<>(
Arrays.asList(
new ResponseFilter1(), new DynamicFeature1()));
}
}
}
Loading

0 comments on commit 93afc60

Please sign in to comment.