From 69886c11eee8ef240d298d625810dba7cce2088d Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Tue, 11 Feb 2020 11:40:44 +0100 Subject: [PATCH 1/3] Implement the RestEasy Mutiny support It allows returning Uni and Multi in RestEasy endpoint It also allows using Uni in the Rest Client There is no `providers` file to avoid registering provider twice - only the processor should do it. --- bom/deployment/pom.xml | 13 +- bom/runtime/pom.xml | 5 + .../builditem/FeatureBuildItem.java | 1 + extensions/pom.xml | 1 + extensions/resteasy-mutiny/deployment/pom.xml | 57 +++++ .../deployment/ResteasyMutinyProcessor.java | 36 +++ .../resteasy/mutiny/test/MutinyInjector.java | 40 +++ .../resteasy/mutiny/test/MutinyResource.java | 42 ++++ .../mutiny/test/RestEasyMutinyTest.java | 63 +++++ .../mutiny/test/annotations/Async.java | 14 ++ extensions/resteasy-mutiny/pom.xml | 23 ++ extensions/resteasy-mutiny/runtime/pom.xml | 70 ++++++ .../mutiny/runtime/MultiInvokerProvider.java | 30 +++ .../mutiny/runtime/MultiProvider.java | 13 + .../mutiny/runtime/MultiRxInvoker.java | 14 ++ .../mutiny/runtime/MultiRxInvokerImpl.java | 236 ++++++++++++++++++ .../mutiny/runtime/UniInvokerProvider.java | 29 +++ .../resteasy/mutiny/runtime/UniProvider.java | 22 ++ .../resteasy/mutiny/runtime/UniRxInvoker.java | 87 +++++++ .../mutiny/runtime/UniRxInvokerImpl.java | 143 +++++++++++ .../resources/META-INF/quarkus-extension.yaml | 11 + .../mutiny/test/MultiProviderTest.java | 26 ++ .../resteasy/mutiny/test/UniProviderTest.java | 38 +++ 23 files changed, 1013 insertions(+), 1 deletion(-) create mode 100644 extensions/resteasy-mutiny/deployment/pom.xml create mode 100644 extensions/resteasy-mutiny/deployment/src/main/java/io/quarkus/resteasy/mutiny/deployment/ResteasyMutinyProcessor.java create mode 100644 extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/MutinyInjector.java create mode 100644 extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/MutinyResource.java create mode 100644 extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/RestEasyMutinyTest.java create mode 100644 extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/annotations/Async.java create mode 100644 extensions/resteasy-mutiny/pom.xml create mode 100644 extensions/resteasy-mutiny/runtime/pom.xml create mode 100644 extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiInvokerProvider.java create mode 100644 extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiProvider.java create mode 100644 extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiRxInvoker.java create mode 100644 extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiRxInvokerImpl.java create mode 100644 extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniInvokerProvider.java create mode 100644 extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniProvider.java create mode 100644 extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniRxInvoker.java create mode 100644 extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniRxInvokerImpl.java create mode 100644 extensions/resteasy-mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 extensions/resteasy-mutiny/runtime/src/test/java/io/quarkus/resteasy/mutiny/test/MultiProviderTest.java create mode 100644 extensions/resteasy-mutiny/runtime/src/test/java/io/quarkus/resteasy/mutiny/test/UniProviderTest.java diff --git a/bom/deployment/pom.xml b/bom/deployment/pom.xml index 3dd2422fe..bd0c3576b 100644 --- a/bom/deployment/pom.xml +++ b/bom/deployment/pom.xml @@ -568,7 +568,18 @@ quarkus-scala-deployment ${project.version} - + + + io.quarkus + quarkus-mutiny-deployment + ${project.version} + + + io.quarkus + quarkus-resteasy-mutiny-deployment + ${project.version} + + io.quarkus quarkus-qute-deployment diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index f990c1576..b5a5cb680 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -982,6 +982,11 @@ quarkus-mutiny ${project.version} + + io.quarkus + quarkus-resteasy-mutiny + ${project.version} + io.reactivex.rxjava2 rxjava diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java index 71e69eb57..6af25d5e4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java @@ -56,6 +56,7 @@ public final class FeatureBuildItem extends MultiBuildItem { public static final String RESTEASY_JACKSON = "resteasy-jackson"; public static final String RESTEASY_JAXB = "resteasy-jaxb"; public static final String RESTEASY_JSONB = "resteasy-jsonb"; + public static final String RESTEASY_MUTINY = "resteasy-mutiny"; public static final String RESTEASY_QUTE = "resteasy-qute"; public static final String REST_CLIENT = "rest-client"; public static final String SCALA = "scala"; diff --git a/extensions/pom.xml b/extensions/pom.xml index 8e5fee9a5..db36ae09c 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -48,6 +48,7 @@ resteasy-jsonb resteasy-jackson resteasy-jaxb + resteasy-mutiny resteasy-qute rest-client smallrye-openapi-common diff --git a/extensions/resteasy-mutiny/deployment/pom.xml b/extensions/resteasy-mutiny/deployment/pom.xml new file mode 100644 index 000000000..68733f295 --- /dev/null +++ b/extensions/resteasy-mutiny/deployment/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + quarkus-resteasy-mutiny-parent + io.quarkus + 999-SNAPSHOT + ../ + + + quarkus-resteasy-mutiny-deployment + Quarkus - RESTEasy - Mutiny - Deployment + + + + io.quarkus + quarkus-resteasy-deployment + + + io.quarkus + quarkus-mutiny-deployment + + + io.quarkus + quarkus-resteasy-mutiny + + + io.quarkus + quarkus-junit5-internal + test + + + io.rest-assured + rest-assured + test + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/resteasy-mutiny/deployment/src/main/java/io/quarkus/resteasy/mutiny/deployment/ResteasyMutinyProcessor.java b/extensions/resteasy-mutiny/deployment/src/main/java/io/quarkus/resteasy/mutiny/deployment/ResteasyMutinyProcessor.java new file mode 100644 index 000000000..21248f31d --- /dev/null +++ b/extensions/resteasy-mutiny/deployment/src/main/java/io/quarkus/resteasy/mutiny/deployment/ResteasyMutinyProcessor.java @@ -0,0 +1,36 @@ +package io.quarkus.resteasy.mutiny.deployment; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.mutiny.runtime.MultiInvokerProvider; +import io.quarkus.resteasy.mutiny.runtime.MultiProvider; +import io.quarkus.resteasy.mutiny.runtime.MultiRxInvoker; +import io.quarkus.resteasy.mutiny.runtime.MultiRxInvokerImpl; +import io.quarkus.resteasy.mutiny.runtime.UniInvokerProvider; +import io.quarkus.resteasy.mutiny.runtime.UniProvider; +import io.quarkus.resteasy.mutiny.runtime.UniRxInvoker; +import io.quarkus.resteasy.mutiny.runtime.UniRxInvokerImpl; + +public class ResteasyMutinyProcessor { + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(FeatureBuildItem.RESTEASY_MUTINY); + } + + @BuildStep + public void registerProviders(BuildProducer jaxrsProvider) { + jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(MultiInvokerProvider.class.getName())); + jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(MultiProvider.class.getName())); + jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(MultiRxInvoker.class.getName())); + jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(MultiRxInvokerImpl.class.getName())); + + jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(UniInvokerProvider.class.getName())); + jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(UniProvider.class.getName())); + jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(UniRxInvoker.class.getName())); + jaxrsProvider.produce(new ResteasyJaxrsProviderBuildItem(UniRxInvokerImpl.class.getName())); + } + +} diff --git a/extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/MutinyInjector.java b/extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/MutinyInjector.java new file mode 100644 index 000000000..48d71116e --- /dev/null +++ b/extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/MutinyInjector.java @@ -0,0 +1,40 @@ +package io.quarkus.resteasy.mutiny.test; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; + +import javax.ws.rs.ext.Provider; + +import org.jboss.resteasy.spi.ContextInjector; + +import io.quarkus.resteasy.mutiny.test.annotations.Async; +import io.smallrye.mutiny.Uni; + +@Provider +public class MutinyInjector implements ContextInjector, Integer> { + + @Override + public Uni resolve(Class> rawType, Type genericType, + Annotation[] annotations) { + boolean async = false; + for (Annotation annotation : annotations) { + if (annotation.annotationType() == Async.class) { + async = true; + } + } + if (!async) { + return Uni.createFrom().item(42); + } + return Uni.createFrom().emitter(emitter -> new Thread(() -> { + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + emitter.fail(e); + return; + } + emitter.complete(42); + }).start()); + } + +} diff --git a/extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/MutinyResource.java b/extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/MutinyResource.java new file mode 100644 index 000000000..005219821 --- /dev/null +++ b/extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/MutinyResource.java @@ -0,0 +1,42 @@ +package io.quarkus.resteasy.mutiny.test; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; + +import org.jboss.resteasy.annotations.Stream; + +import io.quarkus.resteasy.mutiny.test.annotations.Async; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +@Path("/") +public class MutinyResource { + @Path("uni") + @GET + public Uni uni() { + return Uni.createFrom().item("hello"); + } + + @Produces(MediaType.APPLICATION_JSON) + @Path("multi") + @GET + @Stream + public Multi multi() { + return Multi.createFrom().items("hello", "world"); + } + + @Path("injection") + @GET + public Uni injection(@Context Integer value) { + return Uni.createFrom().item(value); + } + + @Path("injection-async") + @GET + public Uni injectionAsync(@Async @Context Integer value) { + return Uni.createFrom().item(value); + } +} diff --git a/extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/RestEasyMutinyTest.java b/extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/RestEasyMutinyTest.java new file mode 100644 index 000000000..f1a786d61 --- /dev/null +++ b/extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/RestEasyMutinyTest.java @@ -0,0 +1,63 @@ +package io.quarkus.resteasy.mutiny.test; + +import java.net.URL; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +import javax.ws.rs.client.ClientBuilder; + +import org.jboss.resteasy.client.jaxrs.ResteasyClient; +import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.resteasy.mutiny.test.annotations.Async; +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class RestEasyMutinyTest { + + private static AtomicReference value = new AtomicReference<>(); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(MutinyResource.class, MutinyInjector.class, Async.class)); + + @TestHTTPResource + URL url; + + private ResteasyClient client; + + @BeforeEach + public void before() { + client = ((ResteasyClientBuilder) ClientBuilder.newBuilder()) + .readTimeout(5, TimeUnit.SECONDS) + .connectionCheckoutTimeout(5, TimeUnit.SECONDS) + .connectTimeout(5, TimeUnit.SECONDS) + .build(); + value.set(null); + CountDownLatch latch = new CountDownLatch(1); + } + + @AfterEach + public void after() { + client.close(); + } + + @Test + public void testInjection() { + Integer data = client.target(url.toExternalForm() + "/injection").request().get(Integer.class); + Assertions.assertEquals((Integer) 42, data); + + data = client.target(url.toExternalForm() + "/injection-async").request().get(Integer.class); + Assertions.assertEquals((Integer) 42, data); + } + +} diff --git a/extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/annotations/Async.java b/extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/annotations/Async.java new file mode 100644 index 000000000..80d9fd90f --- /dev/null +++ b/extensions/resteasy-mutiny/deployment/src/test/java/io/quarkus/resteasy/mutiny/test/annotations/Async.java @@ -0,0 +1,14 @@ +package io.quarkus.resteasy.mutiny.test.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD }) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface Async { + // Marker annotation +} \ No newline at end of file diff --git a/extensions/resteasy-mutiny/pom.xml b/extensions/resteasy-mutiny/pom.xml new file mode 100644 index 000000000..064b5b3b5 --- /dev/null +++ b/extensions/resteasy-mutiny/pom.xml @@ -0,0 +1,23 @@ + + + 4.0.0 + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../build-parent/pom.xml + + + quarkus-resteasy-mutiny-parent + Quarkus - RESTEasy - Mutiny + pom + + + deployment + runtime + + + diff --git a/extensions/resteasy-mutiny/runtime/pom.xml b/extensions/resteasy-mutiny/runtime/pom.xml new file mode 100644 index 000000000..91dd86689 --- /dev/null +++ b/extensions/resteasy-mutiny/runtime/pom.xml @@ -0,0 +1,70 @@ + + + 4.0.0 + + quarkus-resteasy-mutiny-parent + io.quarkus + 999-SNAPSHOT + ../ + + + quarkus-resteasy-mutiny + Quarkus - RESTEasy - Mutiny - Runtime + Mutiny integration for RESTEasy + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-arc + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-mutiny + + + org.jboss.resteasy + resteasy-client + + + commons-logging + commons-logging + + + + + io.quarkus + quarkus-junit5-internal + test + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiInvokerProvider.java b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiInvokerProvider.java new file mode 100644 index 000000000..77ac782b3 --- /dev/null +++ b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiInvokerProvider.java @@ -0,0 +1,30 @@ +package io.quarkus.resteasy.mutiny.runtime; + +import java.util.concurrent.ExecutorService; + +import javax.ws.rs.client.RxInvokerProvider; +import javax.ws.rs.client.SyncInvoker; +import javax.ws.rs.client.WebTarget; + +public class MultiInvokerProvider implements RxInvokerProvider { + WebTarget target; + + @Override + public boolean isProviderFor(Class clazz) { + return MultiRxInvoker.class.equals(clazz); + } + + @Override + public MultiRxInvoker getRxInvoker(SyncInvoker syncInvoker, ExecutorService executorService) { + return new MultiRxInvokerImpl(syncInvoker, executorService); + } + + public WebTarget getTarget() { + return target; + } + + public void setTarget(WebTarget target) { + this.target = target; + } + +} \ No newline at end of file diff --git a/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiProvider.java b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiProvider.java new file mode 100644 index 000000000..210b81906 --- /dev/null +++ b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiProvider.java @@ -0,0 +1,13 @@ +package io.quarkus.resteasy.mutiny.runtime; + +import org.jboss.resteasy.spi.AsyncStreamProvider; +import org.reactivestreams.Publisher; + +import io.smallrye.mutiny.Multi; + +public class MultiProvider implements AsyncStreamProvider> { + @Override + public Publisher toAsyncStream(Multi multi) { + return multi; + } +} \ No newline at end of file diff --git a/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiRxInvoker.java b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiRxInvoker.java new file mode 100644 index 000000000..497b49e1b --- /dev/null +++ b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiRxInvoker.java @@ -0,0 +1,14 @@ +package io.quarkus.resteasy.mutiny.runtime; + +import javax.ws.rs.client.RxInvoker; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.subscription.BackPressureStrategy; + +public interface MultiRxInvoker extends RxInvoker> { + + BackPressureStrategy getBackPressureStrategy(); + + void setBackPressureStrategy(BackPressureStrategy strategy); + +} \ No newline at end of file diff --git a/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiRxInvokerImpl.java b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiRxInvokerImpl.java new file mode 100644 index 000000000..8d7c67b97 --- /dev/null +++ b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/MultiRxInvokerImpl.java @@ -0,0 +1,236 @@ +package io.quarkus.resteasy.mutiny.runtime; + +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.ScheduledExecutorService; +import java.util.function.Function; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.SyncInvoker; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.sse.InboundSseEvent; +import javax.ws.rs.sse.SseEventSource; + +import org.jboss.resteasy.client.jaxrs.internal.ClientInvocationBuilder; +import org.jboss.resteasy.plugins.providers.sse.InboundSseEventImpl; +import org.jboss.resteasy.plugins.providers.sse.client.SseEventSourceImpl; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.subscription.BackPressureStrategy; + +public class MultiRxInvokerImpl implements MultiRxInvoker { + + private static Object monitor = new Object(); + private ClientInvocationBuilder syncInvoker; + private ScheduledExecutorService executorService; + private BackPressureStrategy backpressureStrategy = BackPressureStrategy.BUFFER; + + public MultiRxInvokerImpl(final SyncInvoker syncInvoker, final ExecutorService executorService) { + if (!(syncInvoker instanceof ClientInvocationBuilder)) { + throw new ProcessingException("Expected a ClientInvocationBuilder"); + } + this.syncInvoker = (ClientInvocationBuilder) syncInvoker; + if (executorService instanceof ScheduledExecutorService) { + this.executorService = (ScheduledExecutorService) executorService; + } + } + + @Override + public BackPressureStrategy getBackPressureStrategy() { + return backpressureStrategy; + } + + @Override + public void setBackPressureStrategy(BackPressureStrategy strategy) { + this.backpressureStrategy = strategy; + } + + @Override + public Multi get() { + return eventSourceToMulti(getEventSource(), String.class, "GET", null, getAccept()); + } + + @Override + public Multi get(Class responseType) { + return eventSourceToMulti(getEventSource(), responseType, "GET", null, getAccept()); + } + + @Override + public Multi get(GenericType responseType) { + return eventSourceToMulti(getEventSource(), responseType, "GET", null, getAccept()); + } + + @Override + public Multi put(Entity entity) { + return eventSourceToMulti(getEventSource(), String.class, "PUT", entity, getAccept()); + } + + @Override + public Multi put(Entity entity, Class responseType) { + return eventSourceToMulti(getEventSource(), responseType, "PUT", entity, getAccept()); + } + + @Override + public Multi put(Entity entity, GenericType responseType) { + return eventSourceToMulti(getEventSource(), responseType, "PUT", entity, getAccept()); + } + + @Override + public Multi post(Entity entity) { + return eventSourceToMulti(getEventSource(), String.class, "POST", entity, getAccept()); + } + + @Override + public Multi post(Entity entity, Class responseType) { + return eventSourceToMulti(getEventSource(), responseType, "POST", entity, getAccept()); + } + + @Override + public Multi post(Entity entity, GenericType responseType) { + return eventSourceToMulti(getEventSource(), responseType, "POST", entity, getAccept()); + } + + @Override + public Multi delete() { + return eventSourceToMulti(getEventSource(), String.class, "DELETE", null, getAccept()); + } + + @Override + public Multi delete(Class responseType) { + return eventSourceToMulti(getEventSource(), responseType, "DELETE", null, getAccept()); + } + + @Override + public Multi delete(GenericType responseType) { + return eventSourceToMulti(getEventSource(), responseType, "DELETE", null, getAccept()); + } + + @Override + public Multi head() { + return eventSourceToMulti(getEventSource(), String.class, "HEAD", null, getAccept()); + } + + @Override + public Multi options() { + return eventSourceToMulti(getEventSource(), String.class, "OPTIONS", null, getAccept()); + } + + @Override + public Multi options(Class responseType) { + return eventSourceToMulti(getEventSource(), responseType, "OPTIONS", null, getAccept()); + } + + @Override + public Multi options(GenericType responseType) { + return eventSourceToMulti(getEventSource(), responseType, "OPTIONS", null, getAccept()); + } + + @Override + public Multi trace() { + return eventSourceToMulti(getEventSource(), String.class, "TRACE", null, getAccept()); + } + + @Override + public Multi trace(Class responseType) { + return eventSourceToMulti(getEventSource(), responseType, "TRACE", null, getAccept()); + } + + @Override + public Multi trace(GenericType responseType) { + return eventSourceToMulti(getEventSource(), responseType, "TRACE", null, getAccept()); + } + + @Override + public Multi method(String name) { + return eventSourceToMulti(getEventSource(), String.class, name, null, getAccept()); + } + + @Override + public Multi method(String name, Class responseType) { + return eventSourceToMulti(getEventSource(), responseType, name, null, getAccept()); + } + + @Override + public Multi method(String name, GenericType responseType) { + return eventSourceToMulti(getEventSource(), responseType, name, null, getAccept()); + } + + @Override + public Multi method(String name, Entity entity) { + return eventSourceToMulti(getEventSource(), String.class, name, entity, getAccept()); + } + + @Override + public Multi method(String name, Entity entity, Class responseType) { + return eventSourceToMulti(getEventSource(), responseType, name, entity, getAccept()); + } + + @Override + public Multi method(String name, Entity entity, GenericType responseType) { + return eventSourceToMulti(getEventSource(), responseType, name, entity, getAccept()); + } + + private Multi eventSourceToMulti(SseEventSourceImpl sseEventSource, Class clazz, String verb, + Entity entity, MediaType[] mediaTypes) { + return eventSourceToMulti( + sseEventSource, + (InboundSseEventImpl e) -> e.readData(clazz, e.getMediaType()), + verb, + entity, + mediaTypes); + } + + private Multi eventSourceToMulti(SseEventSourceImpl sseEventSource, GenericType type, String verb, + Entity entity, MediaType[] mediaTypes) { + return eventSourceToMulti( + sseEventSource, + (InboundSseEventImpl e) -> e.readData(type, e.getMediaType()), + verb, + entity, + mediaTypes); + } + + private Multi eventSourceToMulti( + final SseEventSourceImpl sseEventSource, + final Function tSupplier, + final String verb, + final Entity entity, + final MediaType[] mediaTypes) { + final Multi multi = Multi.createFrom().emitter(emitter -> { + sseEventSource.register( + (InboundSseEvent e) -> emitter.emit(tSupplier.apply((InboundSseEventImpl) e)), + (Throwable t) -> emitter.fail(t), + () -> emitter.complete()); + synchronized (monitor) { + if (!sseEventSource.isOpen()) { + sseEventSource.open(null, verb, entity, mediaTypes); + } + } + }, + backpressureStrategy); + return multi; + } + + private SseEventSourceImpl getEventSource() { + SseEventSourceImpl.SourceBuilder builder = (SseEventSourceImpl.SourceBuilder) SseEventSource + .target(syncInvoker.getTarget()); + if (executorService != null) { + builder.executor(executorService); + } + SseEventSourceImpl sseEventSource = (SseEventSourceImpl) builder.build(); + sseEventSource.setAlwaysReconnect(false); + return sseEventSource; + } + + private MediaType[] getAccept() { + if (syncInvoker != null) { + ClientInvocationBuilder builder = syncInvoker; + List accept = builder.getHeaders().getAcceptableMediaTypes(); + return accept.toArray(new MediaType[0]); + } else { + return null; + } + } +} diff --git a/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniInvokerProvider.java b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniInvokerProvider.java new file mode 100644 index 000000000..7bdd0a01e --- /dev/null +++ b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniInvokerProvider.java @@ -0,0 +1,29 @@ +package io.quarkus.resteasy.mutiny.runtime; + +import java.util.concurrent.ExecutorService; + +import javax.ws.rs.ProcessingException; +import javax.ws.rs.client.CompletionStageRxInvoker; +import javax.ws.rs.client.RxInvokerProvider; +import javax.ws.rs.client.SyncInvoker; + +import org.jboss.resteasy.client.jaxrs.internal.ClientInvocationBuilder; + +public class UniInvokerProvider implements RxInvokerProvider { + + @Override + public boolean isProviderFor(Class clazz) { + return UniRxInvoker.class.equals(clazz); + } + + @Override + public UniRxInvoker getRxInvoker(SyncInvoker syncInvoker, ExecutorService executorService) { + if (syncInvoker instanceof ClientInvocationBuilder) { + ClientInvocationBuilder builder = (ClientInvocationBuilder) syncInvoker; + CompletionStageRxInvoker completionStageRxInvoker = builder.rx(); + return new UniRxInvokerImpl(completionStageRxInvoker); + } else { + throw new ProcessingException("Expected a ClientInvocationBuilder"); + } + } +} \ No newline at end of file diff --git a/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniProvider.java b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniProvider.java new file mode 100644 index 000000000..170af9145 --- /dev/null +++ b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniProvider.java @@ -0,0 +1,22 @@ +package io.quarkus.resteasy.mutiny.runtime; + +import java.util.concurrent.CompletionStage; + +import org.jboss.resteasy.spi.AsyncClientResponseProvider; +import org.jboss.resteasy.spi.AsyncResponseProvider; + +import io.smallrye.mutiny.Uni; + +public class UniProvider implements AsyncResponseProvider>, AsyncClientResponseProvider> { + + @Override + public CompletionStage toCompletionStage(Uni uni) { + return uni.subscribeAsCompletionStage(); + } + + @Override + public Uni fromCompletionStage(CompletionStage completionStage) { + return Uni.createFrom().completionStage(completionStage); + } + +} \ No newline at end of file diff --git a/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniRxInvoker.java b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniRxInvoker.java new file mode 100644 index 000000000..adc561e59 --- /dev/null +++ b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniRxInvoker.java @@ -0,0 +1,87 @@ +package io.quarkus.resteasy.mutiny.runtime; + +import javax.ws.rs.client.Entity; +import javax.ws.rs.client.RxInvoker; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; + +import io.smallrye.mutiny.Uni; + +public interface UniRxInvoker extends RxInvoker> { + + @Override + Uni get(); + + @Override + Uni get(Class responseType); + + @Override + Uni get(GenericType responseType); + + @Override + Uni put(Entity entity); + + @Override + Uni put(Entity entity, Class clazz); + + @Override + Uni put(Entity entity, GenericType type); + + @Override + Uni post(Entity entity); + + @Override + Uni post(Entity entity, Class clazz); + + @Override + Uni post(Entity entity, GenericType type); + + @Override + Uni delete(); + + @Override + Uni delete(Class responseType); + + @Override + Uni delete(GenericType responseType); + + @Override + Uni head(); + + @Override + Uni options(); + + @Override + Uni options(Class responseType); + + @Override + Uni options(GenericType responseType); + + @Override + Uni trace(); + + @Override + Uni trace(Class responseType); + + @Override + Uni trace(GenericType responseType); + + @Override + Uni method(String name); + + @Override + Uni method(String name, Class responseType); + + @Override + Uni method(String name, GenericType responseType); + + @Override + Uni method(String name, Entity entity); + + @Override + Uni method(String name, Entity entity, Class responseType); + + @Override + Uni method(String name, Entity entity, GenericType responseType); + +} \ No newline at end of file diff --git a/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniRxInvokerImpl.java b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniRxInvokerImpl.java new file mode 100644 index 000000000..d0ca2e07f --- /dev/null +++ b/extensions/resteasy-mutiny/runtime/src/main/java/io/quarkus/resteasy/mutiny/runtime/UniRxInvokerImpl.java @@ -0,0 +1,143 @@ +package io.quarkus.resteasy.mutiny.runtime; + +import javax.ws.rs.client.CompletionStageRxInvoker; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; + +import io.smallrye.mutiny.Uni; + +public class UniRxInvokerImpl implements UniRxInvoker { + private final CompletionStageRxInvoker completionStageRxInvoker; + private final UniProvider UniProvider; + + public UniRxInvokerImpl(final CompletionStageRxInvoker completionStageRxInvoker) { + this.completionStageRxInvoker = completionStageRxInvoker; + this.UniProvider = new UniProvider(); + } + + @Override + public Uni get() { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.get()); + } + + @Override + public Uni get(Class responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.get(responseType)); + } + + @Override + public Uni get(GenericType responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.get(responseType)); + } + + @Override + public Uni put(Entity entity) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.put(entity)); + } + + @Override + public Uni put(Entity entity, Class responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.put(entity, responseType)); + } + + @Override + public Uni put(Entity entity, GenericType responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.put(entity, responseType)); + } + + @Override + public Uni post(Entity entity) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.post(entity)); + } + + @Override + public Uni post(Entity entity, Class responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.post(entity, responseType)); + } + + @Override + public Uni post(Entity entity, GenericType responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.post(entity, responseType)); + } + + @Override + public Uni delete() { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.delete()); + } + + @Override + public Uni delete(Class responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.delete(responseType)); + } + + @Override + public Uni delete(GenericType responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.delete(responseType)); + } + + @Override + public Uni head() { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.head()); + } + + @Override + public Uni options() { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.options()); + } + + @Override + public Uni options(Class responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.options(responseType)); + } + + @Override + public Uni options(GenericType responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.options(responseType)); + } + + @Override + public Uni trace() { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.trace()); + } + + @Override + public Uni trace(Class responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.trace(responseType)); + } + + @Override + public Uni trace(GenericType responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.trace(responseType)); + } + + @Override + public Uni method(String name) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.method(name)); + } + + @Override + public Uni method(String name, Class responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.method(name, responseType)); + } + + @Override + public Uni method(String name, GenericType responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.method(name, responseType)); + } + + @Override + public Uni method(String name, Entity entity) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.method(name, entity)); + } + + @Override + public Uni method(String name, Entity entity, Class responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.method(name, entity, responseType)); + } + + @Override + public Uni method(String name, Entity entity, GenericType responseType) { + return (Uni) UniProvider.fromCompletionStage(completionStageRxInvoker.method(name, entity, responseType)); + } +} \ No newline at end of file diff --git a/extensions/resteasy-mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/resteasy-mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 000000000..c0b0d4172 --- /dev/null +++ b/extensions/resteasy-mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +--- +name: "RESTEasy Mutiny" +metadata: + keywords: + - "resteasy-mutiny" + - "resteasy" + - "mutiny" + categories: + - "web" + - "reactive" + status: "unstable" \ No newline at end of file diff --git a/extensions/resteasy-mutiny/runtime/src/test/java/io/quarkus/resteasy/mutiny/test/MultiProviderTest.java b/extensions/resteasy-mutiny/runtime/src/test/java/io/quarkus/resteasy/mutiny/test/MultiProviderTest.java new file mode 100644 index 000000000..05ec999bc --- /dev/null +++ b/extensions/resteasy-mutiny/runtime/src/test/java/io/quarkus/resteasy/mutiny/test/MultiProviderTest.java @@ -0,0 +1,26 @@ +package io.quarkus.resteasy.mutiny.test; + +import java.util.List; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.reactivestreams.Publisher; + +import io.quarkus.resteasy.mutiny.runtime.MultiProvider; +import io.smallrye.mutiny.Multi; + +public class MultiProviderTest { + + private final MultiProvider provider = new MultiProvider(); + + @Test + public void test() { + Multi multi = Multi.createFrom().items(1, 2, 3); + Publisher publisher = provider.toAsyncStream(multi); + List list = Multi.createFrom().publisher(publisher).collectItems().asList().await().indefinitely(); + Assertions.assertEquals(1, list.get(0)); + Assertions.assertEquals(2, list.get(1)); + Assertions.assertEquals(3, list.get(2)); + } + +} diff --git a/extensions/resteasy-mutiny/runtime/src/test/java/io/quarkus/resteasy/mutiny/test/UniProviderTest.java b/extensions/resteasy-mutiny/runtime/src/test/java/io/quarkus/resteasy/mutiny/test/UniProviderTest.java new file mode 100644 index 000000000..7fe0a78a7 --- /dev/null +++ b/extensions/resteasy-mutiny/runtime/src/test/java/io/quarkus/resteasy/mutiny/test/UniProviderTest.java @@ -0,0 +1,38 @@ +package io.quarkus.resteasy.mutiny.test; + +import java.util.concurrent.CompletableFuture; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.resteasy.mutiny.runtime.UniProvider; +import io.smallrye.mutiny.Uni; + +public class UniProviderTest { + + private final UniProvider provider = new UniProvider(); + + @Test + public void testFromCompletionStage() { + final CompletableFuture cs = new CompletableFuture<>(); + cs.complete(1); + final Uni uni = provider.fromCompletionStage(cs); + Assertions.assertEquals(1, uni.await().indefinitely()); + } + + @Test + public void testToCompletionStageCase() { + final Object actual = provider.toCompletionStage(Uni.createFrom().item(1)).toCompletableFuture().join(); + Assertions.assertEquals(1, actual); + } + + @Test + public void testToCompletionStageNullCase() { + final CompletableFuture cs = new CompletableFuture<>(); + cs.complete(null); + final Uni uni = Uni.createFrom().completionStage(cs); + final Object actual = provider.toCompletionStage(uni).toCompletableFuture().join(); + Assertions.assertNull(actual); + } + +} \ No newline at end of file From 52ffa0193d9249db993d0568cc568db97f45b3e5 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Wed, 12 Feb 2020 09:24:59 +0100 Subject: [PATCH 2/3] Implement RestEasy Mutiny integration tests It covers: * Method returning Uni * Method returning Multi (chunked and SSE) * Rest client returning Uni --- ci-templates/stages.yml | 3 +- integration-tests/pom.xml | 1 + integration-tests/resteasy-mutiny/pom.xml | 127 ++++++++++++++++++ .../it/resteasy/mutiny/MutinyResource.java | 89 ++++++++++++ .../it/resteasy/mutiny/MyRestService.java | 25 ++++ .../io/quarkus/it/resteasy/mutiny/Pet.java | 25 ++++ .../it/resteasy/mutiny/SomeService.java | 62 +++++++++ .../src/main/resources/application.properties | 1 + .../it/resteasy/mutiny/MutinyTest.java | 122 +++++++++++++++++ 9 files changed, 454 insertions(+), 1 deletion(-) create mode 100644 integration-tests/resteasy-mutiny/pom.xml create mode 100644 integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/MutinyResource.java create mode 100644 integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/MyRestService.java create mode 100644 integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/Pet.java create mode 100644 integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/SomeService.java create mode 100644 integration-tests/resteasy-mutiny/src/main/resources/application.properties create mode 100644 integration-tests/resteasy-mutiny/src/test/java/io/quarkus/it/resteasy/mutiny/MutinyTest.java diff --git a/ci-templates/stages.yml b/ci-templates/stages.yml index 4bec56d7a..c91c21502 100644 --- a/ci-templates/stages.yml +++ b/ci-templates/stages.yml @@ -368,9 +368,10 @@ stages: parameters: poolSettings: ${{parameters.poolSettings}} expectUseVMs: ${{parameters.expectUseVMs}} - timeoutInMinutes: 35 + timeoutInMinutes: 55 modules: - resteasy-jackson + - resteasy-mutiny - vertx - vertx-http - vertx-graphql diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 49f786ab9..1d577b565 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -76,6 +76,7 @@ jackson jsonb resteasy-jackson + resteasy-mutiny jgit jsch virtual-http diff --git a/integration-tests/resteasy-mutiny/pom.xml b/integration-tests/resteasy-mutiny/pom.xml new file mode 100644 index 000000000..7d8f85e80 --- /dev/null +++ b/integration-tests/resteasy-mutiny/pom.xml @@ -0,0 +1,127 @@ + + + 4.0.0 + + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + ../ + + + quarkus-integration-test-resteasy-mutiny + + Quarkus - Integration Tests - RESTEasy Mutiny + + + + + io.quarkus + quarkus-resteasy-mutiny + + + io.quarkus + quarkus-rest-client + + + io.quarkus + quarkus-resteasy-jsonb + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + org.awaitility + awaitility + test + + + org.jboss.resteasy + resteasy-client + test + + + commons-logging + commons-logging + + + + + + + + + io.quarkus + quarkus-maven-plugin + ${project.version} + + + + build + + + + + + + + + + native-image + + + native + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + io.quarkus + quarkus-maven-plugin + ${project.version} + + + native-image + + native-image + + + true + true + ${graalvmHome} + + + + + + + + + + diff --git a/integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/MutinyResource.java b/integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/MutinyResource.java new file mode 100644 index 000000000..8188e6b34 --- /dev/null +++ b/integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/MutinyResource.java @@ -0,0 +1,89 @@ +package io.quarkus.it.resteasy.mutiny; + +import java.io.IOException; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.annotations.SseElementType; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +@Path("/mutiny") +public class MutinyResource { + + @Inject + SomeService service; + + @GET + @Path("/hello") + public Uni hello() { + return service.greeting(); + } + + @GET + @Path("/fail") + public Uni fail() { + return Uni.createFrom().failure(new IOException("boom")); + } + + @GET + @Path("/response") + public Uni response() { + return service.greeting() + .onItem().apply(v -> Response.accepted().type("text/plain").entity(v).build()); + } + + @GET + @Path("/hello/stream") + @Produces(MediaType.APPLICATION_JSON) + public Multi helloAsMulti() { + return service.greetingAsMulti(); + } + + @GET + @Path("/pet") + @Produces(MediaType.APPLICATION_JSON) + public Uni pet() { + return service.getPet(); + } + + @GET + @Path("/pet/stream") + @Produces(MediaType.APPLICATION_JSON) + public Multi pets() { + return service.getPets(); + } + + @GET + @Path("/pets") + @Produces(MediaType.SERVER_SENT_EVENTS) + @SseElementType(MediaType.APPLICATION_JSON) + public Multi sse() { + return service.getMorePets(); + } + + @Inject + @RestClient + MyRestService client; + + @GET + @Path("/client") + public Uni callHello() { + return client.hello(); + } + + @GET + @Path("/client/pet") + @Produces(MediaType.APPLICATION_JSON) + public Uni callPet() { + return client.pet(); + } + +} diff --git a/integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/MyRestService.java b/integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/MyRestService.java new file mode 100644 index 000000000..05d6c916c --- /dev/null +++ b/integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/MyRestService.java @@ -0,0 +1,25 @@ +package io.quarkus.it.resteasy.mutiny; + +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import io.smallrye.mutiny.Uni; + +@RegisterRestClient(configKey = "my-service") +@Path("/mutiny") +public interface MyRestService { + + @GET + @Path("/hello") + Uni hello(); + + @GET + @Path("/pet") + @Consumes(MediaType.APPLICATION_JSON) + Uni pet(); + +} diff --git a/integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/Pet.java b/integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/Pet.java new file mode 100644 index 000000000..f29232b7f --- /dev/null +++ b/integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/Pet.java @@ -0,0 +1,25 @@ +package io.quarkus.it.resteasy.mutiny; + +public class Pet { + + private String name; + private String kind; + + public String getName() { + return name; + } + + public Pet setName(String name) { + this.name = name; + return this; + } + + public String getKind() { + return kind; + } + + public Pet setKind(String kind) { + this.kind = kind; + return this; + } +} diff --git a/integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/SomeService.java b/integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/SomeService.java new file mode 100644 index 000000000..4e34aaf8d --- /dev/null +++ b/integration-tests/resteasy-mutiny/src/main/java/io/quarkus/it/resteasy/mutiny/SomeService.java @@ -0,0 +1,62 @@ +package io.quarkus.it.resteasy.mutiny; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; + +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; + +@ApplicationScoped +public class SomeService { + + private ExecutorService executor; + + @PostConstruct + public void init() { + executor = Executors.newFixedThreadPool(2); + } + + @PreDestroy + public void cleanup() { + executor.shutdownNow(); + } + + Uni greeting() { + return Uni.createFrom().item("hello") + .emitOn(executor); + } + + Multi greetingAsMulti() { + return Multi.createFrom().items("h", "e", "l", "l", "o") + .groupItems().intoMultis().of(2) + .onItem().produceUni(g -> g.collectItems().in(StringBuffer::new, StringBuffer::append)).concatenate() + .emitOn(executor) + .onItem().apply(StringBuffer::toString); + } + + Uni getPet() { + return Uni.createFrom().item(new Pet().setName("neo").setKind("rabbit")) + .emitOn(executor); + } + + public Multi getPets() { + return Multi.createFrom().items( + new Pet().setName("neo").setKind("rabbit"), + new Pet().setName("indy").setKind("dog")) + .emitOn(executor); + } + + public Multi getMorePets() { + return Multi.createFrom().items( + new Pet().setName("neo").setKind("rabbit"), + new Pet().setName("indy").setKind("dog"), + new Pet().setName("plume").setKind("dog"), + new Pet().setName("titi").setKind("bird"), + new Pet().setName("rex").setKind("mouse")) + .emitOn(executor); + } +} diff --git a/integration-tests/resteasy-mutiny/src/main/resources/application.properties b/integration-tests/resteasy-mutiny/src/main/resources/application.properties new file mode 100644 index 000000000..a9de982fb --- /dev/null +++ b/integration-tests/resteasy-mutiny/src/main/resources/application.properties @@ -0,0 +1 @@ +my-service/mp-rest/url=${test.url} diff --git a/integration-tests/resteasy-mutiny/src/test/java/io/quarkus/it/resteasy/mutiny/MutinyTest.java b/integration-tests/resteasy-mutiny/src/test/java/io/quarkus/it/resteasy/mutiny/MutinyTest.java new file mode 100644 index 000000000..d223f8437 --- /dev/null +++ b/integration-tests/resteasy-mutiny/src/test/java/io/quarkus/it/resteasy/mutiny/MutinyTest.java @@ -0,0 +1,122 @@ +package io.quarkus.it.resteasy.mutiny; + +import static io.restassured.RestAssured.get; +import static org.awaitility.Awaitility.await; +import static org.hamcrest.CoreMatchers.is; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +import javax.ws.rs.client.Client; +import javax.ws.rs.client.ClientBuilder; +import javax.ws.rs.client.WebTarget; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.sse.SseEventSource; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; + +@QuarkusTest +public class MutinyTest { + + @Test + public void testHello() { + get("/mutiny/hello") + .then() + .body(is("hello")) + .statusCode(200); + } + + @Test + public void testFail() { + get("/mutiny/fail") + .then() + .statusCode(500); + } + + @Test + public void testResponse() { + get("/mutiny/response") + .then() + .body(is("hello")) + .statusCode(202); + } + + @Test + public void testHelloAsMulti() { + get("/mutiny/hello/stream") + .then() + .contentType("application/json") + .body("[0]", is("he")) + .body("[1]", is("ll")) + .body("[2]", is("o")) + .statusCode(200); + } + + @Test + public void testSerialization() { + get("/mutiny/pet") + .then() + .contentType("application/json") + .body("name", is("neo")) + .body("kind", is("rabbit")) + .statusCode(200); + } + + @Test + public void testMultiWithSerialization() { + get("/mutiny/pet/stream") + .then() + .contentType("application/json") + .body("[0].name", is("neo")) + .body("[0].kind", is("rabbit")) + .body("[1].name", is("indy")) + .body("[1].kind", is("dog")) + .statusCode(200); + } + + @Test + public void testSSE() { + Client client = ClientBuilder.newClient(); + WebTarget target = client.target("http://localhost:" + RestAssured.port + "/mutiny/pets"); + SseEventSource source = SseEventSource.target(target).build(); + List pets = new CopyOnWriteArrayList<>(); + try (SseEventSource eventSource = source) { + eventSource.register(event -> { + Pet pet = event.readData(Pet.class, MediaType.APPLICATION_JSON_TYPE); + pets.add(pet); + }, ex -> { + throw new IllegalStateException("SSE failure", ex); + }); + eventSource.open(); + await().until(() -> pets.size() == 5); + } + Assertions.assertEquals("neo", pets.get(0).getName()); + Assertions.assertEquals("indy", pets.get(1).getName()); + Assertions.assertEquals("plume", pets.get(2).getName()); + Assertions.assertEquals("titi", pets.get(3).getName()); + Assertions.assertEquals("rex", pets.get(4).getName()); + } + + @Test + public void testClientReturningUni() { + get("/mutiny/client") + .then() + .body(is("hello")) + .statusCode(200); + } + + @Test + public void testClientReturningUniOfPet() { + get("/mutiny/client/pet") + .then() + .contentType("application/json") + .body("name", is("neo")) + .body("kind", is("rabbit")) + .statusCode(200); + } + +} From 830696cb02753c18a08cf4527fd3d4ad7147fdc0 Mon Sep 17 00:00:00 2001 From: Clement Escoffier Date: Thu, 13 Feb 2020 11:12:30 +0100 Subject: [PATCH 3/3] Update documentation to use Resteasy-Mutiny * update the getting started async guide * update the vert.x guide to use the Mutiny API * update the rest-client guide to cover Uni * update the asynchronous message passing guide to use Uni and explain the API Co-Authored-By: Guillaume Smet --- docs/src/main/asciidoc/getting-started.adoc | 8 +- .../src/main/asciidoc/reactive-messaging.adoc | 66 +++--- docs/src/main/asciidoc/rest-client.adoc | 68 +++++- docs/src/main/asciidoc/vertx.adoc | 217 +++++++++--------- .../resources/META-INF/quarkus-extension.yaml | 2 +- 5 files changed, 221 insertions(+), 140 deletions(-) diff --git a/docs/src/main/asciidoc/getting-started.adoc b/docs/src/main/asciidoc/getting-started.adoc index af1c587a7..bd959e6a8 100644 --- a/docs/src/main/asciidoc/getting-started.adoc +++ b/docs/src/main/asciidoc/getting-started.adoc @@ -445,16 +445,14 @@ NOTE: Before running the application, don't forget to stop the hot reload mode ( == Async -The resource can also use `CompletionStage` as return type to handle asynchronous actions: +The resource can also use `CompletionStage` or `Uni` (requires the `quarkus-resteasy-mutiny` extension) as return type to handle asynchronous actions: [source,java] ---- @GET @Produces(MediaType.TEXT_PLAIN) -public CompletionStage hello() { - return CompletableFuture.supplyAsync(() -> { - return "hello"; - }); +public Uni hello() { + return Uni.createFrom().item(() -> "hello"); } ---- diff --git a/docs/src/main/asciidoc/reactive-messaging.adoc b/docs/src/main/asciidoc/reactive-messaging.adoc index 24e6205dc..77259f86f 100644 --- a/docs/src/main/asciidoc/reactive-messaging.adoc +++ b/docs/src/main/asciidoc/reactive-messaging.adoc @@ -199,29 +199,32 @@ Sending and publishing messages use the Vert.x event bus: [source, java] ---- -package org.acme; +package org.acme.vertx; -import io.vertx.axle.core.eventbus.EventBus; -import io.vertx.axle.core.eventbus.Message; +import io.smallrye.mutiny.Uni; +import io.vertx.mutiny.core.eventbus.EventBus; +import io.vertx.mutiny.core.eventbus.Message; +import org.jboss.resteasy.annotations.jaxrs.PathParam; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import java.util.concurrent.CompletionStage; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; @Path("/async") public class EventResource { - @Inject - EventBus bus; // <1> + @Inject + EventBus bus; // <1> - @GET - @Path("/{name}") - public CompletionStage hello(String name) { - return bus.request("greeting", name) // <2> - .thenApply(Message::body); - } + @GET + @Produces(MediaType.TEXT_PLAIN) + @Path("{name}") + public Uni greeting(@PathParam String name) { + return bus.request("greeting", name) // <2> + .onItem().apply(Message::body); + } } ---- <1> Inject the Event bus @@ -236,13 +239,12 @@ The `EventBus` object provides methods to: [source, java] ---- // Case 1 -bus.request("address", "hello"); +bus.sendAndForget("greeting", name) // Case 2 -bus.publish("address", "hello"); +bus.publish("greeting", name) // Case 3 -bus.request("address", "hello, how are you?").thenAccept(message -> { - // response -}); +Uni response = bus.request("address", "hello, how are you?") + .onItem().apply(Message::body); ---- == Putting things together - bridging HTTP and messages @@ -272,30 +274,34 @@ Then, creates a new JAX-RS resource with the following content: ---- package org.acme.vertx; -import io.vertx.axle.core.eventbus.EventBus; -import io.vertx.axle.core.eventbus.Message; +import io.smallrye.mutiny.Uni; +import io.vertx.mutiny.core.eventbus.EventBus; +import io.vertx.mutiny.core.eventbus.Message; +import org.jboss.resteasy.annotations.jaxrs.PathParam; import javax.inject.Inject; import javax.ws.rs.GET; import javax.ws.rs.Path; -import javax.ws.rs.PathParam; -import java.util.concurrent.CompletionStage; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; -@Path("/hello") +@Path("/async") public class EventResource { - @Inject EventBus bus; + @Inject + EventBus bus; @GET - @Path("/async/{name}") - public CompletionStage hello(@PathParam("name") String name) { - return bus.request("greeting", name) // <1> - .thenApply(Message::body); // <2> + @Produces(MediaType.TEXT_PLAIN) + @Path("{name}") + public Uni greeting(@PathParam String name) { + return bus.request("greeting", name) // <1> + .onItem().apply(Message::body); // <2> } } ---- -<1> send the `name` to the `greeting` address -<2> when we get the reply, extract the body and send this as response to the user +<1> send the `name` to the `greeting` address and request a response +<2> when we get the response, extract the body and send it to the user If you call this endpoint, you will wait and get a timeout. Indeed, no one is listening. So, we need a consumer listening on the `greeting` address. Create a `GreetingService` bean with the following content: diff --git a/docs/src/main/asciidoc/rest-client.adoc b/docs/src/main/asciidoc/rest-client.adoc index 8fcebfa69..7997d6ce4 100644 --- a/docs/src/main/asciidoc/rest-client.adoc +++ b/docs/src/main/asciidoc/rest-client.adoc @@ -234,7 +234,9 @@ The code above uses link:http://rest-assured.io/[REST Assured]'s link:https://gi == Async Support -The rest client supports asynchronous rest calls. Let's see it in action by adding a `getByNameAsync` method in our `CountriesService` REST interface. The code should look like: +The rest client supports asynchronous rest calls. +Async support comes in 2 flavors: you can return a `CompletionStage` or a `Uni` (requires the `quarkus-resteasy-mutiny` extension). +Let's see it in action by adding a `getByNameAsync` method in our `CountriesService` REST interface. The code should look like: [source, java] ---- @@ -324,6 +326,70 @@ public void testCountryNameAsyncEndpoint() { } ---- +The `Uni` version is very similar: + +[source, java] +---- +package org.acme.restclient; + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; +import org.jboss.resteasy.annotations.jaxrs.PathParam; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import java.util.concurrent.CompletionStage; +import java.util.Set; + +@Path("/v2") +@RegisterRestClient +public interface CountriesService { + + // ... + + @GET + @Path("/name/{name}") + @Produces("application/json") + Uni> getByNameAsUni(@PathParam String name); +} +---- + +The `CountriesResource` becomes: + +[source,java] +---- +package org.acme.restclient; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.annotations.jaxrs.PathParam; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.util.concurrent.CompletionStage; +import java.util.Set; + +@Path("/country") +public class CountriesResource { + + @Inject + @RestClient + CountriesService countriesService; + + + // ... + + @GET + @Path("/name-uni/{name}") + @Produces(MediaType.APPLICATION_JSON) + public Uni> nameAsync(@PathParam String name) { + return countriesService.getByNameAsUni(name); + } +} +---- + == Package and run the application Run the application with: `./mvnw compile quarkus:dev`. diff --git a/docs/src/main/asciidoc/vertx.adoc b/docs/src/main/asciidoc/vertx.adoc index 650a11067..59ace7f08 100644 --- a/docs/src/main/asciidoc/vertx.adoc +++ b/docs/src/main/asciidoc/vertx.adoc @@ -135,13 +135,49 @@ Quarkus web resources support asynchronous processing and streaming results over === Asynchronous processing -Most programming guides start easy with a greeting service and this one makes no exception. - -To asynchronously greet a client, the endpoint method must return a `java.util.concurrent.CompletionStage`: +To asynchronously handle the HTTP request, the endpoint method must return a `java.util.concurrent.CompletionStage` or an `io.smallrye.mutiny.Uni` (requires the `quarkus-resteasy-mutiny` extension): [source,java] ---- -@Path("/hello") +@Path("/lorem") +public class GreetingResource { + + @GET + @Produces(MediaType.TEXT_PLAIN) + public Uni doSomethingAsync() { + // Mimic an asynchronous computation. + return Uni.createFrom() + .item(() -> "Hello!") + .onItem().delayIt().by(Duration.ofMillis(10)); + } +} +---- + + +[source, shell] +---- +./mvnw compile quarkus:dev +---- + +Then, open your browser to 'http://localhost:8080/lorem' and you should get the message. + +So far so good. +Now let's use the Vert.x API instead of this artificial delay: + +[source,java] +---- +package org.acme.vertx; + +import io.smallrye.mutiny.Uni; +import io.vertx.mutiny.core.Vertx; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@Path("/lorem") public class GreetingResource { @Inject @@ -149,59 +185,30 @@ public class GreetingResource { @GET @Produces(MediaType.TEXT_PLAIN) - @Path("{name}") - public CompletionStage greeting(@PathParam String name) { - // When complete, return the content to the client - CompletableFuture future = new CompletableFuture<>(); - - long start = System.nanoTime(); - - // TODO: asynchronous greeting - - return future; + public Uni doSomethingAsync() { + return vertx.fileSystem().readFile("/META-INF/resources/lorem.txt") + .onItem().apply(b -> b.toString("UTF-8")); } } ---- -So far so good. -Now let's use the Vert.x API to implement the artificially delayed reply with the `setTimer` provided by Vert.x: +In this code, we inject the `vertx` instance (`io.vertx.mutiny.core.Vertx`) and read a file from the file system. -[source,java] +Create the `src/main/resources/META_INF/resources/lorem.txt` file with the following content: + +[source,text] ---- -// Delay reply by 10ms -vertx.setTimer(10, l -> { - // Compute elapsed time in milliseconds - long duration = TimeUnit.MILLISECONDS.convert(System.nanoTime() - start, TimeUnit.NANOSECONDS); - - // Format message - String message = String.format("Hello %s! (%d ms)%n", name, duration); - - // Complete - future.complete(message); -}); +Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ---- -That's it. -Now start Quarkus in `dev` mode with: - -[source, shell] ----- -./mvnw compile quarkus:dev ----- - -Eventually, open your browser and navigate to http://localhost:8080/hello/Quarkus, you should see: - -[source, shell] ----- -Hello Quarkus! (10 ms) ----- +Then, refresh the page, you should see the _lorem ipsum_ text. === Streaming using Server-Sent Events Quarkus web resources that need to send content as https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events[server-sent events] must have a method: * declaring the `text/event-stream` response content type -* returning a https://www.reactive-streams.org/[Reactive Streams] `Publisher` or an RX Java 2 `Observable` or `Flowable` +* returning a https://www.reactive-streams.org/[Reactive Streams] `Publisher` or Mutiny `Multi` (requires the `quarkus-resteasy-mutiny` extension) In practice, a streaming greeting service would look like: @@ -212,50 +219,44 @@ public class StreamingResource { @GET @Produces(MediaType.SERVER_SENT_EVENTS) - @Path("{name}/streaming") - public Publisher greeting(@PathParam String name) { - // TODO: create a Reactive Streams publisher + @Path("/{name}") + public Multi greeting(@PathParam String name) { + // TODO: create a Reactive Streams publisher or a Mutiny Multi return publisher; } } ---- -How to create a Reactive Streams publisher? -There are a few ways to do this: - -1. If you use `io.vertx.axle.core.Vertx`, the API provides `toPublisher` methods (and then use RX Java 2 or Reactive Streams Operators to manipulate the stream) -2. You can also use `io.vertx.reactivex.core.Vertx` which already provides RX Java 2 (RX Java 2 `Flowable` implement Reactive Streams `publisher`). - -The first approach can be implemented as follows: +Now we just need to return our `Publisher` or `Multi`: [source, java] ---- -// Use io.vertx.axle.core.Vertx; -@Inject Vertx vertx; +package org.acme.vertx; -@GET -@Produces(MediaType.SERVER_SENT_EVENTS) -@Path("{name}/streaming") -public Publisher greeting(@PathParam String name) { - return vertx.periodicStream(2000).toPublisherBuilder() - .map(l -> String.format("Hello %s! (%s)%n", name, new Date())) - .buildRs(); -} ----- +import io.smallrye.mutiny.Multi; +import io.vertx.mutiny.core.Vertx; +import org.jboss.resteasy.annotations.jaxrs.PathParam; -The second approach slightly differs: +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import java.util.Date; -[source,java] ----- -// Use io.vertx.reactivex.core.Vertx; -@Inject Vertx vertx; +@Path("/stream") +public class StreamingResource { -@GET -@Produces(MediaType.SERVER_SENT_EVENTS) -@Path("{name}/streaming") -public Publisher greeting(@PathParam String name) { - return vertx.periodicStream(2000).toFlowable() - .map(l -> String.format("Hello %s! (%s)%n", name, new Date())); + @Inject + Vertx vertx; + + @GET + @Produces(MediaType.SERVER_SENT_EVENTS) + @Path("/{name}") + public Multi greeting(@PathParam String name) { + return vertx.periodicStream(2000).toMulti() + .map(l -> String.format("Hello %s! (%s)%n", name, new Date())); + } } ---- @@ -284,13 +285,17 @@ The magic, as always, lies in the Javascript code: [source,javascript] .META-INF/resources/streaming.js ---- -var eventSource = new EventSource("/hello/Quarkus/streaming"); -eventSource.onmessage = function (event) { - var container = document.getElementById("container"); - var paragraph = document.createElement("p"); - paragraph.innerHTML = event.data; - container.appendChild(paragraph); -}; +if (!!window.EventSource) { + var eventSource = new EventSource("/stream/Quarkus"); + eventSource.onmessage = function (event) { + var container = document.getElementById("container"); + var paragraph = document.createElement("p"); + paragraph.innerHTML = event.data; + container.appendChild(paragraph); + }; +} else { + window.alert("EventSource not available on this browser.") +} ---- IMPORTANT: Most browsers support SSE but some don't. @@ -301,11 +306,15 @@ A new greeting should show-up every 2 seconds. [source, text] ---- -Hello Quarkus! (Thu Mar 21 17:26:12 CET 2019) +Hello Quarkus! (Wed Feb 12 17:13:55 CET 2020) -Hello Quarkus! (Thu Mar 21 17:26:14 CET 2019) +Hello Quarkus! (Wed Feb 12 17:13:57 CET 2020) -Hello Quarkus! (Thu Mar 21 17:26:16 CET 2019) +Hello Quarkus! (Wed Feb 12 17:13:59 CET 2020) + +Hello Quarkus! (Wed Feb 12 17:14:01 CET 2020) + +Hello Quarkus! (Wed Feb 12 17:14:03 CET 2020) ... ---- @@ -351,7 +360,7 @@ Then, navigate to http://localhost:8080/hello/Quarkus/array: ["Hello","Quarkus"] ---- -Needless to say, this works equally well when the JSON content is a request body or is wrapped in a `CompletionStage` or `Publisher`. +Needless to say, this works equally well when the JSON content is a request body or is wrapped in a `Uni`, `Multi`, `CompletionStage` or `Publisher`. == Using Vert.x Clients @@ -397,7 +406,7 @@ In this guide, we are going to use the Axle API, so: ---- io.smallrye.reactive - smallrye-axle-web-client + smallrye-mutiny-vertx-web-client ---- @@ -408,11 +417,6 @@ Now, create a new resource in your project with the following content: ---- package org.acme.vertx; -import io.vertx.axle.core.Vertx; -import io.vertx.axle.ext.web.client.WebClient; -import io.vertx.axle.ext.web.codec.BodyCodec; -import io.vertx.core.json.JsonObject; -import io.vertx.ext.web.client.WebClientOptions; import javax.annotation.PostConstruct; import javax.inject.Inject; @@ -420,12 +424,17 @@ import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; -import java.util.concurrent.CompletionStage; + +import io.smallrye.mutiny.Uni; import org.jboss.resteasy.annotations.jaxrs.PathParam; -@Path("/swapi") -public class ResourceUsingWebClient { +import io.vertx.mutiny.core.Vertx; +import io.vertx.mutiny.ext.web.client.WebClient; +import io.vertx.core.json.JsonObject; +import io.vertx.ext.web.client.WebClientOptions; +@Path("/fruit-data") +public class ResourceUsingWebClient { @Inject Vertx vertx; @@ -435,16 +444,17 @@ public class ResourceUsingWebClient { @PostConstruct void initialize() { this.client = WebClient.create(vertx, - new WebClientOptions().setDefaultHost("swapi.co").setDefaultPort(443).setSsl(true)); + new WebClientOptions().setDefaultHost("fruityvice.com") + .setDefaultPort(443).setSsl(true).setTrustAll(true)); } @GET @Produces(MediaType.APPLICATION_JSON) - @Path("/{id}") - public CompletionStage getStarWarsCharacter(@PathParam int id) { - return client.get("/api/people/" + id) + @Path("/{name}") + public Uni getFruitData(@PathParam("name") String name) { + return client.get("/api/fruit/" + name) .send() - .thenApply(resp -> { + .onItem().apply(resp -> { if (resp.statusCode() == 200) { return resp.bodyAsJsonObject(); } else { @@ -456,11 +466,12 @@ public class ResourceUsingWebClient { } } + ---- -This resource creates a `WebClient` and upon request use this client to invoke the https://swapi.co/ API. +This resource creates a `WebClient` and upon request use this client to invoke the _fruityvice_ API. Depending on the result the response is forwarded as it's received, or a new JSON object is created with the status and body. -The `WebClient` is obviously asynchronous (and non-blocking), to the endpoint returns a `CompletionStage`. +The `WebClient` is obviously asynchronous (and non-blocking), to the endpoint returns a `Uni`. Run the application with: @@ -469,7 +480,7 @@ Run the application with: ./mvnw compile quarkus:dev ---- -And then, open a browser to: http://localhost:8080/swapi/1. You should get _Luke Skywalker_. +And then, open a browser to: `http://localhost:8080/fruit-data/pear`. You should get some details about pears. The application can also run as a native executable. But, first, we need to instruct Quarkus to enable _ssl_. diff --git a/extensions/resteasy-mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/resteasy-mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml index c0b0d4172..e3d1ac64e 100644 --- a/extensions/resteasy-mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/extensions/resteasy-mutiny/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -8,4 +8,4 @@ metadata: categories: - "web" - "reactive" - status: "unstable" \ No newline at end of file + status: "experimental"