From cff0be1d605d1af49c41a4b0ff38e6b3ad70c00f Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Fri, 16 Sep 2022 08:52:00 +0300 Subject: [PATCH] Detect MessageBodyReader/Writer from META-INF/services/javax.ws.rs.ext.Providers Resolves: #27970 --- .../deployment/ResteasyReactiveProcessor.java | 85 +++++++++++++++++++ ...estCase.java => SharedNormalTestCase.java} | 2 +- ...stCase.java => SharedProfileTestCase.java} | 4 +- .../resteasy-reactive-kotlin/standard/pom.xml | 4 + .../reactive/kotlin/SharedResource.kt | 16 ++++ .../reactive/kotlin/SharedResourceTest.kt | 26 ++++++ .../java/io/quarkus/it/shared/Shared.java | 14 +++ .../io/quarkus/it/shared/SharedProvider.java | 44 ++++++++++ .../services/javax.ws.rs.ext.Providers | 1 + 9 files changed, 193 insertions(+), 3 deletions(-) rename integration-tests/main/src/test/java/io/quarkus/it/main/{GreetingNormalTestCase.java => SharedNormalTestCase.java} (91%) rename integration-tests/main/src/test/java/io/quarkus/it/main/{GreetingProfileTestCase.java => SharedProfileTestCase.java} (97%) create mode 100644 integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SharedResource.kt create mode 100644 integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SharedResourceTest.kt create mode 100644 integration-tests/shared-library/src/main/java/io/quarkus/it/shared/Shared.java create mode 100644 integration-tests/shared-library/src/main/java/io/quarkus/it/shared/SharedProvider.java create mode 100644 integration-tests/shared-library/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java index 531d8a8fded23..fdef1620338ee 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ResteasyReactiveProcessor.java @@ -7,6 +7,7 @@ import static org.jboss.resteasy.reactive.common.processor.ResteasyReactiveDotNames.DATE_FORMAT; import java.io.File; +import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Arrays; @@ -29,11 +30,15 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import javax.ws.rs.Consumes; import javax.ws.rs.Priorities; +import javax.ws.rs.Produces; import javax.ws.rs.RuntimeType; import javax.ws.rs.core.Application; import javax.ws.rs.core.MediaType; +import javax.ws.rs.ext.MessageBodyReader; import javax.ws.rs.ext.MessageBodyWriter; +import javax.ws.rs.ext.Providers; import org.eclipse.microprofile.config.Config; import org.eclipse.microprofile.config.ConfigProvider; @@ -68,6 +73,7 @@ import org.jboss.resteasy.reactive.common.processor.transformation.AnnotationsTransformer; import org.jboss.resteasy.reactive.common.types.AllWriteableMarker; import org.jboss.resteasy.reactive.common.util.Encode; +import org.jboss.resteasy.reactive.common.util.types.Types; import org.jboss.resteasy.reactive.server.core.Deployment; import org.jboss.resteasy.reactive.server.core.DeploymentInfo; import org.jboss.resteasy.reactive.server.core.ExceptionMapping; @@ -93,6 +99,7 @@ import org.jboss.resteasy.reactive.server.vertx.serializers.ServerVertxAsyncFileMessageBodyWriter; import org.jboss.resteasy.reactive.server.vertx.serializers.ServerVertxBufferMessageBodyWriter; import org.jboss.resteasy.reactive.spi.BeanFactory; +import org.jetbrains.annotations.Nullable; import org.objectweb.asm.ClassVisitor; import io.quarkus.arc.Unremovable; @@ -125,6 +132,7 @@ import io.quarkus.deployment.configuration.ConfigurationError; import io.quarkus.deployment.pkg.builditem.CompiledJavaVersionBuildItem; import io.quarkus.deployment.recording.RecorderContext; +import io.quarkus.deployment.util.ServiceUtil; import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.Gizmo; import io.quarkus.gizmo.MethodCreator; @@ -675,6 +683,83 @@ private void handleDateFormatReflection(BuildProducer } } + /** + * RESTEasy Classic also includes the providers that are set in the 'META-INF/services/javax.ws.rs.ext.Providers' file + * This is not a ServiceLoader call, but essentially provides the same functionality. + */ + @BuildStep + public void providersFromClasspath(BuildProducer messageBodyReaderProducer, + BuildProducer messageBodyWriterProducer) { + String fileName = "META-INF/services/" + Providers.class.getName(); + try { + Set detectedProviders = new HashSet<>(ServiceUtil.classNamesNamedIn(getClass().getClassLoader(), + fileName)); + for (String providerClassName : detectedProviders) { + try { + Class providerClass = Class.forName(providerClassName, false, + Thread.currentThread().getContextClassLoader()); + if (MessageBodyReader.class.isAssignableFrom(providerClass)) { + String handledClassName = determineHandledGenericTypeOfProviderInterface(providerClass, + MessageBodyReader.class); + if (handledClassName == null) { + log.warn("Unable to determine which type MessageBodyReader '" + providerClass.getName() + + "' handles so this Provider will be ignored"); + continue; + } + MessageBodyReaderBuildItem.Builder builder = new MessageBodyReaderBuildItem.Builder( + providerClassName, handledClassName); + Consumes consumes = providerClass.getAnnotation(Consumes.class); + if (consumes != null) { + builder.setMediaTypeStrings(Arrays.asList(consumes.value())); + } else { + builder.setMediaTypeStrings(Collections.singletonList(MediaType.WILDCARD_TYPE.toString())); + } + messageBodyReaderProducer.produce(builder.build()); // TODO: does it make sense to limit these to the Server? + } + if (MessageBodyWriter.class.isAssignableFrom(providerClass)) { + String handledClassName = determineHandledGenericTypeOfProviderInterface(providerClass, + MessageBodyWriter.class); + if (handledClassName == null) { + log.warn("Unable to determine which type MessageBodyWriter '" + providerClass.getName() + + "' handles so this Provider will be ignored"); + continue; + } + MessageBodyWriterBuildItem.Builder builder = new MessageBodyWriterBuildItem.Builder( + providerClassName, handledClassName); + Produces produces = providerClass.getAnnotation(Produces.class); + if (produces != null) { + builder.setMediaTypeStrings(Arrays.asList(produces.value())); + } else { + builder.setMediaTypeStrings(Collections.singletonList(MediaType.WILDCARD_TYPE.toString())); + } + messageBodyWriterProducer.produce(builder.build()); // TODO: does it make sense to limit these to the Server? + } + // TODO: handle other providers as well + } catch (ClassNotFoundException e) { + log.warn("Unable to load class '" + providerClassName + + "' when trying to determine what kind of JAX-RS Provider it is.", e); + } + } + } catch (IOException e) { + log.warn("Unable to properly detect and parse the contents of '" + fileName + "'", e); + } + } + + @Nullable + private static String determineHandledGenericTypeOfProviderInterface(Class providerClass, + Class targetProviderInterface) { + + java.lang.reflect.Type[] types = Types.findParameterizedTypes(providerClass, targetProviderInterface); + if ((types == null) || (types.length != 1)) { + return null; + } + try { + return Types.getRawType(types[0]).getName(); + } catch (Exception ignored) { + return null; + } + } + @BuildStep @Record(value = ExecutionTime.STATIC_INIT, useIdentityComparisonForParameters = false) public void serverSerializers(ResteasyReactiveRecorder recorder, diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingNormalTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/SharedNormalTestCase.java similarity index 91% rename from integration-tests/main/src/test/java/io/quarkus/it/main/GreetingNormalTestCase.java rename to integration-tests/main/src/test/java/io/quarkus/it/main/SharedNormalTestCase.java index 71caa9a3cac60..8e3faee33eaff 100644 --- a/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingNormalTestCase.java +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/SharedNormalTestCase.java @@ -8,7 +8,7 @@ import io.restassured.RestAssured; @QuarkusTest -public class GreetingNormalTestCase { +public class SharedNormalTestCase { @Test public void included() { diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingProfileTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/SharedProfileTestCase.java similarity index 97% rename from integration-tests/main/src/test/java/io/quarkus/it/main/GreetingProfileTestCase.java rename to integration-tests/main/src/test/java/io/quarkus/it/main/SharedProfileTestCase.java index 7557d730c224c..aaa514b500a49 100644 --- a/integration-tests/main/src/test/java/io/quarkus/it/main/GreetingProfileTestCase.java +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/SharedProfileTestCase.java @@ -22,8 +22,8 @@ * Tests that QuarkusTestProfile works as expected */ @QuarkusTest -@TestProfile(GreetingProfileTestCase.MyProfile.class) -public class GreetingProfileTestCase { +@TestProfile(SharedProfileTestCase.MyProfile.class) +public class SharedProfileTestCase { @Test public void included() { diff --git a/integration-tests/resteasy-reactive-kotlin/standard/pom.xml b/integration-tests/resteasy-reactive-kotlin/standard/pom.xml index ffe3ee5471bc3..02a465d5f3373 100644 --- a/integration-tests/resteasy-reactive-kotlin/standard/pom.xml +++ b/integration-tests/resteasy-reactive-kotlin/standard/pom.xml @@ -39,6 +39,10 @@ io.quarkus quarkus-kotlin + + io.quarkus + quarkus-integration-test-shared-library + io.quarkus diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SharedResource.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SharedResource.kt new file mode 100644 index 0000000000000..3fd83d25abd4d --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/main/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SharedResource.kt @@ -0,0 +1,16 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import io.quarkus.it.shared.Shared +import javax.ws.rs.Consumes +import javax.ws.rs.POST +import javax.ws.rs.Path +import javax.ws.rs.Produces + +@Path("/shared") +class SharedResource { + + @Consumes("application/json") + @Produces("application/json") + @POST + fun returnAsIs(shared: Shared) = shared +} diff --git a/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SharedResourceTest.kt b/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SharedResourceTest.kt new file mode 100644 index 0000000000000..d190a9cf725a1 --- /dev/null +++ b/integration-tests/resteasy-reactive-kotlin/standard/src/test/kotlin/io/quarkus/it/resteasy/reactive/kotlin/SharedResourceTest.kt @@ -0,0 +1,26 @@ +package io.quarkus.it.resteasy.reactive.kotlin + +import io.quarkus.test.junit.QuarkusTest +import io.restassured.http.ContentType +import io.restassured.module.kotlin.extensions.Given +import io.restassured.module.kotlin.extensions.Then +import io.restassured.module.kotlin.extensions.When +import org.hamcrest.CoreMatchers +import org.junit.jupiter.api.Test + +@QuarkusTest +class SharedResourceTest { + + @Test + fun testReturnAsIs() { + Given { + body("""{ "message": "will not be used" }""") + contentType(ContentType.JSON) + } When { + post("/shared") + } Then { + statusCode(200) + body(CoreMatchers.`is`("""{"message": "canned+canned"}""")) + } + } +} diff --git a/integration-tests/shared-library/src/main/java/io/quarkus/it/shared/Shared.java b/integration-tests/shared-library/src/main/java/io/quarkus/it/shared/Shared.java new file mode 100644 index 0000000000000..cb96bccb290db --- /dev/null +++ b/integration-tests/shared-library/src/main/java/io/quarkus/it/shared/Shared.java @@ -0,0 +1,14 @@ +package io.quarkus.it.shared; + +public class Shared { + + private final String message; + + public Shared(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/integration-tests/shared-library/src/main/java/io/quarkus/it/shared/SharedProvider.java b/integration-tests/shared-library/src/main/java/io/quarkus/it/shared/SharedProvider.java new file mode 100644 index 0000000000000..15b188846a15b --- /dev/null +++ b/integration-tests/shared-library/src/main/java/io/quarkus/it/shared/SharedProvider.java @@ -0,0 +1,44 @@ +package io.quarkus.it.shared; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.nio.charset.StandardCharsets; + +import javax.ws.rs.Consumes; +import javax.ws.rs.Produces; +import javax.ws.rs.WebApplicationException; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.MultivaluedMap; +import javax.ws.rs.ext.MessageBodyReader; +import javax.ws.rs.ext.MessageBodyWriter; + +@Produces("application/json") +@Consumes("application/json") +public class SharedProvider implements MessageBodyReader, MessageBodyWriter { + @Override + public boolean isReadable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return Shared.class.equals(type); + } + + @Override + public Shared readFrom(Class type, Type genericType, Annotation[] annotations, MediaType mediaType, + MultivaluedMap httpHeaders, InputStream entityStream) + throws IOException, WebApplicationException { + return new Shared("canned"); + } + + @Override + public boolean isWriteable(Class type, Type genericType, Annotation[] annotations, MediaType mediaType) { + return Shared.class.equals(type); + } + + @Override + public void writeTo(Shared shared, Class type, Type genericType, Annotation[] annotations, + MediaType mediaType, MultivaluedMap httpHeaders, OutputStream entityStream) + throws IOException, WebApplicationException { + entityStream.write(String.format("{\"message\": \"canned+%s\"}", shared.getMessage()).getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/integration-tests/shared-library/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers b/integration-tests/shared-library/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers new file mode 100644 index 0000000000000..406cbbd640fca --- /dev/null +++ b/integration-tests/shared-library/src/main/resources/META-INF/services/javax.ws.rs.ext.Providers @@ -0,0 +1 @@ +io.quarkus.it.shared.SharedProvider