diff --git a/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsAdd.java b/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsAdd.java index 3b1266ecdae52..4d3e1572d771c 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsAdd.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsAdd.java @@ -17,7 +17,7 @@ public class ProjectExtensionsAdd extends BaseBuildCommand implements Callable extensions; @Override diff --git a/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsRemove.java b/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsRemove.java index adf82223bccfc..2affcc921bf42 100644 --- a/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsRemove.java +++ b/devtools/cli/src/main/java/io/quarkus/cli/ProjectExtensionsRemove.java @@ -17,7 +17,7 @@ public class ProjectExtensionsRemove extends BaseBuildCommand implements Callabl @CommandLine.Mixin RunModeOption runMode; - @CommandLine.Parameters(arity = "1", paramLabel = "EXTENSION", description = "Extension(s) to remove from this project.") + @CommandLine.Parameters(arity = "1", paramLabel = "EXTENSION", description = "Extension(s) to remove from this project.", split = ",") Set extensions; @Override diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliDriver.java b/devtools/cli/src/test/java/io/quarkus/cli/CliDriver.java index 6bbbc63a26463..60af7b5aabb94 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliDriver.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliDriver.java @@ -319,7 +319,7 @@ public static Result invokeExtensionRemoveQute(Path projectRoot, Path file) thro } public static Result invokeExtensionAddMultiple(Path projectRoot, Path file) throws Exception { - // add the qute extension + // add amazon-lambda-http and jackson extensions Result result = execute(projectRoot, "extension", "add", "amazon-lambda-http", "jackson", "-e", "-B", "--verbose"); Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code. Result:\n" + result); @@ -345,7 +345,7 @@ public static Result invokeExtensionAddMultiple(Path projectRoot, Path file) thr } public static Result invokeExtensionRemoveMultiple(Path projectRoot, Path file) throws Exception { - // add the qute extension + // remove amazon-lambda-http and jackson extensions Result result = execute(projectRoot, "extension", "remove", "amazon-lambda-http", "jackson", "-e", "-B", "--verbose"); Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, "Expected OK return code. Result:\n" + result); @@ -370,6 +370,52 @@ public static Result invokeExtensionRemoveMultiple(Path projectRoot, Path file) return result; } + public static Result invokeExtensionAddMultipleCommas(Path projectRoot, Path file) throws Exception { + Result result = execute(projectRoot, "extension", "add", + "quarkus-resteasy-reactive-jsonb,quarkus-resteasy-reactive-jackson", "-e", "-B", "--verbose"); + Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, + "Expected OK return code. Result:\n" + result); + + result = invokeValidateExtensionList(projectRoot); + Assertions.assertTrue(result.stdout.contains("quarkus-qute"), + "Expected quarkus-qute to be in the list of extensions. Result:\n" + result); + Assertions.assertTrue(result.stdout.contains("quarkus-resteasy-reactive-jsonb"), + "Expected quarkus-resteasy-reactive-jsonb to be in the list of extensions. Result:\n" + result); + Assertions.assertTrue(result.stdout.contains("quarkus-resteasy-reactive-jackson"), + "Expected quarkus-resteasy-reactive-jackson to be in the list of extensions. Result:\n" + result); + + String content = CliDriver.readFileAsString(file); + Assertions.assertTrue(content.contains("quarkus-qute"), + "quarkus-qute should still be listed as a dependency. Result:\n" + content); + Assertions.assertTrue(content.contains("quarkus-resteasy-reactive-jsonb"), + "quarkus-resteasy-reactive-jsonb should be listed as a dependency. Result:\n" + content); + Assertions.assertTrue(content.contains("quarkus-resteasy-reactive-jackson"), + "quarkus-resteasy-reactive-jackson should be listed as a dependency. Result:\n" + content); + + return result; + } + + public static Result invokeExtensionRemoveMultipleCommas(Path projectRoot, Path file) throws Exception { + Result result = execute(projectRoot, "extension", "remove", + "quarkus-resteasy-reactive-jsonb,quarkus-resteasy-reactive-jackson", "-e", "-B", "--verbose"); + Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, + "Expected OK return code. Result:\n" + result); + + result = invokeValidateExtensionList(projectRoot); + Assertions.assertFalse(result.stdout.contains("quarkus-resteasy-reactive-jsonb"), + "quarkus-resteasy-reactive-jsonb should not be in the list of extensions. Result:\n" + result); + Assertions.assertFalse(result.stdout.contains("quarkus-resteasy-reactive-jackson"), + "quarkus-resteasy-reactive-jackson should not be in the list of extensions. Result:\n" + result); + + String content = CliDriver.readFileAsString(file); + Assertions.assertFalse(content.contains("quarkus-resteasy-reactive-jsonb"), + "quarkus-resteasy-reactive-jsonb should not be listed as a dependency. Result:\n" + content); + Assertions.assertFalse(content.contains("quarkus-resteasy-reactive-jackson"), + "quarkus-resteasy-reactive-jackson should not be listed as a dependency. Result:\n" + content); + + return result; + } + public static Result invokeExtensionListInstallable(Path projectRoot) throws Exception { Result result = CliDriver.execute(projectRoot, "extension", "list", "-e", "-B", "--verbose", "-i"); Assertions.assertEquals(CommandLine.ExitCode.OK, result.exitCode, diff --git a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java index 4d74100b21eaa..1ceddb4909bf9 100644 --- a/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java +++ b/devtools/cli/src/test/java/io/quarkus/cli/CliProjectMavenTest.java @@ -131,8 +131,10 @@ public void testExtensionList() throws Exception { CliDriver.invokeExtensionAddRedundantQute(project); CliDriver.invokeExtensionListInstallable(project); CliDriver.invokeExtensionAddMultiple(project, pom); + CliDriver.invokeExtensionAddMultipleCommas(project, pom); CliDriver.invokeExtensionRemoveQute(project, pom); CliDriver.invokeExtensionRemoveMultiple(project, pom); + CliDriver.invokeExtensionRemoveMultipleCommas(project, pom); CliDriver.invokeExtensionListInstallableSearch(project); CliDriver.invokeExtensionListFormatting(project); diff --git a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java index 42a2ea257b487..51c468443586b 100644 --- a/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java +++ b/devtools/gradle/gradle-application-plugin/src/main/java/io/quarkus/gradle/tasks/QuarkusBuildTask.java @@ -27,6 +27,7 @@ import io.quarkus.gradle.QuarkusPlugin; import io.quarkus.gradle.tasks.worker.BuildWorker; import io.quarkus.maven.dependency.GACTV; +import io.smallrye.config.Expressions; /** * Base class for the {@link QuarkusBuildDependencies}, {@link QuarkusBuildCacheableAppParts}, {@link QuarkusBuild} tasks @@ -205,12 +206,14 @@ void generateBuild() { ApplicationModel appModel = resolveAppModelForBuild(); Map configMap = new HashMap<>(); - for (Map.Entry entry : extension().buildEffectiveConfiguration(appModel.getAppArtifact()).configMap() - .entrySet()) { - if (entry.getKey().startsWith("quarkus.")) { - configMap.put(entry.getKey(), entry.getValue()); + EffectiveConfig effectiveConfig = extension().buildEffectiveConfiguration(appModel.getAppArtifact()); + Expressions.withoutExpansion(() -> { + for (Map.Entry entry : effectiveConfig.configMap().entrySet()) { + if (entry.getKey().startsWith("quarkus.")) { + configMap.put(entry.getKey(), effectiveConfig.config().getRawValue(entry.getKey())); + } } - } + }); getLogger().info("Starting Quarkus application build for package type {}", packageType); diff --git a/docs/src/main/asciidoc/infinispan-client-reference.adoc b/docs/src/main/asciidoc/infinispan-client-reference.adoc index 7df534fcb7e2d..f188edbe0bae5 100644 --- a/docs/src/main/asciidoc/infinispan-client-reference.adoc +++ b/docs/src/main/asciidoc/infinispan-client-reference.adoc @@ -93,28 +93,42 @@ quarkus.infinispan-client.hosts=localhost:11222 <1> quarkus.infinispan-client.username=admin <2> quarkus.infinispan-client.password=password <3> - -quarkus.infinispan-client.client-intelligence=BASIC <4> ---- <1> Sets Infinispan Server address list, separated with commas <2> Sets the authentication username <3> Sets the authentication password -<4> Sets the client intelligence. Use BASIC as a workaround if using Docker for Mac. Alternatively, you can use uri connection by providing a single connection property [source,properties] ---- quarkus.infinispan-client.uri=hotrod://admin:password@localhost:11222 <1> -quarkus.infinispan-client.client-intelligence=BASIC <2> ---- <1> Sets Infinispan URI connection. The following properties will be ignored: hosts, username and password. -<2> Sets the client intelligence. Use BASIC as a workaround if using Docker for Mac [TIP] ==== Use Infinispan Dev Services to run a server and connect without configuration. ==== +=== Client intelligence +Infinispan client uses intelligence mechanisms to efficiently send requests to Infinispan Server clusters. +By default, the *HASH_DISTRIBUTION_AWARE* intelligence mechanism is enabled. +However, locally with Docker for Mac, you might experience connectivity issues. +In this case, configure the client intelligence to *BASIC*. + +Learn more in the https://infinispan.org/docs/stable/titles/hotrod_java/hotrod_java.html#hotrod-client-intelligence_hotrod-java-client[Infinispan documentation]. + +[source,properties] +---- +quarkus.infinispan-client.client-intelligence=BASIC <1> +---- +<1> Docker for Mac workaround. + +[IMPORTANT] +==== +Don't use *BASIC* in production environments by default, performance might be impacted. +==== + === Default and named connections This extension lets you configure a _default_ Infinispan client connections and _named_ ones. Named connections are essential to connect to multiple Infinispan clusters. diff --git a/docs/src/main/asciidoc/infinispan-client.adoc b/docs/src/main/asciidoc/infinispan-client.adoc index f787de4bd232c..8501b0798b154 100644 --- a/docs/src/main/asciidoc/infinispan-client.adoc +++ b/docs/src/main/asciidoc/infinispan-client.adoc @@ -303,14 +303,22 @@ Then, open the `src/main/resources/application.properties` file and add: %prod.quarkus.infinispan-client.username=admin <2> %prod.quarkus.infinispan-client.password=password <3> -## Docker 4 Mac workaround -%prod.quarkus.infinispan-client.client-intelligence=BASIC <4> +## Docker 4 Mac workaround. Uncomment only if you are using Docker for Mac. +## Read more about it in the Infinispan Reference Guide +# %prod.quarkus.infinispan-client.client-intelligence=BASIC <4> ---- <1> Sets Infinispan Server address list, separated with commas <2> Sets the authentication username <3> Sets the authentication password <4> Sets the client intelligence. Use BASIC as a workaround if using Docker for Mac. +[IMPORTANT] +==== +Client intelligence changes impact your performance in production. +Don't change the client intelligence unless strictly necessary for your case. +Read more in the xref:infinispan-client-reference.adoc[Infinispan Client extension reference guide]. +==== + == Packaging and running in JVM mode You can run the application as a conventional jar file. diff --git a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc index 8921ec8696429..063261c048e36 100644 --- a/docs/src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc +++ b/docs/src/main/asciidoc/security-oidc-bearer-token-authentication-tutorial.adoc @@ -59,51 +59,35 @@ The solution is located in the `security-openid-connect-quickstart` link:{quicks You can either create a new Maven project with the `oidc` extension or you can add the extension to an existing Maven project. Complete one of the following commands: -* To create a new Maven project, use the following command: -+ -==== +To create a new Maven project, use the following command: + :create-app-artifact-id: security-openid-connect-quickstart :create-app-extensions: oidc,resteasy-reactive-jackson include::{includes}/devtools/create-app.adoc[] -==== -This command generates a Maven project, importing the `oidc` extension -which is an implementation of OIDC for Quarkus. -* If you already have your Quarkus project configured, you can add the `oidc` extension -to your project by running the following command in your project base directory: -+ -==== +If you already have your Quarkus project configured, you can add the `oidc` extension to your project by running the following command in your project base directory: + :add-extension-extensions: oidc include::{includes}/devtools/extension-add.adoc[] -==== -The following configuration gets added to your build file: -* Using Maven (pom.xml): -+ -==== --- +This will add the following to your build file: + [source,xml,role="primary asciidoc-tabs-target-sync-cli asciidoc-tabs-target-sync-maven"] +.pom.xml ---- - io.quarkus - quarkus-oidc + io.quarkus + quarkus-oidc ---- --- -==== -+ -* Using Gradle (build.gradle): -+ -==== --- + [source,gradle,role="secondary asciidoc-tabs-target-sync-gradle"] +.build.gradle ---- implementation("io.quarkus:quarkus-oidc") ---- --- -==== -=== Write the application +== Write the application . Implement the `/api/users/me` endpoint as shown in the following example, which is a regular Jakarta REST resource: + diff --git a/docs/sync-web-site.sh b/docs/sync-web-site.sh index 3e323cd748ec0..83dcbc41f5a78 100755 --- a/docs/sync-web-site.sh +++ b/docs/sync-web-site.sh @@ -32,11 +32,16 @@ fi if [ -z $TARGET_DIR ]; then TARGET_DIR=target/web-site + rm -rf ${TARGET_DIR} GIT_OPTIONS="" if [[ "$QUARKUS_WEB_SITE_PUSH" != "true" ]]; then GIT_OPTIONS="--depth=1" fi - git clone -b develop --single-branch $GIT_OPTIONS git@github.com:quarkusio/quarkusio.github.io.git ${TARGET_DIR} + if [ -n "${RELEASE_GITHUB_TOKEN}" ]; then + git clone -b develop --single-branch $GIT_OPTIONS https://github.com/quarkusio/quarkusio.github.io.git ${TARGET_DIR} + else + git clone -b develop --single-branch $GIT_OPTIONS git@github.com:quarkusio/quarkusio.github.io.git ${TARGET_DIR} + fi fi if [ $BRANCH == "main" ] && [ "$QUARKUS_RELEASE" == "true" ]; then diff --git a/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/DuplicatedContextHandlingTest.java b/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/DuplicatedContextHandlingTest.java new file mode 100644 index 0000000000000..32229c90c02ed --- /dev/null +++ b/extensions/cache/deployment/src/test/java/io/quarkus/cache/test/runtime/DuplicatedContextHandlingTest.java @@ -0,0 +1,178 @@ +package io.quarkus.cache.test.runtime; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.enterprise.context.control.ActivateRequestContext; +import jakarta.inject.Inject; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.cache.CacheResult; +import io.quarkus.test.QuarkusUnitTest; +import io.smallrye.mutiny.Uni; +import io.vertx.core.Context; +import io.vertx.core.Vertx; +import io.vertx.core.impl.ContextInternal; + +public class DuplicatedContextHandlingTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest().withApplicationRoot(jar -> jar + .addClass(CachedService.class)); + + @Inject + CachedService cachedService; + + @Inject + Vertx vertx; + + @Test + @ActivateRequestContext + void testDuplicatedContextHandlingWhenCalledFromNoContext() { + cachedService.direct(false).await().indefinitely(); + cachedService.direct(true).await().indefinitely(); + } + + @Test + @ActivateRequestContext + void testDuplicatedContextHandlingWhenCalledOnContext() throws InterruptedException { + ContextInternal context = (ContextInternal) vertx.getOrCreateContext(); + if (context.isDuplicate()) { + context = context.duplicate(); + } + + CountDownLatch latch = new CountDownLatch(1); + Context tmp = context; + context.runOnContext(x -> { + cachedService.direct(false) + .invoke(() -> { + if (!tmp.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch.countDown()); + }); + Assertions.assertTrue(latch.await(1, TimeUnit.SECONDS)); + + CountDownLatch latch2 = new CountDownLatch(1); + context.runOnContext(x -> { + cachedService.direct(true) + .invoke(() -> { + if (!tmp.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch2.countDown()); + }); + Assertions.assertTrue(latch2.await(1, TimeUnit.SECONDS)); + + CountDownLatch latch3 = new CountDownLatch(1); + context.runOnContext(x -> { + cachedService.direct(false) + .invoke(() -> { + if (!tmp.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch3.countDown()); + }); + Assertions.assertTrue(latch3.await(1, TimeUnit.SECONDS)); + + } + + @Test + @ActivateRequestContext + void testDuplicatedContextHandlingWhenCalledOnDifferentContexts() throws InterruptedException { + ContextInternal context = (ContextInternal) vertx.getOrCreateContext(); + context = context.duplicate(); + var context2 = context.duplicate(); + + CountDownLatch latch = new CountDownLatch(1); + Context tmp = context; + context.runOnContext(x -> { + cachedService.direct(false) + .invoke(() -> { + if (!tmp.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch.countDown()); + }); + Assertions.assertTrue(latch.await(1, TimeUnit.SECONDS)); + + CountDownLatch latch2 = new CountDownLatch(1); + context2.runOnContext(x -> { + cachedService.direct(false) + .invoke(() -> { + if (!context2.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch2.countDown()); + }); + Assertions.assertTrue(latch2.await(1, TimeUnit.SECONDS)); + } + + @Test + @ActivateRequestContext + void testDuplicatedContextHandlingWhenCalledContextAndAnsweredFromAnotherContext() throws InterruptedException { + ContextInternal context = (ContextInternal) vertx.getOrCreateContext(); + context = context.duplicate(); + var context2 = context.duplicate(); + + CountDownLatch latch = new CountDownLatch(1); + Context tmp = context; + context.runOnContext(x -> { + cachedService.directOnAnotherContext(false) + .invoke(() -> { + if (!tmp.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch.countDown()); + }); + Assertions.assertTrue(latch.await(1, TimeUnit.SECONDS)); + + CountDownLatch latch2 = new CountDownLatch(1); + context2.runOnContext(x -> { + cachedService.directOnAnotherContext(false) + .invoke(() -> { + if (!context2.equals(Vertx.currentContext())) { + throw new AssertionError("Expected to go back on the caller context"); + } + }) + .subscribe().with(y -> latch2.countDown()); + }); + Assertions.assertTrue(latch2.await(1, TimeUnit.SECONDS)); + } + + @ApplicationScoped + public static class CachedService { + + volatile boolean timedout = false; + + @CacheResult(cacheName = "duplicated-context-cache", lockTimeout = 100) + public Uni direct(boolean timeout) { + if (!timeout || timedout) { + return Uni.createFrom().item("foo"); + } + timedout = true; + return Uni.createFrom().nothing(); + } + + @CacheResult(cacheName = "duplicated-context-cache", lockTimeout = 100) + public Uni directOnAnotherContext(boolean timeout) { + if (!timeout || timedout) { + return Uni.createFrom().item("foo") + .emitOn(c -> ((ContextInternal) Vertx.currentContext().owner()).duplicate().runOnContext(x -> c.run())); + } + timedout = true; + return Uni.createFrom().nothing(); + } + } + +} diff --git a/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/ImmutableEmbeddableFieldAccessTest.java b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/ImmutableEmbeddableFieldAccessTest.java new file mode 100644 index 0000000000000..123a97aa63ef5 --- /dev/null +++ b/extensions/hibernate-orm/deployment/src/test/java/io/quarkus/hibernate/orm/applicationfieldaccess/ImmutableEmbeddableFieldAccessTest.java @@ -0,0 +1,193 @@ +package io.quarkus.hibernate.orm.applicationfieldaccess; + +import static org.assertj.core.api.Assertions.assertThat; + +import jakarta.inject.Inject; +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.Immutable; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.narayana.jta.QuarkusTransaction; +import io.quarkus.test.QuarkusUnitTest; + +/** + * Checks that access to record fields or record getters by the application works correctly. + */ +public class ImmutableEmbeddableFieldAccessTest { + + @RegisterExtension + static QuarkusUnitTest runner = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(MyEntity.class) + .addClasses(MyImmutableEmbeddableWithFieldAccess.class) + .addClasses(MyImmutableEmbeddableWithAccessors.class) + .addClass(AccessDelegate.class)) + .withConfigurationResource("application.properties"); + + @Inject + EntityManager em; + + @Test + public void immutableEmbeddableWithoutAdditionalGetters_field() { + doTestFieldAccess(new AccessDelegate() { + @Override + public void setValue(MyEntity entity, Long value) { + var embedded = new MyImmutableEmbeddableWithFieldAccess(); + embedded.value = value; + entity.embeddedWithoutAccessors = embedded; + } + + @Override + public Long getValue(MyEntity entity) { + return entity.embeddedWithoutAccessors == null ? null : entity.embeddedWithoutAccessors.value; + } + }); + } + + @Test + public void immutableEmbeddableWithAdditionalGetters_field() { + doTestFieldAccess(new AccessDelegate() { + @Override + public void setValue(MyEntity entity, Long value) { + var embedded = new MyImmutableEmbeddableWithAccessors(); + // Assuming this is changed only once on initialization, + // which is the only way the @Immutable annotation would make sense. + embedded.value = value; + entity.embeddedWithFieldAccess = embedded; + } + + @Override + public Long getValue(MyEntity entity) { + return entity.embeddedWithFieldAccess == null ? null : entity.embeddedWithFieldAccess.value; + } + }); + } + + @Test + public void immutableEmbeddableWithAccessors() { + doTestFieldAccess(new AccessDelegate() { + @Override + public void setValue(MyEntity entity, Long value) { + var embedded = new MyImmutableEmbeddableWithAccessors(); + // Assuming this is changed only once on initialization, + // which is the only way the @Immutable annotation would make sense. + embedded.setValue(value); + entity.embeddedWithFieldAccess = embedded; + } + + @Override + public Long getValue(MyEntity entity) { + return entity.embeddedWithFieldAccess == null ? null : entity.embeddedWithFieldAccess.getValue(); + } + }); + } + + // Ideally we'd make this a @ParameterizedTest and pass the access delegate as parameter, + // but we cannot do that due to JUnit using a different classloader than the test. + private void doTestFieldAccess(AccessDelegate delegate) { + Long id = QuarkusTransaction.disallowingExisting().call(() -> { + var entity = new MyEntity(); + em.persist(entity); + return entity.id; + }); + + QuarkusTransaction.disallowingExisting().run(() -> { + var entity = em.find(MyEntity.class, id); + assertThat(delegate.getValue(entity)) + .as("Loaded value before update") + .isNull(); + }); + + QuarkusTransaction.disallowingExisting().run(() -> { + var entity = em.getReference(MyEntity.class, id); + // Since field access is replaced with accessor calls, + // we expect this change to be detected by dirty tracking and persisted. + delegate.setValue(entity, 42L); + }); + + QuarkusTransaction.disallowingExisting().run(() -> { + var entity = em.find(MyEntity.class, id); + // We're working on an initialized entity. + assertThat(entity) + .as("find() should return uninitialized entity") + .returns(true, Hibernate::isInitialized); + // The above should have persisted a value that passes the assertion. + assertThat(delegate.getValue(entity)) + .as("Loaded value after update") + .isEqualTo(42L); + }); + + QuarkusTransaction.disallowingExisting().run(() -> { + var entity = em.getReference(MyEntity.class, id); + // We're working on an uninitialized entity. + assertThat(entity) + .as("getReference() should return uninitialized entity") + .returns(false, Hibernate::isInitialized); + // The above should have persisted a value that passes the assertion. + assertThat(delegate.getValue(entity)) + .as("Lazily loaded value after update") + .isEqualTo(42L); + // Accessing the value should trigger initialization of the entity. + assertThat(entity) + .as("Getting the value should initialize the entity") + .returns(true, Hibernate::isInitialized); + }); + } + + @Entity(name = "myentity") + public static class MyEntity { + @Id + @GeneratedValue + public long id; + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "value1")) + public MyImmutableEmbeddableWithAccessors embeddedWithFieldAccess; + @Embedded + @AttributeOverride(name = "value", column = @Column(name = "value2")) + public MyImmutableEmbeddableWithFieldAccess embeddedWithoutAccessors; + } + + @Immutable + @Embeddable + public static class MyImmutableEmbeddableWithFieldAccess { + public Long value; + + public MyImmutableEmbeddableWithFieldAccess() { + } + } + + @Immutable + @Embeddable + public static class MyImmutableEmbeddableWithAccessors { + private Long value; + + // For Hibernate ORM instantiation + protected MyImmutableEmbeddableWithAccessors() { + } + + public Long getValue() { + return value; + } + + // For Hibernate ORM instantiation + protected void setValue(Long value) { + this.value = value; + } + } + + private interface AccessDelegate { + void setValue(MyEntity entity, Long value); + + Long getValue(MyEntity entity); + } +} diff --git a/extensions/keycloak-admin-client-reactive/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java b/extensions/keycloak-admin-client-reactive/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java index 0a486c5e024d1..27c8d4a6f95c2 100644 --- a/extensions/keycloak-admin-client-reactive/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java +++ b/extensions/keycloak-admin-client-reactive/deployment/src/main/java/io/quarkus/keycloak/admin/client/reactive/KeycloakAdminClientReactiveProcessor.java @@ -25,6 +25,7 @@ import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientInjectionEnabled; import io.quarkus.keycloak.admin.client.reactive.runtime.ResteasyReactiveClientProvider; import io.quarkus.keycloak.admin.client.reactive.runtime.ResteasyReactiveKeycloakAdminClientRecorder; +import io.quarkus.runtime.TlsConfig; public class KeycloakAdminClientReactiveProcessor { @@ -53,8 +54,8 @@ public void nativeImage(BuildProducer serviceProviderP @Record(ExecutionTime.STATIC_INIT) @Produce(ServiceStartBuildItem.class) @BuildStep - public void integrate(ResteasyReactiveKeycloakAdminClientRecorder recorder) { - recorder.setClientProvider(); + public void integrate(ResteasyReactiveKeycloakAdminClientRecorder recorder, TlsConfig tlsConfig) { + recorder.setClientProvider(tlsConfig.trustAll); } @Record(ExecutionTime.RUNTIME_INIT) diff --git a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java index 510b8278e01ea..814bc2442c9b7 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java +++ b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveClientProvider.java @@ -30,9 +30,15 @@ public class ResteasyReactiveClientProvider implements ResteasyClientProvider { private static final List HANDLED_MEDIA_TYPES = List.of(MediaType.APPLICATION_JSON); private static final int PROVIDER_PRIORITY = Priorities.USER + 100; // ensures that it will be used first + private final boolean tlsTrustAll; + + public ResteasyReactiveClientProvider(boolean tlsTrustAll) { + this.tlsTrustAll = tlsTrustAll; + } + @Override public Client newRestEasyClient(Object messageHandler, SSLContext sslContext, boolean disableTrustManager) { - ClientBuilderImpl clientBuilder = new ClientBuilderImpl().trustAll(disableTrustManager); + ClientBuilderImpl clientBuilder = new ClientBuilderImpl().trustAll(tlsTrustAll || disableTrustManager); return registerJacksonProviders(clientBuilder).build(); } diff --git a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java index 61d7605485442..12458c795592f 100644 --- a/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-client-reactive/runtime/src/main/java/io/quarkus/keycloak/admin/client/reactive/runtime/ResteasyReactiveKeycloakAdminClientRecorder.java @@ -21,8 +21,8 @@ public ResteasyReactiveKeycloakAdminClientRecorder( this.keycloakAdminClientConfigRuntimeValue = keycloakAdminClientConfigRuntimeValue; } - public void setClientProvider() { - Keycloak.setClientProvider(new ResteasyReactiveClientProvider()); + public void setClientProvider(boolean tlsTrustAll) { + Keycloak.setClientProvider(new ResteasyReactiveClientProvider(tlsTrustAll)); } public Supplier createAdminClient() { diff --git a/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java b/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java index 056b3c1d5bf93..5ac5f6fef3237 100644 --- a/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java +++ b/extensions/keycloak-admin-client/deployment/src/main/java/io/quarkus/keycloak/adminclient/deployment/KeycloakAdminClientProcessor.java @@ -25,6 +25,7 @@ import io.quarkus.keycloak.admin.client.common.AutoCloseableDestroyer; import io.quarkus.keycloak.admin.client.common.KeycloakAdminClientInjectionEnabled; import io.quarkus.keycloak.adminclient.ResteasyKeycloakAdminClientRecorder; +import io.quarkus.runtime.TlsConfig; public class KeycloakAdminClientProcessor { @@ -48,8 +49,8 @@ ReflectiveClassBuildItem reflect() { @Record(ExecutionTime.STATIC_INIT) @Produce(ServiceStartBuildItem.class) @BuildStep - public void integrate(ResteasyKeycloakAdminClientRecorder recorder) { - recorder.setClientProvider(); + public void integrate(ResteasyKeycloakAdminClientRecorder recorder, TlsConfig tlsConfig) { + recorder.setClientProvider(tlsConfig.trustAll); } @Record(ExecutionTime.RUNTIME_INIT) diff --git a/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java b/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java index 9dda7e9c3c475..75fb6d2924896 100644 --- a/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java +++ b/extensions/keycloak-admin-client/runtime/src/main/java/io/quarkus/keycloak/adminclient/ResteasyKeycloakAdminClientRecorder.java @@ -58,13 +58,13 @@ public Keycloak get() { }; } - public void setClientProvider() { + public void setClientProvider(boolean tlsTrustAll) { Keycloak.setClientProvider(new ResteasyClientClassicProvider() { @Override public Client newRestEasyClient(Object customJacksonProvider, SSLContext sslContext, boolean disableTrustManager) { // point here is to use default Quarkus providers rather than org.keycloak.admin.client.JacksonProvider // as it doesn't work properly in native mode - return ClientBuilderWrapper.create(sslContext, disableTrustManager).build(); + return ClientBuilderWrapper.create(sslContext, tlsTrustAll || disableTrustManager).build(); } }); } diff --git a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java index c958840b8e702..112ca47f5ebc9 100644 --- a/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java +++ b/extensions/opentelemetry/deployment/src/main/java/io/quarkus/opentelemetry/deployment/tracing/TracerProcessor.java @@ -1,5 +1,6 @@ package io.quarkus.opentelemetry.deployment.tracing; +import java.net.URL; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; @@ -126,7 +127,21 @@ void dropNames( // Drop framework paths List nonApplicationUris = new ArrayList<>(); frameworkEndpoints.ifPresent( - frameworkEndpointsBuildItem -> nonApplicationUris.addAll(frameworkEndpointsBuildItem.getEndpoints())); + frameworkEndpointsBuildItem -> { + for (String endpoint : frameworkEndpointsBuildItem.getEndpoints()) { + // Management routes are using full urls -> Extract the path. + if (endpoint.startsWith("http://") || endpoint.startsWith("https://")) { + try { + nonApplicationUris.add(new URL(endpoint).getPath()); + } catch (Exception ignored) { // Not an URL + nonApplicationUris.add(endpoint); + } + } else { + nonApplicationUris.add(endpoint); + } + } + }); + dropNonApplicationUris.produce(new DropNonApplicationUrisBuildItem(nonApplicationUris)); // Drop Static Resources diff --git a/extensions/panache/panache-hibernate-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheHibernateCommonResourceProcessor.java b/extensions/panache/panache-hibernate-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheHibernateCommonResourceProcessor.java index 93037abea56c9..6127afcc2955b 100644 --- a/extensions/panache/panache-hibernate-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheHibernateCommonResourceProcessor.java +++ b/extensions/panache/panache-hibernate-common/deployment/src/main/java/io/quarkus/panache/common/deployment/PanacheHibernateCommonResourceProcessor.java @@ -157,9 +157,12 @@ private EntityModel createEntityModel(ClassInfo classInfo) { // so we need to be careful when we enhance private fields, // because the corresponding `$_hibernate_{read/write}_*()` methods // will only be generated for classes mapped through *annotations*. - boolean willBeEnhancedByHibernateOrm = classInfo.hasAnnotation(DOTNAME_ENTITY) + boolean isManaged = classInfo.hasAnnotation(DOTNAME_ENTITY) || classInfo.hasAnnotation(DOTNAME_MAPPED_SUPERCLASS) || classInfo.hasAnnotation(DOTNAME_EMBEDDABLE); + boolean willBeEnhancedByHibernateOrm = isManaged + // Records are immutable, thus never enhanced + && !classInfo.isRecord(); for (FieldInfo fieldInfo : classInfo.fields()) { String name = fieldInfo.name(); if (!Modifier.isStatic(fieldInfo.flags()) diff --git a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java index fa0765d2559bf..22d8633adeb51 100644 --- a/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/jaxrs-client-reactive/deployment/src/main/java/io/quarkus/jaxrs/client/reactive/deployment/JaxrsClientReactiveProcessor.java @@ -44,7 +44,6 @@ import java.util.function.Predicate; import java.util.function.Supplier; import java.util.regex.Pattern; -import java.util.stream.Collectors; import jakarta.ws.rs.ProcessingException; import jakarta.ws.rs.RuntimeType; @@ -284,9 +283,8 @@ void setupClientProxies(JaxrsClientReactiveRecorder recorder, scannedParameterContainers.addAll(parameterContainersBuildItem.getClassNames()); } reflectiveClassBuildItemBuildProducer.produce(ReflectiveClassBuildItem - .builder(scannedParameterContainers.stream().map(name -> name.toString()).collect(Collectors.toSet()) - .toArray(new String[0])) - .fields().build()); + .builder(scannedParameterContainers.stream().map(DotName::toString).distinct().toArray(String[]::new)) + .methods().fields().build()); if (resourceScanningResultBuildItem.isEmpty() || resourceScanningResultBuildItem.get().getResult().getClientInterfaces().isEmpty()) { @@ -2644,7 +2642,7 @@ private ResultHandle addQueryParam(MethodInfo jandexMethod, BytecodeCreator meth if (isCollection(valueType, index)) { if (valueType.kind() == PARAMETERIZED_TYPE) { Type paramType = valueType.asParameterizedType().arguments().get(0); - if (paramType.kind() == CLASS) { + if ((paramType.kind() == CLASS) || (paramType.kind() == PARAMETERIZED_TYPE)) { componentType = paramType.name().toString(); } } @@ -2671,7 +2669,7 @@ private ResultHandle addQueryParam(MethodInfo jandexMethod, BytecodeCreator meth } else if (isCollection(type, index)) { if (type.kind() == PARAMETERIZED_TYPE) { Type paramType = type.asParameterizedType().arguments().get(0); - if (paramType.kind() == CLASS) { + if ((paramType.kind() == CLASS) || (paramType.kind() == PARAMETERIZED_TYPE)) { componentType = paramType.name().toString(); } } 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 7f8bd584a0244..36cca2e23779e 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 @@ -37,14 +37,14 @@ public void testParser() { testParser("data:foo\ndata:\ndata:bar\n\n", "foo\n\nbar", null, null, null, SseEvent.RECONNECT_NOT_SET); // no data: no event - testParser("\n", null, null, null, null, SseEvent.RECONNECT_NOT_SET); - testParser("data:\n\n", null, null, null, null, SseEvent.RECONNECT_NOT_SET); - testParser("data\n\n", null, null, null, null, SseEvent.RECONNECT_NOT_SET); + testParser("\n", "", null, null, null, SseEvent.RECONNECT_NOT_SET); + testParser("data:\n\n", "", null, null, null, SseEvent.RECONNECT_NOT_SET); + testParser("data\n\n", "", null, null, null, SseEvent.RECONNECT_NOT_SET); // 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 - testParser("id:ID\n:COMMENT\nretry:23\nevent:NAME\n\n", null, "COMMENT", "ID", "NAME", 23); + testParser("id:ID\n:COMMENT\nretry:23\nevent:NAME\n\n", "", "COMMENT", "ID", "NAME", 23); // optional space after colon testParser("data:foo\n\n", "foo", null, null, null, SseEvent.RECONNECT_NOT_SET); 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 7a2135bd216cd..9da6a2fa1a778 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 @@ -264,7 +264,7 @@ public void testSseForMultiWithOutboundSseEvent() throws InterruptedException { }); sse.open(); Assertions.assertTrue(latch.await(20, TimeUnit.SECONDS)); - org.assertj.core.api.Assertions.assertThat(results).containsExactly(null, "uno", "dos", "tres"); + org.assertj.core.api.Assertions.assertThat(results).containsExactly("", "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); diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java index 066d733223757..c0f0de3c3dea8 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/main/java/io/quarkus/rest/client/reactive/deployment/RestClientReactiveProcessor.java @@ -303,18 +303,20 @@ void registerProvidersFromAnnotations(CombinedIndexBuildItem indexBuildItem, continue; } } - DotName providerDotName = providerClass.name(); + + List providerInterfaceNames = providerClass.interfaceNames(); // don't register server specific types - if (providerDotName.equals(ResteasyReactiveDotNames.CONTAINER_REQUEST_FILTER) - || providerDotName.equals(ResteasyReactiveDotNames.CONTAINER_RESPONSE_FILTER) - || providerDotName.equals(ResteasyReactiveDotNames.EXCEPTION_MAPPER)) { + if (providerInterfaceNames.contains(ResteasyReactiveDotNames.CONTAINER_REQUEST_FILTER) + || providerInterfaceNames.contains(ResteasyReactiveDotNames.CONTAINER_RESPONSE_FILTER) + || providerInterfaceNames.contains(ResteasyReactiveDotNames.EXCEPTION_MAPPER)) { continue; } - if (providerClass.interfaceNames().contains(ResteasyReactiveDotNames.FEATURE)) { + if (providerInterfaceNames.contains(ResteasyReactiveDotNames.FEATURE)) { continue; // features should not be automatically registered for the client, see javadoc for Feature } + DotName providerDotName = providerClass.name(); int priority = getAnnotatedPriority(index, providerDotName.toString(), Priorities.USER); constructor.invokeVirtualMethod( diff --git a/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/converter/GenericsConverterTest.java b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/converter/GenericsConverterTest.java new file mode 100644 index 0000000000000..61a8ad069bae6 --- /dev/null +++ b/extensions/resteasy-reactive/rest-client-reactive/deployment/src/test/java/io/quarkus/rest/client/reactive/converter/GenericsConverterTest.java @@ -0,0 +1,127 @@ +package io.quarkus.rest.client.reactive.converter; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.net.URI; +import java.util.List; +import java.util.stream.Collectors; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.ext.ParamConverter; +import jakarta.ws.rs.ext.ParamConverterProvider; +import jakarta.ws.rs.ext.Provider; + +import org.eclipse.microprofile.rest.client.RestClientBuilder; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.http.TestHTTPResource; + +public class GenericsConverterTest { + + @RegisterExtension + static final QuarkusUnitTest TEST = new QuarkusUnitTest(); + + @TestHTTPResource + URI baseUri; + + @Test + void testSingle() { + TestClient client = RestClientBuilder.newBuilder().baseUri(baseUri) + .build(TestClient.class); + var result = client.wrapper(new WrapperClass<>(StatusEnum.ACTIVE)); + assertEquals("ACTIVE", result); + } + + @Test + void testList() { + TestClient client = RestClientBuilder.newBuilder().baseUri(baseUri) + .build(TestClient.class); + var result = client + .wrapperList(List.of(new WrapperClass<>(StatusEnum.ACTIVE), new WrapperClass<>(StatusEnum.INACTIVE))); + assertEquals("ACTIVE,INACTIVE", result); + } + + public enum StatusEnum { + + ACTIVE, + INACTIVE + } + + public static class WrapperClass> { + + private final E value; + + public WrapperClass(final E value) { + this.value = value; + } + + public E getValue() { + return value; + } + } + + public static class WrapperClassParamConverter implements ParamConverter> { + + @Override + public WrapperClass fromString(String value) { + return new WrapperClass<>(Enum.valueOf(StatusEnum.class, value)); + } + + @Override + public String toString(WrapperClass wrapperClass) { + return wrapperClass != null ? wrapperClass.getValue().toString() : null; + } + + } + + @Provider + public static class WrapperClassParamConverterProvider implements ParamConverterProvider { + + @Override + @SuppressWarnings("unchecked") + public ParamConverter getConverter(Class rawType, Type genericType, Annotation[] annotations) { + if (WrapperClass.class.isAssignableFrom(rawType)) { + return (ParamConverter) new WrapperClassParamConverter(); + } + + return null; + } + } + + @Path("/test") + public static class TestResource { + + @GET + @Path("/single") + public String wrapper(@QueryParam("wrapper") final WrapperClass wrapper) { + return wrapper.getValue().toString(); + } + + @GET + @Path("/list") + public String wrapperList(@QueryParam("wrapperList") final List> wrapperList) { + return wrapperList.stream().map(WrapperClass::getValue).map(Enum::name).collect(Collectors.joining(",")); + } + + } + + @Path("/test") + public interface TestClient { + + @GET + @Path("/single") + String wrapper(@QueryParam("wrapper") final WrapperClass wrapper); + + @GET + @Path("/list") + String wrapperList(@QueryParam("wrapperList") final List> wrapperList); + + } + +} diff --git a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java index 7a60af0b2d842..95d36dd17fa4b 100644 --- a/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java +++ b/extensions/resteasy-reactive/rest-client-reactive/runtime/src/test/java/io/quarkus/rest/client/reactive/runtime/RestClientCDIDelegateBuilderTest.java @@ -1,9 +1,10 @@ package io.quarkus.rest.client.reactive.runtime; -import java.io.File; -import java.io.FileOutputStream; import java.io.IOException; +import java.io.OutputStream; import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Path; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; @@ -16,15 +17,12 @@ import jakarta.ws.rs.client.ClientResponseContext; import jakarta.ws.rs.client.ClientResponseFilter; -import org.eclipse.microprofile.config.Config; -import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.eclipse.microprofile.rest.client.ext.QueryParamStyle; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import org.jboss.resteasy.reactive.client.api.QuarkusRestClientProperties; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; import org.mockito.Mockito; import io.netty.handler.codec.http.multipart.HttpPostRequestEncoder; @@ -37,33 +35,45 @@ public class RestClientCDIDelegateBuilderTest { private static final String TRUSTSTORE_PASSWORD = "truststorePassword"; private static final String KEYSTORE_PASSWORD = "keystorePassword"; - @TempDir - static File tempDir; - private static File truststoreFile; - private static File keystoreFile; - private static Config createdConfig; + private static Path truststorePath; + private static Path keystorePath; @BeforeAll public static void beforeAll() throws IOException, KeyStoreException, CertificateException, NoSuchAlgorithmException { // prepare keystore and truststore - truststoreFile = new File(tempDir, "truststore.jks"); - keystoreFile = new File(tempDir, "keystore.jks"); + truststorePath = Files.createTempFile("truststore", ".jks"); - KeyStore truststore = KeyStore.getInstance("JKS"); - truststore.load(null, TRUSTSTORE_PASSWORD.toCharArray()); - truststore.store(new FileOutputStream(truststoreFile), TRUSTSTORE_PASSWORD.toCharArray()); + try (OutputStream truststoreOs = Files.newOutputStream(truststorePath)) { + KeyStore truststore = KeyStore.getInstance("JKS"); + truststore.load(null, TRUSTSTORE_PASSWORD.toCharArray()); + truststore.store(truststoreOs, TRUSTSTORE_PASSWORD.toCharArray()); + } + + keystorePath = Files.createTempFile("keystore", ".jks"); - KeyStore keystore = KeyStore.getInstance("JKS"); - keystore.load(null, KEYSTORE_PASSWORD.toCharArray()); - keystore.store(new FileOutputStream(keystoreFile), KEYSTORE_PASSWORD.toCharArray()); + try (OutputStream keystoreOs = Files.newOutputStream(keystorePath)) { + KeyStore keystore = KeyStore.getInstance("JKS"); + keystore.load(null, KEYSTORE_PASSWORD.toCharArray()); + keystore.store(keystoreOs, KEYSTORE_PASSWORD.toCharArray()); + } } - @AfterEach - public void afterEach() { - if (createdConfig != null) { - ConfigProviderResolver.instance().releaseConfig(createdConfig); - createdConfig = null; + @AfterAll + public static void afterAll() { + if (truststorePath != null) { + try { + Files.deleteIfExists(truststorePath); + } catch (IOException e) { + // ignore it + } + } + if (keystorePath != null) { + try { + Files.deleteIfExists(keystorePath); + } catch (IOException e) { + // ignore it + } } } @@ -178,10 +188,10 @@ private static RestClientsConfig createSampleConfigRoot() { .of("io.quarkus.rest.client.reactive.runtime.RestClientCDIDelegateBuilderTest$MyResponseFilter2"); configRoot.queryParamStyle = Optional.of(QueryParamStyle.MULTI_PAIRS); - configRoot.trustStore = Optional.of(truststoreFile.getAbsolutePath()); + configRoot.trustStore = Optional.of(truststorePath.toAbsolutePath().toString()); configRoot.trustStorePassword = Optional.of("truststorePassword"); configRoot.trustStoreType = Optional.of("JKS"); - configRoot.keyStore = Optional.of(keystoreFile.getAbsolutePath()); + configRoot.keyStore = Optional.of(keystorePath.toAbsolutePath().toString()); configRoot.keyStorePassword = Optional.of("keystorePassword"); configRoot.keyStoreType = Optional.of("JKS"); @@ -215,10 +225,10 @@ private static RestClientConfig createSampleClientConfig() { .of("io.quarkus.rest.client.reactive.runtime.RestClientCDIDelegateBuilderTest$MyResponseFilter1"); clientConfig.queryParamStyle = Optional.of(QueryParamStyle.COMMA_SEPARATED); - clientConfig.trustStore = Optional.of(truststoreFile.getAbsolutePath()); + clientConfig.trustStore = Optional.of(truststorePath.toAbsolutePath().toString()); clientConfig.trustStorePassword = Optional.of("truststorePassword"); clientConfig.trustStoreType = Optional.of("JKS"); - clientConfig.keyStore = Optional.of(keystoreFile.getAbsolutePath()); + clientConfig.keyStore = Optional.of(keystorePath.toAbsolutePath().toString()); clientConfig.keyStorePassword = Optional.of("keystorePassword"); clientConfig.keyStoreType = Optional.of("JKS"); diff --git a/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/JpaSecurityIdentityUtil.java b/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/JpaSecurityIdentityUtil.java index 5501ea4444353..054fcb33d6e69 100644 --- a/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/JpaSecurityIdentityUtil.java +++ b/extensions/security-jpa-common/deployment/src/main/java/io/quarkus/security/jpa/common/deployment/JpaSecurityIdentityUtil.java @@ -45,18 +45,21 @@ public static void buildIdentity(Index index, JpaSecurityDefinition jpaSecurityD PanacheEntityPredicateBuildItem panacheEntityPredicate, FieldDescriptor passwordProviderField, MethodCreator outerMethod, ResultHandle userVar, BytecodeCreator innerMethod) { // if(user == null) throw new AuthenticationFailedException(); + + PasswordType passwordType = passwordTypeValue != null ? PasswordType.valueOf(passwordTypeValue.asEnum()) + : PasswordType.MCF; + try (BytecodeCreator trueBranch = innerMethod.ifNull(userVar).trueBranch()) { + ResultHandle exceptionInstance = trueBranch .newInstance(MethodDescriptor.ofConstructor(AuthenticationFailedException.class)); + trueBranch.invokeStaticMethod(passwordActionMethod(), trueBranch.load(passwordType)); trueBranch.throwException(exceptionInstance); } // :pass = user.pass | user.getPass() ResultHandle pass = jpaSecurityDefinition.password.readValue(innerMethod, userVar); - PasswordType passwordType = passwordTypeValue != null ? PasswordType.valueOf(passwordTypeValue.asEnum()) - : PasswordType.MCF; - if (passwordType == PasswordType.CUSTOM && passwordProviderValue == null) { throw new RuntimeException("Missing password provider for password type: " + passwordType); } @@ -245,4 +248,8 @@ private static MethodDescriptor getUtilMethod(String passwordProviderMethod) { return MethodDescriptor.ofMethod(JpaIdentityProviderUtil.class, passwordProviderMethod, org.wildfly.security.password.Password.class, String.class); } + + private static MethodDescriptor passwordActionMethod() { + return MethodDescriptor.ofMethod(JpaIdentityProviderUtil.class, "passwordAction", void.class, PasswordType.class); + } } diff --git a/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java b/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java index a65f771596a5d..15a3c4710d1c8 100644 --- a/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java +++ b/extensions/security-jpa-common/runtime/src/main/java/io/quarkus/security/jpa/common/runtime/JpaIdentityProviderUtil.java @@ -2,6 +2,7 @@ import java.security.spec.InvalidKeySpecException; import java.util.List; +import java.util.UUID; import org.wildfly.security.credential.PasswordCredential; import org.wildfly.security.evidence.PasswordGuessEvidence; @@ -10,9 +11,11 @@ import org.wildfly.security.password.util.ModularCrypt; import org.wildfly.security.provider.util.ProviderUtil; +import io.quarkus.elytron.security.common.BcryptUtil; import io.quarkus.security.AuthenticationFailedException; import io.quarkus.security.identity.request.TrustedAuthenticationRequest; import io.quarkus.security.identity.request.UsernamePasswordAuthenticationRequest; +import io.quarkus.security.jpa.PasswordType; import io.quarkus.security.runtime.QuarkusPrincipal; import io.quarkus.security.runtime.QuarkusSecurityIdentity; @@ -70,4 +73,13 @@ public static Password getMcfPassword(String pass) { throw new RuntimeException(e); } } + + public static void passwordAction(PasswordType type) { + String uuid = UUID.randomUUID().toString(); + if (type == PasswordType.CLEAR) { + ClearPassword.createRaw(ClearPassword.ALGORITHM_CLEAR, uuid.toCharArray()); + } else { + BcryptUtil.bcryptHash(uuid); + } + } } diff --git a/extensions/spring-security/deployment/src/main/java/io/quarkus/spring/security/deployment/SpringSecurityProcessor.java b/extensions/spring-security/deployment/src/main/java/io/quarkus/spring/security/deployment/SpringSecurityProcessor.java index 70d16090e0090..d5d70eb2fbace 100644 --- a/extensions/spring-security/deployment/src/main/java/io/quarkus/spring/security/deployment/SpringSecurityProcessor.java +++ b/extensions/spring-security/deployment/src/main/java/io/quarkus/spring/security/deployment/SpringSecurityProcessor.java @@ -48,6 +48,7 @@ import io.quarkus.spring.security.runtime.interceptor.SpringPreauthorizeInterceptor; import io.quarkus.spring.security.runtime.interceptor.SpringSecuredInterceptor; import io.quarkus.spring.security.runtime.interceptor.SpringSecurityRecorder; +import io.quarkus.spring.security.runtime.interceptor.check.PrincipalNameFromParameterObjectSecurityCheck; import io.quarkus.spring.security.runtime.interceptor.check.PrincipalNameFromParameterSecurityCheck; class SpringSecurityProcessor { @@ -466,13 +467,18 @@ void addSpringPreAuthorizeSecurityCheck(CombinedIndexBuildItem index, propertyName, index.getIndex(), part); + PrincipalNameFromParameterObjectSecurityCheck.CheckType checkType = part.contains("==") + ? PrincipalNameFromParameterObjectSecurityCheck.CheckType.EQ + : PrincipalNameFromParameterObjectSecurityCheck.CheckType.NEQ; + securityChecks.add(springSecurityRecorder.principalNameFromParameterObjectSecurityCheck( parameterNameAndIndex.getIndex(), stringPropertyAccessorData.getMatchingParameterClassInfo().name().toString(), StringPropertyAccessorGenerator .getAccessorClassName( stringPropertyAccessorData.getMatchingParameterClassInfo().name()), - stringPropertyAccessorData.getMatchingParameterFieldInfo().name())); + stringPropertyAccessorData.getMatchingParameterFieldInfo().name(), + checkType)); } } else if (part.matches(SpringSecurityProcessorUtil.BASIC_BEAN_METHOD_INVOCATION_REGEX)) { diff --git a/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/SpringPreAuthorizeTest.java b/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/SpringPreAuthorizeTest.java index eaab4280b3647..c7dc3deff1f77 100644 --- a/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/SpringPreAuthorizeTest.java +++ b/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/SpringPreAuthorizeTest.java @@ -116,6 +116,16 @@ public void testPrincipalNameFromObject() { assertSuccess(() -> springComponent.principalNameFromObject(new Person("user")), "user", USER); } + @Test + public void testPrincipalNameFromObjectIsNot() { + assertFailureFor(() -> springComponent.principalNameFromObjectIsNot(new Person("whatever")), + UnauthorizedException.class, + ANONYMOUS); + assertSuccess(() -> springComponent.principalNameFromObjectIsNot(new Person("whatever")), "whatever", USER); + assertFailureFor(() -> springComponent.principalNameFromObjectIsNot(new Person("user")), ForbiddenException.class, + USER); + } + @Test public void testNotSecured() { assertSuccess(() -> springComponent.notSecured(), "notSecured", ANONYMOUS); diff --git a/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/springapp/SpringComponent.java b/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/springapp/SpringComponent.java index 0d86e8406fcf3..68a8af6552c6d 100644 --- a/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/springapp/SpringComponent.java +++ b/extensions/spring-security/deployment/src/test/java/io/quarkus/spring/security/deployment/springapp/SpringComponent.java @@ -37,6 +37,11 @@ public String principalNameFromObject(Person person) { return person.getName(); } + @PreAuthorize("#person.name != authentication.principal.username") + public String principalNameFromObjectIsNot(Person person) { + return person.getName(); + } + public String notSecured() { return "notSecured"; } diff --git a/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringSecurityRecorder.java b/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringSecurityRecorder.java index 8101bdd0991b6..591c08d8bd892 100644 --- a/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringSecurityRecorder.java +++ b/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/SpringSecurityRecorder.java @@ -70,8 +70,9 @@ public SecurityCheck fromGeneratedClass(String generatedClassName) { } public SecurityCheck principalNameFromParameterObjectSecurityCheck(int index, String expectedParameterClass, - String stringPropertyAccessorClass, String propertyName) { + String stringPropertyAccessorClass, String propertyName, + PrincipalNameFromParameterObjectSecurityCheck.CheckType checkType) { return PrincipalNameFromParameterObjectSecurityCheck.of(index, expectedParameterClass, stringPropertyAccessorClass, - propertyName); + propertyName, checkType); } } diff --git a/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/check/PrincipalNameFromParameterObjectSecurityCheck.java b/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/check/PrincipalNameFromParameterObjectSecurityCheck.java index c34e97b6d5f7b..63da575e48187 100644 --- a/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/check/PrincipalNameFromParameterObjectSecurityCheck.java +++ b/extensions/spring-security/runtime/src/main/java/io/quarkus/spring/security/runtime/interceptor/check/PrincipalNameFromParameterObjectSecurityCheck.java @@ -23,22 +23,24 @@ public class PrincipalNameFromParameterObjectSecurityCheck implements SecurityCh private final Class expectedParameterClass; private final Class stringPropertyAccessorClass; private final String propertyName; + private final CheckType checkType; private PrincipalNameFromParameterObjectSecurityCheck(int index, String expectedParameterClass, - String stringPropertyAccessorClass, String propertyName) throws ClassNotFoundException { + String stringPropertyAccessorClass, String propertyName, CheckType checkType) throws ClassNotFoundException { this.index = index; this.expectedParameterClass = Class.forName(expectedParameterClass, false, Thread.currentThread().getContextClassLoader()); this.stringPropertyAccessorClass = (Class) Class.forName(stringPropertyAccessorClass, false, Thread.currentThread().getContextClassLoader()); this.propertyName = propertyName; + this.checkType = checkType; } public static PrincipalNameFromParameterObjectSecurityCheck of(int index, String expectedParameterClass, - String stringPropertyAccessorClass, String propertyName) { + String stringPropertyAccessorClass, String propertyName, CheckType checkType) { try { return new PrincipalNameFromParameterObjectSecurityCheck(index, expectedParameterClass, stringPropertyAccessorClass, - propertyName); + propertyName, checkType); } catch (ClassNotFoundException e) { throw new RuntimeException(e); } @@ -70,8 +72,14 @@ private void doApply(SecurityIdentity identity, Object[] parameters, String clas } String name = identity.getPrincipal().getName(); - if (!name.equals(parameterValueStr)) { - throw new ForbiddenException(); + if (checkType == CheckType.EQ) { + if (!name.equals(parameterValueStr)) { + throw new ForbiddenException(); + } + } else if (checkType == CheckType.NEQ) { + if (name.equals(parameterValueStr)) { + throw new ForbiddenException(); + } } } @@ -84,4 +92,9 @@ private IllegalStateException genericNotApplicableException(String className, St "PrincipalNameFromParameterObjectSecurityCheck with index " + index + " cannot be applied to '" + className + "#" + methodName + "'"); } + + public enum CheckType { + EQ, + NEQ + } } diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java index 8f7cdb6ce1495..28c3ea0fa3055 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/VertxHttpProcessor.java @@ -114,16 +114,24 @@ FrameworkEndpointsBuildItem frameworkEndpoints(NonApplicationRootPathBuildItem n for (RouteBuildItem route : routes) { if (FRAMEWORK_ROUTE.equals(route.getRouteType())) { if (route.getConfiguredPathInfo() != null) { - frameworkEndpoints.add(route.getConfiguredPathInfo().getEndpointPath(nonApplicationRootPath, - managementInterfaceBuildTimeConfig, launchModeBuildItem)); + String endpointPath = route.getConfiguredPathInfo().getEndpointPath(nonApplicationRootPath, + managementInterfaceBuildTimeConfig, launchModeBuildItem); + frameworkEndpoints.add(endpointPath); continue; } if (route.getRouteFunction() != null && route.getRouteFunction() instanceof BasicRoute) { BasicRoute basicRoute = (BasicRoute) route.getRouteFunction(); if (basicRoute.getPath() != null) { - // Calling TemplateHtmlBuilder does not see very correct here, but it is the underlying API for ConfiguredPathInfo - frameworkEndpoints - .add(adjustRoot(nonApplicationRootPath.getNonApplicationRootPath(), basicRoute.getPath())); + if (basicRoute.getPath().startsWith(nonApplicationRootPath.getNonApplicationRootPath())) { + // Do not repeat the non application root path. + frameworkEndpoints.add(basicRoute.getPath()); + } else { + // Calling TemplateHtmlBuilder does not see very correct here, but it is the underlying API for ConfiguredPathInfo + String adjustRoot = adjustRoot(nonApplicationRootPath.getNonApplicationRootPath(), + basicRoute.getPath()); + frameworkEndpoints.add(adjustRoot); + } + } } } diff --git a/independent-projects/parent/pom.xml b/independent-projects/parent/pom.xml index 4c93356c91a57..fe7f018fd14e2 100644 --- a/independent-projects/parent/pom.xml +++ b/independent-projects/parent/pom.xml @@ -466,5 +466,30 @@ + + ci + + + env.CI + + + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + + --pinentry-mode + loopback + + + + + + + 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 7728e87ff7098..ae885afd5fb59 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 @@ -156,7 +156,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.length() == 0 ? null : dataBuffer.toString()); + event.setData(dataBuffer.length() == 0 ? "" : 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/ResteasyReactiveRequestContext.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java index dedaf7dd127cd..2248a8f7763ac 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/ResteasyReactiveRequestContext.java @@ -226,18 +226,20 @@ public void setMaxPathParams(int maxPathParams) { } } - public String getPathParam(int index) { - return doGetPathParam(index, pathParamValues); + public String getPathParam(int index, boolean encoded) { + return doGetPathParam(index, pathParamValues, encoded); } - private String doGetPathParam(int index, Object pathParamValues) { + private String doGetPathParam(int index, Object pathParamValues, boolean encoded) { if (pathParamValues instanceof String[]) { - return ((String[]) pathParamValues)[index]; + String pathParam = ((String[]) pathParamValues)[index]; + return encoded ? pathParam : Encode.decodePath(pathParam); } if (index > 1) { throw new IndexOutOfBoundsException(); } - return (String) pathParamValues; + String pathParam = (String) pathParamValues; + return encoded ? pathParam : Encode.decodePath(pathParam); } public ResteasyReactiveRequestContext setPathParamValue(int index, String value) { @@ -926,18 +928,11 @@ public String getPathParameter(String name, boolean encoded) { Integer index = target.getPathParameterIndexes().get(name); String value; if (index != null) { - value = getPathParam(index); - } else { - // Check previous resources if the path is not defined in the current target - value = getResourceLocatorPathParam(name); - } - - // It's possible to inject a path param that's not defined, return null in this case - if (encoded && value != null) { - return Encode.encodeQueryParam(value); + return getPathParam(index, encoded); } - return value; + // Check previous resources if the path is not defined in the current target + return getResourceLocatorPathParam(name, encoded); } @Override @@ -996,8 +991,8 @@ public ResteasyReactiveResourceInfo getResteasyReactiveResourceInfo() { public abstract Runnable registerTimer(long millis, Runnable task); - public String getResourceLocatorPathParam(String name) { - return getResourceLocatorPathParam(name, (PreviousResource) getProperty(PreviousResource.PROPERTY_KEY)); + public String getResourceLocatorPathParam(String name, boolean encoded) { + return getResourceLocatorPathParam(name, (PreviousResource) getProperty(PreviousResource.PROPERTY_KEY), encoded); } public FormData getFormData() { @@ -1009,7 +1004,7 @@ public ResteasyReactiveRequestContext setFormData(FormData formData) { return this; } - private String getResourceLocatorPathParam(String name, PreviousResource previousResource) { + private String getResourceLocatorPathParam(String name, PreviousResource previousResource, boolean encoded) { if (previousResource == null) { return null; } @@ -1020,13 +1015,13 @@ private String getResourceLocatorPathParam(String name, PreviousResource previou for (URITemplate.TemplateComponent component : classPath.components) { if (component.name != null) { if (component.name.equals(name)) { - return doGetPathParam(index, previousResource.locatorPathParamValues); + return doGetPathParam(index, previousResource.locatorPathParamValues, encoded); } index++; } else if (component.names != null) { for (String nm : component.names) { if (nm.equals(name)) { - return doGetPathParam(index, previousResource.locatorPathParamValues); + return doGetPathParam(index, previousResource.locatorPathParamValues, encoded); } } index++; @@ -1036,19 +1031,19 @@ private String getResourceLocatorPathParam(String name, PreviousResource previou for (URITemplate.TemplateComponent component : previousResource.locatorTarget.getPath().components) { if (component.name != null) { if (component.name.equals(name)) { - return doGetPathParam(index, previousResource.locatorPathParamValues); + return doGetPathParam(index, previousResource.locatorPathParamValues, encoded); } index++; } else if (component.names != null) { for (String nm : component.names) { if (nm.equals(name)) { - return doGetPathParam(index, previousResource.locatorPathParamValues); + return doGetPathParam(index, previousResource.locatorPathParamValues, encoded); } } index++; } } - return getResourceLocatorPathParam(name, previousResource.prev); + return getResourceLocatorPathParam(name, previousResource.prev, encoded); } public abstract boolean resumeExternalProcessing(); diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/LocatableResourcePathParamExtractor.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/LocatableResourcePathParamExtractor.java index 835be0b515340..e478141b7c45c 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/LocatableResourcePathParamExtractor.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/LocatableResourcePathParamExtractor.java @@ -12,7 +12,7 @@ public LocatableResourcePathParamExtractor(String name) { @Override public Object extractParameter(ResteasyReactiveRequestContext context) { - return context.getResourceLocatorPathParam(name); + return context.getResourceLocatorPathParam(name, false); } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/PathParamExtractor.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/PathParamExtractor.java index ccfa99edca5c3..d56f72b856bf5 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/PathParamExtractor.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/core/parameters/PathParamExtractor.java @@ -1,6 +1,8 @@ package org.jboss.resteasy.reactive.server.core.parameters; +import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; import org.jboss.resteasy.reactive.common.util.Encode; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; @@ -19,14 +21,14 @@ public PathParamExtractor(int index, boolean encoded, boolean single) { @Override public Object extractParameter(ResteasyReactiveRequestContext context) { - String pathParam = context.getPathParam(index); - if (encoded) { - pathParam = Encode.encodeQueryParam(pathParam); - } + String pathParam = context.getPathParam(index, true); if (single) { - return pathParam; + return encoded ? pathParam : Encode.decodePath(pathParam); } else { - return List.of(pathParam.split("/")); + return encoded + ? List.of(pathParam.split("/")) + : Arrays.stream(pathParam.split("/")).map(Encode::decodePath) + .collect(Collectors.toList()); } } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/UriInfoImpl.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/UriInfoImpl.java index 7cbd13cb272a6..987bbf2838113 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/UriInfoImpl.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/jaxrs/UriInfoImpl.java @@ -152,7 +152,7 @@ public MultivaluedMap getPathParameters(boolean decode) { RuntimeResource target = currentRequest.getTarget(); if (target != null) { // a target can be null if this happens in a filter that runs before the target is set for (Entry pathParam : target.getPathParameterIndexes().entrySet()) { - pathParams.add(pathParam.getKey(), currentRequest.getPathParam(pathParam.getValue())); + pathParams.add(pathParam.getKey(), currentRequest.getPathParam(pathParam.getValue(), false)); } } } diff --git a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java index 5d8d5fc931191..7fd7cb535f518 100644 --- a/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java +++ b/independent-projects/resteasy-reactive/server/runtime/src/main/java/org/jboss/resteasy/reactive/server/mapping/RequestMapper.java @@ -8,8 +8,6 @@ import java.util.Map; import java.util.regex.Matcher; -import org.jboss.resteasy.reactive.common.util.URIDecoder; - public class RequestMapper { private static final String[] EMPTY_STRING_ARRAY = new String[0]; @@ -111,7 +109,7 @@ private RequestMatch mapFromPathMatcher(String path, PathMatcher.PathMatchio.smallrye.common smallrye-common-version + + io.smallrye.common + smallrye-common-os + io.fabric8 maven-model-helper diff --git a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdateCommand.java b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdateCommand.java index b7ea74069b464..f53c3def02dd4 100644 --- a/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdateCommand.java +++ b/independent-projects/tools/devtools-common/src/main/java/io/quarkus/devtools/project/update/rewrite/QuarkusUpdateCommand.java @@ -24,6 +24,7 @@ import io.quarkus.devtools.messagewriter.MessageWriter; import io.quarkus.devtools.project.BuildTool; import io.quarkus.qute.Qute; +import io.smallrye.common.os.OS; public class QuarkusUpdateCommand { @@ -57,6 +58,16 @@ public static void handle(MessageWriter log, BuildTool buildTool, Path baseDir, } } + private static void runMavenUpdate(MessageWriter log, Path baseDir, String rewritePluginVersion, String recipesGAV, + Path recipe, + boolean dryRun) { + final String mvnBinary = findMvnBinary(baseDir); + executeCommand(baseDir, getMavenUpdateCommand(mvnBinary, rewritePluginVersion, recipesGAV, recipe, dryRun), log); + + // format the sources + executeCommand(baseDir, getMavenProcessSourcesCommand(mvnBinary), log); + } + private static void runGradleUpdate(MessageWriter log, Path baseDir, String rewritePluginVersion, String recipesGAV, Path recipe, boolean dryRun) { Path tempInit = null; @@ -119,19 +130,11 @@ private static void propagateSystemPropertyIfSet(String name, List comma } } - private static void runMavenUpdate(MessageWriter log, Path baseDir, String rewritePluginVersion, String recipesGAV, - Path recipe, - boolean dryRun) { - final String mvnBinary = findMvnBinary(baseDir); - executeCommand(baseDir, getMavenUpdateCommand(mvnBinary, rewritePluginVersion, recipesGAV, recipe, dryRun), log); - - // format the sources - executeCommand(baseDir, getMavenProcessSourcesCommand(mvnBinary), log); - } - private static List getMavenProcessSourcesCommand(String mvnBinary) { List command = new ArrayList<>(); command.add(mvnBinary); + command.add("-B"); + command.add("clean"); command.add("process-sources"); final String mavenSettings = getMavenSettingsArg(); if (mavenSettings != null) { @@ -146,6 +149,7 @@ private static List getMavenUpdateCommand(String mvnBinary, String rewri boolean dryRun) { final List command = new ArrayList<>(); command.add(mvnBinary); + command.add("-B"); command.add("-e"); command.add( String.format("%s:%s:%s:%s", MAVEN_REWRITE_PLUGIN_GROUP, MAVEN_REWRITE_PLUGIN_ARTIFACT, rewritePluginVersion, @@ -305,18 +309,8 @@ private static boolean isExecutable(Path file) { return false; } - private static String OS = System.getProperty("os.name").toLowerCase(); - public static boolean isWindows() { - return OS.contains("win"); - } - - static boolean hasGradle(Path dir) { - return Files.exists(dir.resolve("build.gradle")); - } - - private static boolean hasMaven(Path dir) { - return Files.exists(dir.resolve("pom.xml")); + return OS.WINDOWS.isCurrent(); } private enum LogLevel { @@ -351,13 +345,13 @@ private String clean(String line) { return line; } - String pattern = "[" + name() + "]"; + String pattern = "[" + name() + "] "; - if (line.length() < pattern.length()) { + if (!line.startsWith(pattern)) { return line; } - return line.substring(pattern.length()).trim(); + return line.substring(pattern.length()); } private boolean matches(String line) { diff --git a/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json b/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json index d5a2a64e17411..e0ffdfe8e81e6 100644 --- a/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json +++ b/independent-projects/tools/devtools-testing/src/main/resources/fake-catalog.json @@ -75,6 +75,62 @@ "io.quarkus:quarkus-fake-bom:999-FAKE:json:999-FAKE" ] }, + { + "name" : "RESTEasy Reactive Jackson", + "description" : "Jackson serialization support for RESTEasy Reactive. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it", + "metadata" : { + "codestart" : { + "name" : "resteasy-reactive", + "kind" : "core", + "languages" : [ "java", "kotlin", "scala" ], + "artifact" : "io.quarkus:quarkus-project-core-extension-codestarts::jar:999-FAKE" + }, + "minimum-java-version" : "11", + "status" : "stable", + "config" : [ "quarkus.resteasy-reactive.", "quarkus.jackson." ], + "built-with-quarkus-core" : "999-FAKE", + "scm-url" : "https://github.com/quarkus-release/release", + "short-name" : "resteasy-reactive-jackson", + "capabilities" : { + "provides" : [ "io.quarkus.rest.jackson", "io.quarkus.resteasy.reactive.json.jackson" ] + }, + "categories" : [ "web", "reactive" ], + "extension-dependencies" : [ "io.quarkus:quarkus-resteasy-reactive", "io.quarkus:quarkus-resteasy-reactive-common", "io.quarkus:quarkus-mutiny", "io.quarkus:quarkus-smallrye-context-propagation", "io.quarkus:quarkus-vertx", "io.quarkus:quarkus-netty", "io.quarkus:quarkus-vertx-http", "io.quarkus:quarkus-core", "io.quarkus:quarkus-jsonp", "io.quarkus:quarkus-virtual-threads", "io.quarkus:quarkus-arc", "io.quarkus:quarkus-resteasy-reactive-jackson-common", "io.quarkus:quarkus-jackson" ], + "keywords" : [ "rest-jackson", "quarkus-resteasy-reactive-json", "jaxrs-json", "rest", "jaxrs", "json", "jackson", "jakarta-rest" ] + }, + "artifact" : "io.quarkus:quarkus-resteasy-reactive-jackson::jar:999-FAKE", + "origins": [ + "io.quarkus:quarkus-fake-bom:999-FAKE:json:999-FAKE" + ] + }, + { + "name" : "RESTEasy Reactive JSON-B", + "description" : "JSON-B serialization support for RESTEasy Reactive. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it.", + "metadata" : { + "codestart" : { + "name" : "resteasy-reactive", + "kind" : "core", + "languages" : [ "java", "kotlin", "scala" ], + "artifact" : "io.quarkus:quarkus-project-core-extension-codestarts::jar:999-FAKE" + }, + "minimum-java-version" : "11", + "status" : "stable", + "config" : [ "quarkus.resteasy-reactive." ], + "built-with-quarkus-core" : "999-FAKE", + "scm-url" : "https://github.com/quarkus-release/release", + "short-name" : "resteasy-reactive-jsonb", + "capabilities" : { + "provides" : [ "io.quarkus.rest.jsonb", "io.quarkus.resteasy.reactive.json.jsonb" ] + }, + "categories" : [ "web", "reactive" ], + "extension-dependencies" : [ "io.quarkus:quarkus-resteasy-reactive", "io.quarkus:quarkus-resteasy-reactive-common", "io.quarkus:quarkus-mutiny", "io.quarkus:quarkus-smallrye-context-propagation", "io.quarkus:quarkus-vertx", "io.quarkus:quarkus-netty", "io.quarkus:quarkus-vertx-http", "io.quarkus:quarkus-core", "io.quarkus:quarkus-jsonp", "io.quarkus:quarkus-virtual-threads", "io.quarkus:quarkus-arc", "io.quarkus:quarkus-resteasy-reactive-jsonb-common", "io.quarkus:quarkus-jsonb" ], + "keywords" : [ "rest-jsonb", "resteasy-reactive-json", "jaxrs-json", "rest", "jaxrs", "json", "jsonb", "jakarta-rest" ] + }, + "artifact" : "io.quarkus:quarkus-resteasy-reactive-jsonb::jar:999-FAKE", + "origins": [ + "io.quarkus:quarkus-fake-bom:999-FAKE:json:999-FAKE" + ] + }, { "name": "YAML Configuration", "description": "Use YAML to configure your Quarkus application", diff --git a/update-version.sh b/update-version.sh index 3eb07d975cbd4..1ebbaf2382826 100755 --- a/update-version.sh +++ b/update-version.sh @@ -9,7 +9,7 @@ if [ $# -eq 0 ]; then fi VERSION=$1 -./mvnw -Dscan=false -Dgradle.cache.local.enabled=false versions:set -Dtcks -DnewVersion="${VERSION}" -DgenerateBackupPoms=false -DprocessAllModules -Prelocations +./mvnw -e -B -Dscan=false -Dgradle.cache.local.enabled=false versions:set -Dtcks -DnewVersion="${VERSION}" -DgenerateBackupPoms=false -DprocessAllModules -Prelocations if [ -f devtools/gradle/gradle.properties ]; then sed -i -r "s/^version( ?= ?).*$/version\1${VERSION}/" devtools/gradle/gradle.properties