diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 805bde451bd69..a863cd7979a52 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -44,7 +44,7 @@ 3.3.1 3.0.5 2.3.1 - 1.9.0 + 1.9.1 2.1.1 5.6.0 3.5.4 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java index 5238f2e607e2f..947e55779c7b5 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/steps/MainClassBuildStep.java @@ -418,25 +418,15 @@ public MainClassBuildItem mainClassBuildStep(BuildProducer d.equals(mainClassDotName)); + if (!hasQuarkusApplicationInterface) { className += "Kt"; } } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java index aa3ef463c7f52..3e10e02d71243 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/DevMojo.java @@ -382,6 +382,15 @@ public class DevMojo extends AbstractMojo { private Pty pty; private boolean windowsColorSupport; + /** + * Indicates for which launch mode the dependencies should be resolved. + * + * @return launch mode for which the dependencies should be resolved + */ + protected LaunchMode getLaunchModeClasspath() { + return LaunchMode.DEVELOPMENT; + } + @Override public void setLog(Log log) { super.setLog(log); @@ -1089,7 +1098,7 @@ private QuarkusDevModeLauncher newLauncher(Boolean debugPortOk) throws Exception final Path appModelLocation = resolveSerializedModelLocation(); ApplicationModel appModel = bootstrapProvider - .getResolvedApplicationModel(QuarkusBootstrapProvider.getProjectId(project), LaunchMode.DEVELOPMENT); + .getResolvedApplicationModel(QuarkusBootstrapProvider.getProjectId(project), getLaunchModeClasspath()); if (appModel != null) { bootstrapProvider.close(); } else { @@ -1118,6 +1127,7 @@ private QuarkusDevModeLauncher newLauncher(Boolean debugPortOk) throws Exception final BootstrapMavenContext mvnCtx = new BootstrapMavenContext(mvnConfig); appModel = new BootstrapAppModelResolver(new MavenArtifactResolver(mvnCtx)) .setDevMode(true) + .setTest(LaunchMode.TEST.equals(getLaunchModeClasspath())) .setCollectReloadableDependencies(!noDeps) .resolveModel(mvnCtx.getCurrentProject().getAppArtifact()); } diff --git a/devtools/maven/src/main/java/io/quarkus/maven/TestMojo.java b/devtools/maven/src/main/java/io/quarkus/maven/TestMojo.java index a8e3008b568ec..bfbf3eff00656 100644 --- a/devtools/maven/src/main/java/io/quarkus/maven/TestMojo.java +++ b/devtools/maven/src/main/java/io/quarkus/maven/TestMojo.java @@ -9,12 +9,19 @@ import io.quarkus.bootstrap.app.QuarkusBootstrap; import io.quarkus.deployment.dev.DevModeContext; import io.quarkus.deployment.dev.IsolatedTestModeMain; +import io.quarkus.runtime.LaunchMode; /** * The test mojo, that starts continuous testing outside of dev mode */ @Mojo(name = "test", defaultPhase = LifecyclePhase.PREPARE_PACKAGE, requiresDependencyResolution = ResolutionScope.TEST, threadSafe = true) public class TestMojo extends DevMojo { + + @Override + protected LaunchMode getLaunchModeClasspath() { + return LaunchMode.TEST; + } + @Override protected void modifyDevModeContext(MavenDevModeLauncher.Builder builder) { builder.entryPointCustomizer(new Consumer() { diff --git a/docs/src/main/asciidoc/security-openid-connect-client.adoc b/docs/src/main/asciidoc/security-openid-connect-client.adoc index 608ad5f4f3cb9..194345a6458bd 100644 --- a/docs/src/main/asciidoc/security-openid-connect-client.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-client.adoc @@ -139,7 +139,7 @@ public class ProtectedResource { As you can see `ProtectedResource` returns a name from both `userName()` and `adminName()` methods. The name is extracted from the current `JsonWebToken`. -Next let's add a REST Client with `OpenID Connect Client Reactive Filter` and another REST Client with `OpenID Connect Token Propagation Filter`. `FrontendResource` will use these two clients to call `ProtectedResource`: +Next let's add a REST Client with `OidcClientRequestReactiveFilter` and another REST Client with `AccessTokenRequestReactiveFilter`. `FrontendResource` will use these two clients to call `ProtectedResource`: [source,java] ---- diff --git a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc index 87f0feb9c3b87..3aed582620151 100644 --- a/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc +++ b/docs/src/main/asciidoc/security-openid-connect-web-authentication.adoc @@ -447,15 +447,16 @@ quarkus.oidc.credentials.secret=secret quarkus.oidc.application-type=web-app quarkus.oidc.logout.path=/logout +# Logged-out users should be returned to the /welcome.html site which will offer an option to re-login: quarkus.oidc.logout.post-logout-path=/welcome.html # Only the authenticated users can initiate a logout: quarkus.http.auth.permission.authenticated.paths=/logout quarkus.http.auth.permission.authenticated.policy=authenticated -# Logged-out users should be returned to the /welcome.html site which will offer an option to re-login: -quarkus.http.auth.permission.authenticated.paths=/welcome.html -quarkus.http.auth.permission.authenticated.policy=permit +# All users can see the welcome page: +quarkus.http.auth.permission.public.paths=/welcome.html +quarkus.http.auth.permission.public.policy=permit ---- You may also need to set `quarkus.oidc.authentication.cookie-path` to a path value common to all the application resources which is `/` in this example. diff --git a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/QuarkusNettyConnectionCache.java b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/QuarkusNettyConnectionCache.java index f2a124fb7776c..cfe46c1a7ab7b 100644 --- a/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/QuarkusNettyConnectionCache.java +++ b/extensions/agroal/runtime/src/main/java/io/quarkus/agroal/runtime/QuarkusNettyConnectionCache.java @@ -9,7 +9,7 @@ class QuarkusNettyConnectionCache implements ConnectionCache { - volatile FastThreadLocal connectionCache = new FastThreadLocal<>(); + final FastThreadLocal connectionCache = new FastThreadLocal<>(); @Override public Acquirable get() { @@ -36,6 +36,21 @@ public void put(Acquirable acquirable) { @Override public void reset() { - connectionCache = new FastThreadLocal<>(); + // Do our best to release memory. In fact `io.agroal.pool.ConnectionPool` calls + // this method in `housingkeepingExecutor` thread only, so business threads still + // hold references to `ConnectionHandler` objects. + connectionCache.remove(); + + // `FastThreadLocalThread` uses an array and increasing index for `FastThreadLocal`, the thread + // local variables will *never* be expunged until the thread exits, so `FastThreadLocal` instance + // musn't be created again. + // + // For other non-`FastThreadLocalThread` threads, `FastThreadLocal` actually uses static + // `java.lang.ThreadLocal`, it's useless to create fresh `FastThreadLocal` instance. + // + // In summary, this connection cache always holds `ConnectionHandler` instances in thread local + // variables even after the underlying JDBC connections close, this does leak some memory. + + //connectionCache = new FastThreadLocal<>(); } } diff --git a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java index 050bc7cb27e28..043ba96b1ea15 100644 --- a/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java +++ b/extensions/kubernetes/vanilla/deployment/src/main/java/io/quarkus/kubernetes/deployment/KubernetesCommonHelper.java @@ -55,7 +55,6 @@ import io.dekorate.kubernetes.decorator.RemoveAnnotationDecorator; import io.dekorate.kubernetes.decorator.RemoveFromMatchingLabelsDecorator; import io.dekorate.kubernetes.decorator.RemoveFromSelectorDecorator; -import io.dekorate.kubernetes.decorator.RemoveLabelDecorator; import io.dekorate.project.BuildInfo; import io.dekorate.project.FileProjectFactory; import io.dekorate.project.Project; @@ -208,13 +207,11 @@ private static Collection createLabelDecorators(Optional multiStreamJson() { return Multi.createFrom().items(new Message("hello"), new Message("stef")); } + /** + * Reproduce #30044. + */ + @Path("stream-json/multi/fast") + @GET + @Produces(RestMediaType.APPLICATION_STREAM_JSON) + @RestStreamElementType(MediaType.APPLICATION_JSON) + public Multi multiStreamJsonFast() { + List ids = new ArrayList<>(5000); + for (int i = 0; i < 5000; i++) { + ids.add(UUID.randomUUID()); + } + return Multi.createFrom().items(ids::stream) + .onItem().transform(id -> new Message(id.toString())) + .onOverflow().buffer(81920); + } + } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/sse/SseTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/streams/StreamTestCase.java similarity index 78% rename from extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/sse/SseTestCase.java rename to extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/streams/StreamTestCase.java index afb909111ba93..bc82769dd1d7a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/sse/SseTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jackson/deployment/src/test/java/io/quarkus/resteasy/reactive/jackson/deployment/test/streams/StreamTestCase.java @@ -1,4 +1,4 @@ -package io.quarkus.resteasy.reactive.jackson.deployment.test.sse; +package io.quarkus.resteasy.reactive.jackson.deployment.test.streams; import static io.restassured.RestAssured.when; import static org.assertj.core.api.Assertions.assertThat; @@ -33,7 +33,7 @@ import io.quarkus.test.common.http.TestHTTPResource; import io.smallrye.mutiny.Multi; -public class SseTestCase { +public class StreamTestCase { @TestHTTPResource URI uri; @@ -41,16 +41,16 @@ public class SseTestCase { @RegisterExtension static final QuarkusUnitTest config = new QuarkusUnitTest() .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) - .addClasses(SseResource.class, Message.class)); + .addClasses(StreamResource.class, Message.class)); @Test public void testSseFromSse() throws Exception { - testSse("sse"); + testSse("streams"); } @Test public void testSseFromMulti() throws Exception { - testSse("sse/multi"); + testSse("streams/multi"); } private void testSse(String path) throws Exception { @@ -81,12 +81,12 @@ public void accept(Throwable throwable) { @Test public void testMultiFromSse() { - testMulti("sse"); + testMulti("streams"); } @Test public void testMultiFromMulti() { - testMulti("sse/multi"); + testMulti("streams/multi"); } private void testMulti(String path) { @@ -99,24 +99,24 @@ private void testMulti(String path) { @Test public void testJsonMultiFromSse() { - testJsonMulti("sse/json"); - testJsonMulti("sse/json2"); - testJsonMulti("sse/blocking/json"); + testJsonMulti("streams/json"); + testJsonMulti("streams/json2"); + testJsonMulti("streams/blocking/json"); } @Test public void testJsonMultiFromMulti() { - testJsonMulti("sse/json/multi"); + testJsonMulti("streams/json/multi"); } @Test public void testJsonMultiFromMultiWithDefaultElementType() { - testJsonMulti("sse/json/multi2"); + testJsonMulti("streams/json/multi2"); } @Test public void testNdJsonMultiFromMulti() { - when().get(uri.toString() + "sse/ndjson/multi") + when().get(uri.toString() + "streams/ndjson/multi") .then().statusCode(HttpStatus.SC_OK) // @formatter:off .body(is("{\"name\":\"hello\"}\n" @@ -127,7 +127,7 @@ public void testNdJsonMultiFromMulti() { @Test public void testStreamJsonMultiFromMulti() { - when().get(uri.toString() + "sse/stream-json/multi") + when().get(uri.toString() + "streams/stream-json/multi") .then().statusCode(HttpStatus.SC_OK) // @formatter:off .body(is("{\"name\":\"hello\"}\n" @@ -143,4 +143,19 @@ private void testJsonMulti(String path) { List list = multi.collect().asList().await().atMost(Duration.ofSeconds(30)); assertThat(list).extracting("name").containsExactly("hello", "stef"); } + + /** + * Reproduce #30044. + */ + @Test + public void testStreamJsonMultiFromMultiFast() { + String payload = when().get(uri.toString() + "streams/stream-json/multi/fast") + .then().statusCode(HttpStatus.SC_OK) + .header(HttpHeaders.CONTENT_TYPE, containsString(RestMediaType.APPLICATION_STREAM_JSON)) + .extract().response().asString(); + + // the payload include 5000 json objects + assertThat(payload.lines()).hasSize(5000) + .allSatisfy(s -> assertThat(s).matches("\\{\"name\":\".*\"}")); + } } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java index cd018c190b115..9584c5efd89f1 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive-jsonb/deployment/src/test/java/io/quarkus/resteasy/reactive/jsonb/deployment/test/sse/SseParserTest.java @@ -43,8 +43,8 @@ public void testParser() { // all fields testParser("data:DATA\nid:ID\n:COMMENT\nretry:23\nevent:NAME\n\n", "DATA", "COMMENT", "ID", "NAME", 23); - // all fields and no data: no event - testParser("id:ID\n:COMMENT\nretry:23\nevent:NAME\n\n", null, null, null, null, SseEvent.RECONNECT_NOT_SET); + // all fields and no data + testParser("id:ID\n:COMMENT\nretry:23\nevent:NAME\n\n", null, "COMMENT", "ID", "NAME", 23); // optional space after colon testParser("data:foo\n\n", "foo", null, null, null, SseEvent.RECONNECT_NOT_SET); @@ -147,6 +147,13 @@ private void testParser(String event, String data, String comment, String lastId .setId(lastId) .setName(name) .setReconnectDelay(reconnectDelay))); + } else if (comment != null) { + testParser(Collections.singletonList(event), Collections.singletonList(new InboundSseEventImpl(null, null) + .setData(null) + .setComment(comment) + .setId(lastId) + .setName(name) + .setReconnectDelay(reconnectDelay))); } else { testParser(Collections.singletonList(event), Collections.emptyList()); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamResource.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamResource.java index 307d5b77d5d81..31f931789d3de 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamResource.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamResource.java @@ -160,7 +160,8 @@ public Multi sseThrows() { @GET @Produces(MediaType.SERVER_SENT_EVENTS) public Multi sseRaw(@Context Sse sse) { - return Multi.createFrom().items(sse.newEventBuilder().id("one").data("uno").name("eins").build(), + return Multi.createFrom().items(sse.newEventBuilder().comment("dummy").build(), + sse.newEventBuilder().id("one").data("uno").name("eins").build(), sse.newEventBuilder().id("two").data("dos").name("zwei").build(), sse.newEventBuilder().id("three").data("tres").name("drei").build()); } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java index d43b1ddb1249b..f1615e674ad80 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/test/java/io/quarkus/resteasy/reactive/server/test/stream/StreamTestCase.java @@ -3,7 +3,6 @@ import java.net.URI; import java.nio.charset.StandardCharsets; import java.time.Duration; -import java.util.Arrays; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CopyOnWriteArrayList; @@ -211,7 +210,7 @@ public void testSse() throws InterruptedException { }); sse.open(); Assertions.assertTrue(latch.await(20, TimeUnit.SECONDS)); - Assertions.assertEquals(Arrays.asList("a", "b", "c"), results); + org.assertj.core.api.Assertions.assertThat(results).containsExactly("a", "b", "c"); Assertions.assertEquals(0, errors.size()); } } @@ -248,7 +247,9 @@ public void testSseForMultiWithOutboundSseEvent() throws InterruptedException { List results = new CopyOnWriteArrayList<>(); List ids = new CopyOnWriteArrayList<>(); List names = new CopyOnWriteArrayList<>(); + List comments = new CopyOnWriteArrayList<>(); sse.register(event -> { + comments.add(event.getComment()); results.add(event.readData()); ids.add(event.getId()); names.add(event.getName()); @@ -259,9 +260,10 @@ public void testSseForMultiWithOutboundSseEvent() throws InterruptedException { }); sse.open(); Assertions.assertTrue(latch.await(20, TimeUnit.SECONDS)); - Assertions.assertEquals(Arrays.asList("uno", "dos", "tres"), results); - Assertions.assertEquals(Arrays.asList("one", "two", "three"), ids); - Assertions.assertEquals(Arrays.asList("eins", "zwei", "drei"), names); + org.assertj.core.api.Assertions.assertThat(results).containsExactly(null, "uno", "dos", "tres"); + org.assertj.core.api.Assertions.assertThat(ids).containsExactly(null, "one", "two", "three"); + org.assertj.core.api.Assertions.assertThat(names).containsExactly(null, "eins", "zwei", "drei"); + org.assertj.core.api.Assertions.assertThat(comments).containsExactly("dummy", null, null, null); Assertions.assertEquals(0, errors.size()); } } diff --git a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java index 0ac3da55c1ebd..3e8ae58571990 100644 --- a/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java +++ b/extensions/smallrye-graphql/deployment/src/main/java/io/quarkus/smallrye/graphql/deployment/SmallRyeGraphQLProcessor.java @@ -20,6 +20,7 @@ import org.eclipse.microprofile.config.ConfigProvider; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; import org.jboss.jandex.Indexer; @@ -272,8 +273,10 @@ void buildExecutionService( SmallRyeGraphQLRecorder recorder, SmallRyeGraphQLFinalIndexBuildItem graphQLFinalIndexBuildItem, BeanContainerBuildItem beanContainer, + BuildProducer systemPropertyProducer, SmallRyeGraphQLConfig graphQLConfig) { + activateFederation(graphQLConfig, systemPropertyProducer, graphQLFinalIndexBuildItem); Schema schema = SchemaBuilder.build(graphQLFinalIndexBuildItem.getFinalIndex(), graphQLConfig.autoNameStrategy); RuntimeValue initialized = recorder.createExecutionService(beanContainer.getValue(), schema); @@ -627,6 +630,43 @@ void activateEventing(SmallRyeGraphQLConfig graphQLConfig, BuildProducer systemProperties, + SmallRyeGraphQLFinalIndexBuildItem index) { + if (config.federationEnabled.isPresent()) { + String value = config.federationEnabled.get().toString(); + systemProperties.produce(new SystemPropertyBuildItem(ConfigKey.ENABLE_FEDERATION, value)); + System.setProperty(ConfigKey.ENABLE_FEDERATION, value); + } else { + // + boolean foundAnyFederationAnnotation = false; + for (ClassInfo federationAnnotationType : index.getFinalIndex() + .getClassesInPackage("io.smallrye.graphql.api.federation")) { + if (federationAnnotationType.isAnnotation()) { + if (!index.getFinalIndex().getAnnotations(federationAnnotationType.name()).isEmpty()) { + foundAnyFederationAnnotation = true; + } + } + } + String value = Boolean.toString(foundAnyFederationAnnotation); + systemProperties.produce(new SystemPropertyBuildItem(ConfigKey.ENABLE_FEDERATION, value)); + System.setProperty(ConfigKey.ENABLE_FEDERATION, value); + } + } + private boolean shouldActivateService(Optional serviceEnabled, boolean linkedCapabilityIsPresent, String linkedExtensionName, diff --git a/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLFederationDisabledTest.java b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLFederationDisabledTest.java new file mode 100644 index 0000000000000..8c6e5b49f73ad --- /dev/null +++ b/extensions/smallrye-graphql/deployment/src/test/java/io/quarkus/smallrye/graphql/deployment/GraphQLFederationDisabledTest.java @@ -0,0 +1,51 @@ +package io.quarkus.smallrye.graphql.deployment; + +import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.containsString; + +import org.eclipse.microprofile.graphql.GraphQLApi; +import org.eclipse.microprofile.graphql.Query; +import org.jboss.shrinkwrap.api.asset.EmptyAsset; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.restassured.RestAssured; + +/** + * Make sure that if no Federation related annotations are in the application, then Federation is + * disabled (unless explicitly enabled in the config). + */ +public class GraphQLFederationDisabledTest extends AbstractGraphQLTest { + + @RegisterExtension + static QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(FooApi.class, Foo.class) + .addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml")); + + @Test + public void checkSchemaDoesNotIncludeServiceDeclaration() { + RestAssured.given() + .get("/graphql/schema.graphql") + .then() + .body(not(containsString("type _Service {"))); + } + + @GraphQLApi + static class FooApi { + + @Query + public Foo foo() { + return new Foo(); + } + + } + + static class Foo { + + public String name; + + } + +} diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLConfig.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLConfig.java index fa340d09cf5a2..84a9c90c7ce49 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLConfig.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLConfig.java @@ -20,6 +20,13 @@ public class SmallRyeGraphQLConfig { @ConfigItem(defaultValue = "graphql") public String rootPath; + /** + * Enable Apollo Federation. If this value is unspecified, then federation will be enabled + * automatically if any GraphQL Federation annotations are detected in the application. + */ + @ConfigItem(name = "federation.enabled") + public Optional federationEnabled; + /** * Enable metrics. By default, this is false. If set to true, a metrics extension is required. */ diff --git a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java index 7ff8e8d403998..127e1ed8cc759 100644 --- a/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java +++ b/extensions/smallrye-graphql/runtime/src/main/java/io/quarkus/smallrye/graphql/runtime/SmallRyeGraphQLExecutionHandler.java @@ -13,9 +13,12 @@ import javax.json.JsonObjectBuilder; import javax.json.JsonReader; +import org.jboss.logging.Logger; + import graphql.ErrorType; import graphql.ExecutionResult; import graphql.GraphQLError; +import graphql.execution.AbortExecutionException; import io.quarkus.security.identity.CurrentIdentityAssociation; import io.quarkus.vertx.http.runtime.CurrentVertxRequest; import io.smallrye.graphql.execution.ExecutionResponse; @@ -46,6 +49,8 @@ public class SmallRyeGraphQLExecutionHandler extends SmallRyeGraphQLAbstractHand + StandardCharsets.UTF_8.name(); private static final String MISSING_OPERATION = "Missing operation body"; + private static final Logger log = Logger.getLogger(SmallRyeGraphQLExecutionHandler.class); + public SmallRyeGraphQLExecutionHandler(boolean allowGet, boolean allowPostWithQueryParameters, boolean runBlocking, CurrentIdentityAssociation currentIdentityAssociation, CurrentVertxRequest currentVertxRequest) { @@ -320,8 +325,13 @@ class VertxExecutionResponseWriter implements ExecutionResponseWriter { @Override public void write(ExecutionResponse er) { - if (shouldFail(er)) { + er.getExecutionResult() + .getErrors().stream() + .filter(e -> e.getErrorType().equals(ErrorType.ExecutionAborted)) + .forEach(e -> { + log.error("Execution aborted", (AbortExecutionException) e); + }); response.setStatusCode(500) .end(); } else { diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java index bfa14a3f460dc..027526b66551a 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java @@ -406,7 +406,12 @@ public InputStream getResourceAsStream(String unsanitisedName) { if (name.endsWith(".class")) { ClassPathElement[] providers = state.loadableResources.get(name); if (providers != null) { - return new ByteArrayInputStream(providers[0].getResource(name).getData()); + final ClassPathResource resource = providers[0].getResource(name); + if (resource == null) { + throw new IllegalStateException(providers[0] + " from " + getName() + " (closed=" + this.isClosed() + + ") was expected to provide " + name + " but failed"); + } + return new ByteArrayInputStream(resource.getData()); } } else { for (ClassPathElement i : elements) { diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java index c6edb7584aae4..aa4129bbd27b3 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/MultiInvoker.java @@ -1,6 +1,7 @@ package org.jboss.resteasy.reactive.client.impl; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; @@ -18,6 +19,7 @@ import io.vertx.core.buffer.Buffer; import io.vertx.core.http.HttpClientResponse; import io.vertx.core.net.impl.ConnectionBase; +import io.vertx.core.parsetools.RecordParser; public class MultiInvoker extends AbstractRxInvoker> { @@ -121,10 +123,13 @@ public Multi method(String name, Entity entity, GenericType respons } else { HttpClientResponse vertxResponse = restClientRequestContext.getVertxClientResponse(); if (!emitter.isCancelled()) { - // FIXME: this is probably not good enough if (response.getStatus() == 200 && MediaType.SERVER_SENT_EVENTS_TYPE.isCompatible(response.getMediaType())) { registerForSse(multiRequest, responseType, response, vertxResponse); + } else if (response.getStatus() == 200 + && RestMediaType.APPLICATION_STREAM_JSON_TYPE.isCompatible(response.getMediaType())) { + registerForJsonStream(multiRequest, restClientRequestContext, responseType, response, + vertxResponse); } else { // read stuff in chunks registerForChunks(multiRequest, restClientRequestContext, responseType, response, vertxResponse); @@ -267,4 +272,46 @@ private boolean startsWith(byte[] array, byte[] prefix) { }); } + private void registerForJsonStream(MultiRequest multiRequest, + RestClientRequestContext restClientRequestContext, + GenericType responseType, + ResponseImpl response, + HttpClientResponse vertxClientResponse) { + RecordParser parser = RecordParser.newDelimited("\n"); + parser.handler(new Handler() { + @Override + public void handle(Buffer chunk) { + + ByteArrayInputStream in = new ByteArrayInputStream(chunk.getBytes()); + try { + R item = restClientRequestContext.readEntity(in, responseType, response.getMediaType(), + response.getMetadata()); + multiRequest.emitter.emit(item); + } catch (IOException e) { + multiRequest.emitter.fail(e); + } + } + }); + vertxClientResponse.exceptionHandler(t -> { + if (t == ConnectionBase.CLOSED_EXCEPTION) { + // we can ignore this one since we registered a closeHandler + } else { + multiRequest.emitter.fail(t); + } + }); + vertxClientResponse.endHandler(new Handler() { + @Override + public void handle(Void c) { + multiRequest.complete(); + } + }); + + vertxClientResponse.handler(parser); + + // watch for user cancelling + multiRequest.onCancel(() -> { + vertxClientResponse.request().connection().close(); + }); + } + } diff --git a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java index e503570b36274..47b8e54957a69 100644 --- a/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java +++ b/independent-projects/resteasy-reactive/client/runtime/src/main/java/org/jboss/resteasy/reactive/client/impl/SseParser.java @@ -145,7 +145,7 @@ private void parseEvent() { private void dispatchEvent() { // ignore empty events - if (dataBuffer.length() == 0) + if (dataBuffer.length() == 0 && commentBuffer.length() == 0) return; WebTargetImpl webTarget = sseEventSource.getWebTarget(); InboundSseEventImpl event; @@ -159,7 +159,7 @@ private void dispatchEvent() { event.setComment(commentBuffer.length() == 0 ? null : commentBuffer.toString()); // SSE spec says empty string is the default, but JAX-RS says null if not specified event.setId(lastEventId); - event.setData(dataBuffer.toString()); + event.setData(dataBuffer.length() == 0 ? null : dataBuffer.toString()); // SSE spec says "message" is the default, but JAX-RS says null if not specified event.setName(eventType); event.setReconnectDelay(eventReconnectTime); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/SseUtil.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/SseUtil.java index b0969d595fc1b..24de690d468c9 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/SseUtil.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/SseUtil.java @@ -69,8 +69,10 @@ private static String serialiseEvent(ResteasyReactiveRequestContext context, Out if (event.getReconnectDelay() >= 0) serialiseField(context, sb, "retry", Long.toString(event.getReconnectDelay()), false); } - String data = serialiseDataToString(context, event, eventMediaType); - serialiseField(context, sb, "data", data, true); + if (event.getData() != null) { + String data = serialiseDataToString(context, event, eventMediaType); + serialiseField(context, sb, "data", data, true); + } sb.append(NL); // return a UTF8 buffer return sb.toString(); diff --git a/integration-tests/gradle/src/main/resources/kotlin-grpc-project/src/main/kotlin/org/acme/Main.kt b/integration-tests/gradle/src/main/resources/kotlin-grpc-project/src/main/kotlin/org/acme/Main.kt new file mode 100644 index 0000000000000..46b8d7e400e0d --- /dev/null +++ b/integration-tests/gradle/src/main/resources/kotlin-grpc-project/src/main/kotlin/org/acme/Main.kt @@ -0,0 +1,15 @@ +package org.acme + +import io.quarkus.runtime.Quarkus +import io.quarkus.runtime.QuarkusApplication +import io.quarkus.runtime.annotations.QuarkusMain + +@QuarkusMain +class Main : QuarkusApplication { + + @Throws(Exception::class) + override fun run(vararg args: String?): Int { + Quarkus.waitForExit() + return 0 + } +} diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithApplicationPropertiesTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithApplicationPropertiesTest.java index 3156b4763d6bf..78e00fa64cb5e 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithApplicationPropertiesTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/KubernetesWithApplicationPropertiesTest.java @@ -48,7 +48,8 @@ public void assertGeneratedResources() throws IOException { assertThat(i).isInstanceOfSatisfying(Deployment.class, d -> { assertThat(d.getMetadata()).satisfies(m -> { assertThat(m.getName()).isEqualTo("test-it"); - assertThat(m.getLabels()).contains(entry("foo", "bar")); + assertThat(m.getLabels()).contains(entry("foo", "bar")) + .containsKey("app.kubernetes.io/version"); // make sure the version was not removed from the labels assertThat(m.getNamespace()).isEqualTo("applications"); }); diff --git a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithApplicationPropertiesTest.java b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithApplicationPropertiesTest.java index 8962e67945cb0..6e8c0e89b7496 100644 --- a/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithApplicationPropertiesTest.java +++ b/integration-tests/kubernetes/quarkus-standard-way/src/test/java/io/quarkus/it/kubernetes/OpenshiftWithApplicationPropertiesTest.java @@ -45,7 +45,8 @@ public void assertGeneratedResources() throws IOException { assertThat(openshiftList).filteredOn(h -> "DeploymentConfig".equals(h.getKind())).singleElement().satisfies(h -> { assertThat(h.getMetadata()).satisfies(m -> { assertThat(m.getName()).isEqualTo("test-it"); - assertThat(m.getLabels()).contains(entry("foo", "bar")); + assertThat(m.getLabels()).contains(entry("foo", "bar")) + .containsKey("app.kubernetes.io/version"); // make sure the version was not removed from the labels assertThat(m.getNamespace()).isEqualTo("applications"); }); AbstractObjectAssert specAssert = assertThat(h).extracting("spec"); diff --git a/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java index 9e88e5bd86c63..9e94a122f1f6b 100644 --- a/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java +++ b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/ClientCallingResource.java @@ -118,6 +118,9 @@ void init(@Observes Router router) { router.post("/hello").handler(rc -> rc.response().putHeader("content-type", MediaType.TEXT_PLAIN) .end("Hello, " + (rc.getBodyAsString()).repeat(getCount(rc)))); + router.post("/hello/fromMessage").handler(rc -> rc.response().putHeader("content-type", MediaType.TEXT_PLAIN) + .end(rc.body().asJsonObject().getString("message"))); + router.route("/call-hello-client").blockingHandler(rc -> { String url = rc.getBody().toString(); HelloClient client = RestClientBuilder.newBuilder().baseUri(URI.create(url)) @@ -126,6 +129,14 @@ void init(@Observes Router router) { rc.response().end(greeting); }); + router.route("/call-helloFromMessage-client").blockingHandler(rc -> { + String url = rc.getBody().toString(); + HelloClient client = RestClientBuilder.newBuilder().baseUri(URI.create(url)) + .build(HelloClient.class); + String greeting = client.fromMessage(new HelloClient.Message("Hello world")); + rc.response().end(greeting); + }); + router.post("/params/param").handler(rc -> rc.response().putHeader("content-type", MediaType.TEXT_PLAIN) .end(getParam(rc))); diff --git a/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/HelloClient.java b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/HelloClient.java index bc09e13d357ac..4d9ade9614667 100644 --- a/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/HelloClient.java +++ b/integration-tests/rest-client-reactive/src/main/java/io/quarkus/it/rest/client/main/HelloClient.java @@ -9,6 +9,8 @@ import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import com.fasterxml.jackson.annotation.JsonCreator; + import io.quarkus.rest.client.reactive.ClientExceptionMapper; @Path("") @@ -18,6 +20,12 @@ public interface HelloClient { @Consumes(MediaType.TEXT_PLAIN) String greeting(String name, @QueryParam("count") int count); + @Path("fromMessage") + @POST + @Produces(MediaType.TEXT_PLAIN) + @Consumes(MediaType.APPLICATION_JSON) + String fromMessage(Message message); + // this isn't used, but it makes sure that the generated provider can be properly instantiated in native mode @ClientExceptionMapper static RuntimeException toException(Response response) { @@ -26,4 +34,18 @@ static RuntimeException toException(Response response) { } return null; } + + class Message { + + private final String message; + + @JsonCreator + public Message(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + } } diff --git a/integration-tests/rest-client-reactive/src/test/java/io/quarkus/it/rest/client/BasicTest.java b/integration-tests/rest-client-reactive/src/test/java/io/quarkus/it/rest/client/BasicTest.java index 937d70633a9fb..bfe4f408ed506 100644 --- a/integration-tests/rest-client-reactive/src/test/java/io/quarkus/it/rest/client/BasicTest.java +++ b/integration-tests/rest-client-reactive/src/test/java/io/quarkus/it/rest/client/BasicTest.java @@ -42,6 +42,12 @@ public void shouldMakeTextRequest() { assertThat(response.asString()).isEqualTo("Hello, JohnJohn"); } + @Test + public void shouldMakeJsonRequestAndGetTextResponse() { + Response response = RestAssured.with().body(helloUrl).post("/call-helloFromMessage-client"); + assertThat(response.asString()).isEqualTo("Hello world"); + } + @Test public void restResponseShouldWorkWithNonSuccessfulResponse() { Response response = RestAssured.with().body(helloUrl).post("/rest-response"); diff --git a/jakarta/rewrite.yml b/jakarta/rewrite.yml index 43f4b630cb42b..978eed5d96d41 100644 --- a/jakarta/rewrite.yml +++ b/jakarta/rewrite.yml @@ -573,7 +573,7 @@ recipeList: newValue: '6.1.0' - org.openrewrite.maven.ChangePropertyValue: key: smallrye-graphql.version - newValue: '2.0.0' + newValue: '2.0.1' - org.openrewrite.maven.ChangePropertyValue: key: smallrye-health.version newValue: '4.0.1'