Skip to content

Commit

Permalink
Detect MessageBodyReader/Writer from META-INF/services/javax.ws.rs.ex…
Browse files Browse the repository at this point in the history
…t.Providers

Resolves: quarkusio#27970
  • Loading branch information
geoand committed Sep 16, 2022
1 parent 0595c55 commit cff0be1
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 3 deletions.
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -675,6 +683,83 @@ private void handleDateFormatReflection(BuildProducer<ReflectiveClassBuildItem>
}
}

/**
* 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<MessageBodyReaderBuildItem> messageBodyReaderProducer,
BuildProducer<MessageBodyWriterBuildItem> messageBodyWriterProducer) {
String fileName = "META-INF/services/" + Providers.class.getName();
try {
Set<String> 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,
Expand Down
Expand Up @@ -8,7 +8,7 @@
import io.restassured.RestAssured;

@QuarkusTest
public class GreetingNormalTestCase {
public class SharedNormalTestCase {

@Test
public void included() {
Expand Down
Expand Up @@ -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() {
Expand Down
4 changes: 4 additions & 0 deletions integration-tests/resteasy-reactive-kotlin/standard/pom.xml
Expand Up @@ -39,6 +39,10 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-kotlin</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-integration-test-shared-library</artifactId>
</dependency>
<!-- Added only to make sure that the Jaeger exporter compiles in native -->
<dependency>
<groupId>io.quarkus</groupId>
Expand Down
@@ -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
}
@@ -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"}"""))
}
}
}
@@ -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;
}
}
@@ -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<Shared>, MessageBodyWriter<Shared> {
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return Shared.class.equals(type);
}

@Override
public Shared readFrom(Class<Shared> type, Type genericType, Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> 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<String, Object> httpHeaders, OutputStream entityStream)
throws IOException, WebApplicationException {
entityStream.write(String.format("{\"message\": \"canned+%s\"}", shared.getMessage()).getBytes(StandardCharsets.UTF_8));
}
}
@@ -0,0 +1 @@
io.quarkus.it.shared.SharedProvider

0 comments on commit cff0be1

Please sign in to comment.